1

I wanted to add line numbering to a textarea.

I get the number of lines by using

textAreaElement.value.split("\n").length;

and maintain a div with span elements that use the count as content on before to add line numbers.

.line-number {
    width: 100%;
    display: block;
    text-align: right;
    line-height:1.5em;
    border-bottom: thin;
    font-family:'CascadiaCode Nerd Font', monospace;
    font-size: 2rem;
    color: #fff;
    opacity: 0.8;
    padding: 0 0.4em;
}
.line-number::before {
    counter-increment: line;
    content: counter(line);
    font-size: 1em;
    user-select: none;
}

However this has a fixed height, I want to implement this for a text area with word wrap (no horizontal scrollbar), where every line can technically have multiple lines terminated by a "\n".

My line of thought was to prepare an array of heights for each lines but I have no idea how to get the height of each separate line.

update: I switched to an editable div, but i want my line number to be of the same height as its corresponding div.

const   lineEnum    =   {
    state: false,
    count:  0,
    gutter: document.getElementsByClassName("line-numbers")[0],
    update: (box)   => {
        let delta   =   box.children.length -   lineEnum.count;
        if (box.children.length ==  0)  delta++;
        
        console.log({
            delta:  delta,
            count:  lineEnum.count,
            length: box.children.length,
        });
        if (delta   >   0   &&  lineEnum.state) {
            const   frag = document.createDocumentFragment();
            while   (delta  >   0) {
                const   line_number =   document.createElement("span");
                line_number.className   =   "line-num";
                frag.appendChild(line_number);
                lineEnum.count++;
                delta--;
            }
            
            lineEnum.gutter.appendChild(frag);
        }   else {
            if (lineEnum.count  +   delta   === 0) delta++;
            while   (delta < 0 && lineEnum.gutter.lastChild) {
                lineEnum.gutter.removeChild(lineEnum.gutter.lastChild);
                lineEnum.count--;
                delta++;
            }
        }
    },
    init:   (box)   => {
        if (lineEnum.state) return;
        lineEnum.state = true;
        lineEnum.update(box);
    },
    remove: (box)   => {
        if  (!lineEnum.state || !lineEnum.gutter.firstChild) return;
        lineEnum.gutter.innerHtml   =   "";
        lineEnum.state  =   false;
    },
};

const   callback = (mutationList,   observer)   => {
    let mutation =  mutationList[mutationList.length - 1];
    if  (mutation.type === "childList") {
        console.log(mutation);
        lineEnum.update(mutation.target);
    }
};

const   observer = new MutationObserver(callback);
const   config = { childList:   true };

const   editor = document.getElementsByClassName("code-input")[0];
observer.observe(editor, config);

lineEnum.init(editor);
.window-body{
    position: fixed;
    height: 100%;
    top: 25px;
    width: 100%;
    display: flex;
}

.line-numbers {
    width: 5em;
    padding: 0;
    height: 100%;
    word-break: break-all;
    overflow: hidden;
    display: inline-block;
    counter-reset: line;
    background-color: gray;
    opacity: 0.8;
}

.line-num {
    width: 100%;
    display: block;
    text-align: middle;
    line-height:1.5em;
    border-bottom: thin;
    font-family:'Arial', monospace;
    font-size: 2rem;
    color: #fff;
    opacity: 0.8;
    padding: 0 1em;
}
.line-num::before {
    counter-increment: line;
    content: counter(line);
    font-size: 1em;
    user-select: none;
}

.code-input{
    margin: 0;
    border: 0;
    padding: 0;
    outline: 0;
    list-style: none;

    display: inline-block;
    flex-grow: 1;
    height: 100%;
    word-break: break-all;
    overflow: hidden;

    border:none;
    font-family:'Arial', monospace;
    font-size:2rem;
    background: white;
    white-space:pre-wrap;
    line-height:1.5em;
    word-wrap: break-word;
    resize:none;
}
<div class="window-body">
        <div class="line-numbers"></div>
        <div class="code-input" contenteditable="true"></div>
</div>

8
  • 1
    if you are building an editor, the text area wont be the best option instead use a div Commented Jul 4, 2020 at 16:30
  • 1
    Try editable divs Commented Jul 4, 2020 at 16:31
  • @NightKing Any examples? I am doing just that! Commented Jul 4, 2020 at 16:38
  • 1
    You can see an example on MDN Commented Jul 4, 2020 at 16:50
  • I have updated the question with an editable divs approach, my problem still persists. Commented Jul 4, 2020 at 21:06

3 Answers 3

5

Man I wasted so much time on using javascript when css magic would just have done the trick.

Here is how I did it,

body {
    background-color: #000;
    height: 100vh;
    width: 100vw;
    margin: 0px;
}

.editor-wrapper {
  height: 100vh;
  width: 100vw;
  overflow-y: auto;
    counter-reset: line;
}
.editor{
    margin: 0;
    border: 0;
    padding: 0;
    outline: 0;
    list-style: none;

    height: 100%;
    width: 100%;
    word-wrap: break-word;
    word-break: break-all;

    font-size:2rem;
    line-height: 1.5em;
    font-feature-settings: common-ligatures; 
    -ms-font-feature-settings: common-ligatures;
    color:rgba(255, 255, 255, 0.7);
    resize:none;

}

.editor div {
    padding-left: 5rem;
    position: relative;
}

.editor div::before {
    counter-increment: line;
    content: counter(line);
    font-size: 1em;
    user-select: none;
    width: 5rem;
    text-align: right;
    left: 0;

    position: absolute;
}
<div class="editor-wrapper">
    <div class="editor" contenteditable="true">
        <div></div>
    </div>
</div>

Sign up to request clarification or add additional context in comments.

Comments

0

If it's a code block with line numbers you want then https://www.prowaretech.com/Computer/JavaScript/AddLineNumbersToPre demonstrates this well.

    function addLineClass (pre) {
    var lines = pre.innerText.split("\n"); // can use innerHTML also
    while(pre.childNodes.length > 0) {
        pre.removeChild(pre.childNodes[0]);
    }
    for(var i = 0; i < lines.length; i++) {
        var span = document.createElement("span");
        span.className = "line";
        span.innerText = lines[i]; // can use innerHTML also
        pre.appendChild(span);
        pre.appendChild(document.createTextNode("\n"));
    }
}

Along with the CSS using the essential part:

pre span.line::before {
    content: counter(linecounter);

See https://jsfiddle.net/Abeeee/12cx5ruf/6/ for a running example

1 Comment

But this doesnt apply to text area? Or any text input field (like content editable)
-1

You can use a background image like so:

.lined-textarea {
  background: url(http://i.imgur.com/2cOaJ.png);
  background-attachment: local;
  background-repeat: no-repeat;
  padding-left: 35px;
  padding-top: 10px;
  border-color: #ccc;
  font-size: 13px;
  line-height: 16px;
  resize: none;
}
<textarea rows="8" cols="30" class="lined-textarea"></textarea>

2 Comments

This is a cheat - you're just putting an image (i.imgur.com/2cOaJ.png) of numbers up on the side. Surely there's a better way
@user1432181 I already mentioned in the answer that a background image is used. For simple use cases, this method works.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.