// ==========================================================================
// Project: metamorph
// Copyright: ©2013 Tilde, Inc. All rights reserved.
// ==========================================================================
(function(window) {
var K = function(){},
guid = 0,
document = window.document,
disableRange = ('undefined' === typeof ENV ? {} : ENV).DISABLE_RANGE_API,
// Feature-detect the W3C range API, the extended check is for IE9 which only partially supports ranges
supportsRange = (!disableRange) && document && ('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 = document && (function(){
var testEl = document.createElement('div');
testEl.innerHTML = "
";
testEl.firstChild.innerHTML = "";
return testEl.firstChild.innerHTML === '';
})(),
// IE 8 (and likely earlier) likes to move whitespace preceeding
// a script tag to appear after it. This means that we can
// accidentally remove whitespace when updating a morph.
movesWhitespace = document && (function() {
var testEl = document.createElement('div');
testEl.innerHTML = "Test: Value";
return testEl.childNodes[0].nodeValue === 'Test:' &&
testEl.childNodes[2].nodeValue === ' Value';
})();
// 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() {
/*
* We replace chevron by its hex code in order to prevent escaping problems.
* Check this thread for more explaination:
* http://stackoverflow.com/questions/8231048/why-use-x3c-instead-of-when-generating-html-from-javascript
*/
return "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 = "
*
* 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());
var nextSibling;
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);