diff --git a/code-input.css b/code-input.css index 1795174..78d8396 100644 --- a/code-input.css +++ b/code-input.css @@ -20,7 +20,14 @@ code-input { top: 0; left: 0; - color: black; + /* CSS variables rather than inline styles used for values synced from JavaScript + to keep low precedence and thus overridability + The variable names may change and are for internal use. */ + /* --code-input_highlight-text-color: Set by JS to be base text color of pre code element */ + /* --code-input_no-override-color: Set by JS for very short time to get whether color has been overriden */ + color: var(--code-input_no-override-color, black); + /* --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 */ + caret-color: var(--code-input_default-caret-color, inherit); background-color: white; /* Normal inline styles */ @@ -36,7 +43,6 @@ code-input { text-align: start; line-height: 1.5; /* Inherited to child elements */ tab-size: 2; - caret-color: darkgrey; white-space: pre; padding: 0!important; /* Use --padding to set the code-input element's padding */ display: grid; @@ -68,6 +74,12 @@ code-input textarea, code-input:not(.code-input_pre-element-styled) pre code, co code-input:not(.code-input_pre-element-styled) pre code, code-input.code-input_pre-element-styled pre { height: max-content; width: max-content; + + + /* Allow colour change to reflect properly; + transition-behavior: allow-discrete could be used but this is better supported and + works with the color property. */ + transition: color 0.001s; } 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 { /* Make textarea almost completely transparent, except for caret and placeholder */ code-input textarea:not([data-code-input-fallback]) { - color: transparent; background: transparent; - caret-color: inherit!important; /* Or choose your favourite color */ + color: transparent; + caret-color: inherit; } -code-input textarea::placeholder { - color: lightgrey; +code-input textarea:not([data-code-input-fallback]):placeholder-shown { + /* Show placeholder */ + color: var(--code-input_highlight-text-color, inherit); } /* Can be scrolled */ @@ -163,6 +176,11 @@ code-input .code-input_dialog-container { /* Dialog boxes' text is based on text-direction */ text-align: inherit; + + /* Allow colour change to reflect properly; + * transition-behavior: allow-discrete could be used but this is * better supported and works with the color property. */ + color: inherit; + transition: color 0.001s; } [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 code-input:has(textarea[data-code-input-fallback]) { padding: 0!important; /* Padding now in the textarea */ box-sizing: content-box; + + caret-color: revert; /* JS not setting the colour since no highlighting */ } code-input textarea[data-code-input-fallback] { overflow: auto; background-color: inherit; - color: inherit; + 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)); diff --git a/code-input.js b/code-input.js index 42b5ae2..ff973b5 100644 --- a/code-input.js +++ b/code-input.js @@ -152,6 +152,8 @@ var codeInput = { } }, + 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 + /** * Please see `codeInput.templates.prism` or `codeInput.templates.hljs`. * Templates are used in `` elements and once registered with @@ -445,6 +447,16 @@ var codeInput = { */ dialogContainerElement = null; + /** + * Like style attribute, but with a specificity of 1 + * element, 1 class. Present so styles can be set on only + * this element while giving other code freedom of use of + * the style attribute. + * + * For internal use only. + */ + internalStyle = null; + /** * Form-Associated Custom Element Callbacks * https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-face-example @@ -530,22 +542,75 @@ var codeInput = { this.pluginEvt("afterHighlight"); } + getStyledHighlightingElement() { + if(this.templateObject.preElementStyled) { + return this.preElement; + } else { + return this.codeElement; + } + } + /** * Set the size of the textarea element to the size of the pre/code element. */ syncSize() { // Synchronise the size of the pre/code and textarea elements - if(this.templateObject.preElementStyled) { - this.style.backgroundColor = getComputedStyle(this.preElement).backgroundColor; - this.textareaElement.style.height = getComputedStyle(this.preElement).height; - this.textareaElement.style.width = getComputedStyle(this.preElement).width; - } else { - this.style.backgroundColor = getComputedStyle(this.codeElement).backgroundColor; - this.textareaElement.style.height = getComputedStyle(this.codeElement).height; - this.textareaElement.style.width = getComputedStyle(this.codeElement).width; + this.textareaElement.style.height = getComputedStyle(this.getStyledHighlightingElement()).height; + this.textareaElement.style.width = getComputedStyle(this.getStyledHighlightingElement()).width; + } + + /** + * If the color attribute has been defined on the + * code-input element by external code, return true. + * Otherwise, make the aspects the color affects + * (placeholder and caret colour) be the base colour + * of the highlighted text, for best contrast, and + * return false. + */ + isColorOverridenSyncIfNot() { + const oldTransition = this.style.transition; + this.style.transition = "unset"; + window.requestAnimationFrame(() => { + this.internalStyle.setProperty("--code-input_no-override-color", "rgb(0, 0, 0)"); + if(getComputedStyle(this).color == "rgb(0, 0, 0)") { + // May not be overriden + this.internalStyle.setProperty("--code-input_no-override-color", "rgb(255, 255, 255)"); + if(getComputedStyle(this).color == "rgb(255, 255, 255)") { + // Definitely not overriden + this.internalStyle.removeProperty("--code-input_no-override-color"); + this.style.transition = oldTransition; + + const highlightedTextColor = getComputedStyle(this.getStyledHighlightingElement()).color; + + this.internalStyle.setProperty("--code-input_highlight-text-color", highlightedTextColor); + this.internalStyle.setProperty("--code-input_default-caret-color", highlightedTextColor); + return false; + } + } + this.internalStyle.removeProperty("--code-input_no-override-color"); + this.style.transition = oldTransition; + }); + + return true; + } + + /** + * Update the aspects the color affects + * (placeholder and caret colour) to the correct + * colour: either that defined on the code-input + * element, or if none is defined externally the + * base colour of the highlighted text. + */ + syncColorCompletely() { + // color of code-input element + if(this.isColorOverridenSyncIfNot()) { + // color overriden + this.internalStyle.removeProperty("--code-input_highlight-text-color"); + this.internalStyle.setProperty("--code-input_default-caret-color", getComputedStyle(this).color); } } + /** * 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. * @param {string} instructions The instructions to display only if keyboard navigation is being used. If it's blank, no instructions will be shown. @@ -739,7 +804,6 @@ var codeInput = { this.codeElement = code; pre.append(code); this.append(pre); - if (this.templateObject.isCode) { if (lang != undefined && lang != "") { code.classList.add("language-" + lang.toLowerCase()); @@ -765,7 +829,6 @@ var codeInput = { // Update with fallback textarea's state so can keep editing // if loaded slowly if(fallbackSelectionStart !== undefined) { - console.log("sel", fallbackSelectionStart, fallbackSelectionEnd, fallbackSelectionDirection, "scr", fallbackScrollTop, fallbackScrollLeft, "foc", fallbackFocused); textarea.setSelectionRange(fallbackSelectionStart, fallbackSelectionEnd, fallbackSelectionDirection); textarea.scrollTo(fallbackScrollTop, fallbackScrollLeft); } @@ -779,7 +842,44 @@ var codeInput = { // The only element that could be resized is this code-input element. this.syncSize(); }); - resizeObserver.observe(this.textareaElement); + resizeObserver.observe(this); + + + // Add internal style as non-externally-overridable alternative to style attribute for e.g. syncing color + this.classList.add("code-input_styles_" + codeInput.stylesheetI); + const stylesheet = document.createElement("style"); + stylesheet.innerHTML = "code-input.code-input_styles_" + codeInput.stylesheetI + " {}"; + this.appendChild(stylesheet); + this.internalStyle = stylesheet.sheet.cssRules[0].style; + codeInput.stylesheetI++; + + // Synchronise colors + const preColorChangeCallback = (evt) => { + if(evt.propertyName == "color") { + this.isColorOverridenSyncIfNot(); + } + }; + this.preElement.addEventListener("transitionend", preColorChangeCallback); + this.preElement.addEventListener("-webkit-transitionend", preColorChangeCallback); + const thisColorChangeCallback = (evt) => { + if(evt.propertyName == "color") { + this.syncColorCompletely(); + } + if(evt.target == this.dialogContainerElement) { + evt.stopPropagation(); + // Prevent bubbling because code-input + // transitionend is separate + } + }; + // Not on this element so CSS transition property does not override publicly-visible one + this.dialogContainerElement.addEventListener("transitionend", thisColorChangeCallback); + this.dialogContainerElement.addEventListener("-webkit-transitionend", thisColorChangeCallback); + + // For when this code-input element has an externally-defined, different-duration transition + this.addEventListener("transitionend", thisColorChangeCallback); + this.addEventListener("-webkit-transitionend", thisColorChangeCallback); + + this.syncColorCompletely(); this.classList.add("code-input_loaded"); } @@ -914,7 +1014,9 @@ var codeInput = { code.classList.add("language-" + newValue); } - if (mainTextarea.placeholder == oldValue) mainTextarea.placeholder = newValue; + if (mainTextarea.placeholder == oldValue || oldValue == null && mainTextarea.placeholder == "") { + mainTextarea.placeholder = newValue; + } this.scheduleHighlight(); diff --git a/docs/interface/css/_index.md b/docs/interface/css/_index.md index 34ab4a7..2a1c60f 100644 --- a/docs/interface/css/_index.md +++ b/docs/interface/css/_index.md @@ -10,3 +10,6 @@ title = 'Styling `code-input` elements with CSS' * The CSS variable `--padding` should be used rather than the property `padding` (e.g. `...`), 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`). * 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. * For now, elements on top of `code-input` elements should have a CSS `z-index` at least 3 greater than the `code-input` element. +* 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. + +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. diff --git a/tests/hljs.html b/tests/hljs.html index 28f2bc2..25bf95a 100644 --- a/tests/hljs.html +++ b/tests/hljs.html @@ -6,7 +6,7 @@ code-input Tester - + @@ -42,7 +42,7 @@

Test for Prism.js

Test Results (Click to Open)
- diff --git a/tests/prism.html b/tests/prism.html index 50e4570..bfc6906 100644 --- a/tests/prism.html +++ b/tests/prism.html @@ -5,7 +5,7 @@ code-input Tester - + diff --git a/tests/tester.js b/tests/tester.js index 80b54b3..b33f2e7 100644 --- a/tests/tester.js +++ b/tests/tester.js @@ -326,6 +326,34 @@ console.log("I've got another line!", 2 < 3, "should be true."); // A third line with <html> tags `); // Extra newline so line numbers visible if enabled. + // Delete all code + textarea.selectionStart = 0; + textarea.selectionEnd = textarea.value.length; + backspace(textarea); + codeInputElement.setAttribute("language", "JavaScript"); // for placeholder + + await waitAsync(100); // Wait for rendered value to update + testAssertion("Core", "Light theme Caret/Placeholder Color Correct", confirm("Are the caret and placeholder near-black? (OK=Yes)"), "user-judged"); + + if(isHLJS) { + document.getElementById("theme-stylesheet").href = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/dark.min.css"; + } else { + document.getElementById("theme-stylesheet").href = "https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-okaidia.min.css"; + } + await waitAsync(200); // Wait for colours to update + 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); // Wait for colours to update + 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); // Wait for colours to update + 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"); + /*--- Tests for plugins ---*/ // AutoCloseBrackets testAddingText("AutoCloseBrackets", textarea, function(textarea) {