views:

412

answers:

3

I am attempting to create a simple texteditor with HTML5 contenteditable on a div tag. As you know, selected text is handled quite differently in IE.

    this.retrieveAnchorNode = function() {
      var anchorNode;
      if (document.selection)
        anchorNode = document.selection.createRange().parentElement();
      else if (document.getSelection)
        anchorNode = window.getSelection().anchorNode;
    }

I am looking for a way to get the selected textnode (not the text itself), like I can do with "anchorNode" and "focusNode" on other browsers. The only alternative on IE I have found is the "parentElement()" function, which only manages to select the contenteditable div itself.

Any ideas?

A: 

Your best bet for this at the moment is IERange. This library will return you a DOM Range-like object in IE, with the selection provided in terms of nodes and offsets.

Tim Down
I have taken a look at IERange, but even if it works I would prefer a way to do it with the original TextRange object (or at least not involving a whole new library).Any other idea?
Herber
No. IERange works from the `TextRange` generated from the selection and does some quite cunning work to establish just which nodes the `TextRange`'s boundaries lie in. There's no other approach, only variations on the precise work done on the `TextRange`. Besides, IERange isn't very big and you could reasonably easily extract just the bits you want.
Tim Down
Aaargh, I seem to have accidentally made this community wiki and there's no way to undo it. Grr.
Tim Down
A: 

I have come up with a solution. It isn't perfect, but it's able to select the element containing the text node (most often p, h1 etc.). We can get the position of the selection with the TextRange function getBoundingClientRect(), and with document.elementFromPoint(x, y) we are able to get the element containing the text. Example:

var textRange = document.selection.createRange();
var x = textRange.getBoundingClientRect().left;
var y = textRange.getBoundingClientRect().top;
var element = document.elementFromPoint(x, y);

If anyone has a better solution, please share it.

Herber
You don't need to do this to get the containing element. If you want the element containing the start of the selection, for example, you could do `var textRange = document.selection.createRange(); textRange.collapse(true); var element = textRange.parentElement();`.
Tim Down
Does it return the paragraph or textnode in this case? I'll try it when I get home.
Herber
The element, not the text node. See my new answer for something that gets the text node.
Tim Down
+1  A: 

Here's my version of the function you need from IERange, with my comments:

function getChildIndex(node) {
  var i = 0;
  while( (node = node.previousSibling) ) {
    i++;
  }
  return i;
}

function getTextRangeBoundaryPosition(textRange, isStart) {
  var workingRange = textRange.duplicate();
  workingRange.collapse(isStart);
  var containerElement = workingRange.parentElement();
  var workingNode = document.createElement("span");
  var comparison, workingComparisonType = isStart ?
    "StartToStart" : "StartToEnd";

  var boundaryPosition, boundaryNode;

  // Move the working range through the container's children, starting at
  // the end and working backwards, until the working range reaches or goes
  // past the boundary we're interested in
  do {
    containerElement.insertBefore(workingNode, workingNode.previousSibling);
    workingRange.moveToElementText(workingNode);
  } while ( (comparison = workingRange.compareEndPoints(
    workingComparisonType, textRange)) > 0 && workingNode.previousSibling);

  // We've now reached or gone past the boundary of the text range we're
  // interested in so have identified the node we want
  boundaryNode = workingNode.nextSibling;
  if (comparison == -1 && boundaryNode) {
    // This must be a data node (text, comment, cdata) since we've overshot.
    // The working range is collapsed at the start of the node containing
    // the text range's boundary, so we move the end of the working range
    // to the boundary point and measure the length of its text to get
    // the boundary's offset within the node
    workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);

    boundaryPosition = {
      node: boundaryNode,
      offset: workingRange.text.length
    };
  } else {
    // We've hit the boundary exactly, so this must be an element
    boundaryPosition = {
      node: containerElement,
      offset: getChildIndex(workingNode)
    };
  }

  // Clean up
  workingNode.parentNode.removeChild(workingNode);

  return boundaryPosition;
}

var textRange = document.selection.createRange();
var selectionStart = getTextRangeBoundaryPosition(textRange, true);
// selectionStart has properties 'node' and 'offset'
Tim Down
Have you tried your code? getChildIndex is not a valid function. Otherwise it seems to work perfectly.
Herber
Oops. Yes, I copied this from my own library code and thought I'd removed dependencies. I did try it in IE for one case and it worked but missed the case where `getChildIndex` is called. Sorry. Now fixed.
Tim Down
Thanks for fixing!
Herber