Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calculating text selection offsets in nest elements in Javascript

The Problem

I am trying to figure out the offset of a selection from a particular node with javascript.

Say I have the following HTML

<p>Hi there. This <strong>is blowing my mind</strong> with difficulty.</p>

If I select from blowing to difficulty, it gives me the offset from the #text node inside of the <strong>. I need the string offset from the <p>'s innerHTML and the length of the selection. In this case, the offset would be 26 and the length would be 40.

My first thought was to do something with string offsets, etc. but you could easily have something like

<p> Hi there. This <strong>is awesome</strong>. For real. It <strong>is awesome</strong>.</p>

which would break that method because there are identical nodes. I also need the option to throw out nodes. Say I have something like this

<p>Hi there. <a href="#" rel="inserted">This <strong>is blowing</a> my mind</strong> with difficulty.</p>

I want to throw out an elements with rel="inserted" when I do the calculation. I still want 26 and 40 as the result.

What I'm looking for

The solution needs to be recursive. If there was a <span> with a <strong> in it, it would still need to traverse to the <p>.

The solution needs to remove the length of any element with rel="inserted". The contents are important, but the tags themselves are not. All other tags are important. I'd strongly prefer not to remove any elements from the DOM when I do all of this.

I am using document.getSelection() to get the selection object. This solution only has to work in WebKit. jQuery is an option, but I'd prefer to it without it if possible.

Any ideas would be greatly appreciated.

I have no control over the HTML I doing all of this on.

like image 331
Sam Soffes Avatar asked Jul 30 '10 15:07

Sam Soffes


2 Answers

I think I solved my issue. I ended not calculating the offset like I originally planned. I am storing the "path" from the chunk (aka <p>). Here is the code:

function isChunk(node) {
  if (node == undefined || node == null) {
    return false;
  }
  return node.nodeName == "P";
}

function pathToChunk(node) {
  var components = new Array();

  // While the last component isn't a chunk
  var found = false;
  while (found == false) {
    var childNodes = node.parentNode.childNodes;
    var children = new Array(childNodes.length);
    for (var i = 0; i < childNodes.length; i++) {
      children[i] = childNodes[i];
    }        
    components.unshift(children.indexOf(node));

    if (isChunk(node.parentNode) == true) {
      found = true
    } else {
      node = node.parentNode;
    }
  }

  return components.join("/");
}

function nodeAtPathFromChunk(chunk, path) {
  var components = path.split("/");
  var node = chunk;
  for (i in components) {
    var component = components[i];
    node = node.childNodes[component];
  }
  return node;
}

With all of that, you can do something like this:

var p = document.getElementsByTagName('p')[0];
var piece = nodeAtPathFromChunk(p, "1/0"); // returns desired node
var path = pathToChunk(piece); // returns "1/0"

Now I just need to expand all of that to support the beginning and the end of a selection. This is a great building block though.

like image 103
Sam Soffes Avatar answered Oct 26 '22 11:10

Sam Soffes


What does this offset actually mean? An offset within the innerHTML of an element is going to be extremely fragile: any insertion of a new node or change to an attribute of an element preceding the point in the document the offset represents is going to make that offset invalid.

I strongly recommend using the browser's built-in support for this in the form of DOM Range. You can get hold of a range representing the current selection as follows:

var range = window.getSelection().getRangeAt(0);

If you're going to be manipulating the DOM based on this offset that you want, you're best off doing so using nodes instead of string representations of those nodes.

like image 42
Tim Down Avatar answered Oct 26 '22 09:10

Tim Down