Issue
I would like to align the rows of text with the numbers in the yellow vertical bar (without breaking the mouse pointer on the rows, because changing margin or padding also moves the pointer).
PROBLEM: As you can see from the image, the problem is that the first and second rows are not well aligned with the numbers in the yellow column. The rows in the black panel are created in the yellow empty spaces between the numbers
HIGHLIGHT.JS: I would have liked not to use the Highlightjs library (to select the text color) in the question code, but i noticed that using Highlightjs adds css that creates additional empty space at the top-beginning of the black panel. WITHOUT Highlightjs i have the same problem, but with Highlightjs the empty space is greater.
Even if you have never used Highlightjs, it's not difficult and it's not a problem, below you will find the working snippet where everything is included. So to find the solution you have to use Highlightjs which already i inserted in html as tag and CDN:
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs2015.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
DETAILS: In .numbers
css, i managed the vertical space between the numbers with the line-height: 1.3
and also their size with font-size: 16px
, so i would like the space and size to remain the same. I tried changing some paddings
or margins
, but doing so breaks the mouse pointer after the last letter of a word or breaks in other ways. I would like to get this or something very similar:
How can i align the rows to the numbers correctly?
IMPORTANT: Attention to the broken mouse pointer on the rows, because the rows alignment problem seems solved, but the mouse pointer breaks and does not flash correctly on the rows, therefore it may be set in a wrong position between two rows.
In case of help, please please use my code for the solution and not another example code, because as you can see my code is a bit peculiar. Thanks in advance to everyone
/// JS CODE ROWS ///
const textarea = document.querySelector(".hilite-editor");
const numbers = document.querySelector(".numbers");
function updateLineNumbers() {
const num = textarea.innerHTML.split("\n").length;
numbers.innerHTML = Array(num).fill("<span></span>").join("");
}
// Update line numbers when the page loads
updateLineNumbers();
// Update line numbers when the textarea content changes
textarea.addEventListener("input", updateLineNumbers);
// WHEN I PRESS KEY
textarea.addEventListener("keydown", (event) => {
if (event.key === "Tab") {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
textarea.innerHTML =
textarea.innerHTML.substring(0, start) +
"\t" +
textarea.innerHTML.substring(end);
event.preventDefault();
}
});
/// JS CODE EDITOR ///
const elHTML = document.querySelector(`[data-lang="html"]`);
const elCSS = document.querySelector(`[data-lang="css"]`);
const elJS = document.querySelector(`[data-lang="js"]`);
const elPreview = document.querySelector("#preview");
const hilite = (el) => {
const elCode = el.previousElementSibling.querySelector("code");
elCode.textContent = el.textContent;
delete elCode.dataset.highlighted;
hljs.highlightElement(elCode);
};
const preview = () => {
const encodedCSS = encodeURIComponent(`<style>${elCSS.textContent}</style>`);
const encodedHTML = encodeURIComponent(elHTML.textContent);
const encodedJS = encodeURIComponent(`<scr` + `ipt>${elJS.textContent}</scr` + `ipt>`);
const dataURL = `data:text/html;charset=utf-8,${encodedCSS + encodedHTML + encodedJS}`;
elPreview.src = dataURL;
};
// Initialize!
[elHTML, elCSS, elJS].forEach((el) => {
el.addEventListener("input", () => {
hilite(el);
preview();
});
hilite(el);
});
preview();
/******** CODE EDITOR HIGHTLIGHTS.JS **********/
/* Scrollbars */
::-webkit-scrollbar {
width: 5px;
height: 5px;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
border-radius: 0px;
}
::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.3);
border-radius: 1rem;
}
.hilite {
position: relative;
background: #1e1e1e;
height: 120px;
overflow: auto;
width: 100%;
height: 100%;
}
.hilite-colors code,
.hilite-editor {
padding: 1rem !important;
top: 0;
left: 0;
right: 0;
bottom: 0;
white-space: pre-wrap;
font: 13px/1.4 monospace;
width: 100%;
background: transparent;
border: 0;
outline: 0;
}
/* THE OVERLAYING CONTENTEDITABLE WITH TRANSPARENT TEXT */
.hilite-editor {
display: inline-block;
position: relative;
color: transparent; /* Make text invisible */
caret-color: hsl( 50, 75%, 70%); /* But keep caret visible */
width: 100%;
}
.hilite-editor:focus {
outline: transparent;
}
.hilite-editor::selection {
background: hsla(0, 100%, 75%, 0.2);
}
/* THE UNDERLAYING DIV WITH HIGHLIGHT COLORS */
.hilite-colors {
position: absolute;
user-select: none;
width: 100%;
}
#preview {
display: block;
background: #fff;
width: 100%;
height: 100%;
border: 0;
}
/* highlight.js customizations: */
.hljs {
background: none;
}
#preview.resizing {
pointer-events: none;
}
/******** COUNT ROWS **********/
.numbers {
text-align: right;
background: yellow;
/* padding-right: 20px; */
width: 35px;
height: 200px;
/* margin-right: 71px; */
line-height: 1.3;
font-size: 15px;
}
.numbers span {
counter-increment: linenumber;
}
.numbers span::before {
content: counter(linenumber);
display: block;
color: black;
}
/******** CONTAINERS **********/
.container {
/*max-width: 700px; */
min-height: 16em;
border-radius: 4px;
margin: 0;
padding:0;
}
.container--tabs {
list-style: none;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: stretch;
margin: 0;
padding:0;
background-color: #282828;
.tab {
min-height: 2em;
padding: 7px;
box-sizing: border-box;
width: 127px;
text-align: center;
cursor: pointer;
background-color: #363636;
color: white;
transition: background-color 0.25s;
border-right: 1px solid #5a5a5a;
&:hover {
background-color: rgba(0, 0, 0, 0.25);
transition: background-color 0.25s;
color: white;
}
}
.tabs--active {
background-color: #1e1e1e;
color: white;
pointer-events: none;
}
}
.content {
display: none;
padding: 0;
}
.content--active {
display: flex;
height: 800px;
}
h1 {
margin-bottom: 0.5em;
font-weight: 700;
}
p {
font-weight: 300;
}
<!-- Highlight js -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs2015.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<!-- Bootstrap -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://getbootstrap.com/docs/5.3/assets/css/docs.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<div class="container--content">
<div class="content content--active">
<div class="numbers">
<span></span>
</div>
<div class="hilite">
<pre class="hilite-colors"><code class="language-html"></code></pre>
<div
data-lang="html"
class="hilite-editor"
contenteditable="true"
spellcheck="false"
autocorrect="off"
autocapitalize="off"
><h1 class="heading" title="test this" data-some='foo bar'>This is a Heading</h1>
<p>This is a paragraph.</p></div>
</div>
</div>
</div>
</div>
Solution
There's so many things in your approach I corrected, I can just hope to mention some of them (at least the most relevant ones):
- Edit: Since contenteditable have many gotchas regarding inserting custom user-markup on keyboard combos (CTRL+B for bold etc...) or by copy/pasted content (like i.e: from VSCode) with preserved stylings (at least it seems in Firefox, since they don't accept the "plaintext-only" contenteditable attribute value) - here's a variant that uses
<textarea>
. The only hack here is that textareas have by default a scrollbar we need to get rid of by auto-updating the textarea height dynamically: - set the scrollbars to the elements that now wraps numbers and hilite
- set the appropriate
height: max-content
to the numbers wrapper and to the hilite wrapper - don't just split by
\n
newline. Resizing the browser the code might wrap. The split by newline would be just a bad idea. Instead you need to calculate the scrollHeight and line height and deduce from those values the number of lines - if you want your line numbers to perfectly match position with the code editor lines, than obviously you cannot let that text be anything else but the exact same line height, size and font family as the hilite's contenteditable and PRE's CODE ones.
- you're not reusing well your JS code, the
updateLineNumbers()
should be a function that accepts an Element or multiple elements, the elHTML, elCSS, elJS). So use them as an optional Array argument - When resizing the window you'll also have to recall that
updateLineNumbers
function - Added a better Tab key handler function
tabToSpaces
to handle 4 spaces insertion instead of yoursubstring
solution
Related: https://stackoverflow.com/a/77832699/383904
My suggestion (without offtopic HTML/CSS and commented out unused JS stuff):
// Helper functions
const el = (sel, par = document) => par.querySelector(sel);
const els = (sel, par = document) => par.querySelectorAll(sel);
// Code Highlighter App:
const elHTML = el(`[data-lang="html"]`);
const elCSS = el(`[data-lang="css"]`);
const elJS = el(`[data-lang="js"]`);
const elPreview = el("#preview");
const updateLineNumbers = (elLines, elTextarea) => {
requestAnimationFrame(() => {
const scrollHeight = elTextarea.offsetHeight;
const lineHeight = parseFloat(getComputedStyle(elTextarea).lineHeight);
const totLines = Math.round(scrollHeight / lineHeight);
elLines.innerHTML = "<span></span>".repeat(totLines);
});
};
const tabToSpaces = (evt) => {
if (evt.key !== "Tab") return;
const spaces = " ".repeat(4);
evt.preventDefault(); // this will prevent us from tabbing out of the editor
document.execCommand("insertHTML", false, spaces);
};
const updateTextareaHeight = (elTextarea) => {
elTextarea.style.height = 0;
elTextarea.style.height = elTextarea.scrollHeight + "px";
};
const hilite = (elCode, value) => {
elCode.textContent = value;
delete elCode.dataset.highlighted;
hljs.highlightElement(elCode);
};
const preview = () => {
// const encodedHTML = encodeURIComponent(elHTML.value);
// const encodedCSS = encodeURIComponent(`<style>${elCSS.value}</style>`);
// const encodedJS = encodeURIComponent(`<scr` + `ipt>${elJS.value}</scr` + `ipt>`);
// const dataURL = `data:text/html;charset=utf-8,${encodedCSS + encodedHTML + encodedJS}`;
// elPreview.src = dataURL;
};
const makeEditor = (elEditor) => {
const elTextarea = el(".editor-textarea", elEditor);
const elCode = el(".editor-highlight > code", elEditor);
const elLines= el(".editor-lines", elEditor);
elTextarea.addEventListener("keydown", (evt) => {
tabToSpaces(evt);
});
elTextarea.addEventListener("input", () => {
updateTextareaHeight(elTextarea);
hilite(elCode, elTextarea.value);
updateLineNumbers(elLines, elTextarea);
preview();
});
window.addEventListener("resize", () => {
updateLineNumbers(elLines, elTextarea);
}, true);
updateTextareaHeight(elTextarea);
hilite(elCode, elTextarea.value);
updateLineNumbers(elLines, elTextarea);
};
// Initialize!
els(".editor").forEach(makeEditor);
preview();
*,
*::after,
*::before {
margin: 0;
box-sizing: border-box;
}
body {
font: 1em/1.4 sans-serif;
}
::-webkit-scrollbar {
width: 5px;
height: 5px;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
}
::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.3);
border-radius: 1rem;
}
.editor {
--pad: 0.5rem;
display: flex;
height: 120px;
/* DEMO ONLY */
overflow: auto;
background: #1e1e1e;
}
.editor-area {
position: relative;
padding: var(--pad);
height: max-content;
min-height: 100%;
width: 100%;
}
.editor-highlight code,
.editor-textarea {
padding: 0rem !important;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: transparent;
outline: 0;
}
.editor-highlight code,
.editor-textarea,
.editor-lines {
white-space: pre-wrap;
font: normal normal 14px/1.4 monospace;
}
.editor-textarea {
display: block;
position: relative;
overflow: hidden;
resize: none;
width: 100%;
color: transparent; /* Make text invisible */
caret-color: hsl(50, 75%, 70%); /* But keep caret visible */
border: 0;
&:focus {
outline: transparent;
}
&::selection {
background: hsla(0, 100%, 75%, 0.2);
}
}
.editor-highlight {
position: absolute;
left: var(--pad);
right: var(--pad);
user-select: none;
margin-bottom: 0;
min-width: 0;
}
.editor-lines {
display: flex;
flex-direction: column;
text-align: right;
height: max-content;
min-height: 100%;
color: hsl(0 100% 100% / 0.6);
padding: var(--pad); /* use the same padding as .hilite */
overflow: visible !important;
background: hsl(0 100% 100% / 0.05);
margin-bottom: 0;
& span {
counter-increment: linenumber;
&::before {
content: counter(linenumber);
}
}
}
/* highlight.js customizations: */
.hljs {
background: none;
}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs2015.css" />
<link rel="stylesheet" type="text/css" href="index.css" />
<h3>HTML</h3>
<div class="editor">
<pre class="editor-lines"></pre>
<div class="editor-area">
<pre class="editor-highlight"><code class="language-html"></code></pre>
<textarea class="editor-textarea" data-lang="html" spellcheck="false" autocorrect="off" autocapitalize="off"><h1 class="heading" title="test this" data-some='foo bar'>This is a Heading</h1>
<p>This is a paragraph.</p></textarea>
</div>
</div>
<h3>CSS</h3>
<div class="editor">
<pre class="editor-lines"></pre>
<div class="editor-area">
<pre class="editor-highlight"><code class="language-css"></code></pre>
<textarea class="editor-textarea" data-lang="css" spellcheck="false" autocorrect="off" autocapitalize="off">body {
background: gold;
}</textarea>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="index.js"></script>
The above is not thoroughly tested. So make sure to do so.
One additional (but hard) improvement would be to make the numbers elements as tall as the parent, so even with a code of 30000 lines you would have as much as N SPANS to cover the parent height. On scroll you can move them in "snap-reset" and change the line values. I think this approach might be more performant to the browser than bloating the DOM with additional 30000 empty SPAN elements. Just as a food for thought.
Answered By - Roko C. Buljan
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.