Home > Software engineering >  In JavaScript range selection is it possible to prevent selection of a partial node?
In JavaScript range selection is it possible to prevent selection of a partial node?

Time:01-18

In JavaScript range selection is it possible to prevent selection of a partial node?

For example:

"The enormous cat sat on the very small rug."

A user might select "cat" and more often than not, their mouse selection is not that precise and includes the visible space either side as well, and thus the selection range nearly always includes "enormous" and "sat" which we do not want.

Each span contains a single word. The visible space in between words could be true whitespace inside a span tag, spans stacked with line-breaks, it could be padding, it also could be css word-space, or even a non-breaking space. Whichever way if the user's selection strays into another node unintentionally, the default is of course is to return the node as part of the selection.

How can this be avoided?

Any pointers gladly accepted.

Thank you kindly.

Example code:

<span id="a1">The </span>
<span id="a2">enormous</span>
<span id="a3"> cat </span><span id="a4">sat</span>
<span id="a5"> on </span><span id="a6" style="padding-right: 2px;">the</span>
<span id="a7">very </span><span id="a8">small </span><span id="a9">rug</span><span id="a10">. </span>

CodePudding user response:

Here is a script you can build on using Selection API

const container = document.getElementById("container");
const spans = container.querySelectorAll('span');
document.addEventListener('selectionchange', (e) => {
  const sel = window.getSelection();
  const start = sel.anchorNode.parentNode;
  const end = sel.focusNode.parentNode;
  const partialContainment = false;
  if (start != end) {
    console.log("Words: Start", start.textContent, "End:", end.textContent);
    let started = false;
    spans.forEach(span => {
      span.classList = "";

      console.log("containsNode partial", span.id, span.textContent, ':', sel.containsNode(span, partialContainment));
      if (span === start) {
        span.classList.add('start');
        started = true;
      } else if (span === end) {
        span.classList.add('end');
        started = false;
      } else if (started) {
        span.classList.add('middle');
      }
    })
    const middleSpans = [...document.querySelectorAll('span.middle')].map(span => ({
      [span.id]: span.textContent.trim()
    }));
    console.log(JSON.stringify(middleSpans))
  }
})
.start {
  color: green
}

.middle {
  color: orange
}

.end {
  color: red
}
<div id="container">
  <span id="a1">The </span>
  <span id="a2">enormous</span>
  <span id="a3"> cat </span><span id="a4">sat</span>
  <span id="a5"> on </span><span id="a6" style="padding-right: 2px;">the</span>
  <span id="a7">very </span><span id="a8">small </span><span id="a9">rug</span><span id="a10">. </span>
</div>

CodePudding user response:

I think i have finally nailed it. Lets take start and end span from selection range and check if trimmed span content equals to trimmed span selection (that part that is actually selected in span). If there is only white space selected or partially selected text in span exclude it from selection. Check for parentNode !== SPAN is for case when only white space is selected.

$(function() {
$(document).on("mouseup", function(e) {
   const range = document.getSelection().getRangeAt(0);

   let start = range.startContainer.parentNode;
   let end   = range.endContainer.parentNode;

   if(start === end) {
       if(start.parentNode.nodeName !== "SPAN") {
           start = null;
           end = null;
       } else {
           const spanContent = start.textContent.slice().trim();
           const spanSelection = range.endContainer.textContent.slice(0, range.endOffset).trim()

           if(spanContent !== spanSelection) {
               start = null;
               end = null;
           }
       }
   } else {
       if(end.nodeName !== "SPAN") {
           end = range.endContainer.previousElementSibling;
       } else if(range.endOffset > 0) {
           const spanContent = range.endContainer.textContent.trim();
           const spanSelection = range.endContainer.textContent.slice(0,range.endOffset).trim()

           if(!spanSelection || spanContent !== spanSelection) {
               end = end.previousElementSibling;
           }
       }

       if(start.nodeName !== "SPAN") {
           start = range.startContainer.nextElementSibling;
       } else if(range.startOffset > 0) {
           const spanContent = range.startContainer.textContent.trim();
           const spanSelection = range.startContainer.textContent.slice(range.startOffset).trim()

           if(!spanSelection || spanContent !== spanSelection) {
               start = start.nextElementSibling;
           }
       }
   }

   if(start) {
       const x = $(start).index();
       const y = $(end).index();

       const childs = $(start.parentNode).children();
       const result = [];

       for(let i=x; i<=y; i  ) {
           result.push(childs.eq(i).text().trim());
       }

       console.log(result);
   }
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<div id="test">
            <span id="a1">The </span>
            <span id="a2">enormous</span>
            <span id="a3"> cat </span><span id="a4">sat</span>
            <span id="a5"> on </span><span id="a6" style="padding-right: 2px;">the</span>
            <span id="a7">very </span><span id="a8">small </span><span id="a9">rug</span><span id="a10">. </span>
        </div>

  • Related