0

I have a div contentEditable=true where multi-line text is entered. Whenever the user press enter, I would like to inspect the line in which the user was to see what is the context of that line (preferably, of all lines).

Is it possible to have something like window.getSelection().lineContent ?

I can use window.getSelection().anchorNode.textContent, but it will only work for the current node (not the line). I'm assuming the user will press enter to go to next line and I'd like to know if next line should or not be indented (my main goal is to know whether there is a "tab" in the beginning of the line, so far).

EDIT: Current code:

document.getElementById('sampleeditor').addEventListener("keydown", fSubsTab );

function fSubsTab () {      
    e = window.event
    if ( false ) {
    } else if ( e.keyCode == 13 ) {
        e.preventDefault();
        if (!window.getSelection) return;
        sel = window.getSelection();
        node_offset = sel.anchorOffset
        node_text = sel.anchorNode.textContent

        // The problem is how would I get the content of the
        // current line between last line break and next one,
        // or until the end
    }
}

EDIT 2: SOLVED. See answer below.

calebmiranda
  • 156
  • 13

3 Answers3

0

If I understood your question correctly, you could get the desired behaviour by combining:

  1. document.activeElement to get the active text element
  2. document.activeElement.selectionStart to get the cursor position
  3. document.activeElement.value.split("\n")[line] to convert cursor to the active line

document.addEventListener('keyup', (e) => {
  if (e.code === 'Enter' || e.code === 'NumpadEnter') {
    if (document.activeElement.type === 'textarea') {
      let start = $(document.activeElement).prop("selectionStart");
      let line = document.activeElement.value.substr(0, document.activeElement.selectionStart).split("\n").length - 2;
      console.log(`Pressed enter on line ${line + 1}:`);
      console.log(document.activeElement.value.split("\n")[line]);
    }
  }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<textarea rows='5'>
foo
bar
foobar
barfoo
</textarea>
0stone0
  • 34,288
  • 4
  • 39
  • 64
  • I'm not using jQuery. But, anyway, I assume that would get just the active element. The div i'm working on is a contentEditable that may have a variety of styles inside. For example, one line may be a "boldred". So I cannot assume the active element represents the full context of the text. – calebmiranda Jan 10 '22 at 14:54
0

Based on all of the parameters/rules laid out in the original question, there are a few potential issues with how this can work.

The biggest issue is using the contentEditable attribute as the method to edit HTML. Other solutions/examples have you using things like activeElement and selectionStart. This would be fine but you indicated there can be mixed content, such as HTML which would alter the active element in some cases. Originally I thought of using the innerText or innerHTML values but this presented the main problem in a more obvious way.

document.getElementById('sampleeditor').addEventListener("keyup", function(e) {
    if(e.keyCode == 13) {
      console.log(this.innerHTML.split("<div>")[this.innerHTML.split("<div>").length-1]);
      console.log(this.innerText.split("\n")[this.innerText.split("\n").length-1]);
    }
});
<p>Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
<div id="sampleeditor" contentEditable=true>Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of <b><i>classical Latin literature</i></b> from 45 BC, making it over <span>2000 years old</span>.</div>
<p>The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested.</p>
<p>"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."</p>

In this example, using innerText allows us to easily separate each line but fails to grab any HTML code embedded within the main element. Using innerHTML allows us to get the HTML code as well, but due to how contentEditable works, the lines are broken up by a simple <div> tag, which could also be contained inside of the element and could cause conflict.


Alternative Method

I am not sure if there are any limited factors in what you are able to do with the code you have, but my suggestion would be to abandon contentEditable and instead load the contents of that element inside of a <textarea> element for editing. Combining this with the method from 0stone0, you seem to get your desired results.

The example below takes the contents of an element upon clicking it and places that inside of a <textarea>, replacing the contents of that element. Upon hitting the enter/return key in that <textarea> this returns the line number and text from the previous line. And upon clicking outside of the <textarea> the script takes the new code and places it back inside of the parent element.

document.querySelector("#sampleeditor").addEventListener("click", function() {
  _EditEl(this);
  this.removeEventListener('click', arguments.callee);
});

function _EditEl(pEl) {
  let curContent = pEl.innerHTML;
  pEl.innerHTML = `<textarea id="tmpEdit" style="width: 100%; height: 4em;">${curContent}</textarea>`;
  document.querySelector("#tmpEdit").focus();//
  document.querySelector("#tmpEdit").addEventListener("keyup", function(e) {
    if(e.keyCode == 13) {
      let start = document.activeElement.selectionStart;
      let line = document.activeElement.value.substr(0, document.activeElement.selectionStart).split("\n").length - 2;
      console.log(`Pressed enter on line ${line + 1}:`);
      console.log(document.activeElement.value.split("\n")[line]);
    }
  });
  
  document.querySelector("#tmpEdit").addEventListener("blur", function(e) {
    let parentEl = this.parentElement;
    parentEl.innerHTML = this.value;
    parentEl.addEventListener("click", function() {
      _EditEl(this);
      this.removeEventListener('click', arguments.callee);
    });
  });
}
<p>Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
<div id="sampleeditor">Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of <b><i>classical Latin literature</i></b> from 45 BC, making it over <span>2000 years old</span>.</div>
<p>The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested.</p>
<p>"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."</p>
EssXTee
  • 1,783
  • 1
  • 13
  • 18
  • TextArea does not allow rich text. So I cannot use it. And the first mode doesn't work because it doesn't consider the line of the current selection. – calebmiranda Jan 10 '22 at 19:12
0

I've solved it by using a custom function to generate a uuid, add it to the text and then remove it back. The code is a little messy because of the quantity of functions used, but it goes something like this

// function for replacing characters without case sensitivity
String.prototype.replaces = function(str, replace, incaseSensitive) {
    if(!incaseSensitive){
        return this.split(str).join(replace);
    } else { 
        // Replace this part with regex for more performance

        var strLower = this.toLowerCase();
        var findLower = String(str).toLowerCase();
        var strTemp = this.toString();

        var pos = strLower.length;

        

        while((pos = strLower.lastIndexOf(findLower, pos)) != -1){
            tcounter++
        
            strTemp = strTemp.substr(0, pos) + replace + strTemp.substr(pos + findLower.length);
            pos--;
            if (pos<0) {
                break
            }

        }
        return strTemp;
    }
};

// function for generating uuid to be used after
function uuidv4() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
    });
}

// function for dealing with linebreaks from innerhtml and returning as text with line breaks
function innerHTMLtoText ( inner ) {
        text = inner
        prelb = uuidv4() + uuidv4() + uuidv4() + uuidv4()
        
        prelb_list = [];

                    
        lb = prelb + "ddd" + prelb; prelb_list.push(lb);
        tag_ = "<div>"; text = text.replaces ( tag_, lb + tag_, true );
        tag_ = "</div>"; text = text.replaces ( tag_, lb + tag_, true );
        tag_ = "<div "; text = text.replaces ( tag_, lb + tag_, true );
        lb = prelb + "ppp" + prelb; prelb_list.push(lb);
        tag_ = "<p>"; text = text.replaces ( tag_, lb + tag_, true );
        tag_ = "</p>"; text = text.replaces ( tag_, lb + tag_, true );
        tag_ = "<p "; text = text.replaces ( tag_, lb + tag_, true );
        lb = prelb + "bbb" + prelb; prelb_list.push(lb);
        tag_ = "<br>"; text = text.replaces ( tag_, lb + tag_, true );
        tag_ = "<br "; text = text.replaces ( tag_, lb + tag_, true );
        // tag_ = "<br />"; text = text.replaces ( tag_, lb + tag_, true );
        tag_ = "<br/>"; text = text.replaces ( tag_, lb + tag_, true );
        tag_ = "</br>"; text = text.replaces ( tag_, lb + tag_, true );
        
        var temporalDivElement = document.createElement("div");
        temporalDivElement.innerHTML = text;
        text = temporalDivElement.textContent
        
        list_ = prelb_list
        for ( let i = 0 ; i<list_.length ; i++ ) {
            this_ = list_[i]
            
            text = text.replaces ( this_, "\n", true );
        }
        return text
    }

// main function, that generates a uuid, inserts at caret position, checks text of line, and removes the uuid 
document.getElementById('sampleeditor').addEventListener("keyup", function(e) {
    if(e.keyCode == 13) {
        texteditor = document.getElementById('sampleeditor')
  
        e.preventDefault();

        if (!window.getSelection) return;
            
        sel = window.getSelection();

        if (!sel.rangeCount) return;
    
        range = sel.getRangeAt(0);
        range.collapse(true);
            
        span = document.createElement('span');
        span.appendChild(document.createTextNode(' '));
        theSpanId = "span_" + uuidv4() + uuidv4() + uuidv4() + uuidv4()
        span.id = theSpanId
        outerInclude = span.outerHTML
            
        changeText = document.createElement('span');
        idToRemove = uuidv4() + uuidv4() + uuidv4() + uuidv4()
        changeText.innerHTML = idToRemove
        outerRemove = changeText.outerHTML
        range.insertNode(changeText);

        theinner = texteditor.innerHTML
        
        plainText = innerHTMLtoText ( theinner )
        
        posLineBreak = plainText.indexOf (idToRemove)
        textBefore = plainText.substring(0, posLineBreak)
        textBefore = textBefore.split ("\n")
        linePart1 = textBefore[textBefore.length-1]
            
        textAfter = plainText.substring(posLineBreak)
        textAfter = textAfter.split ("\n")
        linePart2 = textAfter[0]
        fullLine = linePart1 + linePart2

        fullLine = fullLine.split ( idToRemove ).join ("")
        texteditor.innerHTML  = texteditor.innerHTML.split(outerRemove).join("<br>" + outerInclude)

        range = sel.getRangeAt(0);
        range.collapse(true);

        span = document.getElementById(theSpanId)

        range.setStartAfter(span);
        range.collapse(true);

        var startNode = span.firstChild;
        var endNode = span.firstChild;

        var range = document.createRange();

        range.setStart(startNode, 0);

        range.setEnd(endNode, 0+1);
        var sel = window.getSelection();

        sel.removeAllRanges();
        sel.addRange(range);

    }
});
calebmiranda
  • 156
  • 13