Given a text selection, get preceding/following character



I am struggling to find a solution to this. My searches usually end with people saying, it’s a very complicated matter — but don’t really offer insight in achieving it.

Take for example the following HTML

<p>
    This is a test, blah blah,
</p>
<p>
    Category: HVAC
</p>
<span>
    <br />
    Location Address:
    <br />
    <span>123 Main St</span><i>,</i> New York, NY 10001
</span>

When rendered looks something like this

This is a test, blah blah,

Category: HVAC


Location Address:
123 Main St, New York, NY 10001

If a user selects the phrase “New York”, I would like to have a javascript routine that gives me 2 outputs:

Preceding character: ‘,’

Following character: ‘,’

Or, if a user selects the phrase “York”, I would like to have a javascript routine that gives me 2 outputs:

Preceding character: ‘w’

Following character: ‘,’

In essence, given a users browser selection, I’d like to get the first non-white space character prior to their selection and after their selection.

In simple cases, if the text selection is contained to a single html element; this is seemingly a trivial matter by converting the “containing” textnode into an array of characters and looping to get the desired results.

But when text selections span different HTML elements (like the first input/output example above), I am getting dizzy on figuring it out.

I’ve tried using libraries like rangy — but they don’t seem to offer much help in solving the multi-range selection example.

I’ve tried wrapping every “word” with a span (https://stackoverflow.com/a/66380709/14949005), so that I could then use jquery to navigate with prev/next to the element in question — but the regex in that answer considers “York,” a whole word — therefore leaving me with “N” as the following character.

Update 1

Example of what I tried was requested. This only semi works for the second input/output example above. E.g., select “York”, and it will give two characters as the output (but the “following” character will be wrong). And if you select “New York” as a whole, it just fails.

$(document).ready(function () {

    var content = $("#content")[0];
    var htmlStr = content.innerHTML;
    htmlStr = htmlStr.replace(/(?<!(</?[^>]*|&[^;]*))([^s<]+)/g, '$1<span class="word">$2</span>');
    content.innerHTML = htmlStr;

    $("#add").click(function () {
        var firstRange = window.getSelection().getRangeAt(0);
        var precedingWord = $(firstRange.startContainer.parentNode).prev(".word")[0].innerText;
        var followingWord = $(firstRange.startContainer.parentNode).next(".word")[0].innerText;

        alert(precedingWord[precedingWord.length-1]);
        alert(followingWord[0]);
    });
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="field-controls">
    <button id='add'>Run</button>
</div>
Select some text below and press "Run" to see preceeding/following character of selection
<div id="content">

    <p>
      This is a test, blah blah,
    </p>
    <p>
      Category: HVAC
    </p>
    <span>
      <br />
      Location Address:
      <br />
      <span>123 Main St</span><i>,</i> New York, NY 10001
    </span>

</div>

Thank you,

Answer

Here is a working implementation. It is derived from R3FL3CT’s answer; I tried to just post this as an edit to his answer so he could get credit, but was told to post separately.

The solution works by getting the text form of the element in question (#content in this case), and then using the first range’s startContainer + startOffset to find where in the full content the selection began.

Once we know the start point of the selection from within the content text string, you can use any number of methods to get the next/preceding non-white space character.

I chose to just split the content in two arrays: leading & trailing characters. With these two arrays, I use the JS array.find to get the first character that is not white space (my predicate for non-white space is regex based).

I cannot guarantee this will work for all cases of selection across different HTML elements. But if all the ranges in question are textnodes it should work.

R3FL3CT, thank you for your answer — I would not have been able to arrive at this without it. I’m not sure why I couldn’t correct your answer and credit you, sorry.

$(document).ready(function() {

    var rawContent = $("#content").text();

  $("#add").click(function() {      
    var selection = window.getSelection();
    var range = selection.getRangeAt(0);
    var selectionString = range.toString();
        
    var indexofStartContainer = rawContent.indexOf(range.startContainer.textContent.trimEnd());
        
    var startIndex = indexofStartContainer + range.startOffset;
    var leadingCharacters = rawContent.slice(0,startIndex).split('').reverse();
    var trailingCharacters = rawContent.slice(startIndex+selectionString.length,rawContent.length).split('');       
   
    let precChar = leadingCharacters.find((letter)=> !/s/.test(letter));
    let follChar = trailingCharacters.find((letter)=> !/s/.test(letter));
    console.log(precChar, follChar)

  });
});
.no-select{
user-select:none;
pointer-events:none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="field-controls">
    <button id='add'>Run</button>
</div>
<span class="no-select">Select some text below and press "Run" to see preceeding/following character of selection</span>
<div id="content">

    <p>
      This is a test, blah blah,
    </p>
    <p>
      Category: HVAC
    </p>
    <span>
      <br />
      Location Address:
      <br />
      <span>123 Main St</span><i>,</i> New York, NY 10001
    </span>

</div>


Source: stackoverflow