Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion LICENSE
Original file line numberDiff line numberDiff line change
@@ -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
Expand Down
6 changes: 4 additions & 2 deletions code-input.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@
*
* License of whole library for bundlers:
*
* Copyright 2021-2025 Oliver Geer and contributors
* Copyright 2021-2026 Oliver Geer and contributors
* @license MIT
*
* **<https://code-input-js.org>**
Expand DownExpand Up@@ -284,5 +284,7 @@ code-input textarea[data-code-input-fallback] {
color: var(--code-input_highlight-text-color, inherit);

/* Don't overlap with message */
min-height: calc(100% - var(--padding-top, 16px) - 2em - var(--padding-bottom, 16px));
min-height: calc(100% - var(--padding-top, 16px) - max(2em, var(--padding-bottom, 16px)));
height: calc(100% - var(--padding-top, 16px) - max(2em, var(--padding-bottom, 16px)));
padding-bottom: max(2em, var(--padding-bottom, 16px))!important;
}
2 changes: 1 addition & 1 deletion code-input.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@
*
* License of whole library for bundlers:
*
* Copyright 2021-2025 Oliver Geer and contributors
* Copyright 2021-2026 Oliver Geer and contributors
* @license MIT
*
* **<https://code-input-js.org>**
Expand Down
1 change: 1 addition & 0 deletions docs/interface/css/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ title = 'Styling `code-input` elements with CSS'
`code-input` elements can be styled like `textarea` elements in most cases; however, there are some exceptions:
* The CSS variable `--padding` should be used rather than the property `padding` (e.g. `<code-input style="--padding: 10px;">...`), or `--padding-left`, `--padding-right`, `--padding-top` and `--padding-bottom` instead of the CSS properties of the same names. For technical reasons, the value must have a unit (i.e. `0px`, not `0`). To avoid overcomplicating the code, this padding is always *included in* any width/heights of `code-input` elements, so if you want to style `textarea`s and `code-input` elements with best consistency set `box-sizing: border-box` on them.
* Background colours set on `code-input` elements will not work with highlighters that set background colours themselves - use `(code-input's selector) pre[class*="language-"]` for Prism.js or `.hljs` for highlight.js to target the highlighted element with higher specificity than the highlighter's theme. You may also set the `background-color` of the code-input element for its appearance when its template is unregistered / there is no JavaScript.
* To minimise the chance of [this bug](https://github.com/WebCoder49/code-input/issues/130): if you import a monospace font family from an online source (for example, Google Fonts or Bunny Fonts) to style `code-input` elements, and use a syntax highlighting theme that includes **bold text**, import both the standard (400) and bold (700) weights of the font.
* The caret and placeholder colour by default follow and give good contrast with the highlighted theme. Setting a CSS rule for `color` and/or `caret-color` properties on the code-input element will override this behaviour.
* For technical reasons, `code-input:focus` won't match anything. Use `code-input:has(textarea:focus)` instead.
* For now, elements on top of `code-input` elements should have a CSS `z-index` at least 3 greater than the `code-input` element.
Expand Down
2 changes: 1 addition & 1 deletion docs/modules-and-frameworks/custom/_index.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
+++
title = 'Using code-input.js with custom highlighting algorihms in projects which use modules or frameworks'
title = 'Using code-input.js with custom highlighting algorithms in projects which use modules or frameworks'
+++

# Using code-input.js with custom highlighting algorithms in projects which use modules or frameworks
Expand Down
6 changes: 3 additions & 3 deletions plugins/autogrow.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -71,7 +71,7 @@ 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)); /* So minimum height possible while containing highlighted code */
height: var(--code-input_autogrow_true-min-height);
}
code-input textarea[data-code-input-fallback] {
height: calc(var(--code-input_autogrow_true-min-height) - var(--padding-top, 16px) - var(--padding-bottom, 16px) - 2em)!important; /* So minimum height possible while containing highlighted code */
min-height: calc(100% - var(--padding-top, 16px) - var(--padding-bottom, 16px) - 2em);
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)));
}
2 changes: 1 addition & 1 deletion tests/prism.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,7 +41,7 @@ <h4><a href="hljs.html">Test for highlight.js</a></h4>
<p>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.</p>

<details id="collapse-results"><summary>Test Results (Click to Open)</summary><pre id="test-results"></pre></details>
<form method="GET" class="match-braces line-numbers" action="afterform.html" target="_blank">
<form method="GET" class="match-braces" action="afterform.html" target="_blank">
<code-input><textarea data-code-input-fallback name="q">console.log("Hello, World!");
// A second line
// A third line with &lt;html> tags</textarea></code-input>
Expand Down
127 changes: 113 additions & 14 deletions tests/tester.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -105,16 +105,83 @@ 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(),
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 = '<button onclick="event.preventDefault(); popupClicked = true;">This is the popup.</button>';
} else {
popupElem.style.display = "none";
}
Expand All@@ -133,7 +200,7 @@ 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 = '<button onclick="event.preventDefault(); popupClicked = true;">This is the popup.</button>';
} else {
popupElem.style.display = "none";
}
Expand All@@ -145,19 +212,21 @@ function beginTest(isHLJS) {
new codeInput.plugins.SpecialChars(true),
]));
}
startLoad(codeInputElem, isHLJS);
}

/* Start loading the tests, using the codeInput load time as one of the tests. */
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);
}

Expand All@@ -178,16 +247,27 @@ 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);

/*--- 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 <html> tags`);
// Code element's displayed value, ignoring appearance with HTML tags, should be the initial value but HTML-escaped
Expand DownExpand Up@@ -439,12 +519,31 @@ console.log("I've got another line!", 2 &lt; 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);
Expand Down