Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 968
Henrik Petterson Avatar asked Feb 14 '16 11:02

Henrik Petterson


People also ask

How do you highlight highlighted text and keep it?

If you want to highlight a whole line of text, move your cursor to the start of the line, hold Shift , and then press Down arrow . You may also use the shortcut key combination Shift + End . If you want to highlight all text (the entire page), press the shortcut key Ctrl + A .

What is the purpose of using highlighted words in a text?

The purpose of highlighting is to draw attention to important information in a text. Effective highlighting is effective because it first asks the reader to pick out the important parts, and then gives an effective way to review that information later.


1 Answers

 

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, i.e. 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(/^(?:\r\n|[\r\n])|\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(/^(?:\r\n|[\r\n])|\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]*((?:\r\n|[\r\n])|\s{1,})[^\s]*$/.test(rangeTryLeft.toString()))
            range.setStart(nAp,oAp);
          else if(/^[^\s]*((?:\r\n|[\r\n])|\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.

like image 163
Arman Ozak Avatar answered Oct 15 '22 09:10

Arman Ozak