How to prevent automatic closing of tags when using .cloneContents()

Viewed 476

Context

I'm trying to get the HTML of selected text based on the text itself. So, if the following HTML was implemented into a page:

<p>Random content <span>here and other</span> random content here.</p>

It would show up like so:

Random content here and other random content here.

If the user selects content here and, I would want to get not only the text, but the surrounding HTML. I found the following function, which does this effectively:

function getSelectionHtml() {

    var html = "";

    if (typeof window.getSelection != "undefined") {

        var sel = window.getSelection();

        if (sel.rangeCount) {

            var container = document.createElement("div");

            for (var i = 0, len = sel.rangeCount; i < len; ++i) {

                container.appendChild(sel.getRangeAt(i).cloneContents());

            }

            html = container.innerHTML;
        }

    } else if (typeof document.selection != "undefined") {

        if (document.selection.type == "Text") {

            html = document.selection.createRange().htmlText;

        }

    }

    return html;
    
}

The result of this would be content <span>here and </span>. The only problem with this is that the <span> tag automatically closes itself, even if I didn't select the text past the closing span tag. When looking at the MDN documentation for .cloneContents(), it says:

Partially selected nodes include the parent tags necessary to make the document fragment valid.

I believe this explains why the tags close themselves.

Question

How do get I the selected text and its HTML without the closing tag (unless I select that as well)? In this case, how do I get the text content <span> here and instead of content <span>here and </span>? If I was to select the whole statement that the span encompassed, though, I would want it to be included.

Things I Have Tried

I tried cutting off the </span> at the end of the string (if it exists) with the following function, but it breaks down when I'm spanning over multiple HTML tags. I want the span, strong, and i tags to be cut off if I don't highlight all the text they encompass. So, when spanning over one element, it works, but when you span over more than one, it doesn't. It also cuts off the span even if I hover over the whole text that the span encompasses, so this solution doesn't work.

$("p").click(function() {

  if (window.getSelection) {

    sel = window.getSelection();

  } else if (document.selection && document.selection.type != "Control") { //for IE

    sel = document.selection.createRange();

  }     

  var selectedText = getSelectionHTML();

  checkTags(selectedText);

});

function checkTags(selectedText) {

  var prohibited = ['</span>', '</strong>', '</i>'];            

  for (var i = 0; i < prohibited.length; i++) {

    if (selectedText.indexOf(prohibited[i]) > -1) {

      var splitText = selectedText.split("</");
      splitText.pop();

    }

  }

  alert(splitText);

}
  
function getSelectionHTML() {

  var html = "";

  if (typeof window.getSelection != "undefined") {

      var sel = window.getSelection();

      if (sel.rangeCount) {

          var container = document.createElement("div");

          for (var i = 0, len = sel.rangeCount; i < len; ++i) {

              container.appendChild(sel.getRangeAt(i).cloneContents());

          }

          html = container.innerHTML;
      }

  } else if (typeof document.selection != "undefined") {

      if (document.selection.type == "Text") {

          html = document.selection.createRange().htmlText;

      }

  }

  return html;
  
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<p>This random content is here <span> within the span <span> <strong>other random content</strong> is here.</p>

1 Answers

I find your question very interesting and have tinkered around a bit. With the slim examples you showed, everything seems to work. However, I can't give a 100% guarantee because there are so much cases that should be proofed. But I hope that I could give an example on how to solve the problem.

Maybe you try the plugin I created and tell me if it works. You can find the fiddle here on jsFiddle and the first version of the code below:

$.fn.selection = function(options) {

  var $el = this,
    lastSelections = [],
    $settings = $.extend({}, $.fn.selection.defaults, options);

  function removeCloseTags(range, selectionText) {
    if (range.endOffset < range.endContainer.length) {
      selectionText = selectionText.replace(
        new RegExp('<\/' + range.endContainer.parentElement.localName + '>(<\/[a-z]+>)*', 'gm'),
        ''
      );
    } else {
      var sibling = range.startContainer.nextSibling;

      if (sibling != null) {
        var siblingName = sibling.localName,
          contents = new RegExp('<' + siblingName + '>(.*)<\/' + siblingName + '>', 'gm').exec(selectionText);

        if (contents != null && typeof contents[1] != 'undefined' && sibling.innerHTML != contents[1]) {
          selectionText = selectionText.replace(
            new RegExp('<\/' + siblingName + '>(<\/[a-z]+>)*', 'gm'),
            ''
          );
        }
      }
    }

    return selectionText;
  }

  function removeOpenTags(element, selectionText) {
    var sibling = element.parentElement;

    if (selectionText.indexOf(sibling.innerHTML) != 0) {
      selectionText = selectionText.replace(
        new RegExp('^(<[a-z]+>)*<' + sibling.localName + '>', 'gm'),
        ''
      );
    }

    return selectionText;
  }

  function getSelectionText(container, selection, range) {
    var selectionText = container.innerHTML;

    container.innerHTML = '';
    container.appendChild(range.cloneContents());
    selectionText = container.innerHTML;

    if ($settings.removeCloseTags) {
      selectionText = removeCloseTags(range, selectionText);
    }

    if ($settings.removeOpenTags) {
      selectionText = removeOpenTags(range.startContainer, selectionText, 0);
    }

    return selectionText;
  }

  function getSelections() {
    lastSelections = [];

    if (typeof window.getSelection != 'undefined') {
      var selection = window.getSelection(),
        container = document.createElement('div');

      if (!selection.isCollapsed && selection.rangeCount) {
        var selectionRangeCount = selection.rangeCount;

        for (var i = 0; i < selectionRangeCount; ++i) {
          lastSelections.push(getSelectionText(container, selection, selection.getRangeAt(i)));
        }
      }
    }
    
    $settings.onSelection.call($el, lastSelections);
    return lastSelections;
  }

  var onMouseUp = function(event) {
    $settings.onMouseUp.call($el, event);
    getSelections();
  };

  var onSelectionChange = function(event) {
    $settings.onSelectionChange.call($el, event);
    getSelections();
  };

  $el.getLastSelections = function() {
    return lastSelections;
  };

  $el.on('mouseup', onMouseUp);
  $(document).on('selectionchange', onSelectionChange);

  var init = function() {
    return $el;
  };

  return init();

};

$.fn.selection.defaults = {
  removeCloseTags: true,
  removeOpenTags: true,
  onMouseUp: function(event) {},
  onSelectionChange: function(event) {},
  onSelection: function(selections) {},
};

$(document).ready(function() {
  var selection = $('body').selection({
    onSelection: function(selections) {
      var selectionString = selections.join()
        .replace(new RegExp('<', 'gm'), '&lt;')
        .replace(new RegExp('>', 'gm'), '&gt;');

      $('pre').html(selectionString);
    }
  });
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<p>Random content <span>here <i>and</i> other</span> random content here.</p>
<p>This random content is here <span> within the span </span> <strong>other random content</strong> is here.</p>

<pre></pre>

Related