7

I'm trying to get a character offsets for the start and end of a selection within an article. However, I would like some elements to be ignored from this process.

In my application, the server is working with the HTML before the JS injects a few elements dynamically and it's quite important that the number of characters in the text remains consistent between the server and client.

I hoped it would be as simple as window.getSelection() along with user-select: none;. Sadly, although the text doesn't look selected it's still included in the selection's range.

I've written a short example below. I've had a shot at writing a removeFromSelection as a workaround without much success. Maybe I need to remove ranges that overlap .unselectable and manually fill in the gaps with brand new range objects. I'm getting the feeling this should be easier than I'm making it. How should I be doing it?

function findAncestorOffset(container, node, offset)
{
 if (node == container)
  return offset;
 var parent = node.parentNode;
 var syblings = parent.childNodes;
 for (var i = 0, len = syblings.length; i < len; i++)
 {
  if (syblings[i] == node)
   break;
  offset += $(syblings[i]).text().length;
 }
 return findAncestorOffset(container, parent, offset);
}

function removeFromSelection(selector, selection)
{
 $(selector).each(function(i, node){
  var range = document.createRange();
  range.selectNodeContents(node);
  selection.removeAllRanges(range);
 });
}

var onSelect = function(){
  var sel = window.getSelection();
  //removeFromSelection('.unselectable', sel);
  var text = sel.toString();
  $('#out').text(text);
  var range = sel.getRangeAt(0).cloneRange();
  var i = findAncestorOffset($('.article')[0], range.startContainer, range.startOffset);
  $('#from').text(i);
  $('#to').text(i + text.length);
}
$('.article').mouseup(onSelect);
$('.article').focusout(onSelect);
.unselectable {
 -webkit-touch-callout: none;
 -webkit-user-select: none;
 -khtml-user-select: none;
 -moz-user-select: none;
 -ms-user-select: none;
 user-select: none;
}

pre, .article {
  border: solid 1px black;
  padding: 12px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<h1>Select This</h1>
<div class="article">
<p>
  This can be selected.
</p>
<p class="unselectable">
  This can't.
</p>
<p>
  And this can again.
</p>
</div>

<h1>Output</h1>
<div>
  From: <span id="from"></span>, To: <span id="to"></span>
</div>
<pre id="out">
</pre>
Frits
  • 7,341
  • 10
  • 42
  • 60
jozxyqk
  • 16,424
  • 12
  • 91
  • 180
  • 1
    You should get the selected HTML: http://stackoverflow.com/questions/5222814/window-getselection-return-html Then you could filter it out, e.g: http://jsfiddle.net/xpca4fp5/ But this would need more testing as i think this could be buggy – A. Wolff Dec 11 '15 at 12:31

1 Answers1

0

Well, it's not a full solution, but it's a start. The idea is to iterate over each paragraph element, and if it's unselectable then remove it from the selection. I convert the selection to an array of characters and pad it at the start so that the index matches the index within the entire document.

I didn't pay any attention to the final to and from values, they're probably not right - but more importantly, if you select the entire text you can see that it's off by a few characters on the second unselectable block. I'm out of time to fiddle with it, but maybe someone else can pick up where I left off.

function findAncestorOffset(container, node, offset)
{
 if (node == container)
  return offset;
 var parent = node.parentNode;
 var syblings = parent.childNodes;
 for (var i = 0, len = syblings.length; i < len; i++)
 {
  if (syblings[i] == node)
   break;
  offset += $(syblings[i]).text().length;
 }
 return findAncestorOffset(container, parent, offset);
}

var onSelect = function(){
  var sel = window.getSelection();

  var textArray = sel.toString().split('');
  var range = sel.getRangeAt(0).cloneRange();
  var from = findAncestorOffset($('.article')[0], range.startContainer, range.startOffset);
  var to = from + textArray.length;
  textArray = (new Array(from)).concat(textArray);
  
  var i = 0;
  $('.article p').each((_, rawElement) => {
    var element = $(rawElement);
    var sectionStart = i;
    var lengthOfSection = element.text().length;
    if(element.hasClass('unselectable')) {
      textArray.splice(sectionStart, lengthOfSection);
    } else {
      i += lengthOfSection;
    }
  });
  
  var text = textArray.join('');
  
  $('#from').text(from);
  $('#to').text(to);
  $('#out').text(text);
}
$('.article').mouseup(onSelect);
$('.article').focusout(onSelect);
.unselectable {
 -webkit-touch-callout: none;
 -webkit-user-select: none;
 -khtml-user-select: none;
 -moz-user-select: none;
 -ms-user-select: none;
 user-select: none;
}

pre, .article {
  border: solid 1px black;
  padding: 12px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<h1>Select This</h1>
<div class="article">
<p>
  This can be selected.
</p>
<p class="unselectable">
  This can't.
</p>
<p>
  And this can again.
</p>
<p>
  This can be selected.
</p>
<p class="unselectable">
  This can't.
</p>
<p>
  And this can again.
</p>
</div>

<h1>Output</h1>
<div>
  From: <span id="from"></span>, To: <span id="to"></span>
</div>
<pre id="out">
</pre>
alexanderbird
  • 3,847
  • 1
  • 26
  • 35