0
votes

I am attempting to build a page builder where you input html, it renders into a div for preview, you are able to edit the div (not html) and it will update the textarea accordingly. The issue I have is when I'm trying to move text back from the div to the text area, some text is falling outside of the containing tags.

I have made a JSFiddle to show you what the problem is:

http://jsfiddle.net/7crysb0L/4/

Jquery :-

$('#dohtml').click(function () {
var textAreaVal = $('#myTextArea').val();
$('#myDiv').html(textAreaVal);
});

$('#dotext').click(function () {
var $div = $('#myDiv'),
    isEditable = $div.is('.editable');
$('#myDiv').prop('contenteditable', !isEditable).toggleClass('editable')
});

$('#dosave').click(function () {
var test = $('#myDiv').html();
alert(test);
$('#myTextArea').val(test);
});

HTML:-

<div id="container">
<textarea id="textinput" placeholder="HTML here"></textarea>
<br/>
<button class="btn btn-primary" id="convert">View</button>
<button class="btn btn-primary" id="edit">Edit Div</button>
<button class="btn btn-primary" id="save">Save Changes</button>
<div id="htmloutput">
</div>
</div>

If you, for example, copy and paste the bootstrap accordion found here:

http://www.tutorialspark.com/twitterBootstrap/TwitterBootstrap_Collapsible_Accordion.php

and edit the div to change the ahref, if you type on the end, or at the beginning or delete too much, the entire div disappears. Is there a work around for this?

I'm pretty sure I know why it's happening:-

When the last character of the div is deleted, the div automatically gets deleted this breaking the html inside the div.

1
it seems to be working fine. I couldn't populate the issue you are talking about. - Caner Akdeniz
You really don't want to do it this way. Edit the rendered output this way. contentEditables are far too quirky to edit complex HTML. A better approach would be to map the dom tree to an editor where people can edit properties from the dom nodes. - Mouser
It works fine for mee too, can you please take a screenshot or something - Leandro Papasidero
@Testing in Firefox 35.0 and working fine for me. Nice work i must add - Master Yoda
Once the accordion html is inserted, try renaming the entire header and see what happens to the ahref.. - Nicholas Mordecai

1 Answers

0
votes

This fiddle provides an alternative answer to your problem. It doesn't make the loaded HTML editable, but allows the user to select elements from it and change the attributes on that element. You can greatly expand this by allowing the insertion of new attributes, the deletion of attributes, rewriting the innerHTML, etc. When you traverse the tree to the a element you can adjust the href without breaking the structure of the element.

http://jsfiddle.net/sm1Lz1jd/

Code:

Javascript:

function recursiveHTMLTraverser(html, el, trace)
{
    var ulNode = document.createElement("ul");
    var html = html.children;
    var elementArray = [];
    for( var i = 0; i < html.length; ++i)
    {
        if (!elementArray[html[i].nodeName])
        {
            elementArray[html[i].nodeName] = [0];
        }
        else
        {
            elementArray[html[i].nodeName].push(elementArray[html[i].nodeName].length);
        }
        var liNode = document.createElement("li");
        var name = "";
        if (html[i].getAttribute("class") || html[i].getAttribute("id") || html[i].getAttribute("name"))
        {    
            name = " (<span>";
            if (html[i].getAttribute("id"))
            {
                name += html[i].getAttribute("id") + "</span>)";
            }
            else if (html[i].getAttribute("class"))
            {
                name += html[i].getAttribute("class") + "</span>)";
            }
            else
            {
                name += html[i].getAttribute("name") + "</span>)";
            }
        }
        var expander = "";
        if (html[i].children.length  > 0)
        {
            expander = "<span class='expander'>+</span>";
        }

        liNode.setAttribute("data-node", html[i].nodeName);
        liNode.setAttribute("data-trace", trace + html[i].nodeName);
        liNode.setAttribute("data-index", elementArray[html[i].nodeName].length-1);
        liNode.innerHTML = "<div>" + expander + html[i].nodeName + name + "</div>";
        ulNode.appendChild(liNode);
        if (html[i].children.length > 0)
        {
            recursiveHTMLTraverser(html[i], liNode, trace + html[i].nodeName + " | ");
        }

    }
    if (ulNode.children.length > 0)
    {
        el.appendChild(ulNode);
    }
}

function loadHTMLNode(e)
{
    if (e.target && e.target.tagName)
    {
        if (e.target.tagName.toLowerCase == "li")
        {
            var source = e.target;
        }
        else if (e.target.className && e.target.className == "expander")
        {
            if (e.target.textContent == "+")
            {
                e.target.textContent = "-";
                e.target.parentElement.parentElement.querySelector("ul").style.display = "block";
            }
            else
            {
                e.target.textContent = "+";
                e.target.parentElement.parentElement.querySelector("ul").style.display = "none";
            }
            return true;
        }
        else
        {
            var source = e.target;
            while(source = source.parentElement)
            {
                if (source.tagName.toLowerCase() == "li")
                {
                    break;
                }

            }
        }
        var selector = source.getAttribute("data-trace").replace(/\|/g, ">");
        document.getElementById("attributeEditor").removeAttribute("data-selector");
        document.getElementById("attributeEditor").removeAttribute("data-index-element");
        document.getElementById("attributeEditor").removeAttribute("data-index-attribute");
        document.getElementById("innerHTMLEditor").value = documentIframe.querySelectorAll(selector)[source.getAttribute("data-index")].innerHTML;
        loadHTMLNodeAttributes(selector, source.getAttribute("data-index"));

    }
}

function loadHTMLNodeAttributes(selector, index)
{
    var element = documentIframe.querySelectorAll(selector)[index];
    var selectElement = document.getElementById("attributeSelector").cloneNode();
    document.getElementById("attributeSelector").removeEventListener("change", changeAttribute(selector, index), false);
    for (var i = 0; i < element.attributes.length; ++i)
    {
        var optionNode = document.createElement("option");
        optionNode.value = i;
        optionNode.textContent = element.attributes[i].name;
        selectElement.appendChild(optionNode);
    }

    document.getElementById("attributeEditor").value = "";
    if (element.attributes.length > 0)
    {
        selectElement.addEventListener("change", changeAttribute(selector, index), false);
        var starter = changeAttribute(selector, index); //mimic call to change function;
        starter.call({value : 0}); //insert object with value 0, to select the first element.
    }

    document.getElementById("attributeSelector").parentElement.replaceChild(selectElement, document.getElementById("attributeSelector"));
}

function changeAttribute(selector, index)
{
    return function(){

        document.getElementById("attributeEditor").value = documentIframe.querySelectorAll(selector)[index].attributes[this.value].value;
        document.getElementById("attributeEditor").setAttribute("data-selector", selector);
        document.getElementById("attributeEditor").setAttribute("data-index-element", index);
        document.getElementById("attributeEditor").setAttribute("data-index-attribute", this.value);

    }
}

function changeAttributeValue(e)
{
    //change the attribute real life.
    if (e.target.hasAttribute("data-selector") && e.target.hasAttribute("data-index-element") && e.target.hasAttribute("data-index-attribute"))
    {
        documentIframe.querySelectorAll(e.target.getAttribute("data-selector"))[e.target.getAttribute("data-index-element")].attributes[e.target.getAttribute("data-index-attribute")].value = document.getElementById("attributeEditor").value;
    }
}

function startHTMLPreview()
{

    documentIframe.write(document.getElementById("myTextArea").value);
    recursiveHTMLTraverser(documentIframe.querySelector("html"), document.getElementById("domTreeDiv"), "");
}

document.getElementById("dohtml").addEventListener("click", startHTMLPreview, false);
document.getElementById("domTreeDiv").addEventListener("click", loadHTMLNode, false);
document.getElementById("attributeEditor").addEventListener("keyup", changeAttributeValue, false);
documentIframe = document.getElementById("myDiv").contentDocument.document || document.getElementById("myDiv").contentDocument;

HTML:

<textarea id="myTextArea" placeholder="Type here, then click the button to update the div">

</textarea>
<br />

<button id="dohtml">Render HTML</button>

<br /><br />
Element tree:
<div id="domTreeDiv"></div>
<div id="domTreeEditor">
    Attributes:
    <select id="attributeSelector"></select> = <input id="attributeEditor"><br />
    innerHTML: <textarea id="innerHTMLEditor" ></textarea>
</div>

Preview: <br />
<iframe id="myDiv" seamless sandbox="allow-same-origin"></iframe>

CSS:

    body{
      font-size: 11px;
      font-family: verdana;
    }

    #myDiv {
        border:solid 1px #999;
        border-radius:4px;
        min-height:300px;
        min-width:600px;
        margin-top:1em;
        padding:0.5em;
    }

    #myTextArea {
        min-width:600px;
        height: 200px;
    }

    #domTreeDiv {
        border: 1px solid #e2e2e2;
        border-radius: 3px 3px 3px 3px;
        min-height: 50px;
        max-height: 450px;
    }


    #domTreeDiv ul {
        list-style-type: none;
        padding: 0px 0px 0px 20px;
    }

    #domTreeDiv li > ul {
        display: none;

    }

    #domTreeDiv ul li > div {
        padding: 8px 8px 8px 8px;
        cursor: pointer;
        border: 1px solid transparent;
    }

    #domTreeDiv ul li > div:before {
        content:"";
    }       

    #domTreeDiv li > div:hover {
        padding: 8px 8px 8px 8px;
        cursor: pointer;
        border: 1px solid #BFDFFF;
        background-color: #99CCFF;
    }

    #domTreeDiv li > div > span {
        color: #8C0000;
    }           

    #domTreeDiv li > div > span.expander {
        color: #000000;
        display: inline-block;
        width: 24px;
        height: 100%;
        font-weight: bold;
    }   

    #domTreeDiv li > div > span.expander:hover {
        color: #EEEEEE;
    }   

    #attributeEditor{
      width: 400px;
      padding: 8px 8px 8px 8px;
      margin: 10px 0px 10px 0px;
    }

    #attributeSelector{
      width: 100px;
      padding: 8px 8px 8px 8px;
    }