Surrounding Selected Text With Span Tags

I’m trying to surround highlighted text in a DIV with a span tag dynamically. I tried running my script in jsFiddle but doesn’t work. It some what works if I take the script, markups, etc., and paste them in an html file then view the result in Chrome Dev environment. Please see my code here and help point out why it’s not working as it should in Chrome.

Hi @liagapi555, not sure what you mean with chrome dev environment but that fiddle works fine for me… which version of chrome are you using for testing?

Hi,Thanks for replying. I’m using Chrome version 84.0.4147.89. Also when I say Chrome Dev environment I’m talking about Chrome DevTool. When I view my page in Chrome DevTool and highlight a text string, upon releasing the mouse after highlighting the text, the highlighted text has a blue background.

The problem is the highlighted text should have a red color but instead they remain the same. When I save my markup, css, and script in an html file, and view my page in Chrom DevTool, the highlighted text has a blue background but the text color change to red when I click anywhere on the page. In jsFiddle no matter how many times I click on the page the blue background still remains and the text color is not red.

I should not have to click anywhere on the page after highlighting to get rid of the blue background and see the text red color. I don’t know whether the fact that I’m using my laptop’s touch pad to highlight the text causes the mouseup event not to work.

I checked your Fiddle with Chromium and get the same behavior as you do in Chrome.

I also get the same sticky background in Firefox and Opera; the select background stays until I click elsewhere on the page to get the selected part loose the background and turn red.

I’m not a Javascript guy, but I could at least confirm the behaviour. Isn’t that the normal way the select works? :slight_smile:

1 Like

I don’t know if I have got this right, but in CSS you have the pseudo ::selection property.

A test of my own

<!DOCTYPE html>
<html lang='en'>
<head>
  <meta charset='UTF-8'>
  <meta name='viewport' content='width=device-width, initial-scale=1.0'>
  <title>Document</title>
  <style>
    .test {
      position: relative; 
      top: 50px; 
      text-align: center;
    }

    .test::selection {
      color: red;
      background-color: transparent;
    }
  </style>
</head>
<body>
  <main>
    <div class='test'>Test text</div>
  </main>
</body>
</html>

So you want to clear the selection altogether? You’re actually already doing this, but then you’re immediately adding the range to the selection again so it will remain the same:

// ...
range.surroundContents(span);
sel.removeAllRanges();
sel.addRange(range);

If you remove that last addRange() it should work as described.

2 Likes

If you change your setAttribute to this

span.setAttribute('style', 'color: red; backgroundColor: transparent;')

It kind of does what you require, the issue though is with multiple clicks on the page you quickly end up with multiple spans and nested spans, so some sort of cleanup of the existing span seems to be required.

Edit: Furthermore rangeCount doesn’t appear to be a reliable way of checking the selection count as it appears to come up with 1 regardless.

I think you need to be working with startOffset and endOffset instead.

1 Like

This is just a work in progress

<!DOCTYPE html>
<html lang='en'>
<head>
  <meta charset='UTF-8'>
  <meta name='viewport' content='width=device-width, initial-scale=1.0'>
  <title>Document</title>
  <style>
    .test {
      position:relative;
      top:50px;
      text-align:center;
      outline: 1px solid red;
    }
  </style>
</head>
<body>
  <main>
    <div class='test'>Test text</div>
  </main>
  <script>
    document.addEventListener('DOMContentLoaded', function(event){

      function surroundSelectionWithSpan() {
        const selection = window.getSelection()
        const range = selection.getRangeAt(0).cloneRange()

        if (range.endOffset - range.startOffset > 0) {
          const span = document.createElement('span')
          span.setAttribute('style', 'color: red; backgroundColor: transparent;')

          console.log('range', range)

          range.surroundContents(span)
          selection.removeAllRanges()
        }
      }

      document.querySelector('.test').addEventListener('mouseup', surroundSelectionWithSpan);
    })

  </script>
</body>
</html>

As mentioned you would want a way to remove previous ‘span’ tags. So for instance if you select ‘test’ => <span>test</span> and then select ‘es’ you would want to remove the outer spans. You wouldn’t want <span>t<span>es</span>t</span>

1 Like

I’m sure this is convoluted, but a bit of a play with the other piece of the puzzle and that is removing existing span tags.

The idea being if on a second, third selection etc you highlight an area which already contains highlighted areas, it will remove those inner span tags — if that make sense?!

Instead of targeting span tags specifically I have changed it to target an element with a specific className.

<!DOCTYPE html>
<html lang='en'>
<head>
  <meta charset='UTF-8'>
  <meta name='viewport' content='width=device-width, initial-scale=1.0'>
  <title>Document</title>
  <style>
    h1 {
      font-size: 1.2rem;
    }

    .test {
      position:relative;
      max-width: 600px;
      margin: 50px auto;
      text-align:center;
      border: 1px solid teal;
    }

    .info,
    .danger {
      padding: .2rem .4rem;
      border-radius: 3px;
    }

    .info {
      background-color: #d1ecf1;
    }

    .danger {
      background-color: #f8d7da;
    }
  </style>
</head>
<body>
  <main>
    <div class='test'>
      <h1>Click Here</h1>
      <p>Some text <span class='info'>here <span class='info'>with</span> a <a href='#'>link</a></span> and <span class='danger'>some more text</span> here</p>
      <p>Some other <span class='danger'>text</span> here</p>
    </div>
  </main>
<script>
  document.addEventListener('DOMContentLoaded', function (event) {

    // function to perform a callback on an element and all of it's child elements
    const walkDom = (node, func) => {
      while (node) {
        if (node.firstChild) walkDom(node.firstChild, func)
        func(node)
        node = node.nextSibling
      }
    }

    // Using the 'template' element to replace an element with it's innerHTML
    // This way we can keep inner tags in place e.g. links etc.
    // Note: Not supported in IE
    const stripWithClassName = className => elem => {
      if (elem.classList && elem.classList.contains(className)) {
        const template = document.createElement('template')

        template.innerHTML = elem.innerHTML
        elem.parentNode.replaceChild(template.content, elem)
      }
    }

    document.querySelector('.test').addEventListener('click', event => {
      walkDom(event.currentTarget, stripWithClassName('info'))
    })
  })
</script>
</body>
</html>
2 Likes

Thank you all for your replies. I have not tried out each of the solutions that you all have suggested but I have found that m3g4p0p’s solution does exactly what I needed. I will look at each of the solutions and see if there are advantages each offers.

2 Likes