tags:

views:

326

answers:

3

I'm working on a rich text editor like web application, basically a XML editor written in javascript.

My javascript code needs to wrap a selection of nodes from the contentEditable div container. I'm using the methods described at MDC. But since I need to synchronize the div containers content to my XML DOM I would like to avoid partial selections as described in w3c ranges:

<BODY><H1>Title</H1><P>Blah xyz.</P></BODY

............^----------------^............

This selection starts inside H1 and ends inside P, I'd like it to include H1,P completely.

Is there an easy way to extend the selection to cover partially selected children completely? Basically I want to use range.surroundContents() without running into an exception.

(The code doesn't need to work with opera/IE)

A: 

If you mean you want to include the tags H1 and P (i.e., the valid markup), don't worry. You get that for free. If you mean you want to it to include all the content within the (partial) selection, you need to access the Selection object. Read about it on Quirksmode's introduction to Range.

Robusto
Like the quirksmode article states, browsers will fix up the 'invalid' HTML: "the browsers add the minimum amount of HTML to make the Range valid". I'd rather have no invalid selections to begin with ;)
ko-dos
+2  A: 

Looking at the MDC documentation, I manage do something like this:

Selection.prototype.coverAll = function() {
    var ranges = [];
    for(var i=0; i<this.rangeCount; i++) {
        var range = this.getRangeAt(i);
        while(range.startContainer.nodeType == 3
              || range.startContainer.childNodes.length == 1)
            range.setStartBefore(range.startContainer);
        while(range.endContainer.nodeType == 3
              || range.endContainer.childNodes.length == 1)
            range.setEndAfter(range.endContainer);
        ranges.push(range);
    }
    this.removeAllRanges();
    for(var i=0; i<ranges.length; i++) {
        this.addRange(ranges[i]);
    }
    return;
};

You can try it here : http://jsfiddle.net/GFuX6/9/

edit: Updated to have the browser display correctly the augmented selection. It does what you asked for, even if the selection contains several ranges (with Ctrl).

To make several partial nodes Bold, here is a solution:

Selection.prototype.boldinize = function() {
    this.coverAll();
    for(var i=0; i<this.rangeCount; i++) {
        var range = this.getRangeAt(i);
        var parent = range.commonAncestorContainer;
        var b = document.createElement('b');
        if(parent.nodeType == 3) {
            range.surroundContents(b);
        } else {
            var content = range.extractContents();
            b.appendChild(content);
            range.insertNode(b);
        }
    }
};
Alsciende
Indeed, but I think this is the right track. I'll try to create a custom selection from the original one that has these properties.
ko-dos
I'll extend my question to make it clearer.Also I updated the example at http://jsfiddle.net/GFuX6/5/
ko-dos
Select some text, press escape and the text becomes bold. There is a long line in the fiddle: `window.getSelection().getRangeAt(0).surroundContents(tag);`The current fiddle extends the text nodes boundaries, but I'm unable to find a way to cover partially included nodes.
ko-dos
Found a small tweak that makes everything work.
Alsciende
A: 

Thanks to Alsciende I finally came up with the code at http://jsfiddle.net/wesUV/21/. This method isn't as greedy as the other one. After coverAll(), surroundContents() should always work.

Selection.prototype.coverAll = function() {
  var ranges = [];   
  for(var i=0; i<this.rangeCount; i++) {
    var range = this.getRangeAt(i);
    var ancestor = range.commonAncestorContainer;
    if (ancestor.nodeType == 1) {            
        if (range.startContainer.parentNode != ancestor && this.containsNode(range.startContainer.parentNode, true)) {
            range.setStartBefore(range.startContainer.parentNode);
        }
        if (range.endContainer.parentNode != ancestor && this.containsNode(range.endContainer.parentNode, true)) {
                range.setEndAfter(range.endContainer.parentNode);
        }
    }
    ranges.push(range);
  }
  this.removeAllRanges();
  for(var i=0; i<ranges.length; i++) {
    this.addRange(ranges[i]);
  }
  return;
};

And the boldinize function:

Selection.prototype.boldinize = function() {
  for(var i=0; i<this.rangeCount; i++) {        
    var range = this.getRangeAt(i);
    var b = document.createElement('b');
    try {
        range.surroundContents(b);
    } catch (e) {
        alert(e);
    }
  }
};
ko-dos