views:

111

answers:

4

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.

A: 

Maybe you could use the jQuery selectors to ignore the rel="inserted"?

$('a[rel!=inserted]').doSomething();

http://api.jquery.com/attribute-not-equal-selector/

What code are you using now to select from blowing to difficulty?

Justin Campbell
I'm using `document.getSelection()` to get the selection object. That's a neat idea, but I still need to do stuff with those nodes, I just need to remove them from the calculation.
Sam Soffes
Whatever you're doing you're doing it wrong ;)
Justin Campbell
A: 

Just check if your selected element is a paragraph, and if not use something like Prototype's Element.up() method to select the first paragraph parent.

For example:

if(selected_element.nodeName != 'P') {
  parent_paragraph = $(selected_element).up('p');
}

Then just find the difference between the parent_paragraph's text offset and your selected_element's text offset.

John Fredrickson
What's a "text offset"?
Tim Down
I'm not sure I follow. How do you see the "text offset" from a give element to its parent. In my example, how would I find the text offset from the first strong to the beginning of the first character in the p.
Sam Soffes
+1  A: 

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.

Tim Down
I agree that Range is the way to go on the Javascript side. The main thing is I need to extract the data and store it in a database, and then be able to recreate the range on subsequent loads. After looking at the Range documentation, I didn't see a good way to do this. Any ideas? That's what I'm trying to solve with all of this. I'm not set on a specific way.
Sam Soffes
+1  A: 

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.

Sam Soffes