0

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,

David G
  • 41
  • 6
  • This seems like a rather strange thing to want to do. What sort of problem does this solve or what feature does this serve? See [xy problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem/233676#233676) – ggorlen May 25 '21 at 03:11
  • 1
    And what code have you actually tried in order to do this? Please post it in your question. – dale landry May 25 '21 at 03:11
  • I mean, this is a fairly straightforward solution no? Find the starting index of the character, move back one character (excluding empty), and similarly, move forward one character. – Frontear May 25 '21 at 03:20
  • @dalelandry the "rangy" approach really didn't lead me to anything viable. I'll post what I have for the span-wrap approach -- but really it's just been me experimenting the the dev console. – David G May 25 '21 at 03:20
  • @Frontear, the nuance is that what is "rendered" to the user isn't what I have available to me via the typical text selection browser methods. And if I just get "innerText" on the whole document -- or something else, I loose all context into which word they selected. What if there are multiple "New York" phrases, or "York". So it's important I maintain the context. – David G May 25 '21 at 03:30
  • @dalelandry updated with my attempt. – David G May 25 '21 at 03:51
  • @ggorlen I am having troubling adding context into the "why" without revealing things that I'm not supposed to reveal about this project. I appreciate the perspective of the "xy problem"; in light of that I still feel that this is a legitimate ask. I don't mean to come off as disrespectful, and appreciate the time you took to comment. – David G May 25 '21 at 04:00
  • The issue isn't one of respect or legitimacy -- your question is totally respectful and legitimate. It's a matter of you getting a poor/hacky solution when the problem X isn't fully provided. People may humor you with approach Y becauase that's what you asked about, but an obvious, clear-cut better solution Z may exist that is missed because of a lack of context. But if you're under NDA I understand. – ggorlen May 25 '21 at 06:21

2 Answers2

1

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>
David G
  • 41
  • 6
0

I think I found a solution for you. It works fine for me, so try it out.

$(document).ready(function() {

  var content = $("#content").text().replace(/\s/g, '');

  $("#add").click(function() {
    var firstRange = window.getSelection().getRangeAt(0).toString().replace(/\s/g, '');
    let index = content.indexOf(firstRange)
    let precChar = content[index - 1]
    let follChar = content[index + firstRange.length]
    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>

It works by taking the text of your content div, and finding the index of your selected range. Then it computes the following and preceding characters. No DOM traversing really needed

R3FL3CT
  • 551
  • 3
  • 14
  • Wow, this is super close! Thank you so much for taking the time to look into this. If I select the second "blah" word, it gives me the preceding/following characters of the first blah. Tonight I will fiddle with this some more to see if I can't get it across the last mile. Thank you very much – David G May 25 '21 at 16:56
  • If it helped, could you click the green check next to the answer? And I think that's happening because it's looking for the first match. – R3FL3CT May 25 '21 at 16:57
  • Once I've had some time to fiddle with it, I will mark it for you. I just want to be sure this doesn't "bury" the context of the user selection. For example, it's getting first match -- but how would it know to instead get the second match? I can't currently do a deep dive on this, but will do so tonight. – David G May 25 '21 at 16:59
  • I think there's a property on the selection object you might be able to use. I'll edit my answer if I find anything. – R3FL3CT May 25 '21 at 17:06
  • I'm not quite there yet, but very close. I have to call it quits for tonight, but will pick this back up in a couple of days. Here is a JSFiddle of the latest progress: https://jsfiddle.net/oegtjruz/25/ Seems to work, except for when including the last line of content in the selection – David G May 26 '21 at 01:53
  • @DavidG you can add new answer to your question separately instead of modifying this one. – Pranav Singh May 27 '21 at 05:40