Skip to content

Commit f7fe452

Browse files
authored
Merge pull request #195 from WebCoder49/synchronise-caret-color
Synchronise caret/placeholder color
2 parents 243abe7 + af1099f commit f7fe452

File tree

6 files changed

+175
-21
lines changed

6 files changed

+175
-21
lines changed

code-input.css

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,14 @@ code-input {
2020
top: 0;
2121
left: 0;
2222

23-
color: black;
23+
/* CSS variables rather than inline styles used for values synced from JavaScript
24+
to keep low precedence and thus overridability
25+
The variable names may change and are for internal use. */
26+
/* --code-input_highlight-text-color: Set by JS to be base text color of pre code element */
27+
/* --code-input_no-override-color: Set by JS for very short time to get whether color has been overriden */
28+
color: var(--code-input_no-override-color, black);
29+
/* --code-input_default-caret-color: Set by JS to be same as color property - currentColor won't work because it's lazily evaluated so gives transparent for the textarea */
30+
caret-color: var(--code-input_default-caret-color, inherit);
2431
background-color: white;
2532

2633
/* Normal inline styles */
@@ -36,7 +43,6 @@ code-input {
3643
text-align: start;
3744
line-height: 1.5; /* Inherited to child elements */
3845
tab-size: 2;
39-
caret-color: darkgrey;
4046
white-space: pre;
4147
padding: 0!important; /* Use --padding to set the code-input element's padding */
4248
display: grid;
@@ -68,6 +74,12 @@ code-input textarea, code-input:not(.code-input_pre-element-styled) pre code, co
6874
code-input:not(.code-input_pre-element-styled) pre code, code-input.code-input_pre-element-styled pre {
6975
height: max-content;
7076
width: max-content;
77+
78+
79+
/* Allow colour change to reflect properly;
80+
transition-behavior: allow-discrete could be used but this is better supported and
81+
works with the color property. */
82+
transition: color 0.001s;
7183
}
7284

7385
code-input:not(.code-input_pre-element-styled) pre, code-input.code-input_pre-element-styled pre code {
@@ -118,12 +130,13 @@ code-input pre {
118130
/* Make textarea almost completely transparent, except for caret and placeholder */
119131

120132
code-input textarea:not([data-code-input-fallback]) {
121-
color: transparent;
122133
background: transparent;
123-
caret-color: inherit!important; /* Or choose your favourite color */
134+
color: transparent;
135+
caret-color: inherit;
124136
}
125-
code-input textarea::placeholder {
126-
color: lightgrey;
137+
code-input textarea:not([data-code-input-fallback]):placeholder-shown {
138+
/* Show placeholder */
139+
color: var(--code-input_highlight-text-color, inherit);
127140
}
128141

129142
/* Can be scrolled */
@@ -163,6 +176,11 @@ code-input .code-input_dialog-container {
163176

164177
/* Dialog boxes' text is based on text-direction */
165178
text-align: inherit;
179+
180+
/* Allow colour change to reflect properly;
181+
* transition-behavior: allow-discrete could be used but this is * better supported and works with the color property. */
182+
color: inherit;
183+
transition: color 0.001s;
166184
}
167185

168186
[dir=rtl] code-input .code-input_dialog-container, code-input[dir=rtl] .code-input_dialog-container {
@@ -252,11 +270,13 @@ code-input:not(.code-input_loaded) pre, code-input:not(.code-input_loaded) texta
252270
code-input:has(textarea[data-code-input-fallback]) {
253271
padding: 0!important; /* Padding now in the textarea */
254272
box-sizing: content-box;
273+
274+
caret-color: revert; /* JS not setting the colour since no highlighting */
255275
}
256276
code-input textarea[data-code-input-fallback] {
257277
overflow: auto;
258278
background-color: inherit;
259-
color: inherit;
279+
color: var(--code-input_highlight-text-color, inherit);
260280

261281
/* Don't overlap with message */
262282
min-height: calc(100% - var(--padding-top, 16px) - 2em - var(--padding-bottom, 16px));

code-input.js

Lines changed: 114 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ var codeInput = {
152152
}
153153
},
154154

155+
stylesheetI: 0, // Increments to give different classes to each code-input element so they can have custom styles synchronised internally without affecting the inline style
156+
155157
/**
156158
* Please see `codeInput.templates.prism` or `codeInput.templates.hljs`.
157159
* Templates are used in `<code-input>` elements and once registered with
@@ -445,6 +447,16 @@ var codeInput = {
445447
*/
446448
dialogContainerElement = null;
447449

450+
/**
451+
* Like style attribute, but with a specificity of 1
452+
* element, 1 class. Present so styles can be set on only
453+
* this element while giving other code freedom of use of
454+
* the style attribute.
455+
*
456+
* For internal use only.
457+
*/
458+
internalStyle = null;
459+
448460
/**
449461
* Form-Associated Custom Element Callbacks
450462
* https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-face-example
@@ -530,22 +542,75 @@ var codeInput = {
530542
this.pluginEvt("afterHighlight");
531543
}
532544

545+
getStyledHighlightingElement() {
546+
if(this.templateObject.preElementStyled) {
547+
return this.preElement;
548+
} else {
549+
return this.codeElement;
550+
}
551+
}
552+
533553
/**
534554
* Set the size of the textarea element to the size of the pre/code element.
535555
*/
536556
syncSize() {
537557
// Synchronise the size of the pre/code and textarea elements
538-
if(this.templateObject.preElementStyled) {
539-
this.style.backgroundColor = getComputedStyle(this.preElement).backgroundColor;
540-
this.textareaElement.style.height = getComputedStyle(this.preElement).height;
541-
this.textareaElement.style.width = getComputedStyle(this.preElement).width;
542-
} else {
543-
this.style.backgroundColor = getComputedStyle(this.codeElement).backgroundColor;
544-
this.textareaElement.style.height = getComputedStyle(this.codeElement).height;
545-
this.textareaElement.style.width = getComputedStyle(this.codeElement).width;
558+
this.textareaElement.style.height = getComputedStyle(this.getStyledHighlightingElement()).height;
559+
this.textareaElement.style.width = getComputedStyle(this.getStyledHighlightingElement()).width;
560+
}
561+
562+
/**
563+
* If the color attribute has been defined on the
564+
* code-input element by external code, return true.
565+
* Otherwise, make the aspects the color affects
566+
* (placeholder and caret colour) be the base colour
567+
* of the highlighted text, for best contrast, and
568+
* return false.
569+
*/
570+
isColorOverridenSyncIfNot() {
571+
const oldTransition = this.style.transition;
572+
this.style.transition = "unset";
573+
window.requestAnimationFrame(() => {
574+
this.internalStyle.setProperty("--code-input_no-override-color", "rgb(0, 0, 0)");
575+
if(getComputedStyle(this).color == "rgb(0, 0, 0)") {
576+
// May not be overriden
577+
this.internalStyle.setProperty("--code-input_no-override-color", "rgb(255, 255, 255)");
578+
if(getComputedStyle(this).color == "rgb(255, 255, 255)") {
579+
// Definitely not overriden
580+
this.internalStyle.removeProperty("--code-input_no-override-color");
581+
this.style.transition = oldTransition;
582+
583+
const highlightedTextColor = getComputedStyle(this.getStyledHighlightingElement()).color;
584+
585+
this.internalStyle.setProperty("--code-input_highlight-text-color", highlightedTextColor);
586+
this.internalStyle.setProperty("--code-input_default-caret-color", highlightedTextColor);
587+
return false;
588+
}
589+
}
590+
this.internalStyle.removeProperty("--code-input_no-override-color");
591+
this.style.transition = oldTransition;
592+
});
593+
594+
return true;
595+
}
596+
597+
/**
598+
* Update the aspects the color affects
599+
* (placeholder and caret colour) to the correct
600+
* colour: either that defined on the code-input
601+
* element, or if none is defined externally the
602+
* base colour of the highlighted text.
603+
*/
604+
syncColorCompletely() {
605+
// color of code-input element
606+
if(this.isColorOverridenSyncIfNot()) {
607+
// color overriden
608+
this.internalStyle.removeProperty("--code-input_highlight-text-color");
609+
this.internalStyle.setProperty("--code-input_default-caret-color", getComputedStyle(this).color);
546610
}
547611
}
548612

613+
549614
/**
550615
* Show some instructions to the user only if they are using keyboard navigation - for example, a prompt on how to navigate with the keyboard if Tab is repurposed.
551616
* @param {string} instructions The instructions to display only if keyboard navigation is being used. If it's blank, no instructions will be shown.
@@ -741,7 +806,6 @@ var codeInput = {
741806
this.codeElement = code;
742807
pre.append(code);
743808
this.append(pre);
744-
745809
if (this.templateObject.isCode) {
746810
if (lang != undefined && lang != "") {
747811
code.classList.add("language-" + lang.toLowerCase());
@@ -780,7 +844,44 @@ var codeInput = {
780844
// The only element that could be resized is this code-input element.
781845
this.syncSize();
782846
});
783-
resizeObserver.observe(this.textareaElement);
847+
resizeObserver.observe(this);
848+
849+
850+
// Add internal style as non-externally-overridable alternative to style attribute for e.g. syncing color
851+
this.classList.add("code-input_styles_" + codeInput.stylesheetI);
852+
const stylesheet = document.createElement("style");
853+
stylesheet.innerHTML = "code-input.code-input_styles_" + codeInput.stylesheetI + " {}";
854+
this.appendChild(stylesheet);
855+
this.internalStyle = stylesheet.sheet.cssRules[0].style;
856+
codeInput.stylesheetI++;
857+
858+
// Synchronise colors
859+
const preColorChangeCallback = (evt) => {
860+
if(evt.propertyName == "color") {
861+
this.isColorOverridenSyncIfNot();
862+
}
863+
};
864+
this.preElement.addEventListener("transitionend", preColorChangeCallback);
865+
this.preElement.addEventListener("-webkit-transitionend", preColorChangeCallback);
866+
const thisColorChangeCallback = (evt) => {
867+
if(evt.propertyName == "color") {
868+
this.syncColorCompletely();
869+
}
870+
if(evt.target == this.dialogContainerElement) {
871+
evt.stopPropagation();
872+
// Prevent bubbling because code-input
873+
// transitionend is separate
874+
}
875+
};
876+
// Not on this element so CSS transition property does not override publicly-visible one
877+
this.dialogContainerElement.addEventListener("transitionend", thisColorChangeCallback);
878+
this.dialogContainerElement.addEventListener("-webkit-transitionend", thisColorChangeCallback);
879+
880+
// For when this code-input element has an externally-defined, different-duration transition
881+
this.addEventListener("transitionend", thisColorChangeCallback);
882+
this.addEventListener("-webkit-transitionend", thisColorChangeCallback);
883+
884+
this.syncColorCompletely();
784885

785886
this.classList.add("code-input_loaded");
786887
}
@@ -938,7 +1039,9 @@ var codeInput = {
9381039
code.classList.add("language-" + newValue);
9391040
}
9401041

941-
if (mainTextarea.placeholder == oldValue) mainTextarea.placeholder = newValue;
1042+
if (mainTextarea.placeholder == oldValue || oldValue == null && mainTextarea.placeholder == "") {
1043+
mainTextarea.placeholder = newValue;
1044+
}
9421045

9431046
this.scheduleHighlight();
9441047

docs/interface/css/_index.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@ title = 'Styling `code-input` elements with CSS'
1010
* 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`).
1111
* 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.
1212
* For now, elements on top of `code-input` elements should have a CSS `z-index` at least 3 greater than the `code-input` element.
13+
* The caret and placeholder colour by default follow and give good contrast with the highlighted theme. Setting a CSS rule (with a specificity higher than one element and one class, for good backwards compatibility) for `color` and/or `caret-color` properties on the code-input element will override this behaviour.
14+
15+
Please do **not** use `className` in JavaScript referring to code-input elements, because the code-input library needs to add its own classes to code-input elements for easier progressive enhancement. You can, however, use `classList` and `style` as much as you want - it will make your code cleaner anyway.

tests/hljs.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<title>code-input Tester</title>
77

88
<!--Import Highlight.JS-->
9-
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
9+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css" id="theme-stylesheet">
1010
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
1111
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/xml.min.js"></script>
1212
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/css.min.js"></script>
@@ -42,7 +42,7 @@ <h4><a href="prism.html">Test for Prism.js</a></h4>
4242

4343
<details id="collapse-results"><summary>Test Results (Click to Open)</summary><pre id="test-results"></pre></details>
4444
<form method="GET" action="afterform.html" target="_blank">
45-
<code-input><textarea data-code-input-fallback name="q">console.log("Hello, World!");
45+
<code-input><textarea data-code-input-fallback name="q" placeholder="language auto-detected">console.log("Hello, World!");
4646
// A second line
4747
// A third line with &lt;html> tags</textarea></code-input>
4848
<input type="submit" value="Test HTML Form"/>

tests/prism.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
66
<title>code-input Tester</title>
77
<!--Import Prism-->
8-
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css">
8+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css" id="theme-stylesheet">
99
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js" data-manual></script><!--Remove data-manual if also using Prism normally-->
1010
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
1111
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.js"></script>

tests/tester.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,34 @@ console.log("I've got another line!", 2 &lt; 3, "should be true.");
383383
// A third line with &lt;html&gt; tags
384384
`); // Extra newline so line numbers visible if enabled.
385385

386+
// Delete all code
387+
textarea.selectionStart = 0;
388+
textarea.selectionEnd = textarea.value.length;
389+
backspace(textarea);
390+
codeInputElement.setAttribute("language", "JavaScript"); // for placeholder
391+
392+
await waitAsync(100); // Wait for rendered value to update
393+
testAssertion("Core", "Light theme Caret/Placeholder Color Correct", confirm("Are the caret and placeholder near-black? (OK=Yes)"), "user-judged");
394+
395+
if(isHLJS) {
396+
document.getElementById("theme-stylesheet").href = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/dark.min.css";
397+
} else {
398+
document.getElementById("theme-stylesheet").href = "https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-okaidia.min.css";
399+
}
400+
await waitAsync(200); // Wait for colours to update
401+
testAssertion("Core", "Dark theme Caret/Placeholder Color Correct", confirm("Are the caret and placeholder near-white? (OK=Yes)"), "user-judged");
402+
403+
codeInputElement.style.color = "red";
404+
await waitAsync(200); // Wait for colours to update
405+
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");
406+
407+
codeInputElement.style.removeProperty("color");
408+
codeInputElement.style.caretColor = "red";
409+
await waitAsync(200); // Wait for colours to update
410+
testAssertion("Core", "Overriden caret-color Caret/Placeholder Color Correct", confirm("Is the caret red and placeholder near-white? (OK=Yes)"), "user-judged");
411+
412+
codeInputElement.style.removeProperty("caret-color");
413+
386414
/*--- Tests for plugins ---*/
387415
// AutoCloseBrackets
388416
testAddingText("AutoCloseBrackets", textarea, function(textarea) {

0 commit comments

Comments
 (0)