14

I have a fiddle showing what my code is doing. Using javascript/jquery I am trying to insert a table into a content editable div at the current caret position. I am using Tim Down's Rangy library to accomplish this. I am doing this with the following javascript.

var range = getFirstRange();
var el = document.createElement("table");
var tableHtml = "";
for (var a = 0; a <= tableY; a++) {
    if(a%2==0){
       tableHtml += '<tr class="zebra">';
    }
    else{
       tableHtml += '<tr>';
    }
    for (var b = 0; b <= tableX; b++) {
       tableHtml += '<td>&nbsp;</td>';
    }
    tableHtml += '</tr>';   
}
$(el).html(tableHtml); 
range.insertNode(el);
rangy.getSelection().setSingleRange(range);

Just in case it helps here is the getFirstRange function.

function getFirstRange() {
   var sel = rangy.getSelection();
   return sel.rangeCount ? sel.getRangeAt(0) : null;
} 

I need to make valid html wherever this table is placed. for example if the caret is in the middle of a link I am trying to avoid the following html.

<p>some text <a href="#">text 
                         <table>
                             <tr>
                               <td>table content</td>
                             </tr>
                         </table> 
              text</a> more text</p> 

I would like it to look like this instead.

<p>some text <a href="#">text</a></p>
<table>
   <tr>
     <td>table content</td>
   </tr>
</table>
<p><a href="#">text</a> more text</p>
Brian Tompsett - 汤莱恩
  • 5,195
  • 62
  • 50
  • 120
Blake Plumb
  • 5,667
  • 2
  • 28
  • 49
  • My answer seems to have missed the point. You want the inserted table to be placed at that precise location, but to close and reopen all invalid tags around it? – svidgen Apr 17 '13 at 20:01
  • @svidgen That is correct! – Blake Plumb Apr 17 '13 at 20:03
  • Did you notice that browsers, like Safari, try to auto-correct the source, thus making the JavaScript matching differ from your actual source code. This is good in some kind of way, but also quite annoying of course, in a case where you want to modify your structure. In my Safari your code has been auto-altered to; `

    `. So now you almost have what you already want to achieve, but the last `` is not being wrapped by a `

    `. The browser does this because it can't house blocks and weirdness `display`-modes (like tables) inside inline elements.

    –  Apr 17 '13 at 21:31
  • Check my fiddle (and the `` block it returns (yes, the table's parent turned to be the body magically :P); http://jsfiddle.net/Allendar/jBrxb/ –  Apr 17 '13 at 21:41
  • 50 reputation to implement an DOM or SAX parser that can mutate a target range to contain any abstract DOM node. https://github.com/tinymce/tinymce/tree/master/jscripts/tiny_mce/classes/html – wewals Apr 18 '13 at 02:30

4 Answers4

3

If you want to drop the new node immediately after a selected node(s) that cannot validly contain it, replace this:

range.insertNode(el);

With something like this:

var badNodes = {a: 1, p: 1};

// starting with the node at the beginning of the range,
// iterate to the "left" until we find a node that isn't
// a text node
var n = range.startContainer;
var tag = n.nodeName;
while (tag == '#text') {
    n = n.parentNode;
    tag = n.nodeName;
}

// if the node we landed on isn't one of our bad nodes ...
if (badNodes[tag.toLowerCase()]) {

    // that we refuse to insert 'el' into, continue iterating to the
    // "left" until we find a node we're willing to place 'el' after.
    while (badNodes[n.parentNode.nodeName.toLowerCase()]) {
        n = n.parentNode;
        tag = n.nodeName;
    }
    n.parentNode.insertBefore(el, n.nextSibling);

} else {
    range.insertNode(el);
}

See my fiddle fork: http://jsfiddle.net/zntwL/29/


UPDATE (I think this is what you want)

If you want to split the invalid node(s) and drop the new node in, use something like this instead:

var badNodes = {a: 1, p: 1};

// starting with the node at the beginning of the range,
// iterate to the "left" until we find a node that isn't
// a text node
var n = range.startContainer;
var tag = n.nodeName;
while (tag == '#text') {
    n = n.parentNode;
    tag = n.nodeName;
}

// if the node we landed on is one of our "bad" nodes ...
if (badNodes[tag.toLowerCase()]) {

    // continue iterating to the "left" until we find a "good" node
    while (badNodes[n.parentNode.nodeName.toLowerCase()]) {
        n = n.parentNode;
        tag = n.nodeName;
    }

    // remove everything from our "good" node from the start of the
    // range to the end of the node. this causes all bad nodes to be
    // severed and auto-closed and auto-opened as necessary at the cut.
    range.setEndAfter(n);
    var clipped = range.extractContents();

    // drop 'el' in after the break (right where we want it)
    n.parentNode.insertBefore(el, n.nextSibling);

    // and re-attach the clipped portion of the "good" node, which
    // includes the auto-opened "bad" nodes.
    el.parentNode.insertBefore(clipped, el.nextSibling);

} else {
    range.insertNode(el);
}

http://jsfiddle.net/zntwL/31/

Your final solution may need some tweaking. You may need to detect #text nodes differently to be cross-browser compliant. And you'll want to modularize this and populate the badNodes array appropriately. But, I think that's the general idea.

svidgen
  • 13,099
  • 4
  • 31
  • 51
  • While this does make sure that the table is never inserted inside of a link, it does not escape the table from the parent paragraph. – Blake Plumb Apr 17 '13 at 19:13
  • @BlakePlumb Sure. But .. Firstly, you can't ensure perfect compliance of the whole doc without writing a full compliance checker - best bet is a heuristic, if I can call it that, that looks up the node chain until it finds a node that allows tables. Secondly, any answer you get here will probably only get you started. A really comprehensive answer would be .. *large*. And there's no "automatic" way to ensure compliance that I know of. So, *you* need to build the checks. That said, I'll play with this a few more minutes to make it a little better. But, you have to "complete" it to your liking. – svidgen Apr 17 '13 at 19:28
  • @BlakePlumb I've edited my answer as far as I think I can take it right now. Good luck! – svidgen Apr 17 '13 at 19:39
  • Thanks for the help! My current solution is along the lines of your first comment. What I am trying to see is if someone knows how to insert the table at the current caret position while keeping valid html. If I don't get a better solution I'll give you the bounty. – Blake Plumb Apr 17 '13 at 19:55
  • @BlakePlumb I may have lost sight of the real need while I was fooling around with the fiddle. I'll revisit this later to see if I can't address the question more accurately. – svidgen Apr 17 '13 at 19:59
  • @BlakePlumb Updated. (The range actually made this much easier and "automatic" than I imagined it would be.) – svidgen Apr 18 '13 at 02:14
  • @BlakePlumb Comments added in haste -- gotta run. Let me know if anything doesn't make sense and I'll edit. – svidgen Apr 18 '13 at 02:42
0

What about something like

var parentNode = range.commonAncestorContainer;
parentNode.inertNode(el);

to replace your

range.insertNode(el);

You might have to tweak it to get the table exactly where you want it in all cases, but at least it will never show up in the middle of an element.

user1827044
  • 176
  • 5
  • Thanks for the input, but I get the following error. TypeError: parentNode.insertNode is not a function parentNode.insertNode(el); – Blake Plumb Nov 20 '12 at 15:38
  • Hmm sounds like range.commonAncestorContainer isn't returning the right kind of object that insertNode can work from. – user1827044 Nov 20 '12 at 16:08
0

As an iPhone application developer I found following that works for me on editable content

function TableOfContentForSubHeading1() {
    var level = 0;
    document.getElementById("content").innerHTML =
        document.getElementById("content").innerHTML.replace(/<h([\d])>([^<]+)<\/h([\d])>/gi, function (str, openLevel, titleText, closeLevel) {
            if (openLevel != closeLevel) {
                return str;
            }
            if (openLevel > level) {
                toc += (new Array(openLevel - level + 1)).join("<ol>");
            } else if (openLevel < level) {
                toc += (new Array(level - openLevel + 1)).join("</ol>");
            }
            level = parseInt(openLevel);
            var anchor = titleText.replace(/ /g, "_");
            toc += "<li><a href=\"#" + anchor + "\">" + titleText + "</a></li>";
            return "<h" + openLevel + "><a name=\"" + anchor + "\">" + titleText + "</a></h" + closeLevel + ">";
        });
    if (level) {
        toc += (new Array(level + 1)).join("</ol>");
    }
    document.getElementById("content").innerHTML += toc;
}

Hope it works.

Shadow The Vaccinated Wizard
  • 62,584
  • 26
  • 129
  • 194
Gagan_iOS
  • 3,248
  • 2
  • 24
  • 41
0

Here below I made some improvement in case the Headings include also a CLASS reference or ID.

<script>
window.onload = function () {
var toc = "";
var level = 0;

document.getElementById("contents").innerHTML =
    document.getElementById("contents").innerHTML.replace(/<h([\d])([^<]+)>([^<]+)<\/h([\d])>/gi,
        function (str, openLevel, classHeading, titleText, closeLevel) {
            if (openLevel != closeLevel) {
                return str;
            }

            if (openLevel > level) {
                toc += (new Array(openLevel - level + 1)).join("<ul>");
            } else if (openLevel < level) {
                toc += (new Array(level - openLevel + 1)).join("</ul>");
            }

            level = parseInt(openLevel);

            var anchor = titleText.replace(/ /g, "_");
            toc += "<li><a href=\"#" + anchor + "\">" + titleText
                + "</a></li>";

            return "<h" + openLevel + classHeading +"><a name=\"" + anchor + "\">"
                + titleText + "</a></h" + closeLevel + ">";
        }
    );

if (level) {
    toc += (new Array(level + 1)).join("</ul>");
}

document.getElementById("toc").innerHTML += toc;
};
</script>

I've added an additional parameter to the replace function:

([^<]+)

named classHeading and adding it into return function

return "<h" + openLevel + classHeading + ....

So that the class used for is kept.

BenMorel
  • 30,280
  • 40
  • 163
  • 285