handling intersection between selection markings

Viewed 286

I have a mark button on UI, clicking which, any user selection is marked red. No problems here. I achieve this by document.execCommand("insertHTML")

But I have an additional requirement that if the new selection is created which is the intersection of old selections markings, old selection's red marking should disappear.

As an example:

In the following image: this and testing are marked. Now if I select his is tes from the start and click mark, old markings of this and testing should go away and only his is tes should be marked because there is an intersection.

enter image description here

code:

const button = document.getElementById("button");

button.addEventListener('click', ()=>{
 const s = window.getSelection();
  const selectionStr = s.toString();
  document.execCommand("insertHTML", false, `<span class="bg-red">${selectionStr}<span>`);
})
.bg-red {  
background: red;
}
<div contenteditable="true">
 this is testing  this is testing  this is testing
</div>

<button id="button">mark</button>

3 Answers

If you don't need IE support, you could use selection.containsNode: https://developer.mozilla.org/en-US/docs/Web/API/Selection/containsNode

This allows you to flag the nodes that are contained in the selection. You need to set the partialContainment flag to true, so it'll detect nodes that are only partially selected.

So in a first time you flag the nodes that are contained by selection with a specific class name. Then you do your execCommand to apply your style. Then, you deleted the tags that were flagged previously by setting outerHTML on these nodes to innerHTML. You'll keep the style you just applied, but will remove previous ones. Like this:

const button = document.getElementById("button");

button.addEventListener('click', () => {
  const s = window.getSelection();
  const selectionStr = s.toString();

  tagIntersection(s)
  document.execCommand("insertHTML", false, `<span class="bg-red">${selectionStr}<span>`);
  removeIntersection(s)
})

function tagIntersection(s) {
  const redSpans = document.getElementsByClassName('bg-red')
  for (let i = 0; i < redSpans.length; i++) {
    if (s.containsNode(redSpans[i], true)) {
      redSpans[i].classList.add('to-remove');
    }
  }
}

function removeIntersection(s) {
  // using querySelector because getElements returns a live nodelist 
  // which is a problem when you manipulate the nodes
  const toRemovespans = document.querySelectorAll('.to-remove')
  for (let i = 0; i < toRemovespans.length; i++) {
    toRemovespans[i].outerHTML = toRemovespans[i].innerHTML;
  }
}
.bg-red {
  background: red;
}
<div contenteditable="true" id="editableDiv">
  this is testing this is testing this is testing
</div>

<button id="button">mark</button>

You can find the startContainer & endContainer of the selected text with getRangeAt, and compare them with contentContainer. And if they are not equal with contentContainer, then remove the bg-red class.

const button = document.getElementById("button");

button.addEventListener('click', ()=>{
  let contentContainer = document.getElementById("contentContainer");
  const selection = window.getSelection();
  if (selection.rangeCount > 0){
    let startContainer =  selection.getRangeAt(0).startContainer.parentNode;
    let endContainer =  selection.getRangeAt(0).endContainer.parentNode;
    if(startContainer != contentContainer)
      startContainer.classList.remove('bg-red')
    if(endContainer != contentContainer)
      endContainer.classList.remove('bg-red')
  }
  const selectionStr = selection.toString();
  document.execCommand("insertHTML", false, `<span class="bg-red">${selectionStr}<span>`); 
})
.bg-red {  
  background: red;
}
<div id="contentContainer" contenteditable="true">
 this is testing  this is testing  this is testing
</div>

<button id="button">mark</button>

I've tried to detect if 'getSelection'.anchorNode.parentNode has class bg-red, and replace it, but too many side-effects appear.

So, the solution with regexp...

(*1) — If you inspect the code of content-editable element, you will see it's structure: Each new line is a separate div. You must replace each new-line symbol \n with "line-breaking" HTML, to allow multi-line selections.

(*2) — Browser inserts it's own corrections to HTML and prevents correct replacements. I was forced to insert any Non-HTML string, which is not likely to appear in text. And after all replacements - insert valid HTML.

(*3) — I recommend to analyze all regular expressions here → https://regex101.com/r/j88wc0/1 The main idea is to replace all double appearances of <span class="bg-red"> anything instead of closing span <span class="bg-red"> or </span> anything instead of starting span tag </span>.

Note, that this code refreshes all innerHTML, each time you hit the button. It will not work as well with text from few thousand lines)

let button = document.getElementById("button");
let block = document.getElementById("block");

button.addEventListener('click', function(){
  let s = window.getSelection();
  let str = s.toString().replace(/\n/g,'</span></div><div><span class="bg-red">'); // (*1)

  document.execCommand("insertHTML", false, `bubufication`); // (*2)

  block.innerHTML = block.innerHTML
    .replace(/bubufication/, `<span class="bg-red">${str}</span>`)
    .replace(/<span class="bg-red">(.*?)<\/span><span class="bg-red">/g,'$1<span class="bg-red">')
    .replace(/<span class="bg-red">(((?!<\/span>).)*?<span class="bg-red">)/g,"$1")
    .replace(/(<\/span>((?!<span class="bg-red">).)*)<\/span>/g,"$1"); // (*3)
});
.bg-red {
  background-color: red;
}
<div id="block" contenteditable="true">
  <div>this is testing  this is testing  this is testing</div>
  <div>this is testing  this is testing  this is testing</div>
</div>

<button id="button">mark</button>

Related