// ========================================================================== // Project: metamorph // Copyright: ©2011 My Company Inc. All rights reserved. // ========================================================================== (function(window) { var K = function(){}, guid = 0, document = window.document, // Feature-detect the W3C range API, the extended check is for IE9 which only partially supports ranges supportsRange = ('createRange' in document) && (typeof Range !== 'undefined') && Range.prototype.createContextualFragment, // Internet Explorer prior to 9 does not allow setting innerHTML if the first element // is a "zero-scope" element. This problem can be worked around by making // the first node an invisible text node. We, like Modernizr, use ­ needsShy = (function(){ var testEl = document.createElement('div'); testEl.innerHTML = "
"; testEl.firstChild.innerHTML = ""; return testEl.firstChild.innerHTML === ''; })(); // Constructor that supports either Metamorph('foo') or new // Metamorph('foo'); // // Takes a string of HTML as the argument. var Metamorph = function(html) { var self; if (this instanceof Metamorph) { self = this; } else { self = new K(); } self.innerHTML = html; var myGuid = 'metamorph-'+(guid++); self.start = myGuid + '-start'; self.end = myGuid + '-end'; return self; }; K.prototype = Metamorph.prototype; var rangeFor, htmlFunc, removeFunc, outerHTMLFunc, appendToFunc, afterFunc, prependFunc, startTagFunc, endTagFunc; outerHTMLFunc = function() { return this.startTag() + this.innerHTML + this.endTag(); }; startTagFunc = function() { return ""; }; endTagFunc = function() { return ""; }; // If we have the W3C range API, this process is relatively straight forward. if (supportsRange) { // Get a range for the current morph. Optionally include the starting and // ending placeholders. rangeFor = function(morph, outerToo) { var range = document.createRange(); var before = document.getElementById(morph.start); var after = document.getElementById(morph.end); if (outerToo) { range.setStartBefore(before); range.setEndAfter(after); } else { range.setStartAfter(before); range.setEndBefore(after); } return range; }; htmlFunc = function(html, outerToo) { // get a range for the current metamorph object var range = rangeFor(this, outerToo); // delete the contents of the range, which will be the // nodes between the starting and ending placeholder. range.deleteContents(); // create a new document fragment for the HTML var fragment = range.createContextualFragment(html); // insert the fragment into the range range.insertNode(fragment); }; removeFunc = function() { // get a range for the current metamorph object including // the starting and ending placeholders. var range = rangeFor(this, true); // delete the entire range. range.deleteContents(); }; appendToFunc = function(node) { var range = document.createRange(); range.setStart(node); range.collapse(false); var frag = range.createContextualFragment(this.outerHTML()); node.appendChild(frag); }; afterFunc = function(html) { var range = document.createRange(); var after = document.getElementById(this.end); range.setStartAfter(after); range.setEndAfter(after); var fragment = range.createContextualFragment(html); range.insertNode(fragment); }; prependFunc = function(html) { var range = document.createRange(); var start = document.getElementById(this.start); range.setStartAfter(start); range.setEndAfter(start); var fragment = range.createContextualFragment(html); range.insertNode(fragment); }; } else { /** * This code is mostly taken from jQuery, with one exception. In jQuery's case, we * have some HTML and we need to figure out how to convert it into some nodes. * * In this case, jQuery needs to scan the HTML looking for an opening tag and use * that as the key for the wrap map. In our case, we know the parent node, and * can use its type as the key for the wrap map. **/ var wrapMap = { select: [ 1, "" ], fieldset: [ 1, "
", "
" ], table: [ 1, "", "
" ], tbody: [ 2, "", "
" ], tr: [ 3, "", "
" ], colgroup: [ 2, "", "
" ], map: [ 1, "", "" ], _default: [ 0, "", "" ] }; /** * Given a parent node and some HTML, generate a set of nodes. Return the first * node, which will allow us to traverse the rest using nextSibling. * * We need to do this because innerHTML in IE does not really parse the nodes. **/ var firstNodeFor = function(parentNode, html) { var arr = wrapMap[parentNode.tagName.toLowerCase()] || wrapMap._default; var depth = arr[0], start = arr[1], end = arr[2]; if (needsShy) { html = '­'+html; } var element = document.createElement('div'); element.innerHTML = start + html + end; for (var i=0; i<=depth; i++) { element = element.firstChild; } // Look for ­ to remove it. if (needsShy) { var shyElement = element; // Sometimes we get nameless elements with the shy inside while (shyElement.nodeType === 1 && !shyElement.nodeName) { shyElement = shyElement.firstChild; } // At this point it's the actual unicode character. if (shyElement.nodeType === 3 && shyElement.nodeValue.charAt(0) === "\u00AD") { shyElement.nodeValue = shyElement.nodeValue.slice(1); } } return element; }; /** * In some cases, Internet Explorer can create an anonymous node in * the hierarchy with no tagName. You can create this scenario via: * * div = document.createElement("div"); * div.innerHTML = "­
hi
"; * div.firstChild.firstChild.tagName //=> "" * * If our script markers are inside such a node, we need to find that * node and use *it* as the marker. **/ var realNode = function(start) { while (start.parentNode.tagName === "") { start = start.parentNode; } return start; }; /** * When automatically adding a tbody, Internet Explorer inserts the * tbody immediately before the first . Other browsers create it * before the first node, no matter what. * * This means the the following code: * * div = document.createElement("div"); * div.innerHTML = "
hi
* * Generates the following DOM in IE: * * + div * + table * - script id='first' * + tbody * + tr * + td * - "hi" * - script id='last' * * Which means that the two script tags, even though they were * inserted at the same point in the hierarchy in the original * HTML, now have different parents. * * This code reparents the first script tag by making it the tbody's * first child. **/ var fixParentage = function(start, end) { if (start.parentNode !== end.parentNode) { end.parentNode.insertBefore(start, end.parentNode.firstChild); } }; htmlFunc = function(html, outerToo) { // get the real starting node. see realNode for details. var start = realNode(document.getElementById(this.start)); var end = document.getElementById(this.end); var parentNode = end.parentNode; var node, nextSibling, last; // make sure that the start and end nodes share the same // parent. If not, fix it. fixParentage(start, end); // remove all of the nodes after the starting placeholder and // before the ending placeholder. node = start.nextSibling; while (node) { nextSibling = node.nextSibling; last = node === end; // if this is the last node, and we want to remove it as well, // set the `end` node to the next sibling. This is because // for the rest of the function, we insert the new nodes // before the end (note that insertBefore(node, null) is // the same as appendChild(node)). // // if we do not want to remove it, just break. if (last) { if (outerToo) { end = node.nextSibling; } else { break; } } node.parentNode.removeChild(node); // if this is the last node and we didn't break before // (because we wanted to remove the outer nodes), break // now. if (last) { break; } node = nextSibling; } // get the first node for the HTML string, even in cases like // tables and lists where a simple innerHTML on a div would // swallow some of the content. node = firstNodeFor(start.parentNode, html); // copy the nodes for the HTML between the starting and ending // placeholder. while (node) { nextSibling = node.nextSibling; parentNode.insertBefore(node, end); node = nextSibling; } }; // remove the nodes in the DOM representing this metamorph. // // this includes the starting and ending placeholders. removeFunc = function() { var start = realNode(document.getElementById(this.start)); var end = document.getElementById(this.end); this.html(''); start.parentNode.removeChild(start); end.parentNode.removeChild(end); }; appendToFunc = function(parentNode) { var node = firstNodeFor(parentNode, this.outerHTML()); while (node) { nextSibling = node.nextSibling; parentNode.appendChild(node); node = nextSibling; } }; afterFunc = function(html) { // get the real starting node. see realNode for details. var end = document.getElementById(this.end); var insertBefore = end.nextSibling; var parentNode = end.parentNode; var nextSibling; var node; // get the first node for the HTML string, even in cases like // tables and lists where a simple innerHTML on a div would // swallow some of the content. node = firstNodeFor(parentNode, html); // copy the nodes for the HTML between the starting and ending // placeholder. while (node) { nextSibling = node.nextSibling; parentNode.insertBefore(node, insertBefore); node = nextSibling; } }; prependFunc = function(html) { var start = document.getElementById(this.start); var parentNode = start.parentNode; var nextSibling; var node; node = firstNodeFor(parentNode, html); var insertBefore = start.nextSibling; while (node) { nextSibling = node.nextSibling; parentNode.insertBefore(node, insertBefore); node = nextSibling; } } } Metamorph.prototype.html = function(html) { this.checkRemoved(); if (html === undefined) { return this.innerHTML; } htmlFunc.call(this, html); this.innerHTML = html; }; Metamorph.prototype.replaceWith = function(html) { this.checkRemoved(); htmlFunc.call(this, html, true); }; Metamorph.prototype.remove = removeFunc; Metamorph.prototype.outerHTML = outerHTMLFunc; Metamorph.prototype.appendTo = appendToFunc; Metamorph.prototype.after = afterFunc; Metamorph.prototype.prepend = prependFunc; Metamorph.prototype.startTag = startTagFunc; Metamorph.prototype.endTag = endTagFunc; Metamorph.prototype.isRemoved = function() { var before = document.getElementById(this.start); var after = document.getElementById(this.end); return !before || !after; }; Metamorph.prototype.checkRemoved = function() { if (this.isRemoved()) { throw new Error("Cannot perform operations on a Metamorph that is not in the DOM."); } }; window.Metamorph = Metamorph; })(this);