If a word is highlighted and user clicks the connecting word, highlight both

I recently posted a question asking for a way to highlight words smarter by:

  • Single-click highlights the whole word (default behavior is double-click).

  • Click-drag will hightlight full words/terms only.

  • Beautiful solution was posted by Arman.

    jsFiddle for testing.

    My aim with this question is to allow the user to single-click two or more connecting words and highlight them (extend the range of the highlight).

    To demonstrate. If world, is selected by the cursor:

    Hello world, lorem ipsum attack on titan.

    And user clicks on lorem , it should select both words like this:

    Hello world, lorem ipsum attack on titan.

    Same behavior if user clicks Hello .

    So it only extends the highlight if the word is connecting. Example, if worlds, is selected, and user clicks on ipsum , it should just select ipsum .

    What's the approach to extend the highlight reach?

    Code in jsFiddle is:

    jQuery(document).ready(function(e){
    
        (function(els){
            for(var i=0;i<els.length;i++){
                var el = els[i];
                el.addEventListener('mouseup',function(evt){
                    if (document.createRange) { // Works on all browsers, including IE 9+
                        var selected = window.getSelection();
                        /* if(selected.toString().length){ */
                        var d = document,
                            nA = selected.anchorNode,
                            oA = selected.anchorOffset,
                            nF = selected.focusNode,
                            oF = selected.focusOffset,
                            range = d.createRange();
    
                        range.setStart(nA,oA);
                        range.setEnd(nF,oF);
    
                        // Check if direction of selection is right to left
                        if(range.startContainer !== nA || (nA === nF && oF < oA)){
                            range.setStart(nF,oF);
                            range.setEnd(nA,oA);
                        }
    
                        // Extend range to the next space or end of node
                        while(range.endOffset < range.endContainer.textContent.length && !/s$/.test(range.toString())){
                            range.setEnd(range.endContainer, range.endOffset + 1);
                        }
                        // Extend range to the previous space or start of node
                        while(range.startOffset > 0 && !/^s/.test(range.toString())){
                            range.setStart(range.startContainer, range.startOffset - 1);
                        }
    
                        // Remove spaces
                        if(/s$/.test(range.toString()) && range.endOffset > 0)
                            range.setEnd(range.endContainer, range.endOffset - 1);
                        if(/^s/.test(range.toString()))
                            range.setStart(range.startContainer, range.startOffset + 1);
    
                        // Assign range to selection
                        selected.addRange(range);
    
                        el.style.MozUserSelect = '-moz-none';
                        /* } */
                    } else {
                        // Fallback for Internet Explorer 8 and earlier
                        // (if you think it still is worth the effort of course)
                    }
                });
    
                /* This part is necessary to eliminate a FF specific dragging behavior */
                el.addEventListener('mousedown',function(){
                    if (window.getSelection) {  // Works on all browsers, including IE 9+
                        var selection = window.getSelection ();
                        selection.collapse (selection.anchorNode, selection.anchorOffset);
                    } else {
                        // Fallback for Internet Explorer 8 and earlier
                        // (if you think it still is worth the effort of course)
                    }
                    el.style.MozUserSelect = 'text';
                });
            }
        })(document.getElementsByClassName('taggable'));
    
    });
    

    HTML:

    <p class="taggable">
       Hello world, lorem ipsum attack on titan.
    </p>
    
    <p>
       JS doesn't affect this text. 
    </p>
    

    Bounty info

    Rewarding the existing answer because it's profoundly useful. No need to post more solutions as this one is as complete as it gets.


    UPGRADE

    Ok, I am putting this at top, because it is a major update and, I believe, can even be considered as an upgrade on the previous function.

    The request was to make the previous function work in reverse, ie when a highlighted word is clicked again, it would be removed from the total selection.

    The challenge was that when a highlighted word at the edge of <p> and </p> tags or the edge of <b> and </b> tags inside the paragraphs was clicked, the startContainer or endContainer of the range had to be carried into or out of the current element they were positioned and the startOffset or endOffset had to be reset as well. I am not sure if this has been a clear expression of the problem, but, in brief, due to the way Range objects work, the words closest to HTML tags proved to be quite a challenge.

    The Solution was to introduce a few new regex tests, several if checks, and a local function for finding the next/previous sibling. During the process, I have also fixed a few things which had escaped my attention before. The new function is below and the updated fiddle is here.

    (function(el){
      // variable declaration for previous range info
      // and function for finding the sibling
        var prevRangeInfo = {},
        findSibling = function(thisNode, direction){
          // get the child node list of the parent node
          var childNodeList = thisNode.parentNode.childNodes,
            children = [];
    
            // convert the child node list to an array
            for(var i=0, l=childNodeList.length; i<l; i++) children.push(childNodeList[i]);
    
            return children[children.indexOf(thisNode) + direction];
        };
    
        el.addEventListener('mouseup',function(evt){
            if (document.createRange) { // Works on all browsers, including IE 9+
    
                var selected = window.getSelection();
          // Removing the following line from comments will make the function drag-only
                /* if(selected.toString().length){ */
                    var d = document,
                        nA = selected.anchorNode,
                        oA = selected.anchorOffset,
                        nF = selected.focusNode,
                        oF = selected.focusOffset,
                        range = d.createRange(),
              rangeLength = 0;
    
                    range.setStart(nA,oA);
                    range.setEnd(nF,oF);
    
                    // Check if direction of selection is right to left
                    if(range.startContainer !== nA || (nA === nF && oF < oA)){
                        range.setStart(nF,oF);
                        range.setEnd(nA,oA);
                    }
    
                    // Extend range to the next space or end of node
                    while(range.endOffset < range.endContainer.textContent.length && !/s$/.test(range.toString())){
                        range.setEnd(range.endContainer, range.endOffset + 1);
                    }
                    // Extend range to the previous space or start of node
                    while(range.startOffset > 0 && !/^s/.test(range.toString())){
                        range.setStart(range.startContainer, range.startOffset - 1);
                    }
    
                    // Remove spaces
                    if(/s$/.test(range.toString()) && range.endOffset > 0)
                        range.setEnd(range.endContainer, range.endOffset - 1);
                    if(/^s/.test(range.toString()))
                        range.setStart(range.startContainer, range.startOffset + 1);
    
            // Store the length of the range
            rangeLength = range.toString().length;
    
            // Check if another range was previously selected
            if(prevRangeInfo.startContainer && nA === nF && oA === oF){
                var rangeTryContain = d.createRange(),
                rangeTryLeft = d.createRange(),
                rangeTryRight = d.createRange(),
                nAp = prevRangeInfo.startContainer;
                oAp = prevRangeInfo.startOffset;
                nFp = prevRangeInfo.endContainer;
                oFp = prevRangeInfo.endOffset;
    
              rangeTryContain.setStart(nAp, oAp);
              rangeTryContain.setEnd(nFp, oFp);
              rangeTryLeft.setStart(nFp, oFp-1);
              rangeTryLeft.setEnd(range.endContainer, range.endOffset);
              rangeTryRight.setStart(range.startContainer, range.startOffset);
              rangeTryRight.setEnd(nAp, oAp+1);
    
              // Store range boundary comparisons
              // & inner nodes close to the range boundary --> stores null if none
              var compareStartPoints = range.compareBoundaryPoints(0, rangeTryContain) === 0,
                compareEndPoints = range.compareBoundaryPoints(2, rangeTryContain) === 0,
                leftInnerNode = range.endContainer.previousSibling,
                rightInnerNode = range.startContainer.nextSibling;
    
              // Do nothing if clicked on the right end of a word
              if(range.toString().length < 1){
                range.setStart(nAp,oAp);
                range.setEnd(nFp,oFp);
              }
    
              // Collapse the range if clicked on last highlighted word
              else if(compareStartPoints && compareEndPoints)
                range.collapse();
    
              // Remove a highlighted word from left side if clicked on
              // This part is quite tricky!
              else if(compareStartPoints){
                range.setEnd(nFp,oFp);
    
                if(range.startOffset + rangeLength + 1 >= range.startContainer.length){
                  if(rightInnerNode)
                    // there is a right inner node, set its start point as range start
                    range.setStart(rightInnerNode.firstChild, 0);
    
                  else {
                    // there is no right inner node
                    // there must be a text node on the right side of the clicked word
    
                    // set start of the next text node as start point of the range
                    var rightTextNode = findSibling(range.startContainer.parentNode, 1),
                        rightTextContent = rightTextNode.textContent,
                        level=1;
    
                    // if beginning of paragraph, find the first child of the paragraph
                    if(/^(?:rn|[rn])|s{2,}$/.test(rightTextContent)){
                        rightTextNode = findSibling(rightTextNode, 1).firstChild;
                      level--;
                    }
    
                    range.setStart(rightTextNode, level);
    
                  }
                }
                else
                  range.setStart(range.startContainer, range.startOffset + rangeLength + 1);
              }
    
              // Remove a hightlighted word from right side if clicked on
              // This part is also tricky!
              else if (compareEndPoints){
                range.setStart(nAp,oAp);
    
                if(range.endOffset - rangeLength - 1 <= 0){
                  if(leftInnerNode)
                    // there is a right inner node, set its start point as range start
                    range.setEnd(leftInnerNode.lastChild, leftInnerNode.lastChild.textContent.length);
    
                  else {
                    // there is no left inner node
                    // there must be a text node on the left side of the clicked word
    
                    // set start of the previous text node as start point of the range
                    var leftTextNode = findSibling(range.endContainer.parentNode, -1),
                        leftTextContent = leftTextNode.textContent,
                        level = 1;
    
                    // if end of paragraph, find the last child of the paragraph
                    if(/^(?:rn|[rn])|s{2,}$/.test(leftTextContent)){
                        leftTextNode = findSibling(leftTextNode, -1).lastChild;
                      level--;
                    }
    
                    range.setEnd(leftTextNode, leftTextNode.length - level);
                  }
                }
                else
                  range.setEnd(range.endContainer, range.endOffset - rangeLength - 1);
              }
    
              // Add previously selected range if adjacent
              // Upgraded to include previous/next word even in a different paragraph
              else if(/^[^s]*((?:rn|[rn])|s{1,})[^s]*$/.test(rangeTryLeft.toString()))
                range.setStart(nAp,oAp);
              else if(/^[^s]*((?:rn|[rn])|s{1,})[^s]*$/.test(rangeTryRight.toString()))
                range.setEnd(nFp,oFp);
    
              // Detach the range objects we are done with, clear memory
              rangeTryContain.detach();
              rangeTryRight.detach();
              rangeTryLeft.detach();
            }
    
            // Save the current range --> not the whole Range object but what is neccessary
            prevRangeInfo = {
                startContainer: range.startContainer,
              startOffset: range.startOffset,
              endContainer: range.endContainer,
              endOffset: range.endOffset
            };
    
            // Clear the saved range info if clicked on last highlighted word
            if(compareStartPoints && compareEndPoints)
              prevRangeInfo = {};
    
            // Remove all ranges from selection --> necessary due to potential removals
            selected.removeAllRanges();
    
                    // Assign the current range as selection
                    selected.addRange(range);
    
            // Detach the range object we are done with, clear memory
            range.detach();
    
            el.style.MozUserSelect = '-moz-none';
    
          // Removing the following line from comments will make the function drag-only
                /* } */
    
            } else { 
               // Fallback for Internet Explorer 8 and earlier
               // (if you think it still is worth the effort of course)
            }
        });
    
      /* This part is necessary to eliminate a FF specific dragging behavior */
      el.addEventListener('mousedown',function(e){
        if (window.getSelection) {  // Works on all browsers, including IE 9+
             var selection = window.getSelection ();
           selection.collapse (selection.anchorNode, selection.anchorOffset);
        } else {
           // Fallback for Internet Explorer 8 and earlier
               // (if you think it still is worth the effort of course)
        }
        el.style.MozUserSelect = 'text';
      });
    })(document.getElementById('selectable'));
    


    BEFORE UPGRADE

    Storing the last range in an object and checking if the previously selected range is adjacent to the new range every time a new selection is made, does the job:

    (function(el){
        var prevRangeInfo = {};
        el.addEventListener('mouseup',function(evt){
            if (document.createRange) { // Works on all browsers, including IE 9+
    
                var selected = window.getSelection();
                /* if(selected.toString().length){ */
                    var d = document,
                        nA = selected.anchorNode,
                        oA = selected.anchorOffset,
                        nF = selected.focusNode,
                        oF = selected.focusOffset,
                        range = d.createRange();
    
                    range.setStart(nA,oA);
                    range.setEnd(nF,oF);
    
                    // Check if direction of selection is right to left
                    if(range.startContainer !== nA || (nA === nF && oF < oA)){
                        range.setStart(nF,oF);
                        range.setEnd(nA,oA);
                    }
    
                    // Extend range to the next space or end of node
                    while(range.endOffset < range.endContainer.textContent.length && !/s$/.test(range.toString())){
                        range.setEnd(range.endContainer, range.endOffset + 1);
                    }
                    // Extend range to the previous space or start of node
                    while(range.startOffset > 0 && !/^s/.test(range.toString())){
                        range.setStart(range.startContainer, range.startOffset - 1);
                    }
    
                    // Remove spaces
                    if(/s$/.test(range.toString()) && range.endOffset > 0)
                        range.setEnd(range.endContainer, range.endOffset - 1);
                    if(/^s/.test(range.toString()))
                        range.setStart(range.startContainer, range.startOffset + 1);
    
            // Check if another range was previously selected
            if(prevRangeInfo.startContainer){
                var rangeTryLeft = d.createRange(),
                rangeTryRight = d.createRange(),
                nAp = prevRangeInfo.startContainer;
                oAp = prevRangeInfo.startOffset;
                nFp = prevRangeInfo.endContainer;
                oFp = prevRangeInfo.endOffset;
              rangeTryLeft.setStart(nFp,oFp-1);
              rangeTryLeft.setEnd(range.endContainer,range.endOffset);
              rangeTryRight.setStart(range.startContainer,range.startOffset);
              rangeTryRight.setEnd(nAp,oAp+1);
    
              // Add previously selected range if adjacent
              if(/^[^s]*s{1}[^s]*$/.test(rangeTryLeft.toString())) range.setStart(nAp,oAp);
              else if(/^[^s]*s{1}[^s]*$/.test(rangeTryRight.toString())) range.setEnd(nFp,oFp);
            }
    
            // Save the current range
            prevRangeInfo = {
                startContainer: range.startContainer,
              startOffset: range.startOffset,
              endContainer: range.endContainer,
              endOffset: range.endOffset
            };
    
                    // Assign range to selection
                    selected.addRange(range);
    
            el.style.MozUserSelect = '-moz-none';
                /* } */
            } else { 
               // Fallback for Internet Explorer 8 and earlier
               // (if you think it still is worth the effort of course)
            }
        });
    
      /* This part is necessary to eliminate a FF specific dragging behavior */
      el.addEventListener('mousedown',function(e){
        if (window.getSelection) {  // Works on all browsers, including IE 9+
             var selection = window.getSelection ();
           selection.collapse (selection.anchorNode, selection.anchorOffset);
        } else {
           // Fallback for Internet Explorer 8 and earlier
               // (if you think it still is worth the effort of course)
        }
        el.style.MozUserSelect = 'text';
      });
    })(document.getElementById('selectable'));
    

    JS Fiddle here.

    Update (was done before upgrade):

    If you want to this feature to be effective when clicking but not dragging, all you have to do is to change the if(prevRangeInfo.startContainer) condition as follows:

    if(prevRangeInfo.startContainer && nA === nF && oA === oF){
        // rest of the code is the same...
    

    The updated JS Fiddle is here.

    链接地址: http://www.djcxy.com/p/32188.html

    上一篇: 动画UIStackView排列的子视图内容大小更改

    下一篇: 如果突出显示一个单词并且用户单击连接单词,则突出显示这两个单词