diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0970fdd..89d3695 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,10 +1,10 @@ -# Contributing to `code-input` +# Contributing to `code-input.js` -🎉**Here at `code-input`, contributions of all sizes are more than welcome. Below are some scenarios where you could contribute and how to do so.** Contributions are generally accepted when they help achieve at least one of the aims below, but others will be considered: +🎉**Here at `code-input.js`, contributions of all sizes are more than welcome. Below are some scenarios where you could contribute and how to do so.** Contributions are generally accepted when they help achieve at least one of the aims below, but others will be considered: -* The `code-input` element should act like a normal HTML `textarea` in all browsers, working with all of the normal attributes, events and other types of behaviour. -* 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. +* The `code-input.js` element should act like a normal HTML `textarea` in all browsers, working with all of the normal attributes, events and other types of behaviour. +* The `code-input.js` element should be easy to use with all popular syntax-highlighting libraries. +* Any modifications of `code-input.js` 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. 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. @@ -14,23 +14,23 @@ This said, if you're not sure whether your change will be accepted, please ask i --- -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). +To keep this community productive and enjoyable, please [don't break our code of conduct](CODE_OF_CONDUCT.md). --- # Ways you could contribute: -If you don't want to use GitHub, for all of these you can alternatively [get in touch via email so the maintainer can add your contribution for you](mailto:code-input-js@webcoder49.dev). +If you don't want to use either of Codeberg or GitHub, for all of these you can alternatively [get in touch via email so the maintainer can add your contribution for you](mailto:code-input-js@webcoder49.dev). ## 1. I've found a bug but don't know how / don't have time to fix it. -If you think you've found a bug, please [submit an issue](https://github.com/WebCoder49/code-input/issues) with screenshots, how you found the bug, and copies of the console's logs if an error is in them. Please also mention the template and plugins you used (your `codeInput.registerTemplate(...)` snippet). We'd be more than happy to help you fix it. A demo using [CodePen](https://codepen.io/) would be incredibly useful. +If you think you've found a bug, please submit an issue on [GitHub](https://github.com/WebCoder49/code-input/issues) or [Codeberg](https://codeberg.org/code-input-js/code-input-js/issues), with screenshots, how you found the bug, and copies of the console's logs if an error is in them. Please also mention the template and plugins you used (your `codeInput.registerTemplate(...)` snippet). We'd be more than happy to help you fix it. A piece of code demonstrating your bug would be incredibly useful. ## 2. I have implemented a feature / have thought of a potential feature for the library and think it could be useful for others. -The best way to implement a feature is as a plugin like those in the `plugins` folder of `code-input`. If you do this, you could contribute it as in point 3 below. -Otherwise, if you do not have the time needed / do not want to implement it, any potential plugins would be [welcome as an Issue](https://github.com/WebCoder49/code-input/issues) which specifies the uses and desired characteristics. +The best way to implement a feature is as a plugin like those in the `plugins` folder of `code-input.js`. If you do this, you could contribute it as in point 3 below. +Otherwise, if you do not have the time needed / do not want to implement it, any potential plugins would be (on [GitHub](https://github.com/WebCoder49/code-input/issues) or [Codeberg](https://codeberg.org/code-input-js/code-input-js/issues)) which specifies the uses and desired characteristics. ## 3. I want to contribute code that I need / have seen in the Issues. Firstly, thank you for doing this! This is probably the most time consuming way of contributing but is also the most rewarding for both you and me as a maintainer. -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). +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 (how-tos by [Codeberg](https://docs.codeberg.org/collaborating/pull-requests-and-git-flow/) and [GitHub](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! diff --git a/LICENSE b/LICENSE index a0a9e97..19fa604 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021-2025 Oliver Geer and contributors +Copyright (c) 2021-2026 Oliver Geer and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index c974739..1da79c6 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # code-input -![Click to Switch](https://img.shields.io/static/v1?label=&message=Click%20to%20Switch:%20&color=grey&style=for-the-badge)[![Website and Demos](https://img.shields.io/static/v1?label=&message=Website&color=black&style=for-the-badge)](https://code-input-js.org)[![GitHub](https://img.shields.io/static/v1?label=&message=GitHub&color=navy&style=for-the-badge&logo=github)](https://github.com/WebCoder49/code-input)[![Yarn](https://img.shields.io/static/v1?label=&message=Yarn&color=purple&style=for-the-badge&logo=yarn)](https://yarnpkg.com/package?name=@webcoder49/code-input)[![NPM](https://img.shields.io/static/v1?label=&message=NPM&color=red&style=for-the-badge&logo=npm)](https://www.npmjs.com/package/@webcoder49/code-input) +![Click to Switch](https://img.shields.io/static/v1?label=&message=Click%20to%20Switch:%20&color=grey&style=for-the-badge)[![Website and Demos](https://img.shields.io/static/v1?label=&message=Website&color=444400&style=for-the-badge)](https://code-input-js.org)[![GitHub](https://img.shields.io/static/v1?label=&message=GitHub&color=black&style=for-the-badge&logo=github)](https://github.com/WebCoder49/code-input)[![Codeberg](https://img.shields.io/static/v1?label=&message=Codeberg&color=navy&style=for-the-badge&logo=codeberg)](https://codeberg.org/code-input-js/code-input-js)[![Yarn](https://img.shields.io/static/v1?label=&message=Yarn&color=purple&style=for-the-badge&logo=yarn)](https://yarnpkg.com/package?name=@webcoder49/code-input)[![NPM](https://img.shields.io/static/v1?label=&message=NPM&color=red&style=for-the-badge&logo=npm)](https://www.npmjs.com/package/@webcoder49/code-input) -[![View License](https://img.shields.io/github/license/webcoder49/code-input?style=for-the-badge)](LICENSE) [![View Releases](https://img.sHields.io/github/v/release/webcoder49/code-input?style=for-the-badge)](https://github.com/WebCoder49/code-input/releases) +[![View License](https://img.shields.io/github/license/webcoder49/code-input?color=darkgreen&style=for-the-badge)](LICENSE) [![View Releases](https://img.shields.io/github/v/release/webcoder49/code-input?style=for-the-badge)](https://github.com/WebCoder49/code-input/releases) > **An editable <textarea> that supports *any* syntax highlighting algorithm, for code or something else. Also, added plugins.** > > Aiming to be more flexible, lightweight, modular, progressively enhanced and standards-based than the alternatives, we support HTML forms, the ` - - + + - + - - + + - + - - + + - + - - + + - + - +

This uses both the auto-close brackets and indent plugins. Try typing some brackets / double quotes!

- + ``` @@ -60,12 +60,12 @@ Right now, you can only add one plugin of each type (e.g. one SelectTokenCallbac - - + + - - + +

Start typing some HTML tags to see the autocomplete in action. You can click an autocomplete suggestion, or press the Tab key to select the first.

- + + + +``` + +#### `Autogrow`: Let code-input elements resize to fit their content (optionally between limits) {#playground-preset-autogrow} + +``` + + + + + + + + + + + + + + + + +

Enter newlines to grow vertically:

+ +

Type to grow horizontally:

+ +

Grows vertically between 100px and 200px:

+ +

Grows horizontally between 100px and 200px:

+ ``` @@ -135,17 +170,19 @@ Right now, you can only add one plugin of each type (e.g. one SelectTokenCallbac - - + + - - + + -

When focused in the editor: Try Ctrl+F, or click to find. Try Ctrl+H, or click to replace.

- # Hickory dickory dock +

When focused in the editor: Try Ctrl/Cmd+F, or click to find. Try Ctrl+H, or click to replace.

+ +

Please Note

+

When setting the size of code-input elements with this plugin, make sure they are large enough to fit the dialog, at least 500px wide and 170px tall with the default styling. (If you are using the Autogrow plugin, this will be done for you as long as your --code-input_autogrow_max-width and --code-input_autogrow_max-height are large enough.) For technical reasons the dialog cannot overflow the editing area.

+

The keyboard shortcuts may be intercepted by the browser and unable to be used by code-input.js, for example in GNOME Web or Konqueror. Thus if you want this plugin to be available to all users, provide separate buttons to trigger the dialogs as shown above.

``` @@ -184,18 +224,19 @@ Hickory dickory dock.
- - + + - - + + - +

Try Ctrl+G when focused in the editor, or click

- # Hickory dickory dock + +

Please Note

+

When setting the size of code-input elements with this plugin, make sure they are large enough to fit the dialog, at least 300px wide and 150px tall with the default styling. (If you are using the Autogrow plugin, this will be done for you as long as your --code-input_autogrow_max-width and --code-input_autogrow_max-height are large enough.) For technical reasons the dialog cannot overflow the editing area.

+

The keyboard shortcuts may be intercepted by the browser and unable to be used by code-input.js, for example in GNOME Web or Konqueror. Thus if you want this plugin to be available to all users, provide a separate button to trigger the dialog as shown above.

``` @@ -228,11 +272,11 @@ Hickory dickory dock.
- - + + - +

Try Tab or Shift+Tab when selecting or editing text.

- { + ``` @@ -501,11 +545,11 @@ Hickory dickory dock. - - + + - + @@ -535,14 +579,14 @@ Hickory dickory dock. ]));

Try selecting some code with a bracket. This is just one demo use of this very flexible plugin, which lets you use more features from your highlighter (here, Prism.js' match-braces plugin)!

- function hello() { + ``` @@ -568,12 +612,12 @@ See https://github.com/WebCoder49/code-input/issues?q=is%3Aissue%20state%3Aopen% - - + + - - + + - + - - + + - +

Start typing code of any language. Detected language: . Inaccurate language detection should be reported to highlight.js, not code-input-js.

- +

@@ -646,14 +690,14 @@ See https://github.com/WebCoder49/code-input/issues?q=is%3Aissue%20state%3Aopen% - - + + - +

The lines are numbered!

- # Hickory dickory dock + ``` diff --git a/package.json b/package.json index fcabbe6..85abb1e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@webcoder49/code-input", - "version": "2.7.1", + "version": "2.8.3", "description": "An editable <textarea> that supports *any* syntax highlighting algorithm, for code or something else. Also, added plugins.", "browser": "code-input.js", "exports": { @@ -90,14 +90,14 @@ "highlight", "textarea", "editable", - "web-components", + "web-component", "code-editor", "text-editor" ], "author": { "name": "Oliver Geer and contributors", "email": "hi@webcoder49.dev", - "url": "https://oliver.geer.im/" + "url": "https://ogeer.org/" }, "license": "MIT", "bugs": { diff --git a/plugins/README.md b/plugins/README.md index 67debcb..a161abd 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -3,7 +3,7 @@ 💡 Do you just want to get a quick editor working? We suggest the [Indent](#indent) and [Prism Line Numbers](#prism-line-numbers) plugins. -**Lots of plugins are very customisable - please see the JavaScript files for parameters and if you want more features let us know via GitHub Issues.** +**Lots of plugins are very customisable - please see the JavaScript files for parameters and if you want more features let us know via Issues.** --- @@ -13,49 +13,56 @@ is activated for. Files: [auto-close-brackets.js](./auto-close-brackets.js) -[🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/qBgGGKR) +[🚀 *Demo*](https://v2.code-input-js.org/plugins/#playground-preset-auto-close-brackets) ### Autocomplete Display a popup under the caret using the text in the code-input element. This works well with autocomplete suggestions. Files: [autocomplete.js](./autocomplete.js) / [autocomplete.css](./autocomplete.css) -[🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/xxapjXB) +[🚀 *Demo*](https://v2.code-input-js.org/plugins/#playground-preset-autocomplete) ### Autodetect Autodetect the language live and change the `lang` attribute using the syntax highlighter's autodetect capabilities. Works with highlight.js. Files: [autodetect.js](./autodetect.js) -[🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/eYLyMae) +[🚀 *Demo*](https://v2.code-input-js.org/plugins/#playground-preset-autodetect) + +### Autogrow +Make code-input elements resize automatically and fit their content live using CSS classes, optionally between a minimum and maximum size specified using CSS variables. + +Files: [autogrow.css](./autogrow.css) (NO JS FILE) + +[🚀 *Demo*](https://v2.code-input-js.org/plugins/#playground-preset-autogrow) ### Find and Replace Add Find-and-Replace (Ctrl+F for find, Ctrl+H for replace by default, or when JavaScript triggers it) functionality to the code editor. Files: [find-and-replace.js](./find-and-replace.js) / [find-and-replace.css](./find-and-replace.css) -[🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/oNVVBBz) +[🚀 *Demo*](https://v2.code-input-js.org/plugins/#playground-preset-find-and-replace) ### 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. Files: [go-to-line.js](./go-to-line.js) / [go-to-line.css](./go-to-line.css) -[🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/YzBMOXP) +[🚀 *Demo*](https://v2.code-input-js.org/plugins/#playground-preset-go-to-line) ### Indent Add indentation using the `Tab` key, and auto-indents after a newline, as well as making it possible to indent/unindent multiple lines using Tab/Shift+Tab. **Supports tab characters and custom numbers of spaces as indentation, as well as (optionally) brackets typed affecting indentation.** Files: [indent.js](./indent.js) -[🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/WNgdzar) +[🚀 *Demo*](https://v2.code-input-js.org/plugins/#playground-preset-indent) ### Prism Line Numbers Allow code-input elements to be used with the Prism.js line-numbers plugin, as long as the code-input element or a parent element of it has the CSS class `line-numbers`. [Prism.js Plugin Docs](https://prismjs.com/plugins/line-numbers/) Files: [prism-line-numbers.css](./prism-line-numbers.css) (NO JS FILE) -[🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/XWPVrWv) +[🚀 *Demo*](https://v2.code-input-js.org/plugins/#playground-preset-prism-line-numbers) ### Special Chars Render special characters and control characters as a symbol @@ -65,14 +72,14 @@ with their hex code. Files: [special-chars.js](./special-chars.js) / [special-chars.css](./special-chars.css) -[🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/jOeYJbm) +[🚀 *Demo*](https://v2.code-input-js.org/plugins/#playground-preset-special-chars) ### Select Token Callbacks Make tokens in the `
` element that are included within the selected text of the `` gain a CSS class while selected, or trigger JavaScript callbacks.
 
 Files: select-token-callbacks.js
 
-[🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/WNVZXxM)
+[🚀 *Demo*](https://v2.code-input-js.org/plugins/#playground-preset-select-token-callbacks)
 
 ## Using Plugins
 Plugins allow you to add extra features to a template, like [automatic indentation](./indent.js) or [support for highlight.js's language autodetection](./autodetect.js). To use them, just:
diff --git a/plugins/autocomplete.js b/plugins/autocomplete.js
index 9080d34..dfc128a 100644
--- a/plugins/autocomplete.js
+++ b/plugins/autocomplete.js
@@ -65,7 +65,7 @@ codeInput.plugins.Autocomplete = class extends codeInput.Plugin {
                 // Hasn't saved text in test pre to find pos
                 // Need to regenerate text in test pre
                 return this.getCaretCoordinates(codeInput, textarea, charIndex, false);
-            } 
+            }
             afterSpan = spans[1];
         } else {
             /* Inspired by https://github.com/component/textarea-caret-position */
@@ -74,7 +74,8 @@ codeInput.plugins.Autocomplete = class extends codeInput.Plugin {
             let beforeSpan = document.createElement("span");
             beforeSpan.textContent = textarea.value.substring(0, charIndex);
             afterSpan = document.createElement("span");
-            afterSpan.textContent = "."; // Placeholder
+            afterSpan.textContent = ""; // Text here would potentially make the
+            // invisible pre and thus the autogrowing code-input element wider
 
             // Clear test pre - https://developer.mozilla.org/en-US/docs/Web/API/Node/removeChild
             while (testPosElem.firstChild) {
diff --git a/plugins/autocomplete.min.js b/plugins/autocomplete.min.js
index 0d67387..7b7a674 100644
--- a/plugins/autocomplete.min.js
+++ b/plugins/autocomplete.min.js
@@ -1 +1 @@
-"use strict";codeInput.plugins.Autocomplete=class extends codeInput.Plugin{constructor(a){super([]),this.updatePopupCallback=a}updatePopup(a,b){let c=a.textareaElement,d=this.getCaretCoordinates(a,c,c.selectionEnd,b),e=a.querySelector(".code-input_autocomplete_popup");e.style.top=d.top+"px",e.style.left=d.left+"px",b||this.updatePopupCallback(e,c,c.selectionEnd,c.selectionStart)}afterElementsAdded(a){let b=document.createElement("div");b.classList.add("code-input_autocomplete_popup"),a.appendChild(b);let c=document.createElement("pre");if(c.classList.add("code-input_autocomplete_invisiblepre"),c.setAttribute("aria-hidden",!0),a.templateObject.preElementStyled)c.classList.add("code-input_autocomplete_testpos"),a.appendChild(c);else{let b=document.createElement("code");b.classList.add("code-input_autocomplete_testpos"),c.appendChild(b),a.appendChild(c)}let d=a.textareaElement;d.addEventListener("input",()=>{this.updatePopup(a,!1)}),d.addEventListener("selectionchange",()=>{this.updatePopup(a,!1)})}getCaretCoordinates(a,b,c,d){let e;if(d){let d=a.querySelector(".code-input_autocomplete_testpos").querySelectorAll("span");if(2>d.length)return this.getCaretCoordinates(a,b,c,!1);e=d[1]}else{let d=a.querySelector(".code-input_autocomplete_testpos"),f=document.createElement("span");for(f.textContent=b.value.substring(0,c),e=document.createElement("span"),e.textContent=".";d.firstChild;)d.removeChild(d.firstChild);d.appendChild(f),d.appendChild(e)}return{top:e.offsetTop-b.scrollTop,left:e.offsetLeft-b.scrollLeft}}updatePopupCallback=function(){}};
\ No newline at end of file
+"use strict";codeInput.plugins.Autocomplete=class extends codeInput.Plugin{constructor(a){super([]),this.updatePopupCallback=a}updatePopup(a,b){let c=a.textareaElement,d=this.getCaretCoordinates(a,c,c.selectionEnd,b),e=a.querySelector(".code-input_autocomplete_popup");e.style.top=d.top+"px",e.style.left=d.left+"px",b||this.updatePopupCallback(e,c,c.selectionEnd,c.selectionStart)}afterElementsAdded(a){let b=document.createElement("div");b.classList.add("code-input_autocomplete_popup"),a.appendChild(b);let c=document.createElement("pre");if(c.classList.add("code-input_autocomplete_invisiblepre"),c.setAttribute("aria-hidden",!0),a.templateObject.preElementStyled)c.classList.add("code-input_autocomplete_testpos"),a.appendChild(c);else{let b=document.createElement("code");b.classList.add("code-input_autocomplete_testpos"),c.appendChild(b),a.appendChild(c)}let d=a.textareaElement;d.addEventListener("input",()=>{this.updatePopup(a,!1)}),d.addEventListener("selectionchange",()=>{this.updatePopup(a,!1)})}getCaretCoordinates(a,b,c,d){let e;if(d){let d=a.querySelector(".code-input_autocomplete_testpos").querySelectorAll("span");if(2>d.length)return this.getCaretCoordinates(a,b,c,!1);e=d[1]}else{let d=a.querySelector(".code-input_autocomplete_testpos"),f=document.createElement("span");for(f.textContent=b.value.substring(0,c),e=document.createElement("span"),e.textContent="";d.firstChild;)d.removeChild(d.firstChild);d.appendChild(f),d.appendChild(e)}return{top:e.offsetTop-b.scrollTop,left:e.offsetLeft-b.scrollLeft}}updatePopupCallback=function(){}};
\ No newline at end of file
diff --git a/plugins/autogrow.css b/plugins/autogrow.css
new file mode 100644
index 0000000..9595dc5
--- /dev/null
+++ b/plugins/autogrow.css
@@ -0,0 +1,77 @@
+/**
+ * Make code-input elements resize automatically and fit their content live using CSS
+ * classes, optionally between a minimum and maximum size specified using CSS variables.
+ *
+ * Files: autogrow.css
+ */
+
+/* Inspired greatly by https://css-tricks.com/the-cleanest-trick-for-autogrowing-textareas/
+and the fact code-input.js already implements the article's main structure. */
+
+code-input.code-input_autogrow_height {
+    --code-input_autogrow_min-height: 0px;
+    --code-input_autogrow_max-height: calc(infinity * 1px);
+
+    /* See FindAndReplace/GoToLine dialog visibility */
+    --code-input_autogrow_internal-min-height: 0px;
+    --code-input_autogrow_true-min-height: max(var(--code-input_autogrow_min-height), var(--code-input_autogrow_internal-min-height));
+
+    height: max-content;
+    max-height: var(--code-input_autogrow_max-height);
+}
+code-input.code-input_autogrow_height textarea {
+    height: calc(var(--code-input_autogrow_true-min-height) - var(--padding-top, 16px) - var(--padding-bottom, 16px))!important; /* So minimum height possible while containing highlighted code */
+    min-height: max(var(--code-input_synced-height), calc(100% - var(--padding-top, 16px) - var(--padding-bottom, 16px)));
+}
+code-input.code-input_autogrow_height > pre,
+code-input.code-input_autogrow_height > pre code {
+    min-height: calc(var(--code-input_autogrow_true-min-height) - var(--padding-top, 16px) - var(--padding-bottom, 16px));
+}
+
+code-input.code-input_autogrow_width {
+    --code-input_autogrow_min-width: 100px;
+    --code-input_autogrow_max-width: 100%;
+
+    /* See FindAndReplace/GoToLine dialog visibility */
+    --code-input_autogrow_internal-min-width: 0px;
+    --code-input_autogrow_true-min-width: max(var(--code-input_autogrow_min-width), var(--code-input_autogrow_internal-min-width));
+
+    width: max-content; /* Using unset rather than max-content makes always 100% width. */
+    max-width: var(--code-input_autogrow_max-width);
+}
+code-input.code-input_autogrow_width textarea {
+    width: calc(var(--code-input_autogrow_true-min-width) - var(--padding-left, 16px) - var(--padding-right, 16px))!important; /* So minimum width possible while containing highlighted code */
+    min-width: max(var(--code-input_synced-width), calc(100% - var(--padding-left, 16px) - var(--padding-right, 16px)));
+}
+code-input.code-input_autogrow_width pre code,
+code-input.code-input_autogrow_width pre {
+    min-width: calc(var(--code-input_autogrow_true-min-width) - var(--padding-left, 16px) - var(--padding-right, 16px));
+}
+
+/* FindAndReplace / GoToLine dialog visibility */
+code-input.code-input_autogrow_width:has(.code-input_go-to-line_dialog:not(.code-input_go-to-line_hidden-dialog)) {
+    --code-input_autogrow_internal-min-width: 300px;
+}
+code-input.code-input_autogrow_height:has(.code-input_go-to-line_dialog:not(.code-input_go-to-line_hidden-dialog)) {
+    --code-input_autogrow_internal-min-height: 150px;
+}
+code-input.code-input_autogrow_width:has(.code-input_find-and-replace_dialog:not(.code-input_find-and-replace_hidden-dialog)) {
+    --code-input_autogrow_internal-min-width: 500px;
+}
+code-input.code-input_autogrow_height:has(.code-input_find-and-replace_dialog:not(.code-input_find-and-replace_hidden-dialog)) {
+    --code-input_autogrow_internal-min-height: 170px;
+}
+
+/* Autogrow doesn't work with fallback textarea (since its functioning depends on
+synchronisation to pre code element). Stick to the minimum size. */
+
+/* At least one line high, leave space for no-JS instructions */
+code-input.code-input_autogrow_height:has(textarea[data-code-input-fallback]) {
+    --code-input_autogrow_internal-min-height: calc(4em + var(--padding-top, 16px) + var(--padding-bottom, 16px)); /* For browsers that don't support 1lh */
+    --code-input_autogrow_internal-min-height: calc(1lh + 2em + var(--padding-top, 16px) + var(--padding-bottom, 16px)); /* So minimum height possible while containing highlighted code */
+    height: var(--code-input_autogrow_true-min-height);
+}
+code-input.code-input_autogrow_height textarea[data-code-input-fallback] {
+    height: calc(var(--code-input_autogrow_true-min-height) - var(--padding-top, 16px) - max(2em, var(--padding-bottom, 16px)))!important;
+    min-height: calc(var(--code-input_autogrow_true-min-height) - var(--padding-top, 16px) - max(2em, var(--padding-bottom, 16px)));
+}
diff --git a/plugins/autogrow.min.css b/plugins/autogrow.min.css
new file mode 100644
index 0000000..2f5d154
--- /dev/null
+++ b/plugins/autogrow.min.css
@@ -0,0 +1 @@
+code-input.code-input_autogrow_height{--code-input_autogrow_min-height:0px;--code-input_autogrow_max-height:calc(infinity * 1px);--code-input_autogrow_internal-min-height:0px;--code-input_autogrow_true-min-height:max(var(--code-input_autogrow_min-height), var(--code-input_autogrow_internal-min-height));height:max-content;max-height:var(--code-input_autogrow_max-height)}code-input.code-input_autogrow_height textarea{min-height:max(var(--code-input_synced-height), calc(100% - var(--padding-top,16px) - var(--padding-bottom,16px)));height:calc(var(--code-input_autogrow_true-min-height) - var(--padding-top,16px) - var(--padding-bottom,16px))!important}code-input.code-input_autogrow_height>pre,code-input.code-input_autogrow_height>pre code{min-height:calc(var(--code-input_autogrow_true-min-height) - var(--padding-top,16px) - var(--padding-bottom,16px))}code-input.code-input_autogrow_width{--code-input_autogrow_min-width:100px;--code-input_autogrow_max-width:100%;--code-input_autogrow_internal-min-width:0px;--code-input_autogrow_true-min-width:max(var(--code-input_autogrow_min-width), var(--code-input_autogrow_internal-min-width));width:max-content;max-width:var(--code-input_autogrow_max-width)}code-input.code-input_autogrow_width textarea{min-width:max(var(--code-input_synced-width), calc(100% - var(--padding-left,16px) - var(--padding-right,16px)));width:calc(var(--code-input_autogrow_true-min-width) - var(--padding-left,16px) - var(--padding-right,16px))!important}code-input.code-input_autogrow_width pre code,code-input.code-input_autogrow_width pre{min-width:calc(var(--code-input_autogrow_true-min-width) - var(--padding-left,16px) - var(--padding-right,16px))}code-input.code-input_autogrow_width:has(.code-input_go-to-line_dialog:not(.code-input_go-to-line_hidden-dialog)){--code-input_autogrow_internal-min-width:300px}code-input.code-input_autogrow_height:has(.code-input_go-to-line_dialog:not(.code-input_go-to-line_hidden-dialog)){--code-input_autogrow_internal-min-height:150px}code-input.code-input_autogrow_width:has(.code-input_find-and-replace_dialog:not(.code-input_find-and-replace_hidden-dialog)){--code-input_autogrow_internal-min-width:500px}code-input.code-input_autogrow_height:has(.code-input_find-and-replace_dialog:not(.code-input_find-and-replace_hidden-dialog)){--code-input_autogrow_internal-min-height:170px}code-input.code-input_autogrow_height:has(textarea[data-code-input-fallback]){--code-input_autogrow_internal-min-height:calc(1lh + 2em + var(--padding-top,16px) + var(--padding-bottom,16px));height:var(--code-input_autogrow_true-min-height)}code-input.code-input_autogrow_height textarea[data-code-input-fallback]{min-height:calc(var(--code-input_autogrow_true-min-height) - var(--padding-top,16px) - max(2em, var(--padding-bottom,16px)));height:calc(var(--code-input_autogrow_true-min-height) - var(--padding-top,16px) - max(2em, var(--padding-bottom,16px)))!important}
\ No newline at end of file
diff --git a/plugins/find-and-replace.css b/plugins/find-and-replace.css
index 07c0171..8675556 100644
--- a/plugins/find-and-replace.css
+++ b/plugins/find-and-replace.css
@@ -15,13 +15,14 @@
 /* Find-and-replace dialog */
 
 @keyframes code-input_find-and-replace_roll-in {
-    0% {opacity: 0; transform: translateY(-34px);}
-    100% {opacity: 1; transform: translateY(0px);}
-}
-
-@keyframes code-input_find-and-replace_roll-out {
-    0% {opacity: 1;top: 0;}
-    100% {opacity: 0;top: -34px;}
+    0% {
+      opacity: 0;
+      transform: translateY(-34px);
+    }
+    100% {
+      opacity: 1;
+      transform: translateY(0px);
+    }
 }
 
 .code-input_find-and-replace_dialog {
@@ -38,14 +39,11 @@
 
 .code-input_find-and-replace_dialog:not(.code-input_find-and-replace_hidden-dialog) {
   animation: code-input_find-and-replace_roll-in .2s;
-  opacity: 1;
-  pointer-events: all;
+  display: block;
 }
 
 .code-input_find-and-replace_dialog.code-input_find-and-replace_hidden-dialog {
-  animation: code-input_find-and-replace_roll-out .2s;
-  opacity: 0;
-  pointer-events: none;
+  display: none;
 }
 
 .code-input_find-and-replace_dialog input::placeholder {
@@ -57,11 +55,12 @@
   width: 240px; height: 32px; top: -3px;
   font-size: large;
   color: #000000aa;
+  caret-color: currentColor; /* Don't inherit from the code-input element. */
   border: 0;
 }
 
 .code-input_find-and-replace_dialog input.code-input_find-and-replace_error {
-  color: #ff0000aa;
+  color: #b60000;
 }
 
 .code-input_find-and-replace_dialog button, .code-input_find-and-replace_dialog input[type="checkbox"] {
diff --git a/plugins/find-and-replace.js b/plugins/find-and-replace.js
index 3d4417c..e312ed9 100644
--- a/plugins/find-and-replace.js
+++ b/plugins/find-and-replace.js
@@ -1,5 +1,5 @@
 /**
- * Add Find-and-Replace (Ctrl+F for find, Ctrl+H for replace by default) functionality to the code editor.
+ * Add Find-and-Replace (Ctrl/Cmd+F for find, Ctrl+H for replace by default) functionality to the code editor.
  * Files: find-and-replace.js / find-and-replace.css
  */
 "use strict";
@@ -32,15 +32,17 @@ codeInput.plugins.FindAndReplace = class extends codeInput.Plugin {
     };
 
     /**
-     * Create a find-and-replace command plugin to pass into a template
-     * @param {boolean} useCtrlF Should Ctrl+F be overriden for find-and-replace find functionality? Either way, you can also trigger it yourself using (instance of this plugin)`.showPrompt(code-input element, false)`.
-     * @param {boolean} useCtrlH Should Ctrl+H be overriden for find-and-replace replace functionality? Either way, you can also trigger it yourself using (instance of this plugin)`.showPrompt(code-input element, true)`.
-     * @param {Object} instructionTranslations: user interface string keys mapped to translated versions for localisation. Look at the find-and-replace.js source code for the English text and available keys.
+     * Create a find-and-replace command plugin to pass into a template. To ensure keyboard shortcuts remain intuitive, set the alwaysCtrl parameter to false.
+     * @param {boolean} useCtrlF Should Ctrl/Cmd+F be overridden for find-and-replace find functionality? Either way, you can also trigger it yourself using (instance of this plugin)`.showPrompt(code-input element, false)`.
+     * @param {boolean} useCtrlH Should Ctrl+H be overridden for find-and-replace replace functionality? Either way, you can also trigger it yourself using (instance of this plugin)`.showPrompt(code-input element, true)`.
+     * @param {Object} instructionTranslations: user interface string keys mapped to translated versions for localisation. Look at the find-and-replace.js source code for the English text.
+     * @param {boolean} alwaysCtrl Setting this to false makes the keyboard shortcuts follow the operating system while avoiding clashes (right now: Cmd+F/Ctrl+H on Apple, Ctrl+F/Ctrl+H otherwise.) and is recommended; true forces Ctrl+F/Ctrl+H and is default for backwards compatibility.
      */
-    constructor(useCtrlF = true, useCtrlH = true, instructionTranslations = {}) {
+    constructor(useCtrlF = true, useCtrlH = true, instructionTranslations = {}, alwaysCtrl = true) {
         super([]); // No observed attributes
         this.useCtrlF = useCtrlF;
         this.useCtrlH = useCtrlH;
+        this.alwaysCtrl = alwaysCtrl;
         this.addTranslations(this.instructions, instructionTranslations);
     }
 
@@ -121,6 +123,9 @@ codeInput.plugins.FindAndReplace = class extends codeInput.Plugin {
                         // No matches - error
                         dialog.findInput.classList.add('code-input_find-and-replace_error');
                     }
+                } else {
+                    // Input box is empty - no error
+                    dialog.findInput.classList.remove('code-input_find-and-replace_error');
                 }
                 this.updateMatchDescription(dialog);
             }
@@ -420,11 +425,18 @@ codeInput.plugins.FindAndReplace = class extends codeInput.Plugin {
         this.updateFindMatches(dialog);
     }
 
-    /* Event handler for keydown event that makes Ctrl+F open find dialog */
+    /* Event handler for keydown event that makes Ctrl/Cmd+F open find dialog */
     checkCtrlF(codeInput, event) {
-        if (event.ctrlKey && event.key == 'f') {
-            event.preventDefault();
-            this.showPrompt(codeInput, false);
+        if(!this.alwaysCtrl && (navigator.platform.startsWith("Mac") || navigator.platform === "iPhone")) {
+            if (event.metaKey && event.key == 'f') { // Cmd+F
+                event.preventDefault();
+                this.showPrompt(codeInput, false);
+            }
+        } else {
+            if (event.ctrlKey && event.key == 'f') {
+                event.preventDefault();
+                this.showPrompt(codeInput, false);
+            }
         }
     }
 
diff --git a/plugins/find-and-replace.min.css b/plugins/find-and-replace.min.css
index 32aa8d9..2f1c43e 100644
--- a/plugins/find-and-replace.min.css
+++ b/plugins/find-and-replace.min.css
@@ -1 +1 @@
-.code-input_find-and-replace_find-match{color:inherit;text-shadow:none!important;background-color:#ff0!important}.code-input_find-and-replace_find-match-focused,.code-input_find-and-replace_find-match-focused *{color:#000!important;background-color:#f80!important}.code-input_find-and-replace_start-newline:before{content:"⤶"}@keyframes code-input_find-and-replace_roll-in{0%{opacity:0;transform:translateY(-34px)}to{opacity:1;transform:translateY(0)}}@keyframes code-input_find-and-replace_roll-out{0%{opacity:1;top:0}to{opacity:0;top:-34px}}.code-input_find-and-replace_dialog{background-color:#fff;border:1px solid #0004;border-radius:6px;padding:8px 6px 6px;position:absolute;top:0;right:14px;box-shadow:0 .2em 1em .2em #00000029}.code-input_find-and-replace_dialog:not(.code-input_find-and-replace_hidden-dialog){opacity:1;pointer-events:all;animation:.2s code-input_find-and-replace_roll-in}.code-input_find-and-replace_dialog.code-input_find-and-replace_hidden-dialog{opacity:0;pointer-events:none;animation:.2s code-input_find-and-replace_roll-out}.code-input_find-and-replace_dialog input::placeholder{font-size:80%}.code-input_find-and-replace_dialog input{color:#000a;border:0;width:240px;height:32px;font-size:large;position:relative;top:-3px}.code-input_find-and-replace_dialog input.code-input_find-and-replace_error{color:#f00a}.code-input_find-and-replace_dialog button,.code-input_find-and-replace_dialog input[type=checkbox]{cursor:pointer;appearance:none;text-align:center;color:#000;vertical-align:top;background-color:#ddd;border:0;width:min-content;margin:5px;padding:5px;font-size:22px;line-height:24px;display:inline-block}.code-input_find-and-replace_dialog input[type=checkbox].code-input_find-and-replace_case-sensitive-checkbox:before{content:"Aa"}.code-input_find-and-replace_dialog input[type=checkbox].code-input_find-and-replace_reg-exp-checkbox:before{content:".*"}.code-input_find-and-replace_dialog button:hover,.code-input_find-and-replace_dialog input[type=checkbox]:hover{background-color:#bbb}.code-input_find-and-replace_dialog input[type=checkbox]:checked{color:#fff;background-color:#222}.code-input_find-and-replace_match-description{color:#444;display:block}.code-input_find-and-replace_dialog details summary,.code-input_find-and-replace_dialog button{cursor:pointer}.code-input_find-and-replace_dialog button.code-input_find-and-replace_button-hidden{opacity:0;pointer-events:none}.code-input_find-and-replace_dialog span{float:right;text-align:center;color:#000;opacity:.6;border-radius:50%;width:24px;margin:5px;padding:5px;font-family:system-ui;font-size:22px;font-weight:500;line-height:24px;display:block}.code-input_find-and-replace_dialog span:before{content:"×"}.code-input_find-and-replace_dialog span:hover{opacity:.8;background-color:#00000018}
\ No newline at end of file
+.code-input_find-and-replace_find-match{color:inherit;text-shadow:none!important;background-color:#ff0!important}.code-input_find-and-replace_find-match-focused,.code-input_find-and-replace_find-match-focused *{color:#000!important;background-color:#f80!important}.code-input_find-and-replace_start-newline:before{content:"⤶"}@keyframes code-input_find-and-replace_roll-in{0%{opacity:0;transform:translateY(-34px)}to{opacity:1;transform:translateY(0)}}.code-input_find-and-replace_dialog{background-color:#fff;border:1px solid #0004;border-radius:6px;padding:8px 6px 6px;position:absolute;top:0;right:14px;box-shadow:0 .2em 1em .2em #00000029}.code-input_find-and-replace_dialog:not(.code-input_find-and-replace_hidden-dialog){animation:.2s code-input_find-and-replace_roll-in;display:block}.code-input_find-and-replace_dialog.code-input_find-and-replace_hidden-dialog{display:none}.code-input_find-and-replace_dialog input::placeholder{font-size:80%}.code-input_find-and-replace_dialog input{color:#000a;caret-color:currentColor;border:0;width:240px;height:32px;font-size:large;position:relative;top:-3px}.code-input_find-and-replace_dialog input.code-input_find-and-replace_error{color:#b60000}.code-input_find-and-replace_dialog button,.code-input_find-and-replace_dialog input[type=checkbox]{cursor:pointer;appearance:none;text-align:center;color:#000;vertical-align:top;background-color:#ddd;border:0;width:min-content;margin:5px;padding:5px;font-size:22px;line-height:24px;display:inline-block}.code-input_find-and-replace_dialog input[type=checkbox].code-input_find-and-replace_case-sensitive-checkbox:before{content:"Aa"}.code-input_find-and-replace_dialog input[type=checkbox].code-input_find-and-replace_reg-exp-checkbox:before{content:".*"}.code-input_find-and-replace_dialog button:hover,.code-input_find-and-replace_dialog input[type=checkbox]:hover{background-color:#bbb}.code-input_find-and-replace_dialog input[type=checkbox]:checked{color:#fff;background-color:#222}.code-input_find-and-replace_match-description{color:#444;display:block}.code-input_find-and-replace_dialog details summary,.code-input_find-and-replace_dialog button{cursor:pointer}.code-input_find-and-replace_dialog button.code-input_find-and-replace_button-hidden{opacity:0;pointer-events:none}.code-input_find-and-replace_dialog span{float:right;text-align:center;color:#000;opacity:.6;border-radius:50%;width:24px;margin:5px;padding:5px;font-family:system-ui;font-size:22px;font-weight:500;line-height:24px;display:block}.code-input_find-and-replace_dialog span:before{content:"×"}.code-input_find-and-replace_dialog span:hover{opacity:.8;background-color:#00000018}
\ No newline at end of file
diff --git a/plugins/find-and-replace.min.js b/plugins/find-and-replace.min.js
index 6025c1a..c51b137 100644
--- a/plugins/find-and-replace.min.js
+++ b/plugins/find-and-replace.min.js
@@ -1 +1 @@
-"use strict";codeInput.plugins.FindAndReplace=class extends codeInput.Plugin{useCtrlF=!1;useCtrlH=!1;findMatchesOnValueChange=!0;instructions={start:"Search for matches in your code.",none:"No matches",oneFound:"1 match found.",matchIndex:(a,b)=>`${a} of ${b} matches.`,error:a=>`Error: ${a}`,infiniteLoopError:"Causes an infinite loop",closeDialog:"Close Dialog and Return to Editor",findPlaceholder:"Find",findCaseSensitive:"Match Case Sensitive",findRegExp:"Use JavaScript Regular Expression",replaceTitle:"Replace",replacePlaceholder:"Replace with",findNext:"Find Next Occurrence",findPrevious:"Find Previous Occurrence",replaceActionShort:"Replace",replaceAction:"Replace This Occurrence",replaceAllActionShort:"Replace All",replaceAllAction:"Replace All Occurrences"};constructor(a=!0,b=!0,c={}){super([]),this.useCtrlF=a,this.useCtrlH=b,this.addTranslations(this.instructions,c)}afterElementsAdded(a){const b=a.textareaElement;this.useCtrlF&&b.addEventListener("keydown",b=>{this.checkCtrlF(a,b)}),this.useCtrlH&&b.addEventListener("keydown",b=>{this.checkCtrlH(a,b)})}afterHighlight(a){a.pluginData.findAndReplace==null||a.pluginData.findAndReplace.dialog==null||a.pluginData.findAndReplace.dialog.classList.contains("code-input_find-and-replace_hidden-dialog")||(a.pluginData.findAndReplace.dialog.findMatchState.rehighlightMatches(),this.updateMatchDescription(a.pluginData.findAndReplace.dialog),0==a.pluginData.findAndReplace.dialog.findMatchState.numMatches&&a.pluginData.findAndReplace.dialog.findInput.classList.add("code-input_find-and-replace_error"))}text2RegExp(a,b,c){return new RegExp(c?a:a.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),b?"g":"gi")}updateMatchDescription(a){a.matchDescription.textContent=0==a.findInput.value.length?this.instructions.start:0>=a.findMatchState.numMatches?this.instructions.none:1==a.findMatchState.numMatches?this.instructions.oneFound:this.instructions.matchIndex(a.findMatchState.focusedMatchID+1,a.findMatchState.numMatches)}updateFindMatches(a){let b=a.findInput.value;setTimeout(()=>{if(b==a.findInput.value){if(a.findMatchState.clearMatches(),0{j.setAttribute("open",!0)}),p.className="code-input_find-and-replace_button-hidden",p.innerText=this.instructions.replaceAllActionShort,p.title=this.instructions.replaceAllAction,p.addEventListener("focus",()=>{j.setAttribute("open",!0)}),m.addEventListener("click",a=>{a.preventDefault(),c.findMatchState.nextMatch(),this.updateMatchDescription(c)}),n.addEventListener("click",()=>{event.preventDefault(),c.findMatchState.previousMatch(),this.updateMatchDescription(c)}),o.addEventListener("click",a=>{a.preventDefault(),c.findMatchState.replaceOnce(i.value),c.focus()}),p.addEventListener("click",a=>{a.preventDefault(),c.findMatchState.replaceAll(i.value),p.focus()}),j.addEventListener("toggle",()=>{o.classList.toggle("code-input_find-and-replace_button-hidden"),p.classList.toggle("code-input_find-and-replace_button-hidden")}),c.findMatchState=new codeInput.plugins.FindAndReplace.FindMatchState(a),c.codeInput=a,c.textarea=d,c.findInput=e,c.findCaseSensitiveCheckbox=f,c.findRegExpCheckbox=g,c.matchDescription=h,c.replaceInput=i,c.replaceDropdown=j,this.checkCtrlH&&e.addEventListener("keydown",a=>{a.ctrlKey&&"h"==a.key&&(a.preventDefault(),j.setAttribute("open",!0))}),e.addEventListener("keypress",a=>{"Enter"==a.key&&a.preventDefault()}),i.addEventListener("keypress",a=>{"Enter"==a.key&&a.preventDefault()}),i.addEventListener("input",()=>{c.classList.contains("code-input_find-and-replace_hidden-dialog")?this.showPrompt(c.codeInput,!0):!c.replaceDropdown.hasAttribute("open")&&c.replaceDropdown.setAttribute("open",!0)}),c.addEventListener("keyup",b=>{"Escape"==b.key&&this.cancelPrompt(c,a,b)}),e.addEventListener("keyup",b=>{this.checkFindPrompt(c,a,b)}),e.addEventListener("input",()=>{this.findMatchesOnValueChange&&this.updateFindMatches(c),c.classList.contains("code-input_find-and-replace_hidden-dialog")&&this.showPrompt(c.codeInput,!1)}),f.addEventListener("click",()=>{this.updateFindMatches(c)}),g.addEventListener("click",()=>{this.updateFindMatches(c)}),i.addEventListener("keyup",b=>{this.checkReplacePrompt(c,a,b),i.focus()}),q.addEventListener("click",b=>{this.cancelPrompt(c,a,b)}),q.addEventListener("keypress",b=>{("Space"==b.key||"Enter"==b.key)&&this.cancelPrompt(c,a,b)}),a.dialogContainerElement.appendChild(c),a.pluginData.findAndReplace={dialog:c},e.focus(),b&&j.setAttribute("open",!0),c.selectionStart=a.textareaElement.selectionStart,c.selectionEnd=a.textareaElement.selectionEnd,c.selectionStart=this.matchStartIndexes.length&&(a=0)}this.focusedMatchStartIndex=this.matchStartIndexes[a],this.focusedMatchID=a;let b=this.codeInput.codeElement.querySelectorAll(".code-input_find-and-replace_find-match-focused");for(let c=0;c=c){if(g.length>=d){if(h){let b=document.createElement("span");b.classList.add("code-input_find-and-replace_find-match"),b.setAttribute("data-code-input_find-and-replace_match-id",a),b.classList.add("code-input_find-and-replace_temporary-span"),b.textContent=g.substring(0,d),"\n"==b.textContent[0]&&b.classList.add("code-input_find-and-replace_start-newline");let c=g.substring(d);return f.textContent=c,f.insertAdjacentElement("beforebegin",b),void e++}return void this.highlightMatchNewlineOnlyAtStart(a,f,0,d)}f.classList.add("code-input_find-and-replace_find-match"),f.setAttribute("data-code-input_find-and-replace_match-id",a),"\n"==f.textContent[0]&&f.classList.add("code-input_find-and-replace_start-newline")}else if(g.length>c){if(!h)this.highlightMatchNewlineOnlyAtStart(a,f,c,d);else if(g.length>d){let b=document.createElement("span");b.classList.add("code-input_find-and-replace_temporary-span"),b.textContent=g.substring(0,c);let h=g.substring(c,d);f.textContent=h,f.classList.add("code-input_find-and-replace_find-match"),f.setAttribute("data-code-input_find-and-replace_match-id",a),"\n"==f.textContent[0]&&f.classList.add("code-input_find-and-replace_start-newline");let i=document.createElement("span");i.classList.add("code-input_find-and-replace_temporary-span"),i.textContent=g.substring(d),f.insertAdjacentElement("beforebegin",b),f.insertAdjacentElement("afterend",i),e++}else{let b=g.substring(0,c);f.textContent=b;let d=document.createElement("span");d.classList.add("code-input_find-and-replace_find-match"),d.setAttribute("data-code-input_find-and-replace_match-id",a),d.classList.add("code-input_find-and-replace_temporary-span"),d.textContent=g.substring(c),"\n"==d.textContent[0]&&d.classList.add("code-input_find-and-replace_start-newline"),f.insertAdjacentElement("afterend",d),e++}if(g.length>d)return}c-=g.length,d-=g.length}}};
\ No newline at end of file
+"use strict";codeInput.plugins.FindAndReplace=class extends codeInput.Plugin{useCtrlF=!1;useCtrlH=!1;findMatchesOnValueChange=!0;instructions={start:"Search for matches in your code.",none:"No matches",oneFound:"1 match found.",matchIndex:(a,b)=>`${a} of ${b} matches.`,error:a=>`Error: ${a}`,infiniteLoopError:"Causes an infinite loop",closeDialog:"Close Dialog and Return to Editor",findPlaceholder:"Find",findCaseSensitive:"Match Case Sensitive",findRegExp:"Use JavaScript Regular Expression",replaceTitle:"Replace",replacePlaceholder:"Replace with",findNext:"Find Next Occurrence",findPrevious:"Find Previous Occurrence",replaceActionShort:"Replace",replaceAction:"Replace This Occurrence",replaceAllActionShort:"Replace All",replaceAllAction:"Replace All Occurrences"};constructor(a=!0,b=!0,c={},d=!0){super([]),this.useCtrlF=a,this.useCtrlH=b,this.alwaysCtrl=d,this.addTranslations(this.instructions,c)}afterElementsAdded(a){const b=a.textareaElement;this.useCtrlF&&b.addEventListener("keydown",b=>{this.checkCtrlF(a,b)}),this.useCtrlH&&b.addEventListener("keydown",b=>{this.checkCtrlH(a,b)})}afterHighlight(a){a.pluginData.findAndReplace==null||a.pluginData.findAndReplace.dialog==null||a.pluginData.findAndReplace.dialog.classList.contains("code-input_find-and-replace_hidden-dialog")||(a.pluginData.findAndReplace.dialog.findMatchState.rehighlightMatches(),this.updateMatchDescription(a.pluginData.findAndReplace.dialog),0==a.pluginData.findAndReplace.dialog.findMatchState.numMatches&&a.pluginData.findAndReplace.dialog.findInput.classList.add("code-input_find-and-replace_error"))}text2RegExp(a,b,c){return new RegExp(c?a:a.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),b?"g":"gi")}updateMatchDescription(a){a.matchDescription.textContent=0==a.findInput.value.length?this.instructions.start:0>=a.findMatchState.numMatches?this.instructions.none:1==a.findMatchState.numMatches?this.instructions.oneFound:this.instructions.matchIndex(a.findMatchState.focusedMatchID+1,a.findMatchState.numMatches)}updateFindMatches(a){let b=a.findInput.value;setTimeout(()=>{if(b==a.findInput.value){if(a.findMatchState.clearMatches(),0{j.setAttribute("open",!0)}),p.className="code-input_find-and-replace_button-hidden",p.innerText=this.instructions.replaceAllActionShort,p.title=this.instructions.replaceAllAction,p.addEventListener("focus",()=>{j.setAttribute("open",!0)}),m.addEventListener("click",a=>{a.preventDefault(),c.findMatchState.nextMatch(),this.updateMatchDescription(c)}),n.addEventListener("click",()=>{event.preventDefault(),c.findMatchState.previousMatch(),this.updateMatchDescription(c)}),o.addEventListener("click",a=>{a.preventDefault(),c.findMatchState.replaceOnce(i.value),c.focus()}),p.addEventListener("click",a=>{a.preventDefault(),c.findMatchState.replaceAll(i.value),p.focus()}),j.addEventListener("toggle",()=>{o.classList.toggle("code-input_find-and-replace_button-hidden"),p.classList.toggle("code-input_find-and-replace_button-hidden")}),c.findMatchState=new codeInput.plugins.FindAndReplace.FindMatchState(a),c.codeInput=a,c.textarea=d,c.findInput=e,c.findCaseSensitiveCheckbox=f,c.findRegExpCheckbox=g,c.matchDescription=h,c.replaceInput=i,c.replaceDropdown=j,this.checkCtrlH&&e.addEventListener("keydown",a=>{a.ctrlKey&&"h"==a.key&&(a.preventDefault(),j.setAttribute("open",!0))}),e.addEventListener("keypress",a=>{"Enter"==a.key&&a.preventDefault()}),i.addEventListener("keypress",a=>{"Enter"==a.key&&a.preventDefault()}),i.addEventListener("input",()=>{c.classList.contains("code-input_find-and-replace_hidden-dialog")?this.showPrompt(c.codeInput,!0):!c.replaceDropdown.hasAttribute("open")&&c.replaceDropdown.setAttribute("open",!0)}),c.addEventListener("keyup",b=>{"Escape"==b.key&&this.cancelPrompt(c,a,b)}),e.addEventListener("keyup",b=>{this.checkFindPrompt(c,a,b)}),e.addEventListener("input",()=>{this.findMatchesOnValueChange&&this.updateFindMatches(c),c.classList.contains("code-input_find-and-replace_hidden-dialog")&&this.showPrompt(c.codeInput,!1)}),f.addEventListener("click",()=>{this.updateFindMatches(c)}),g.addEventListener("click",()=>{this.updateFindMatches(c)}),i.addEventListener("keyup",b=>{this.checkReplacePrompt(c,a,b),i.focus()}),q.addEventListener("click",b=>{this.cancelPrompt(c,a,b)}),q.addEventListener("keypress",b=>{("Space"==b.key||"Enter"==b.key)&&this.cancelPrompt(c,a,b)}),a.dialogContainerElement.appendChild(c),a.pluginData.findAndReplace={dialog:c},e.focus(),b&&j.setAttribute("open",!0),c.selectionStart=a.textareaElement.selectionStart,c.selectionEnd=a.textareaElement.selectionEnd,c.selectionStart=this.matchStartIndexes.length&&(a=0)}this.focusedMatchStartIndex=this.matchStartIndexes[a],this.focusedMatchID=a;let b=this.codeInput.codeElement.querySelectorAll(".code-input_find-and-replace_find-match-focused");for(let c=0;c=c){if(g.length>=d){if(h){let b=document.createElement("span");b.classList.add("code-input_find-and-replace_find-match"),b.setAttribute("data-code-input_find-and-replace_match-id",a),b.classList.add("code-input_find-and-replace_temporary-span"),b.textContent=g.substring(0,d),"\n"==b.textContent[0]&&b.classList.add("code-input_find-and-replace_start-newline");let c=g.substring(d);return f.textContent=c,f.insertAdjacentElement("beforebegin",b),void e++}return void this.highlightMatchNewlineOnlyAtStart(a,f,0,d)}f.classList.add("code-input_find-and-replace_find-match"),f.setAttribute("data-code-input_find-and-replace_match-id",a),"\n"==f.textContent[0]&&f.classList.add("code-input_find-and-replace_start-newline")}else if(g.length>c){if(!h)this.highlightMatchNewlineOnlyAtStart(a,f,c,d);else if(g.length>d){let b=document.createElement("span");b.classList.add("code-input_find-and-replace_temporary-span"),b.textContent=g.substring(0,c);let h=g.substring(c,d);f.textContent=h,f.classList.add("code-input_find-and-replace_find-match"),f.setAttribute("data-code-input_find-and-replace_match-id",a),"\n"==f.textContent[0]&&f.classList.add("code-input_find-and-replace_start-newline");let i=document.createElement("span");i.classList.add("code-input_find-and-replace_temporary-span"),i.textContent=g.substring(d),f.insertAdjacentElement("beforebegin",b),f.insertAdjacentElement("afterend",i),e++}else{let b=g.substring(0,c);f.textContent=b;let d=document.createElement("span");d.classList.add("code-input_find-and-replace_find-match"),d.setAttribute("data-code-input_find-and-replace_match-id",a),d.classList.add("code-input_find-and-replace_temporary-span"),d.textContent=g.substring(c),"\n"==d.textContent[0]&&d.classList.add("code-input_find-and-replace_start-newline"),f.insertAdjacentElement("afterend",d),e++}if(g.length>d)return}c-=g.length,d-=g.length}}};
\ No newline at end of file
diff --git a/plugins/go-to-line.css b/plugins/go-to-line.css
index 91cedd6..edc3a07 100644
--- a/plugins/go-to-line.css
+++ b/plugins/go-to-line.css
@@ -1,11 +1,12 @@
 @keyframes code-input_go-to-line_roll-in {
-    0% {opacity: 0; transform: translateY(-34px);}
-    100% {opacity: 1; transform: translateY(0px);}
-}
-
-@keyframes code-input_go-to-line_roll-out {
-    0% {opacity: 1; transform: translateY(0px);}
-    100% {opacity: 0; transform: translateY(-34px);}
+    0% {
+      opacity: 0;
+      transform: translateY(-34px);
+    }
+    100% {
+      opacity: 1;
+      transform: translateY(0px);
+    }
 }
 
 .code-input_go-to-line_dialog {
@@ -21,14 +22,11 @@
 
 .code-input_go-to-line_dialog:not(.code-input_go-to-line_hidden-dialog) {
   animation: code-input_go-to-line_roll-in .2s;
-  opacity: 1;
-  pointer-events: all;
+  display: block;
 }
 
 .code-input_go-to-line_dialog.code-input_go-to-line_hidden-dialog {
-  animation: code-input_go-to-line_roll-out .2s;
-  opacity: 0;
-  pointer-events: none;
+  display: none;
 }
 
 .code-input_go-to-line_dialog input::placeholder {
@@ -40,11 +38,12 @@
   width: 240px; height: 32px; top: -3px;
   font-size: large;
   color: #000000aa;
+  caret-color: currentColor; /* Don't inherit from the code-input element. */
   border: 0;
 }
 
 .code-input_go-to-line_dialog input.code-input_go-to-line_error {
-  color: #ff0000aa;
+  color: #b60000;
 }
 
 /* Cancel icon */
diff --git a/plugins/go-to-line.js b/plugins/go-to-line.js
index 54182da..f0420f5 100644
--- a/plugins/go-to-line.js
+++ b/plugins/go-to-line.js
@@ -1,5 +1,5 @@
 /**
- * Add basic Go-To-Line (Ctrl+G by default) functionality to the code editor.
+ * Add Go-To-Line (Ctrl/Cmd+G by default) functionality to the code editor.
  * Files: go-to-line.js / go-to-line.css
  */
 "use strict";
@@ -18,9 +18,9 @@ codeInput.plugins.GoToLine = class extends codeInput.Plugin {
     };
 
     /**
-     * Create a go-to-line command plugin to pass into a template
-     * @param {boolean} useCtrlG Should Ctrl+G be overriden for go-to-line functionality? Either way, you can trigger it yourself using (instance of this plugin)`.showPrompt(code-input element)`.
-     * @param {Object} instructionTranslations: user interface string keys mapped to translated versions for localisation. Look at the go-to-line.js source code for the available keys and English text.
+     * Create a go-to-line command plugin to pass into a template.
+     * @param {boolean} useCtrlG Should Ctrl/Cmd+G be overridden for go-to-line functionality? Either way, you can trigger it yourself using (instance of this plugin)`.showPrompt(code-input element)`.
+     * @param {Object} instructionTranslations: user interface string keys mapped to translated versions for localisation. Look at the go-to-line.js source code for the English text.
      */
     constructor(useCtrlG = true, instructionTranslations = {}) {
         super([]); // No observed attributes
@@ -81,8 +81,9 @@ codeInput.plugins.GoToLine = class extends codeInput.Plugin {
                 }
             }
         } else {
-            // No value
+            // No value, so no message or error
             dialog.guidance.textContent = "";
+            dialog.input.classList.remove('code-input_go-to-line_error');
         }
 
         if (event.key == 'Enter') {
diff --git a/plugins/go-to-line.min.css b/plugins/go-to-line.min.css
index 2aa8e76..e3449cb 100644
--- a/plugins/go-to-line.min.css
+++ b/plugins/go-to-line.min.css
@@ -1 +1 @@
-@keyframes code-input_go-to-line_roll-in{0%{opacity:0;transform:translateY(-34px)}to{opacity:1;transform:translateY(0)}}@keyframes code-input_go-to-line_roll-out{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(-34px)}}.code-input_go-to-line_dialog{background-color:#fff;border:1px solid #0004;border-radius:6px;padding:8px 6px 6px;position:absolute;top:0;right:14px;box-shadow:0 .2em 1em .2em #00000029}.code-input_go-to-line_dialog:not(.code-input_go-to-line_hidden-dialog){opacity:1;pointer-events:all;animation:.2s code-input_go-to-line_roll-in}.code-input_go-to-line_dialog.code-input_go-to-line_hidden-dialog{opacity:0;pointer-events:none;animation:.2s code-input_go-to-line_roll-out}.code-input_go-to-line_dialog input::placeholder{font-size:80%}.code-input_go-to-line_dialog input{color:#000a;border:0;width:240px;height:32px;font-size:large;position:relative;top:-3px}.code-input_go-to-line_dialog input.code-input_go-to-line_error{color:#f00a}.code-input_go-to-line_dialog span{text-align:center;color:#000;opacity:.6;vertical-align:top;border-radius:50%;width:24px;font-family:system-ui;font-size:22px;font-weight:500;line-height:24px;display:inline-block}.code-input_go-to-line_dialog span:before{content:"×"}.code-input_go-to-line_dialog span:hover{opacity:.8;background-color:#00000018}.code-input_go-to-line_dialog p{width:264px;white-space:wrap;margin:0;font-family:monospace;overflow:hidden}
\ No newline at end of file
+@keyframes code-input_go-to-line_roll-in{0%{opacity:0;transform:translateY(-34px)}to{opacity:1;transform:translateY(0)}}.code-input_go-to-line_dialog{background-color:#fff;border:1px solid #0004;border-radius:6px;padding:8px 6px 6px;position:absolute;top:0;right:14px;box-shadow:0 .2em 1em .2em #00000029}.code-input_go-to-line_dialog:not(.code-input_go-to-line_hidden-dialog){animation:.2s code-input_go-to-line_roll-in;display:block}.code-input_go-to-line_dialog.code-input_go-to-line_hidden-dialog{display:none}.code-input_go-to-line_dialog input::placeholder{font-size:80%}.code-input_go-to-line_dialog input{color:#000a;caret-color:currentColor;border:0;width:240px;height:32px;font-size:large;position:relative;top:-3px}.code-input_go-to-line_dialog input.code-input_go-to-line_error{color:#b60000}.code-input_go-to-line_dialog span{text-align:center;color:#000;opacity:.6;vertical-align:top;border-radius:50%;width:24px;font-family:system-ui;font-size:22px;font-weight:500;line-height:24px;display:inline-block}.code-input_go-to-line_dialog span:before{content:"×"}.code-input_go-to-line_dialog span:hover{opacity:.8;background-color:#00000018}.code-input_go-to-line_dialog p{width:264px;white-space:wrap;margin:0;font-family:monospace;overflow:hidden}
\ No newline at end of file
diff --git a/plugins/go-to-line.min.js b/plugins/go-to-line.min.js
index 46a84ff..96f8b27 100644
--- a/plugins/go-to-line.min.js
+++ b/plugins/go-to-line.min.js
@@ -1 +1 @@
-"use strict";codeInput.plugins.GoToLine=class extends codeInput.Plugin{useCtrlG=!1;instructions={closeDialog:"Close Dialog and Return to Editor",input:"Line:Column / Line no. then Enter",guidanceFormat:"Wrong format. Enter a line number (e.g. 1) or a line number then colon then column number (e.g. 1:3).",guidanceLineRange:(a,b)=>`Line number (currently ${a}) should be between 1 and ${b} inclusive.`,guidanceColumnRange:(a,b,c)=>`On line ${a}, column number (currently ${b}) should be between 1 and ${c} inclusive.`,guidanceValidLine:a=>`Press Enter to go to line ${a}.`,guidanceValidColumn:(a,b)=>`Press Enter to go to line ${a}, column ${b}.`};constructor(a=!0,b={}){super([]),this.useCtrlG=a,this.addTranslations(this.instructions,b)}afterElementsAdded(a){const b=a.textareaElement;this.useCtrlG&&b.addEventListener("keydown",b=>{this.checkCtrlG(a,b)})}checkPrompt(a,b){if("Escape"==b.key)return this.cancelPrompt(a,b);const c=a.textarea.value.split("\n"),d=c.length,e=+a.input.value.split(":")[0];let f=0,g=1;const h=a.input.value.split(":");if(2e||e>d)return a.guidance.textContent=this.instructions.guidanceLineRange(e,d),a.input.classList.add("code-input_go-to-line_error");if(2<=h.length&&(f=+h[1],g=c[e-1].length+1),0>f||f>g)return a.guidance.textContent=this.instructions.guidanceColumnRange(e,f,g),a.input.classList.add("code-input_go-to-line_error");a.guidance.textContent=0===f?this.instructions.guidanceValidLine(e):this.instructions.guidanceValidColumn(e,f),a.input.classList.remove("code-input_go-to-line_error")}else a.guidance.textContent="";"Enter"==b.key&&(this.goTo(a.textarea,e,f),this.cancelPrompt(a,b))}cancelPrompt(a,b){b.preventDefault(),a.codeInput.handleEventsFromTextarea=!1,a.textarea.focus(),a.codeInput.handleEventsFromTextarea=!0,a.setAttribute("inert",!0),a.setAttribute("tabindex",-1),a.setAttribute("aria-hidden",!0),a.classList.add("code-input_go-to-line_hidden-dialog"),a.input.value=""}showPrompt(a){if(a.pluginData.goToLine==null||a.pluginData.goToLine.dialog==null){const b=a.textareaElement,c=document.createElement("div"),d=document.createElement("input"),e=document.createElement("span");e.setAttribute("role","button"),e.setAttribute("aria-label",this.instructions.closeDialog),e.setAttribute("tabindex",0),e.setAttribute("title",this.instructions.closeDialog);const f=document.createElement("p");f.setAttribute("aria-live","assertive"),f.textContent="",c.appendChild(d),c.appendChild(e),c.appendChild(f),c.className="code-input_go-to-line_dialog",d.spellcheck=!1,d.placeholder=this.instructions.input,c.codeInput=a,c.textarea=b,c.input=d,c.guidance=f,d.addEventListener("keypress",a=>{"Enter"==a.key&&a.preventDefault()}),d.addEventListener("keyup",a=>this.checkPrompt(c,a)),e.addEventListener("click",a=>{this.cancelPrompt(c,a)}),e.addEventListener("keypress",a=>{("Space"==a.key||"Enter"==a.key)&&this.cancelPrompt(c,a)}),a.dialogContainerElement.appendChild(c),a.pluginData.goToLine={dialog:c},d.focus()}else a.pluginData.goToLine.dialog.classList.remove("code-input_go-to-line_hidden-dialog"),a.pluginData.goToLine.dialog.removeAttribute("inert"),a.pluginData.goToLine.dialog.setAttribute("tabindex",0),a.pluginData.goToLine.dialog.removeAttribute("aria-hidden"),a.pluginData.goToLine.dialog.input.focus()}goTo(a,b,c=0){let d,e,f,g,h=-1,i=a.value.split("\n");if(0`Line number (currently ${a}) should be between 1 and ${b} inclusive.`,guidanceColumnRange:(a,b,c)=>`On line ${a}, column number (currently ${b}) should be between 1 and ${c} inclusive.`,guidanceValidLine:a=>`Press Enter to go to line ${a}.`,guidanceValidColumn:(a,b)=>`Press Enter to go to line ${a}, column ${b}.`};constructor(a=!0,b={}){super([]),this.useCtrlG=a,this.addTranslations(this.instructions,b)}afterElementsAdded(a){const b=a.textareaElement;this.useCtrlG&&b.addEventListener("keydown",b=>{this.checkCtrlG(a,b)})}checkPrompt(a,b){if("Escape"==b.key)return this.cancelPrompt(a,b);const c=a.textarea.value.split("\n"),d=c.length,e=+a.input.value.split(":")[0];let f=0,g=1;const h=a.input.value.split(":");if(2e||e>d)return a.guidance.textContent=this.instructions.guidanceLineRange(e,d),a.input.classList.add("code-input_go-to-line_error");if(2<=h.length&&(f=+h[1],g=c[e-1].length+1),0>f||f>g)return a.guidance.textContent=this.instructions.guidanceColumnRange(e,f,g),a.input.classList.add("code-input_go-to-line_error");a.guidance.textContent=0===f?this.instructions.guidanceValidLine(e):this.instructions.guidanceValidColumn(e,f),a.input.classList.remove("code-input_go-to-line_error")}else a.guidance.textContent="",a.input.classList.remove("code-input_go-to-line_error");"Enter"==b.key&&(this.goTo(a.textarea,e,f),this.cancelPrompt(a,b))}cancelPrompt(a,b){b.preventDefault(),a.codeInput.handleEventsFromTextarea=!1,a.textarea.focus(),a.codeInput.handleEventsFromTextarea=!0,a.setAttribute("inert",!0),a.setAttribute("tabindex",-1),a.setAttribute("aria-hidden",!0),a.classList.add("code-input_go-to-line_hidden-dialog"),a.input.value=""}showPrompt(a){if(a.pluginData.goToLine==null||a.pluginData.goToLine.dialog==null){const b=a.textareaElement,c=document.createElement("div"),d=document.createElement("input"),e=document.createElement("span");e.setAttribute("role","button"),e.setAttribute("aria-label",this.instructions.closeDialog),e.setAttribute("tabindex",0),e.setAttribute("title",this.instructions.closeDialog);const f=document.createElement("p");f.setAttribute("aria-live","assertive"),f.textContent="",c.appendChild(d),c.appendChild(e),c.appendChild(f),c.className="code-input_go-to-line_dialog",d.spellcheck=!1,d.placeholder=this.instructions.input,c.codeInput=a,c.textarea=b,c.input=d,c.guidance=f,d.addEventListener("keypress",a=>{"Enter"==a.key&&a.preventDefault()}),d.addEventListener("keyup",a=>this.checkPrompt(c,a)),e.addEventListener("click",a=>{this.cancelPrompt(c,a)}),e.addEventListener("keypress",a=>{("Space"==a.key||"Enter"==a.key)&&this.cancelPrompt(c,a)}),a.dialogContainerElement.appendChild(c),a.pluginData.goToLine={dialog:c},d.focus()}else a.pluginData.goToLine.dialog.classList.remove("code-input_go-to-line_hidden-dialog"),a.pluginData.goToLine.dialog.removeAttribute("inert"),a.pluginData.goToLine.dialog.setAttribute("tabindex",0),a.pluginData.goToLine.dialog.removeAttribute("aria-hidden"),a.pluginData.goToLine.dialog.input.focus()}goTo(a,b,c=0){let d,e,f,g,h=-1,i=a.value.split("\n");if(0code{position:static}code-input .line-numbers .line-numbers-rows{left:max(0em,calc(var(--padding-left,16px) - 3.8em));top:var(--padding-top)}code-input:not(:has(.code-input_keyboard-navigation-instructions:empty)):has(textarea:not([data-code-input-fallback]):focus):not(.code-input_mouse-focused) .line-numbers .line-numbers-rows{top:calc(var(--padding-top) + 3em)}
\ No newline at end of file
+code-input.line-numbers textarea,code-input.line-numbers.code-input_pre-element-styled pre,code-input.line-numbers:not(.code-input_pre-element-styled) pre code,.line-numbers code-input textarea,.line-numbers code-input.code-input_pre-element-styled pre,.line-numbers code-input:not(.code-input_pre-element-styled) pre code{padding-left:max(3.8em, var(--padding-left,16px))!important}code-input.line-numbers,.line-numbers code-input{grid-template-columns:calc(100% - max(0em, calc(3.8em - var(--padding-left,16px))))}code-input.line-numbers .code-input_dialog-container,.line-numbers code-input .code-input_dialog-container{width:calc(100% + max(0em, calc(3.8em - var(--padding-left,16px))))}code-input pre[class*=language-].line-numbers>code{position:static}code-input .line-numbers .line-numbers-rows{left:max(0em, calc(var(--padding-left,16px) - 3.8em));top:var(--padding-top)}code-input:not(:has(.code-input_keyboard-navigation-instructions:empty)):has(textarea:not([data-code-input-fallback]):focus):not(.code-input_mouse-focused) .line-numbers .line-numbers-rows{top:calc(var(--padding-top) + 3em)}code-input.line-numbers.code-input_autogrow_width textarea,.line-numbers code-input.code-input_autogrow_width textarea{min-width:max(var(--code-input_synced-width), calc(100% - max(3.8em, var(--padding-left,16px)) - var(--padding-right,16px)));width:calc(var(--code-input_autogrow_true-min-width) - max(3.8em, var(--padding-left,16px)) - var(--padding-right,16px))!important}code-input.line-numbers.code-input_autogrow_width pre code,code-input.line-numbers.code-input_autogrow_width pre,.line-numbers code-input.code-input_autogrow_width pre code,.line-numbers code-input.code-input_autogrow_width pre{min-width:calc(var(--code-input_autogrow_true-min-width) - max(3.8em, var(--padding-left,16px)) - var(--padding-right,16px))}
\ No newline at end of file
diff --git a/plugins/special-chars.min.css b/plugins/special-chars.min.css
index 48bd958..1a19046 100644
--- a/plugins/special-chars.min.css
+++ b/plugins/special-chars.min.css
@@ -1 +1 @@
-:root,body{--code-input_special-chars_0:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABtJREFUGFdjZGBgYPj///9/RhCAMcA0bg6yHgAPmh/6BoxTcQAAAABJRU5ErkJgggAA);--code-input_special-chars_1:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABZJREFUGFdjZGBgYPj///9/RhAggwMAitIUBr9U6sYAAAAASUVORK5CYII=);--code-input_special-chars_2:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB9JREFUGFdj/P///38GKGCEMUCCjCgyYBFGRrAKFBkAuLYT9kYcIu0AAAAASUVORK5CYII=);--code-input_special-chars_3:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABhJREFUGFdj/P///38GKGCEMUCCjMTJAACYiBPyG8sfAgAAAABJRU5ErkJggg==);--code-input_special-chars_4:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB5JREFUGFdj/P///39GRkZGMI3BYYACRhgDrAKZAwAYxhvyz0DRIQAAAABJRU5ErkJggg==);--code-input_special-chars_5:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAACJJREFUGFdj/P///38GKGAEcRgZGRlBfDAHLgNjgFUgywAAuR4T9hxJl2YAAAAASUVORK5CYII=);--code-input_special-chars_6:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAACBJREFUGFdj/P///38GKGAEcRgZGRlBfDAHQwasAlkGABcdF/Y4yco2AAAAAElFTkSuQmCC);--code-input_special-chars_7:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABZJREFUGFdj/P///38GKGCEMUCCRHIAWMgT8kue3bQAAAAASUVORK5CYII=);--code-input_special-chars_8:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABlJREFUGFdj/P///38GKGAEcRgZGSE0cTIAvHcb8v+mIfAAAAAASUVORK5CYII=);--code-input_special-chars_9:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB9JREFUGFdj/P///38GKGAEcRgZGSE0igxMCVgGmQMAPqcX8hWL1K0AAAAASUVORK5CYII=);--code-input_special-chars_A:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAACBJREFUGFdjZGBgYPj///9/RhCAMcA0iADJggCmDEw5ALdxH/aGuYHqAAAAAElFTkSuQmCC);--code-input_special-chars_B:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABlJREFUGFdj/P///38GBgYGRhAAceA0cTIAvc0b/vRDnVoAAAAASUVORK5CYII=);--code-input_special-chars_C:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB5JREFUGFdjZGBgYPj///9/EM0IYjAyMjIS4CDrAQC57hP+uLwvFQAAAABJRU5ErkJggg==);--code-input_special-chars_D:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABtJREFUGFdj/P///38GBgYGRhAAceA0fg5MDwAveh/6ToN9VwAAAABJRU5ErkJggg==);--code-input_special-chars_E:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABxJREFUGFdj/P///38GKGAEcRgZGRlBfDCHsAwA2UwT+mVIH1MAAAAASUVORK5CYII=);--code-input_special-chars_F:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB5JREFUGFdj/P///38GKGAEcRgZGRlBfDAHtwxMGQDZZhP+BnB1kwAAAABJRU5ErkJggg==)}.code-input_special-char{height:1em;text-shadow:none;vertical-align:middle;color:#0000;--hex-0:var(--code-input_special-chars_0);--hex-1:var(--code-input_special-chars_0);--hex-2:var(--code-input_special-chars_0);--hex-3:var(--code-input_special-chars_0);outline:.1px solid;font-size:0;text-decoration:none;display:inline-block;position:relative;top:0;left:0;overflow:hidden}.code-input_special-char:before{content:" ";background-color:var(--code-input_special-char_color,currentColor);image-rendering:pixelated;width:calc(100%-2px);height:100%;mask-image:var(--hex-0),var(--hex-1),var(--hex-2),var(--hex-3);mask-position:10% 10%,90% 10%,10% 90%,90% 90%;mask-size:min(40%,.25em),min(40%,.25em),min(40%,.25em),min(40%,.25em);mask-repeat:no-repeat,no-repeat,no-repeat,no-repeat;-webkit-mask-image:var(--hex-0),var(--hex-1),var(--hex-2),var(--hex-3);margin-left:50%;display:inline-block;transform:translate(-50%);-webkit-mask-position:10% 10%,min(90%,.5em) 10%,10% 90%,min(90%,.5em) 90%;-webkit-mask-size:min(40%,.25em),min(40%,.25em),min(40%,.25em),min(40%,.25em);-webkit-mask-repeat:no-repeat,no-repeat,no-repeat,no-repeat}.code-input_special-char_zero-width{z-index:1;opacity:.75;width:1em;margin-left:-.5em;margin-right:-.5em;position:relative}.code-input_special-char_one-byte:before{content:attr(data-hex2);height:1.5em;top:-1em}.code-input_special-char_one-byte:after{content:attr(data-hex3);height:1.5em;bottom:-1em}
\ No newline at end of file
+:root,body{--code-input_special-chars_0:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABtJREFUGFdjZGBgYPj///9/RhCAMcA0bg6yHgAPmh/6BoxTcQAAAABJRU5ErkJgggAA);--code-input_special-chars_1:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABZJREFUGFdjZGBgYPj///9/RhAggwMAitIUBr9U6sYAAAAASUVORK5CYII=);--code-input_special-chars_2:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB9JREFUGFdj/P///38GKGCEMUCCjCgyYBFGRrAKFBkAuLYT9kYcIu0AAAAASUVORK5CYII=);--code-input_special-chars_3:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABhJREFUGFdj/P///38GKGCEMUCCjMTJAACYiBPyG8sfAgAAAABJRU5ErkJggg==);--code-input_special-chars_4:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB5JREFUGFdj/P///39GRkZGMI3BYYACRhgDrAKZAwAYxhvyz0DRIQAAAABJRU5ErkJggg==);--code-input_special-chars_5:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAACJJREFUGFdj/P///38GKGAEcRgZGRlBfDAHLgNjgFUgywAAuR4T9hxJl2YAAAAASUVORK5CYII=);--code-input_special-chars_6:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAACBJREFUGFdj/P///38GKGAEcRgZGRlBfDAHQwasAlkGABcdF/Y4yco2AAAAAElFTkSuQmCC);--code-input_special-chars_7:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABZJREFUGFdj/P///38GKGCEMUCCRHIAWMgT8kue3bQAAAAASUVORK5CYII=);--code-input_special-chars_8:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABlJREFUGFdj/P///38GKGAEcRgZGSE0cTIAvHcb8v+mIfAAAAAASUVORK5CYII=);--code-input_special-chars_9:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB9JREFUGFdj/P///38GKGAEcRgZGSE0igxMCVgGmQMAPqcX8hWL1K0AAAAASUVORK5CYII=);--code-input_special-chars_A:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAACBJREFUGFdjZGBgYPj///9/RhCAMcA0iADJggCmDEw5ALdxH/aGuYHqAAAAAElFTkSuQmCC);--code-input_special-chars_B:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABlJREFUGFdj/P///38GBgYGRhAAceA0cTIAvc0b/vRDnVoAAAAASUVORK5CYII=);--code-input_special-chars_C:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB5JREFUGFdjZGBgYPj///9/EM0IYjAyMjIS4CDrAQC57hP+uLwvFQAAAABJRU5ErkJggg==);--code-input_special-chars_D:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABtJREFUGFdj/P///38GBgYGRhAAceA0fg5MDwAveh/6ToN9VwAAAABJRU5ErkJggg==);--code-input_special-chars_E:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABxJREFUGFdj/P///38GKGAEcRgZGRlBfDCHsAwA2UwT+mVIH1MAAAAASUVORK5CYII=);--code-input_special-chars_F:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB5JREFUGFdj/P///38GKGAEcRgZGRlBfDAHtwxMGQDZZhP+BnB1kwAAAABJRU5ErkJggg==)}.code-input_special-char{height:1em;text-shadow:none;vertical-align:middle;color:#0000;--hex-0:var(--code-input_special-chars_0);--hex-1:var(--code-input_special-chars_0);--hex-2:var(--code-input_special-chars_0);--hex-3:var(--code-input_special-chars_0);outline:.1px solid;font-size:0;text-decoration:none;display:inline-block;position:relative;top:0;left:0;overflow:hidden}.code-input_special-char:before{content:" ";background-color:var(--code-input_special-char_color,currentColor);image-rendering:pixelated;width:calc(100%-2px);height:100%;mask-image:var(--hex-0), var(--hex-1), var(--hex-2), var(--hex-3);mask-position:10% 10%,90% 10%,10% 90%,90% 90%;mask-size:min(40%,.25em),min(40%,.25em),min(40%,.25em),min(40%,.25em);mask-repeat:no-repeat,no-repeat,no-repeat,no-repeat;-webkit-mask-image:var(--hex-0), var(--hex-1), var(--hex-2), var(--hex-3);margin-left:50%;display:inline-block;transform:translate(-50%);-webkit-mask-position:10% 10%,min(90%,.5em) 10%,10% 90%,min(90%,.5em) 90%;-webkit-mask-size:min(40%,.25em),min(40%,.25em),min(40%,.25em),min(40%,.25em);-webkit-mask-repeat:no-repeat,no-repeat,no-repeat,no-repeat}.code-input_special-char_zero-width{z-index:1;opacity:.75;width:1em;margin-left:-.5em;margin-right:-.5em;position:relative}.code-input_special-char_one-byte:before{content:attr(data-hex2);height:1.5em;top:-1em}.code-input_special-char_one-byte:after{content:attr(data-hex3);height:1.5em;bottom:-1em}
\ No newline at end of file
diff --git a/tests/hljs.html b/tests/hljs.html
index 25bf95a..179e207 100644
--- a/tests/hljs.html
+++ b/tests/hljs.html
@@ -22,6 +22,7 @@
     
     
     
+    
     
     
     
diff --git a/tests/i18n-hljs.html b/tests/i18n-hljs.html
index a3a83f9..70c64cc 100644
--- a/tests/i18n-hljs.html
+++ b/tests/i18n-hljs.html
@@ -62,8 +62,8 @@
                     }
                 }),
                 new codeInput.plugins.Autodetect(),
-                new codeInput.plugins.FindAndReplace(true, true, findAndReplaceTranslations),
-                new codeInput.plugins.GoToLine(true, goToLineTranslations),
+                new codeInput.plugins.FindAndReplace(true, true, findAndReplaceTranslations, false),
+                new codeInput.plugins.GoToLine(true, goToLineTranslations, false),
                 new codeInput.plugins.Indent(true, 2, {"(": ")", "[": "]", "{": "}"}, true, indentTranslations),
                 new codeInput.plugins.SelectTokenCallbacks(codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks.createClassSynchronisation("in-selection"), false, true, true, true, true, false),
                 //new codeInput.plugins.SpecialChars(true),
diff --git a/tests/i18n-prism.html b/tests/i18n-prism.html
index 3697c0c..7778d02 100644
--- a/tests/i18n-prism.html
+++ b/tests/i18n-prism.html
@@ -61,8 +61,8 @@
                         popupElem.style.display = "none";
                     }
                 }),
-                new codeInput.plugins.FindAndReplace(true, true, findAndReplaceTranslations),
-                new codeInput.plugins.GoToLine(true, goToLineTranslations),
+                new codeInput.plugins.FindAndReplace(true, true, findAndReplaceTranslations, false),
+                new codeInput.plugins.GoToLine(true, goToLineTranslations, false),
                 new codeInput.plugins.Indent(true, 2, {"(": ")", "[": "]", "{": "}"}, true, indentTranslations),
                 new codeInput.plugins.SelectTokenCallbacks(new codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks(selectBrace, deselectAllBraces), true),
                 //new codeInput.plugins.SpecialChars(true),
diff --git a/tests/prism-match-braces-compatibility.min.js b/tests/prism-match-braces-compatibility.min.js
index c5696ce..aefb32a 100644
--- a/tests/prism-match-braces-compatibility.min.js
+++ b/tests/prism-match-braces-compatibility.min.js
@@ -1 +1 @@
-let selectBrace,deselectAllBraces;(function(){function a(a){var b=Prism.plugins.customClass;return b?b.apply(a,"none"):a}function b(a){var b=m.exec(a.id);return document.querySelector("#"+b[1]+("open"==b[2]?"close":"open"))}function c(){Prism.util.isActive(this,"brace-hover",!0)&&[this,b(this)].forEach(function(b){b.classList.add(a("brace-hover"))})}function d(){[this,b(this)].forEach(function(b){b.classList.remove(a("brace-hover"))})}function f(){Prism.util.isActive(this,"brace-select",!0)&&[this,b(this)].forEach(function(b){b.classList.add(a("brace-selected"))})}if("undefined"!=typeof Prism&&"undefined"!=typeof document){var g={"(":")","[":"]","{":"}"},h={"(":"brace-round","[":"brace-square","{":"brace-curly"},j={"${":"{"},k=12,l=0,m=/^(pair-\d+-)(close|open)$/;selectBrace=a=>{m.test(a.id)&&c.apply(a)},deselectAllBraces=b=>{for(let c=b.getElementsByClassName(a("brace-hover"));0{m.test(a.id)&&c.apply(a)},deselectAllBraces=b=>{for(let c=b.getElementsByClassName(a("brace-hover"));0
     
     
+    
     
     
     
@@ -40,7 +41,7 @@ 

Test for highlight.js

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

Test Results (Click to Open)
-
+ diff --git a/tests/tester.js b/tests/tester.js index ed5b293..f619b12 100644 --- a/tests/tester.js +++ b/tests/tester.js @@ -105,8 +105,75 @@ function waitAsync(milliseconds) { /* --- Running the tests --- */ /* Start the test, for Prism.js if isHLJS is false, or for highlight.js if isHLJS is true. */ -function beginTest(isHLJS) { +var loadEventFired = false; // Global variable so can check the load event is fired in startLoad function +var popupClicked = false; // Global variable for Autocomplete plugin +async function beginTest(isHLJS) { let codeInputElem = document.querySelector("code-input"); + codeInputElem.addEventListener("code-input_load", () => { + loadEventFired = true; + testAssertion("Load", "code-input_load Event Fired Late Enough", codeInputElem.querySelector("textarea:not([data-code-input-fallback])") != null, "code-input_load event fired before non-fallback textarea element appeared"); + }); + const fallbackTextarea = codeInputElem.querySelector("textarea[data-code-input-fallback]"); + + + startLoad(codeInputElem, isHLJS); + codeInputElem.style.height = "calc(1lh + 2em)"; // 2em for the "No highlighting." message + codeInputElem.style.setProperty("--padding", "0px"); + await waitAsync(50); // Wait for display to update + // Select the "A" in the fallback textarea's last line + fallbackTextarea.selectionStart = 50; + fallbackTextarea.selectionEnd = 51; + fallbackTextarea.focus(); + fallbackTextarea.scrollTo(0, codeInputElem.clientHeight * 3); // At least 3 lines + + await waitAsync(50); // Wait for scroll to occur + testAssertion("FallbackTextarea", "Scrolls Correctly", confirm("Is the phrase 'A third', with 'A' highlighted, visible? "), "user-judged"); + codeInputElem.style.removeProperty("--padding"); + codeInputElem.style.removeProperty("height"); + + // Select the "log" in the fallback textarea's initial line + fallbackTextarea.selectionStart = 8; + fallbackTextarea.selectionEnd = 11; + fallbackTextarea.focus(); + + codeInputElem.style.setProperty("--padding", "100px"); + await waitAsync(50); // Wait for display to update + testAssertion("FallbackTextarea", "Displayed Correctly With More Padding", confirm("Is the highlighted 'log' properly aligned after the 'console.', which is then properly aligned inside the visible (non-highlighted) textarea? Also, there should be a lot of padding."), "user-judged"); + codeInputElem.style.setProperty("--padding", "0px"); + await waitAsync(50); // Wait for display to update + testAssertion("FallbackTextarea", "Displayed Correctly With No Padding", confirm("Does the element have zero padding, but otherwise the display remains correct?"), "user-judged"); + codeInputElem.style.removeProperty("--padding"); + await waitAsync(50); // Wait for display to update + testAssertion("FallbackTextarea", "Displayed Correctly By Default", confirm("Is the highlighted 'log' properly aligned after the 'console.', which is then properly aligned inside the visible (non-highlighted) textarea?"), "user-judged"); + + + codeInputElem.classList.add("code-input_autogrow_height"); + await waitAsync(50); // Wait for display to update + testAssertion("FallbackTextarea-Autogrow", "Displayed Correctly With Default Autogrow Height", confirm("Is the element not very tall (but tall enough to properly show some code), but otherwise the display remains correct?"), "user-judged"); + + codeInputElem.style.setProperty("--code-input_autogrow_min-height", "200px"); + await waitAsync(50); // Wait for display to update + assertEqual("FallbackTextarea-Autogrow", "--code-input_autogrow_min-height Sets Height", codeInputElem.clientHeight, 200); + codeInputElem.style.removeProperty("--code-input_autogrow_min-height"); + codeInputElem.classList.remove("code-input_autogrow_height"); + + codeInputElem.classList.add("code-input_autogrow_width"); + await waitAsync(50); // Wait for display to update + testAssertion("FallbackTextarea-Autogrow", "Displayed Correctly With Default Autogrow Width", confirm("Is the element a sensible but narrow width, and otherwise the display remains correct?"), "user-judged"); + + codeInputElem.style.setProperty("--code-input_autogrow_min-width", "200px"); + await waitAsync(50); // Wait for display to update + assertEqual("FallbackTextarea-Autogrow", "--code-input_autogrow_min-height Sets Height", codeInputElem.clientWidth, 200); + codeInputElem.style.removeProperty("--code-input_autogrow_min-width"); + codeInputElem.classList.remove("code-input_autogrow_width"); + + + if(!isHLJS) { + codeInputElem.classList.add("line-numbers"); + await waitAsync(50); // Wait for display to update + testAssertion("FallbackTextarea-PrismLineNumbers", "Displayed Correctly With line-numbers Class", confirm("Is there more padding to the left now, but otherwise the display remains correct?"), "user-judged"); + } + if(isHLJS) { codeInput.registerTemplate("code-editor", new codeInput.templates.Hljs(hljs, [ new codeInput.plugins.AutoCloseBrackets(), @@ -114,38 +181,37 @@ function beginTest(isHLJS) { if(selectionStart == selectionEnd && textarea.value.substring(selectionEnd-5, selectionEnd) == "popup") { // Show popup popupElem.style.display = "block"; - popupElem.innerHTML = "Here's your popup!"; + popupElem.innerHTML = ''; } else { popupElem.style.display = "none"; } }), new codeInput.plugins.Autodetect(), - new codeInput.plugins.FindAndReplace(), - new codeInput.plugins.GoToLine(), + new codeInput.plugins.FindAndReplace(true, true, {}, false), + new codeInput.plugins.GoToLine(true, {}), new codeInput.plugins.Indent(true, 2), new codeInput.plugins.SelectTokenCallbacks(codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks.createClassSynchronisation("in-selection"), false, true, true, true, true, false), new codeInput.plugins.SpecialChars(true), ])); } else { codeInput.registerTemplate("code-editor", new codeInput.templates.Prism(Prism, [ - new codeInput.plugins.AutoCloseBrackets(), + new codeInput.plugins.AutoCloseBrackets(), new codeInput.plugins.Autocomplete(function(popupElem, textarea, selectionEnd, selectionStart) { if(selectionStart == selectionEnd && textarea.value.substring(selectionEnd-5, selectionEnd) == "popup") { // Show popup popupElem.style.display = "block"; - popupElem.innerHTML = "Here's your popup!"; + popupElem.innerHTML = ''; } else { popupElem.style.display = "none"; } }), - new codeInput.plugins.FindAndReplace(), - new codeInput.plugins.GoToLine(), + new codeInput.plugins.FindAndReplace(true, true, {}, false), + new codeInput.plugins.GoToLine(true, {}), new codeInput.plugins.Indent(true, 2), new codeInput.plugins.SelectTokenCallbacks(new codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks(selectBrace, deselectAllBraces), true), new codeInput.plugins.SpecialChars(true), ])); } - startLoad(codeInputElem, isHLJS); } /* Start loading the tests, using the codeInput load time as one of the tests. */ @@ -153,11 +219,14 @@ 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); + textarea = codeInputElem.querySelector("textarea:not([data-code-input-fallback])"); + if(textarea != null) { + testAssertion("Load", "code-input_load Event Fired Early Enough", loadEventFired, "non-fallback textarea element appeared before code-input_load event"); + window.clearInterval(interval); + timeToLoad += 10; + testData("Load", "Time Taken for Textarea to Appear", timeToLoad+"ms (nearest 10)"); + beginTestsAfterLoad(textarea, isHLJS); + } }, 10); } @@ -178,8 +247,16 @@ function allowInputEvents(inputElement, codeInputElement=undefined) { } /* Start the tests using the textarea inside the code-input element and whether highlight.js is being used (as the Autodetect plugin only works with highlight.js, for example) */ -async function startTests(textarea, isHLJS) { +async function beginTestsAfterLoad(textarea, isHLJS) { textarea.focus(); + if(!isHLJS) { + await waitAsync(200); // Wait for display to update + testAssertion("FallbackTextarea-PrismLineNumbers", "Alignment between Fallback and Loaded Texareas", confirm("Now with the highlighting, is all the code in the same horizontal position (don't mind the vertical offset by keyboard navigation instructions) as with the last question?"), "user-judged"); + + } else { + await waitAsync(200); // Wait for display to update + testAssertion("FallbackTextarea+PrismLineNumbers", "Alignment between Fallback and Loaded Texareas", confirm("Now with the highlighting, is all the code in the same horizontal position (don't mind the vertical offset by keyboard navigation instructions) as with the last question?"), "user-judged"); + } codeInputElement = textarea.parentElement; allowInputEvents(textarea, codeInputElement); @@ -187,7 +264,10 @@ async function startTests(textarea, isHLJS) { /*--- Tests for core functionality ---*/ // Textarea's initial value should be correct. - assertEqual("Core", "Initial Textarea Value", textarea.value, `console.log("Hello, World!"); + assertEqual("FallbackTextarea", "Textarea selectionStart Once Loaded same as Fallback Textarea's", textarea.selectionStart, 8); + assertEqual("FallbackTextarea", "Textarea selectionEnd Once Loaded same as Fallback Textarea's", textarea.selectionEnd, 11); + + assertEqual("FallbackTextarea", "Textarea Value Once Loaded same as Fallback Textarea's", textarea.value, `console.log("Hello, World!"); // A second line // A third line with tags`); // Code element's displayed value, ignoring appearance with HTML tags, should be the initial value but HTML-escaped @@ -411,6 +491,25 @@ console.log("I've got another line!", 2 < 3, "should be true."); codeInputElement.style.removeProperty("caret-color"); + if(!isHLJS) { + // These tests require autodetect plugin to be absent + + codeInputElement.setAttribute("language", "css"); + await waitAsync(50); // Wait for language to propogate to class + testAssertion("Core", "Language Class Propogates", codeInputElement.querySelector("pre").classList.contains("language-css"), `Class name of pre element was "${codeInputElement.querySelector("pre").className}" but code-input element had language="css"`); + + window.requestAnimationFrame(function() { + codeInputElement.setAttribute("placeholder", "Gimme some HTML!"); + codeInputElement.setAttribute("language", "html"); + }); + await waitAsync(500); // Wait for animation frame; language to propogate to class + testAssertion("Core", "Language Class Propogates When Set Immediately After Placeholder Set", codeInputElement.querySelector("pre").classList.contains("language-html"), `Class name of pre element was "${codeInputElement.querySelector("pre").className}" but code-input element had language="css"`); + + codeInputElement.removeAttribute("placeholder"); + codeInputElement.setAttribute("language", "JavaScript"); + await waitAsync(50); // Wait for language to propogate to class + } + /*--- Tests for plugins ---*/ // AutoCloseBrackets testAddingText("AutoCloseBrackets", textarea, function(textarea) { @@ -439,12 +538,31 @@ console.log("I've got another line!", 2 < 3, "should be true."); await waitAsync(50); // Wait for popup to be rendered testAssertion("Autocomplete", "Popup Shows on arrow key", confirm("Does the autocomplete popup display correctly? (OK=Yes)"), "user-judged"); - backspace(textarea); - await waitAsync(50); // Wait for popup disappearance to be rendered + backspace(textarea); + await waitAsync(50); // Wait for popup disappearance to be rendered testAssertion("Autocomplete", "Popup Disappears on backspace", confirm("Has the popup disappeared? (OK=Yes)"), "user-judged"); - move(textarea, 1); + + addText(textarea, "p"); + await waitAsync(50); + + const beforeClickSelectionStart = textarea.selectionStart; + const beforeClickSelectionEnd = textarea.selectionEnd; + alert("Dismiss this alert, then click the popup in the next 3 seconds."); + // To speed up test, wait until 3s have passed or a click has fired, whichever is earlier. + let timePassed = 0; + while(timePassed < 3000 && !popupClicked) { + await waitAsync(10); + timePassed += 10; + } + testAssertion("Autocomplete", "Popup Clickable", popupClicked, "The onclick event of the popup element didn't fire"); + // In case clicking the popup moved the caret inside the textarea: + textarea.selectionStart = beforeClickSelectionStart; + textarea.selectionEnd = beforeClickSelectionEnd; + textarea.focus(); + + backspace(textarea); backspace(textarea); backspace(textarea); backspace(textarea); @@ -478,6 +596,76 @@ console.log("I've got another line!", 2 < 3, "should be true."); assertEqual("Autodetect", "Detects CSS", codeInputElement.getAttribute("language"), "css"); } + // Autogrow + // Replace all code + textarea.selectionStart = 0; + textarea.selectionEnd = textarea.value.length; + backspace(textarea); + textarea.parentElement.classList.add("code-input_autogrow_height"); + await waitAsync(100); // Wait for height to update + + const emptyHeight = textarea.parentElement.clientHeight; + addText(textarea, "// a\n// b\n// c\n// d\n// e\n// f\n// g"); + await waitAsync(100); // Wait for height to update + + const fullHeight = textarea.parentElement.clientHeight; + testAssertion("Autogrow", "Content Increases Height", fullHeight > emptyHeight, `${fullHeight} should be > ${emptyHeight}`); + textarea.parentElement.style.setProperty("font-size", "50%"); + await waitAsync(200); // Wait for height to update + + testAssertion("Autogrow", "font-size Decrease Decreases Height", textarea.parentElement.clientHeight < fullHeight, `${textarea.parentElement.clientHeight} should be < ${fullHeight}`); + textarea.parentElement.style.setProperty("font-size", "100%"); + textarea.parentElement.style.removeProperty("font-size"); + textarea.parentElement.style.setProperty("--code-input_autogrow_min-height", (fullHeight + 10) + "px"); + textarea.blur(); // Prevent focus instruction bar from affecting height + await waitAsync(200); // Wait for height to update + + assertEqual("Autogrow", "--code-input_autogrow_min-height Sets Height", textarea.parentElement.clientHeight, fullHeight + 10); + textarea.focus(); // Enable inserting text again + textarea.parentElement.style.removeProperty("--code-input_autogrow_min-height"); + textarea.parentElement.style.setProperty("--code-input_autogrow_max-height", (fullHeight - 10) + "px"); + await waitAsync(100); // Wait for height to update + + assertEqual("Autogrow", "--code-input_autogrow_max-height Sets Height", textarea.parentElement.clientHeight, fullHeight - 10); + textarea.parentElement.style.removeProperty("--code-input_autogrow_max-height"); + textarea.parentElement.style.removeProperty("--code-input_autogrow_max-height"); + await waitAsync(100); // Wait for height to update + + textarea.selectionStart = 0; + textarea.selectionEnd = textarea.value.length; + backspace(textarea); + textarea.parentElement.classList.add("code-input_autogrow_width"); + await waitAsync(100); // Wait for width to update + + const emptyWidth = textarea.parentElement.clientWidth; + textarea.parentElement.style.setProperty("--code-input_autogrow_max-width", "calc(infinity * 1px)"); // So can grow beyond width of viewport - default CSS limits to width + addText(textarea, "// A very very very very extremely vastly very very very very long line of code is written here in this very comment; yes, this very comment!"); + await waitAsync(100); // Wait for width to update + + const fullWidth = textarea.parentElement.clientWidth; + testAssertion("Autogrow", "Content Increases Width", fullWidth > emptyWidth, `${fullWidth} should be > ${emptyWidth}`); + textarea.parentElement.style.setProperty("font-size", "50%"); + await waitAsync(200); // Wait for width to update + + testAssertion("Autogrow", "font-size Decrease Decreases Width", textarea.parentElement.clientWidth < fullWidth, `${textarea.parentElement.clientWidth} should be < ${fullWidth}`); + textarea.parentElement.style.removeProperty("font-size"); + textarea.parentElement.style.setProperty("--code-input_autogrow_min-width", (fullWidth + 10) + "px"); + await waitAsync(200); // Wait for width to update + + assertEqual("Autogrow", "--code-input_autogrow_min-width Sets Width", textarea.parentElement.clientWidth, fullWidth + 10); + textarea.parentElement.style.removeProperty("--code-input_autogrow_min-width"); + textarea.parentElement.style.setProperty("--code-input_autogrow_max-width", (fullWidth - 10) + "px"); + await waitAsync(100); // Wait for width to update + + assertEqual("Autogrow", "--code-input_autogrow_max-width Sets Width", textarea.parentElement.clientWidth, fullWidth - 10); + textarea.parentElement.style.removeProperty("--code-input_autogrow_max-width"); + textarea.parentElement.style.removeProperty("--code-input_autogrow_max-width"); + + textarea.parentElement.classList.remove("code-input_autogrow_height"); + textarea.parentElement.classList.remove("code-input_autogrow_width"); + + // TODO add reaons to above testAssertions; check why min-width failing; manual tests to check properly fits; prompt tests; fix no-content hljs.html autogrow-both-ways sizing; test with unregistered. + // FindAndReplace // Replace all code textarea.selectionStart = 0; @@ -489,7 +677,12 @@ console.log("I've got another line!", 2 < 3, "should be true."); await waitAsync(50); // Wait for highlighting so text updates // Open dialog and get interactive elements - textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "f", "ctrlKey": true })); + // Thanks to https://developer.mozilla.org/en-US/docs/Web/API/Navigator/platform + if(navigator.platform.startsWith("Mac") || navigator.platform === "iPhone") { + textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "f", "metaKey": true })); + } else { + textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "f", "ctrlKey": true })); + } let inputBoxes = codeInputElement.querySelectorAll(".code-input_find-and-replace_dialog input"); let findInput = inputBoxes[0]; let regExpCheckbox = inputBoxes[1]; @@ -587,7 +780,7 @@ console.log("I've got another line!", 2 < 3, "should be true."); lineInput.dispatchEvent(new KeyboardEvent("keydown", { "key": "Enter" })); lineInput.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 })); lineInput.value = "10"; lineInput.dispatchEvent(new KeyboardEvent("keydown", { "key": "Enter" })); diff --git a/tests/tester.min.js b/tests/tester.min.js index 88683fa..083b35c 100644 --- a/tests/tester.min.js +++ b/tests/tester.min.js @@ -1,4 +1,4 @@ -var testsFailed=!1;function testData(a,b,c){let d=document.getElementById("test-results"),e=d.querySelector("#test-"+a);e==null&&(e=document.createElement("span"),e.innerHTML=`Group ${a}:\n`,e.id="test-"+a,d.append(e)),e.innerHTML+=`\t${b}: ${c}\n`}function testAssertion(a,b,c,d){let e=document.getElementById("test-results"),f=e.querySelector("#test-"+a);f==null&&(f=document.createElement("span"),f.innerHTML=`Group ${a}:\n`,f.id="test-"+a,e.append(f)),f.innerHTML+=`\t${b}: ${c?"passed":"failed ("+d+")"}\n`,c||(testsFailed=!0)}function assertEqual(a,b,c,d){let e=c==d;testAssertion(a,b,e,"see console output"),e||console.error(a,b,c,"should be",d)}function testAddingText(a,b,c,d,e,f){let g=b.selectionStart,h=b.value.substring(0,b.selectionStart),i=b.value.substring(b.selectionEnd);c(b);let j=h+d+i;assertEqual(a,"Text Output",b.value,j),assertEqual(a,"Code-Input Value JS Property Output",b.parentElement.value,j),assertEqual(a,"Selection Start",b.selectionStart,g+e),assertEqual(a,"Selection End",b.selectionEnd,g+f)}function addText(a,b,c=!1){for(let d=0;d{setTimeout(()=>{b()},a)})}function beginTest(a){let b=document.querySelector("code-input");a?codeInput.registerTemplate("code-editor",new codeInput.templates.Hljs(hljs,[new codeInput.plugins.AutoCloseBrackets,new codeInput.plugins.Autocomplete(function(a,b,c,d){d==c&&"popup"==b.value.substring(c-5,c)?(a.style.display="block",a.innerHTML="Here's your popup!"):a.style.display="none"}),new codeInput.plugins.Autodetect,new codeInput.plugins.FindAndReplace,new codeInput.plugins.GoToLine,new codeInput.plugins.Indent(!0,2),new codeInput.plugins.SelectTokenCallbacks(codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks.createClassSynchronisation("in-selection"),!1,!0,!0,!0,!0,!1),new codeInput.plugins.SpecialChars(!0)])):codeInput.registerTemplate("code-editor",new codeInput.templates.Prism(Prism,[new codeInput.plugins.AutoCloseBrackets,new codeInput.plugins.Autocomplete(function(a,b,c,d){d==c&&"popup"==b.value.substring(c-5,c)?(a.style.display="block",a.innerHTML="Here's your popup!"):a.style.display="none"}),new codeInput.plugins.FindAndReplace,new codeInput.plugins.GoToLine,new codeInput.plugins.Indent(!0,2),new codeInput.plugins.SelectTokenCallbacks(new codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks(selectBrace,deselectAllBraces),!0),new codeInput.plugins.SpecialChars(!0)])),startLoad(b,a)}function startLoad(a,b){let c,d=0,e=window.setInterval(()=>{c=a.querySelector("textarea"),null!=c&&window.clearInterval(e),d+=10,testData("TimeTaken","Textarea Appears",d+"ms (nearest 10)"),startTests(c,b)},10)}function allowInputEvents(a,b=void 0){a.addEventListener("input",function(a){a.isTrusted||(a.preventDefault(),b!==void 0&&(b.pluginData.autoCloseBrackets.automatedKeypresses=!0),document.execCommand("insertText",!1,a.data),b!==void 0&&(b.pluginData.autoCloseBrackets.automatedKeypresses=!1))},!1)}async function startTests(a,b){a.focus(),codeInputElement=a.parentElement,allowInputEvents(a,codeInputElement),assertEqual("Core","Initial Textarea Value",a.value,`console.log("Hello, World!"); +var testsFailed=!1;function testData(a,b,c){let d=document.getElementById("test-results"),e=d.querySelector("#test-"+a);e==null&&(e=document.createElement("span"),e.innerHTML=`Group ${a}:\n`,e.id="test-"+a,d.append(e)),e.innerHTML+=`\t${b}: ${c}\n`}function testAssertion(a,b,c,d){let e=document.getElementById("test-results"),f=e.querySelector("#test-"+a);f==null&&(f=document.createElement("span"),f.innerHTML=`Group ${a}:\n`,f.id="test-"+a,e.append(f)),f.innerHTML+=`\t${b}: ${c?"passed":"failed ("+d+")"}\n`,c||(testsFailed=!0)}function assertEqual(a,b,c,d){let e=c==d;testAssertion(a,b,e,"see console output"),e||console.error(a,b,c,"should be",d)}function testAddingText(a,b,c,d,e,f){let g=b.selectionStart,h=b.value.substring(0,b.selectionStart),i=b.value.substring(b.selectionEnd);c(b);let j=h+d+i;assertEqual(a,"Text Output",b.value,j),assertEqual(a,"Code-Input Value JS Property Output",b.parentElement.value,j),assertEqual(a,"Selection Start",b.selectionStart,g+e),assertEqual(a,"Selection End",b.selectionEnd,g+f)}function addText(a,b,c=!1){for(let d=0;d{setTimeout(()=>{b()},a)})}var loadEventFired=!1,popupClicked=!1;async function beginTest(a){let b=document.querySelector("code-input");b.addEventListener("code-input_load",()=>{loadEventFired=!0,testAssertion("Load","code-input_load Event Fired Late Enough",null!=b.querySelector("textarea:not([data-code-input-fallback])"),"code-input_load event fired before non-fallback textarea element appeared")});const c=b.querySelector("textarea[data-code-input-fallback]");startLoad(b,a),b.style.height="calc(1lh + 2em)",b.style.setProperty("--padding","0px"),await waitAsync(50),c.selectionStart=50,c.selectionEnd=51,c.focus(),c.scrollTo(0,3*b.clientHeight),await waitAsync(50),testAssertion("FallbackTextarea","Scrolls Correctly",confirm("Is the phrase 'A third', with 'A' highlighted, visible? "),"user-judged"),b.style.removeProperty("--padding"),b.style.removeProperty("height"),c.selectionStart=8,c.selectionEnd=11,c.focus(),b.style.setProperty("--padding","100px"),await waitAsync(50),testAssertion("FallbackTextarea","Displayed Correctly With More Padding",confirm("Is the highlighted 'log' properly aligned after the 'console.', which is then properly aligned inside the visible (non-highlighted) textarea? Also, there should be a lot of padding."),"user-judged"),b.style.setProperty("--padding","0px"),await waitAsync(50),testAssertion("FallbackTextarea","Displayed Correctly With No Padding",confirm("Does the element have zero padding, but otherwise the display remains correct?"),"user-judged"),b.style.removeProperty("--padding"),await waitAsync(50),testAssertion("FallbackTextarea","Displayed Correctly By Default",confirm("Is the highlighted 'log' properly aligned after the 'console.', which is then properly aligned inside the visible (non-highlighted) textarea?"),"user-judged"),b.classList.add("code-input_autogrow_height"),await waitAsync(50),testAssertion("FallbackTextarea-Autogrow","Displayed Correctly With Default Autogrow Height",confirm("Is the element not very tall (but tall enough to properly show some code), but otherwise the display remains correct?"),"user-judged"),b.style.setProperty("--code-input_autogrow_min-height","200px"),await waitAsync(50),assertEqual("FallbackTextarea-Autogrow","--code-input_autogrow_min-height Sets Height",b.clientHeight,200),b.style.removeProperty("--code-input_autogrow_min-height"),b.classList.remove("code-input_autogrow_height"),b.classList.add("code-input_autogrow_width"),await waitAsync(50),testAssertion("FallbackTextarea-Autogrow","Displayed Correctly With Default Autogrow Width",confirm("Is the element a sensible but narrow width, and otherwise the display remains correct?"),"user-judged"),b.style.setProperty("--code-input_autogrow_min-width","200px"),await waitAsync(50),assertEqual("FallbackTextarea-Autogrow","--code-input_autogrow_min-height Sets Height",b.clientWidth,200),b.style.removeProperty("--code-input_autogrow_min-width"),b.classList.remove("code-input_autogrow_width"),a||(b.classList.add("line-numbers"),await waitAsync(50),testAssertion("FallbackTextarea-PrismLineNumbers","Displayed Correctly With line-numbers Class",confirm("Is there more padding to the left now, but otherwise the display remains correct?"),"user-judged")),a?codeInput.registerTemplate("code-editor",new codeInput.templates.Hljs(hljs,[new codeInput.plugins.AutoCloseBrackets,new codeInput.plugins.Autocomplete(function(a,b,c,d){d==c&&"popup"==b.value.substring(c-5,c)?(a.style.display="block",a.innerHTML=""):a.style.display="none"}),new codeInput.plugins.Autodetect,new codeInput.plugins.FindAndReplace(!0,!0,{},!1),new codeInput.plugins.GoToLine(!0,{}),new codeInput.plugins.Indent(!0,2),new codeInput.plugins.SelectTokenCallbacks(codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks.createClassSynchronisation("in-selection"),!1,!0,!0,!0,!0,!1),new codeInput.plugins.SpecialChars(!0)])):codeInput.registerTemplate("code-editor",new codeInput.templates.Prism(Prism,[new codeInput.plugins.AutoCloseBrackets,new codeInput.plugins.Autocomplete(function(a,b,c,d){d==c&&"popup"==b.value.substring(c-5,c)?(a.style.display="block",a.innerHTML=""):a.style.display="none"}),new codeInput.plugins.FindAndReplace(!0,!0,{},!1),new codeInput.plugins.GoToLine(!0,{}),new codeInput.plugins.Indent(!0,2),new codeInput.plugins.SelectTokenCallbacks(new codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks(selectBrace,deselectAllBraces),!0),new codeInput.plugins.SpecialChars(!0)]))}function startLoad(a,b){let c,d=0,e=window.setInterval(()=>{c=a.querySelector("textarea:not([data-code-input-fallback])"),null!=c&&(testAssertion("Load","code-input_load Event Fired Early Enough",loadEventFired,"non-fallback textarea element appeared before code-input_load event"),window.clearInterval(e),d+=10,testData("Load","Time Taken for Textarea to Appear",d+"ms (nearest 10)"),beginTestsAfterLoad(c,b))},10)}function allowInputEvents(a,b=void 0){a.addEventListener("input",function(a){a.isTrusted||(a.preventDefault(),b!==void 0&&(b.pluginData.autoCloseBrackets.automatedKeypresses=!0),document.execCommand("insertText",!1,a.data),b!==void 0&&(b.pluginData.autoCloseBrackets.automatedKeypresses=!1))},!1)}async function beginTestsAfterLoad(a,b){a.focus(),b?(await waitAsync(200),testAssertion("FallbackTextarea+PrismLineNumbers","Alignment between Fallback and Loaded Texareas",confirm("Now with the highlighting, is all the code in the same horizontal position (don't mind the vertical offset by keyboard navigation instructions) as with the last question?"),"user-judged")):(await waitAsync(200),testAssertion("FallbackTextarea-PrismLineNumbers","Alignment between Fallback and Loaded Texareas",confirm("Now with the highlighting, is all the code in the same horizontal position (don't mind the vertical offset by keyboard navigation instructions) as with the last question?"),"user-judged")),codeInputElement=a.parentElement,allowInputEvents(a,codeInputElement),assertEqual("FallbackTextarea","Textarea selectionStart Once Loaded same as Fallback Textarea's",a.selectionStart,8),assertEqual("FallbackTextarea","Textarea selectionEnd Once Loaded same as Fallback Textarea's",a.selectionEnd,11),assertEqual("FallbackTextarea","Textarea Value Once Loaded same as Fallback Textarea's",a.value,`console.log("Hello, World!"); // A second line // A third line with tags`);let c=codeInputElement.codeElement.innerHTML.replace(/<[^>]+>/g,"");assertEqual("Core","Initial Rendered Value",c,`console.log("Hello, World!"); // A second line @@ -18,4 +18,4 @@ console.log("I've got another line!", 2 < 3, "should be true."); // A third line with tags`),c=codeInputElement.codeElement.innerHTML.replace(/<[^>]+>/g,""),assertEqual("Core","Form Reset resets Rendered Value",c,`console.log("Hello, World!"); // A second line // A third line with <html> tags -`),a.selectionStart=0,a.selectionEnd=a.value.length,backspace(a),codeInputElement.setAttribute("language","JavaScript"),await waitAsync(100),testAssertion("Core","Light theme Caret/Placeholder Color Correct",confirm("Are the caret and placeholder near-black? (OK=Yes)"),"user-judged"),document.getElementById("theme-stylesheet").href=b?"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/dark.min.css":"https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-okaidia.min.css",await waitAsync(200),testAssertion("Core","Dark theme Caret/Placeholder Color Correct",confirm("Are the caret and placeholder near-white? (OK=Yes)"),"user-judged"),codeInputElement.style.color="red",await waitAsync(200),testAssertion("Core","Overriden color Caret/Placeholder Color Correct",confirm("Are the caret and placeholder (for Firefox) or just caret (for Chromium/WebKit, for consistency with textareas) red? (OK=Yes)"),"user-judged"),codeInputElement.style.removeProperty("color"),codeInputElement.style.caretColor="red",await waitAsync(200),testAssertion("Core","Overriden caret-color Caret/Placeholder Color Correct",confirm("Is the caret red and placeholder near-white? (OK=Yes)"),"user-judged"),codeInputElement.style.removeProperty("caret-color"),testAddingText("AutoCloseBrackets",a,function(a){addText(a,`\nconsole.log("A test message`),move(a,2),addText(a,`;\nconsole.log("Another test message");\n{[{[]}(([[`),backspace(a),backspace(a),backspace(a),addText(a,`)`)},"\nconsole.log(\"A test message\");\nconsole.log(\"Another test message\");\n{[{[]}()]}",77,77),addText(a,"popup"),await waitAsync(50),testAssertion("Autocomplete","Popup Shows on input",confirm("Does the autocomplete popup display correctly? (OK=Yes)"),"user-judged"),move(a,-1),await waitAsync(50),testAssertion("Autocomplete","Popup Disappears on arrow key",confirm("Has the popup disappeared? (OK=Yes)"),"user-judged"),move(a,1),await waitAsync(50),testAssertion("Autocomplete","Popup Shows on arrow key",confirm("Does the autocomplete popup display correctly? (OK=Yes)"),"user-judged"),backspace(a),await waitAsync(50),testAssertion("Autocomplete","Popup Disappears on backspace",confirm("Has the popup disappeared? (OK=Yes)"),"user-judged"),move(a,1),backspace(a),backspace(a),backspace(a),backspace(a),b&&(a.selectionStart=0,a.selectionEnd=a.value.length,backspace(a),addText(a,"console.log(\"Hello, World!\");\nfunction sayHello(name) {\n console.log(\"Hello, \" + name + \"!\");\n}\nsayHello(\"code-input\");"),await waitAsync(50),assertEqual("Autodetect","Detects JavaScript",codeInputElement.getAttribute("language"),"javascript"),a.selectionStart=0,a.selectionEnd=a.value.length,backspace(a),addText(a,"#!/usr/bin/python\nprint(\"Hello, World!\")\nfor i in range(5):\n print(i)"),await waitAsync(50),assertEqual("Autodetect","Detects Python",codeInputElement.getAttribute("language"),"python"),a.selectionStart=0,a.selectionEnd=a.value.length,backspace(a),addText(a,"body, html {\n height: 100%;\n background-color: blue;\n color: red;\n}"),await waitAsync(50),assertEqual("Autodetect","Detects CSS",codeInputElement.getAttribute("language"),"css")),a.selectionStart=0,a.selectionEnd=a.value.length,backspace(a),addText(a,"// hello /\\S/g\nhe('llo', /\\s/g);\nhello\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\na very very very very very very very very very very very very long line with many many many many many many many many many many many words\nline\nline\nline\nline\nline\nline\nline"),a.selectionStart=a.selectionEnd=0,await waitAsync(50),a.dispatchEvent(new KeyboardEvent("keydown",{cancelable:!0,key:"f",ctrlKey:!0}));let p=codeInputElement.querySelectorAll(".code-input_find-and-replace_dialog input"),q=p[0],r=p[1],s=p[2],t=p[3],u=codeInputElement.querySelectorAll(".code-input_find-and-replace_dialog button"),v=u[0],w=u[1],x=u[2],y=u[3],z=codeInputElement.querySelector(".code-input_find-and-replace_dialog details summary");q.value="/\\s/g",s.click(),await waitAsync(150),testAssertion("FindAndReplace","Finds Case-Sensitive Matches Correctly",confirm("Is there a match on only the lowercase '/\\s/g'?"),"user-judged"),q.value="he[^l]*llo",r.click(),s.click(),await waitAsync(150),testAssertion("FindAndReplace","Finds RegExp Matches Correctly",confirm("Are there matches on all 'he...llo's?"),"user-judged"),z.click(),w.click(),t.value="do('hello",x.click(),await waitAsync(50),assertEqual("FindAndReplace","Replaces Once Correctly",a.value,"// hello /\\S/g\ndo('hello', /\\s/g);\nhello\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\na very very very very very very very very very very very very long line with many many many many many many many many many many many words\nline\nline\nline\nline\nline\nline\nline"),v.click(),codeInputElement.querySelector(".code-input_find-and-replace_dialog").dispatchEvent(new KeyboardEvent("keydown",{key:"Escape"})),codeInputElement.querySelector(".code-input_find-and-replace_dialog").dispatchEvent(new KeyboardEvent("keyup",{key:"Escape"})),assertEqual("FindAndReplace","Selection Start on Focused Match when Dialog Exited",a.selectionStart,3),assertEqual("FindAndReplace","Selection End on Focused Match when Dialog Exited",a.selectionEnd,8),a.dispatchEvent(new KeyboardEvent("keydown",{cancelable:!0,key:"h",ctrlKey:!0})),q.value="",q.focus(),allowInputEvents(q),addText(q,"hello"),await waitAsync(200),t.value="hi",y.click(),assertEqual("FindAndReplace","Replaces All Correctly",a.value,"// hi /\\S/g\ndo('hi', /\\s/g);\nhi\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\na very very very very very very very very very very very very long line with many many many many many many many many many many many words\nline\nline\nline\nline\nline\nline\nline"),q.value="line",r.click(),await waitAsync(200),codeInputElement.style="height: 100px; width: 1000px;",w.click(),w.click(),w.click(),w.click(),w.click(),w.click(),w.click(),w.click(),await waitAsync(200),testAssertion("FindAndReplace","Scrolls to Match Correctly",confirm("Is the match on the very long line highlighted orange and near the centre of the element?"),"user-judged"),codeInputElement.style="",codeInputElement.querySelector(".code-input_find-and-replace_dialog").dispatchEvent(new KeyboardEvent("keydown",{key:"Escape"})),codeInputElement.querySelector(".code-input_find-and-replace_dialog").dispatchEvent(new KeyboardEvent("keyup",{key:"Escape"})),a.selectionStart=0,a.selectionEnd=a.value.length,backspace(a),addText(a,"// 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"),a.dispatchEvent(new KeyboardEvent("keydown",{cancelable:!0,key:"g",ctrlKey:!0}));let A=codeInputElement.querySelector(".code-input_go-to-line_dialog input");A.value="1",A.dispatchEvent(new KeyboardEvent("keydown",{key:"Enter"})),A.dispatchEvent(new KeyboardEvent("keyup",{key:"Enter"})),assertEqual("GoToLine","Line Only",a.selectionStart,0),a.dispatchEvent(new KeyboardEvent("keydown",{cancelable:!0,key:"g",ctrlKey:!0})),A.value="3:18",A.dispatchEvent(new KeyboardEvent("keydown",{key:"Enter"})),A.dispatchEvent(new KeyboardEvent("keyup",{key:"Enter"})),assertEqual("GoToLine","Line and Column",a.selectionStart,45),a.dispatchEvent(new KeyboardEvent("keydown",{cancelable:!0,key:"g",ctrlKey:!0})),A.value="10",A.dispatchEvent(new KeyboardEvent("keydown",{key:"Enter"})),A.dispatchEvent(new KeyboardEvent("keyup",{key:"Enter"})),assertEqual("GoToLine","Rejects Out-of-range Line",A.classList.contains("code-input_go-to-line_error"),!0),a.dispatchEvent(new KeyboardEvent("keydown",{cancelable:!0,key:"g",ctrlKey:!0})),A.value="2:12",A.dispatchEvent(new KeyboardEvent("keydown",{key:"Enter"})),A.dispatchEvent(new KeyboardEvent("keyup",{key:"Enter"})),assertEqual("GoToLine","Rejects Out-of-range Column",A.classList.contains("code-input_go-to-line_error"),!0),a.dispatchEvent(new KeyboardEvent("keydown",{cancelable:!0,key:"g",ctrlKey:!0})),A.value="sausages",A.dispatchEvent(new KeyboardEvent("keydown",{key:"Enter"})),A.dispatchEvent(new KeyboardEvent("keyup",{key:"Enter"})),assertEqual("GoToLine","Rejects Invalid Input",A.classList.contains("code-input_go-to-line_error"),!0),assertEqual("GoToLine","Stays open when Rejects Input",A.parentElement.classList.contains("code-input_go-to-line_hidden-dialog"),!1),A.dispatchEvent(new KeyboardEvent("keydown",{key:"Escape"})),A.dispatchEvent(new KeyboardEvent("keyup",{key:"Escape"})),assertEqual("GoToLine","Exits when Esc pressed",A.parentElement.classList.contains("code-input_go-to-line_hidden-dialog"),!0),a.selectionStart=a.selectionEnd=a.value.length,addText(a,"\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}"),a.selectionStart=0,a.selectionEnd=a.value.length,a.dispatchEvent(new KeyboardEvent("keydown",{key:"Tab",shiftKey:!1})),a.dispatchEvent(new KeyboardEvent("keyup",{key:"Tab",shiftKey:!1})),assertEqual("Indent","Indents Lines",a.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 }"),a.dispatchEvent(new KeyboardEvent("keydown",{key:"Tab",shiftKey:!0})),a.dispatchEvent(new KeyboardEvent("keyup",{key:"Tab",shiftKey:!0})),assertEqual("Indent","Unindents Lines",a.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}"),a.dispatchEvent(new KeyboardEvent("keydown",{key:"Tab",shiftKey:!0})),a.dispatchEvent(new KeyboardEvent("keyup",{key:"Tab",shiftKey:!0})),assertEqual("Indent","Unindents Lines where some are already fully unindented",a.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}"),a.selectionStart=255,a.selectionEnd=274,a.dispatchEvent(new KeyboardEvent("keydown",{key:"Tab",shiftKey:!1})),a.dispatchEvent(new KeyboardEvent("keyup",{key:"Tab",shiftKey:!1})),assertEqual("Indent","Indents Lines by Selection",a.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}"),a.selectionStart=265,a.selectionEnd=265,a.dispatchEvent(new KeyboardEvent("keydown",{key:"Tab",shiftKey:!0})),a.dispatchEvent(new KeyboardEvent("keyup",{key:"Tab",shiftKey:!0})),assertEqual("Indent","Unindents Lines by Selection",a.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}"),a.selectionStart=0,a.selectionEnd=a.value.length,backspace(a),testAddingText("Indent-AutoCloseBrackets",a,function(a){addText(a,`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...\n}//Test auto-unindent\n{`,!0),move(a,1),backspace(a)},"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 }//Test auto-unindent\n {\n }\n }\n }\n}",221,221),b?(addText(a,"\nlet x = 1;\nlet y = 2;\nconsole.log(`${x} + ${y} = ${x+y}`);"),move(a,-4),a.selectionStart-=35,await waitAsync(50),assertEqual("SelectTokenCallbacks","Number of Selected Tokens",codeInputElement.querySelectorAll(".in-selection").length,13),assertEqual("SelectTokenCallbacks","Number of Selected .hljs-string Tokens",codeInputElement.querySelectorAll(".hljs-string.in-selection").length,0),assertEqual("SelectTokenCallbacks","Number of Selected .hljs-subst Tokens",codeInputElement.querySelectorAll(".hljs-subst.in-selection").length,2)):(addText(a,"\n[(),((),'Hi')]"),await waitAsync(50),move(a,-2),await waitAsync(50),assertEqual("SelectTokenCallbacks","Number of Selected Braces 1",codeInputElement.getElementsByClassName("brace-hover").length,2),move(a,1),await waitAsync(50),assertEqual("SelectTokenCallbacks","Number of Selected Braces 2",codeInputElement.getElementsByClassName("brace-hover").length,4)),a.selectionStart=0,a.selectionEnd=a.value.length,backspace(a),addText(a,"\"Some special characters: \x96,\x01\x03,\x02...\""),a.selectionStart=a.value.length-4,a.selectionEnd=a.value.length,await waitAsync(50),testAssertion("SpecialChars","Displays Correctly",confirm("Do the special characters read (0096),(0001)(0003),(0002) and align with the ellipsis? (OK=Yes)"),"user-judged"),a.selectionStart=0,a.selectionEnd=a.value.length,backspace(a),fetch(new Request("https://cdn.jsdelivr.net/gh/webcoder49/code-input@2.1/code-input.js")).then(a=>a.text()).then(b=>{a.value="// code-input v2.1: A large code file (not the latest version!)\n// Editing this here should give little latency.\n\n"+b,a.selectionStart=112,a.selectionEnd=112,addText(a,"\n",!0),document.getElementById("collapse-results").setAttribute("open",!0)}),testsFailed?(document.querySelector("h2").style.backgroundColor="red",document.querySelector("h2").textContent="Some Tests have Failed."):(document.querySelector("h2").style.backgroundColor="lightgreen",document.querySelector("h2").textContent="All Tests have Passed.")} \ No newline at end of file +`),a.selectionStart=0,a.selectionEnd=a.value.length,backspace(a),codeInputElement.setAttribute("language","JavaScript"),await waitAsync(100),testAssertion("Core","Light theme Caret/Placeholder Color Correct",confirm("Are the caret and placeholder near-black? (OK=Yes)"),"user-judged"),document.getElementById("theme-stylesheet").href=b?"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/dark.min.css":"https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-okaidia.min.css",await waitAsync(200),testAssertion("Core","Dark theme Caret/Placeholder Color Correct",confirm("Are the caret and placeholder near-white? (OK=Yes)"),"user-judged"),codeInputElement.style.color="red",await waitAsync(200),testAssertion("Core","Overriden color Caret/Placeholder Color Correct",confirm("Are the caret and placeholder (for Firefox) or just caret (for Chromium/WebKit, for consistency with textareas) red? (OK=Yes)"),"user-judged"),codeInputElement.style.removeProperty("color"),codeInputElement.style.caretColor="red",await waitAsync(200),testAssertion("Core","Overriden caret-color Caret/Placeholder Color Correct",confirm("Is the caret red and placeholder near-white? (OK=Yes)"),"user-judged"),codeInputElement.style.removeProperty("caret-color"),b||(codeInputElement.setAttribute("language","css"),await waitAsync(50),testAssertion("Core","Language Class Propogates",codeInputElement.querySelector("pre").classList.contains("language-css"),`Class name of pre element was "${codeInputElement.querySelector("pre").className}" but code-input element had language="css"`),window.requestAnimationFrame(function(){codeInputElement.setAttribute("placeholder","Gimme some HTML!"),codeInputElement.setAttribute("language","html")}),await waitAsync(500),testAssertion("Core","Language Class Propogates When Set Immediately After Placeholder Set",codeInputElement.querySelector("pre").classList.contains("language-html"),`Class name of pre element was "${codeInputElement.querySelector("pre").className}" but code-input element had language="css"`),codeInputElement.removeAttribute("placeholder"),codeInputElement.setAttribute("language","JavaScript"),await waitAsync(50)),testAddingText("AutoCloseBrackets",a,function(a){addText(a,`\nconsole.log("A test message`),move(a,2),addText(a,`;\nconsole.log("Another test message");\n{[{[]}(([[`),backspace(a),backspace(a),backspace(a),addText(a,`)`)},"\nconsole.log(\"A test message\");\nconsole.log(\"Another test message\");\n{[{[]}()]}",77,77),addText(a,"popup"),await waitAsync(50),testAssertion("Autocomplete","Popup Shows on input",confirm("Does the autocomplete popup display correctly? (OK=Yes)"),"user-judged"),move(a,-1),await waitAsync(50),testAssertion("Autocomplete","Popup Disappears on arrow key",confirm("Has the popup disappeared? (OK=Yes)"),"user-judged"),move(a,1),await waitAsync(50),testAssertion("Autocomplete","Popup Shows on arrow key",confirm("Does the autocomplete popup display correctly? (OK=Yes)"),"user-judged"),backspace(a),await waitAsync(50),testAssertion("Autocomplete","Popup Disappears on backspace",confirm("Has the popup disappeared? (OK=Yes)"),"user-judged"),addText(a,"p"),await waitAsync(50);const p=a.selectionStart,q=a.selectionEnd;alert("Dismiss this alert, then click the popup in the next 3 seconds.");for(let c=0;3e3>c&&!popupClicked;)await waitAsync(10),c+=10;testAssertion("Autocomplete","Popup Clickable",popupClicked,"The onclick event of the popup element didn't fire"),a.selectionStart=p,a.selectionEnd=q,a.focus(),backspace(a),backspace(a),backspace(a),backspace(a),backspace(a),b&&(a.selectionStart=0,a.selectionEnd=a.value.length,backspace(a),addText(a,"console.log(\"Hello, World!\");\nfunction sayHello(name) {\n console.log(\"Hello, \" + name + \"!\");\n}\nsayHello(\"code-input\");"),await waitAsync(50),assertEqual("Autodetect","Detects JavaScript",codeInputElement.getAttribute("language"),"javascript"),a.selectionStart=0,a.selectionEnd=a.value.length,backspace(a),addText(a,"#!/usr/bin/python\nprint(\"Hello, World!\")\nfor i in range(5):\n print(i)"),await waitAsync(50),assertEqual("Autodetect","Detects Python",codeInputElement.getAttribute("language"),"python"),a.selectionStart=0,a.selectionEnd=a.value.length,backspace(a),addText(a,"body, html {\n height: 100%;\n background-color: blue;\n color: red;\n}"),await waitAsync(50),assertEqual("Autodetect","Detects CSS",codeInputElement.getAttribute("language"),"css")),a.selectionStart=0,a.selectionEnd=a.value.length,backspace(a),a.parentElement.classList.add("code-input_autogrow_height"),await waitAsync(100);const r=a.parentElement.clientHeight;addText(a,"// a\n// b\n// c\n// d\n// e\n// f\n// g"),await waitAsync(100);const s=a.parentElement.clientHeight;testAssertion("Autogrow","Content Increases Height",s>r,`${s} should be > ${r}`),a.parentElement.style.setProperty("font-size","50%"),await waitAsync(200),testAssertion("Autogrow","font-size Decrease Decreases Height",a.parentElement.clientHeightt,`${u} should be > ${t}`),a.parentElement.style.setProperty("font-size","50%"),await waitAsync(200),testAssertion("Autogrow","font-size Decrease Decreases Width",a.parentElement.clientWidtha.text()).then(b=>{a.value="// code-input v2.1: A large code file (not the latest version!)\n// Editing this here should give little latency.\n\n"+b,a.selectionStart=112,a.selectionEnd=112,addText(a,"\n",!0),document.getElementById("collapse-results").setAttribute("open",!0)}),testsFailed?(document.querySelector("h2").style.backgroundColor="red",document.querySelector("h2").textContent="Some Tests have Failed."):(document.querySelector("h2").style.backgroundColor="lightgreen",document.querySelector("h2").textContent="All Tests have Passed.")} \ No newline at end of file