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;
}
contentEditablesare 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