Skip to content

Commit

Permalink
create DomUtils package
Browse files Browse the repository at this point in the history
  • Loading branch information
dgreensp committed Jul 27, 2012
1 parent 2f4e1d0 commit 328a2af
Show file tree
Hide file tree
Showing 17 changed files with 199 additions and 13 deletions.
155 changes: 155 additions & 0 deletions packages/domutils/domutils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@


DomUtils = {};

(function() {

///// Common look-up tables used by htmlToFragment et al.

var testDiv = document.createElement("div");
testDiv.innerHTML = " <link/><table></table>";

// Tests that, if true, indicate browser quirks present.
var quirks = {
// IE loses initial whitespace when setting innerHTML.
leadingWhitespaceKilled: (testDiv.firstChild.nodeType !== 3),

// IE may insert an empty tbody tag in a table.
tbodyInsertion: testDiv.getElementsByTagName("tbody").length > 0,

// IE loses some tags in some environments (requiring extra wrapper).
tagsLost: testDiv.getElementsByTagName("link").length === 0
};

// Set up map of wrappers for different nodes.
var wrapMap = {
option: [ 1, "<select multiple='multiple'>", "</select>" ],
legend: [ 1, "<fieldset>", "</fieldset>" ],
thead: [ 1, "<table>", "</table>" ],
tr: [ 2, "<table><tbody>", "</tbody></table>" ],
td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
area: [ 1, "<map>", "</map>" ],
_default: [ 0, "", "" ]
};
_.extend(wrapMap, {
optgroup: wrapMap.option,
tbody: wrapMap.thead,
tfoot: wrapMap.thead,
colgroup: wrapMap.thead,
caption: wrapMap.thead,
th: wrapMap.td
});
if (quirks.tagsLost) {
// trick from jquery. initial text is ignored when we take lastChild.
wrapMap._default = [ 1, "div<div>", "</div>" ];
}

var rleadingWhitespace = /^\s+/,
rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,
rtagName = /<([\w:]+)/,
rtbody = /<tbody/i,
rhtml = /<|&#?\w+;/,
rnoInnerhtml = /<(?:script|style)/i;


// Parse an HTML string, which may contain multiple top-level tags,
// and return a DocumentFragment.
DomUtils.htmlToFragment = function(html) {
var doc = document; // node factory
var frag = doc.createDocumentFragment();

if (! html.length) {
// empty, do nothing
} else if (! rhtml.test(html)) {
// Just text.
frag.appendChild(doc.createTextNode(html));
} else {
// General case.
// Replace self-closing tags
html = html.replace(rxhtmlTag, "<$1></$2>");
// Use first tag to determine wrapping needed.
var firstTagMatch = rtagName.exec(html);
var firstTag = (firstTagMatch ? firstTagMatch[1].toLowerCase() : "");
var wrapData = wrapMap[firstTag] || wrapMap._default;

var container = doc.createElement("div");
// insert wrapped HTML into a DIV
container.innerHTML = wrapData[1] + html + wrapData[2];
// set "container" to inner node of wrapper
var unwraps = wrapData[0];
while (unwraps--) {
container = container.lastChild;
}

if (quirks.tbodyInsertion && ! rtbody.test(html)) {
// Any tbody we find was created by the browser.
var tbodies = container.getElementsByTagName("tbody");
_.each(tbodies, function(n) {
if (! n.firstChild) {
// spurious empty tbody
n.parentNode.removeChild(n);
}
});
}

if (quirks.leadingWhitespaceKilled) {
var wsMatch = rleadingWhitespace.exec(html);
if (wsMatch) {
container.insertBefore(doc.createTextNode(wsMatch[0]),
container.firstChild);
}
}

// Reparent children of container to frag.
while (container.firstChild)
frag.appendChild(container.firstChild);
}

return frag;
};

// Return an HTML string representing the contents of frag,
// a DocumentFragment. (This is what innerHTML would do if
// it were defined on DocumentFragments.)
DomUtils.fragmentToHtml = function(frag) {
frag = frag.cloneNode(true); // deep copy, don't touch original!

return DomUtils.fragmentToContainer(frag).innerHTML;
};

// Given a DocumentFragment, return a node whose children are the
// reparented contents of the DocumentFragment. In most cases this
// is as simple as creating a DIV, but in the case of a fragment
// containing TRs, for example, it's necessary to create a TABLE and
// a TBODY and return the TBODY.
DomUtils.fragmentToContainer = function(frag) {
var doc = document; // node factory

var firstElement = frag.firstChild;
while (firstElement && firstElement.nodeType !== 1) {
firstElement = firstElement.nextSibling;
}

var container = doc.createElement("div");

if (! firstElement) {
// no tags!
container.appendChild(frag);
} else {
var firstTag = firstElement.nodeName;
var wrapData = wrapMap[firstTag] || wrapMap._default;

container.innerHTML = wrapData[1] + wrapData[2];
var unwraps = wrapData[0];
while (unwraps--) {
container = container.lastChild;
}

container.appendChild(frag);
}

return container;
};

})();
1 change: 1 addition & 0 deletions packages/domutils/domutils_tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// TESTS GO HERE
17 changes: 17 additions & 0 deletions packages/domutils/package.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Package.describe({
summary: "Utility functions for DOM manipulation",
internal: true
});

Package.on_use(function (api) {
api.add_files('domutils.js', 'client');
});

Package.on_test(function (api) {
api.use(['tinytest']);
api.use(['domutils', 'test-helpers'], 'client');

api.add_files([
'domutils_tests.js'
], 'client');
});
11 changes: 10 additions & 1 deletion packages/liverange/liverange_test_helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,13 @@ var check_liverange_integrity = function (range) {
throw new Error("integrity check failed - missing close tags");
};


// Dump out the contents of a LiveRange as an HTML string.
var rangeToHtml = function(liverange) {
var frag = document.createDocumentFragment();
for(var n = liverange.firstNode(),
after = liverange.lastNode().nextSibling;
n && n !== after;
n = n.nextSibling)
frag.appendChild(n.cloneNode(true)); // deep copy
return DomUtils.fragmentToHtml(frag);
};
2 changes: 1 addition & 1 deletion packages/liverange/package.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Package.on_use(function (api) {

Package.on_test(function (api) {
api.use(['tinytest']);
api.use(['liverange', 'test-helpers'], 'client');
api.use(['liverange', 'test-helpers', 'domutils'], 'client');

api.add_files([
'liverange_test_helpers.js',
Expand Down
2 changes: 1 addition & 1 deletion packages/liveui/domutils.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Meteor.ui._findElement = function(contextNode, selector) {
// Sizzle doesn't work on a DocumentFragment, but it does work on
// a descendent of one.
var frag = contextNode;
var container = Meteor.ui._fragmentToContainer(frag);
var container = DomUtils.fragmentToContainer(frag);
var results = $(container).find(selector);
// put nodes back into frag
while (container.firstChild)
Expand Down
2 changes: 1 addition & 1 deletion packages/liveui/livedocument.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ Meteor.ui._doc = Meteor.ui._doc || {};
};

var makeFrag = function(html) {
var frag = Meteor.ui._htmlToFragment(html);
var frag = DomUtils.htmlToFragment(html);
// empty frag becomes HTML comment <!--empty-->
if (! frag.firstChild)
frag.appendChild(document.createComment("empty"));
Expand Down
2 changes: 1 addition & 1 deletion packages/liveui/livedocument_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Tinytest.add("livedocument - assembly", function(test) {
var tempRange = new LiveRange(Meteor.ui._TAG, frag);
tempRange.visit(function(isStart, rng) {
if (! isStart)
actualGroups.push(Meteor.ui._rangeToHtml(rng));
actualGroups.push(rangeToHtml(rng));
});
test.equal(actualGroups.join(','), groups.join(','));

Expand Down
2 changes: 1 addition & 1 deletion packages/liveui/liveui.js
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ Meteor.ui = Meteor.ui || {};
try { var html = htmlFunc(); }
finally { Materializer.current = previous; }

var frag = Meteor.ui._htmlToFragment(html);
var frag = DomUtils.htmlToFragment(html);
// empty frag becomes HTML comment <!--empty-->
if (! frag.firstChild)
frag.appendChild(document.createComment("empty"));
Expand Down
2 changes: 1 addition & 1 deletion packages/liveui/liveui_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ Tinytest.add("liveui - tables", function(test) {
test.equal(R.numListeners(), 0);

div = OnscreenDiv();
div.node().appendChild(Meteor.ui._htmlToFragment("<table><tr></tr></table>"));
div.node().appendChild(DomUtils.htmlToFragment("<table><tr></tr></table>"));
R.set(3);
div.node().getElementsByTagName("tr")[0].appendChild(Meteor.ui.render(
function() {
Expand Down
2 changes: 1 addition & 1 deletion packages/liveui/patcher_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ Tinytest.add("patcher - copyAttributes", function(test) {
});
buf.push('></', tagName, '>');
var nodeHtml = buf.join('');
var frag = Meteor.ui._htmlToFragment(nodeHtml);
var frag = DomUtils.htmlToFragment(nodeHtml);
var n = frag.firstChild;
if (! node) {
node = n;
Expand Down
1 change: 0 additions & 1 deletion packages/spark/spark.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ Spark.render = function (htmlFunc) {

// HERE
//
// - Move LiveRange to global scope
// - Create DomUtils package (or something like that)
// - First thing in DomUtils is htmlToFragment from innerhtml.js
// - Later, will add stuff from domutils.js
Expand Down
2 changes: 1 addition & 1 deletion packages/templating/package.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ Package.register_extension(
Package.on_test(function (api) {
api.use('tinytest');
api.use('htmljs');
api.use('test-helpers', 'client');
api.use(['test-helpers', 'domutils'], 'client');
api.add_files([
'templating_tests.js',
'templating_tests.html'
Expand Down
2 changes: 1 addition & 1 deletion packages/templating/templating_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Tinytest.add("templating - assembly", function (test) {
// Test for a bug that made it to production -- after a replacement,
// we need to also check the newly replaced node for replacements
var frag = Meteor.ui.render(Template.test_assembly_a0);
test.equal(canonicalizeHtml(Meteor.ui._fragmentToHtml(frag)),
test.equal(canonicalizeHtml(DomUtils.fragmentToHtml(frag)),
"Hi");

// Another production bug -- we must use LiveRange to replace the
Expand Down
2 changes: 1 addition & 1 deletion packages/test-helpers/onscreendiv.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ var OnscreenDiv = function(optFrag) {
if (! (this instanceof OnscreenDiv))
return new OnscreenDiv(optFrag);

this.div = Meteor.ui._htmlToFragment(
this.div = DomUtils.htmlToFragment(
'<div class="OnscreenDiv" style="display: none"></div>').firstChild;
document.body.appendChild(this.div);

Expand Down
5 changes: 5 additions & 0 deletions packages/test-helpers/package.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ Package.describe({
Package.on_use(function (api, where) {
where = where || ["client", "server"];

// XXX These files have various dependencies on other packages
// that aren't specified here. :(
// This package should probably get split into several packages,
// each with correct dependencies.

api.add_files('try_all_permutations.js', where);
api.add_files('async_multi.js', where);
api.add_files('event_simulation.js', where);
Expand Down
2 changes: 1 addition & 1 deletion packages/test-helpers/wrappedfrag.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ WrappedFrag = function(frag) {
};

WrappedFrag.prototype.rawHtml = function() {
return Meteor.ui._fragmentToHtml(this.frag);
return DomUtils.fragmentToHtml(this.frag);
};

WrappedFrag.prototype.html = function() {
Expand Down

0 comments on commit 328a2af

Please sign in to comment.