Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to extend selected text to include whole words

Lets say I have a sentence: "This is a test sentence." I want a user to be able to select subsections of the text, but without punctuation, or incomplete words.

So "test sentence." should become "test sentence" and "est sentenc" should also become "test sentence"

Here is my current code:

var getSelectedText = function() {
    var text = "";
    if (window.getSelection) {
        text = window.getSelection().toString().trim();
    } else if (document.selection && document.selection.type != "Control") {
        text = document.selection.createRange().text;
    }
    return text;
}

fyi: jQuery code is ok.

EDIT for Bender:

Ok this is almost there. I have over 50k sentences and the user selection is variable, so I'll have to do something like this:

var selection = getSelectedText();
var exp = new RegExp("\\w*" + selection + "\\w+");
text.match(exp);

However, this won't match if a user DOES select "test sentence" which is more likely than not.

like image 337
Peter R Avatar asked May 04 '15 13:05

Peter R


2 Answers

Interesting challenge.

The code below wraps the selection in a span with class selected.

It grabs the nodeValue of the previousSibling and nextSibling of the new element – getting the appropriate text by splitting on non-word characters, and popping (previousSibling) or shifting (nextSibling).

It then removes the selected span (while leaving its contents). This has to be done within a timeout so that the element has time to be added to the DOM.

At this point, we're left with three adjacent text nodes. The code joins them by calling normalize() on the document body.

$(document).mouseup(function() {
  alert(getSelectedText());
});

var getSelectedText = function() {
  var el= document.createElement('span'),
      sel= window.getSelection().getRangeAt(0),
      prev= '',
      next= '';
  
  el.className= 'selected';
  sel.surroundContents(el);
  if(!sel.toString().match(/^\W/)) {
    prev= el.previousSibling.nodeValue;
    if(prev.match(/\W$/)) {
      prev= '';
    }
    else {
      prev= prev.split(/\W/).pop();
    }
  }
  if(!sel.toString().match(/\W$/)) {
    next= el.nextSibling.nodeValue;
    if(next.match(/^\W/)) {
      next= '';
    }
    else {
      next= next.split(/\W/).shift();
    }
  }
  setTimeout(function() {
    $('.selected').contents().unwrap()
    $('.selected').remove();
    document.body.normalize();
  });
  return prev+sel.toString()+next;
}
.selected {
  color: red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
This is a test sentence.  Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
like image 112
Rick Hitchcock Avatar answered Nov 03 '22 22:11

Rick Hitchcock


I managed to get it working by adding spans to each word. The idea is that you will not be able to select less than one span/word, this way the entire span will be selected. This answer does not depend on jQuery.

function customSelect(target, onSelect) {
  var oldHTML = target.innerHTML;
  
  // this is the regex that wraps the words with spans:
  target.innerHTML = oldHTML.replace(/[\d\w']+[\s,.]*/g, '<span>$&</span>');
  var spans = target.querySelectorAll('span');
  
  // I used a basic blue/white style, but you can change it in the CSS
  // using the ".text-selected" selector 
  var alreadySelected = [];
  var setSpanOn = function(span) {
    alreadySelected.push(span);
    span.className = 'text-selected';
  };
  var setSpanOff = function(span) {
    span.className = '';
  };
  
  // here starts the logic
  var isSelecting = false;
  for (var i=0, l=spans.length; i<l; i++) {
    (function span_handlers(span, pos) {
    
      // when the user starts holding the mouse button
      span.onmousedown = function() {
        // deselect previous selection, if any:
        alreadySelected.splice(0).forEach(setSpanOff);
        
        // and enable selection:
        isSelecting = true;
        span.onmouseenter();
      };
      
      // the main logic, we check if we need to set or not this span as selected:
      span.onmouseenter = function() {
        if (!isSelecting)
          return;
        
        // if already selected
        var j = alreadySelected.indexOf(span);
        if (j >= 0) {
          // then deselect the spans that were selected after this span
          alreadySelected.splice(j+1).forEach(setSpanOff);
        }
        else {
          // else if is not the first, check if the user selected another word 
          // one line down or up. This is done by checking the indexes:
          if (alreadySelected.length) {
            var last = alreadySelected[alreadySelected.length-1];
            var posLast = [].indexOf.call(spans, last);
            var typeSibling = pos > posLast ? 'nextSibling' : 'previousSibling';
            while (1) {
              last = last[typeSibling];
              if (last !== span)
                setSpanOn(last);
              else break;
            }
          }
          setSpanOn(span);
        }
      };
      
      // when the user hold up the mouse button:
      span.onmouseup = function() {
        isSelecting = false;
        
        //  call the onSelect function passing the selected spans content:
        if (typeof onSelect === 'function') {
          var spansSelected = target.querySelectorAll('.text-selected');
          var text = [].map.call(spansSelected, function(span) {
            return span.textContent || '';
          }).join('').trim();
          onSelect(text);
        }
      };
    })(spans[i], i);
  }
};

// Usage:
var element = document.getElementById('target');
var onSelect = function(text) {
  console.log(text);
};
customSelect(element, onSelect);
#target {
  user-select: none;
}

.text-selected {
  background-color: blue;
  color: white;
}
<div id="target">
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
</div>

I documented the code with some comments, however if you have any doubts feel free to ask.

Hope it helps :)

like image 33
Washington Guedes Avatar answered Nov 03 '22 22:11

Washington Guedes