diff --git a/packages/ui/domrange.js b/packages/ui/domrange.js index fd89d59cfd..22be68fff9 100644 --- a/packages/ui/domrange.js +++ b/packages/ui/domrange.js @@ -201,6 +201,10 @@ _extend(DomRange.prototype, { var range = newMember.dom; range.owner = this.component; var nodes = range.getNodes(); + + if (tbodyFixNeeded(nodes, parentNode)) + parentNode = moveWithOwnersIntoTbody(this); + for (var i = 0; i < nodes.length; i++) insertNode(nodes[i], parentNode, nextNode); } else { @@ -210,6 +214,10 @@ _extend(DomRange.prototype, { var node = newMember; if (node.nodeType !== 3) node.$ui = this.component; + + if (tbodyFixNeeded(node, parentNode)) + parentNode = moveWithOwnersIntoTbody(this); + insertNode(node, parentNode, nextNode); } }, @@ -580,8 +588,66 @@ DomRange.insert = function (component, parentNode, nextNode) { if (! range) throw new Error("Expected a component with a DomRange"); var nodes = range.getNodes(); + if (tbodyFixNeeded(nodes, parentNode)) + parentNode = makeOrFindTbody(parentNode, nextNode); for (var i = 0; i < nodes.length; i++) insertNode(nodes[i], parentNode, nextNode); }; -UI.DomRange = DomRange; +///// TBODY FIX for compatibility with jQuery. +// +// Because people might use jQuery from UI hooks, and +// jQuery is unable to do $(myTable).append(myTR) without +// adding a TBODY (for historical reasons), we move any DomRange +// that gains a TR, and its immediately enclosing DomRanges, +// into a TBODY. +// +// See http://www.quora.com/David-Greenspan/Posts/The-Great-TBODY-Debacle +var tbodyFixNeeded = function (childOrChildren, parent) { + if (parent.nodeName !== 'TABLE') + return false; + + if (isArray(childOrChildren)) { + var foundTR = false; + for (var i = 0, N = childOrChildren.length; i < N; i++) { + var n = childOrChildren[i]; + if (n.nodeType === 1 && n.nodeName === 'TR') { + foundTR = true; + break; + } + } + if (! foundTR) + return false; + } else { + var n = childOrChildren; + if (! (n.nodeType === 1 && n.nodeName === 'TR')) + return false; + } + + return true; +}; + +var makeOrFindTbody = function (parent, next) { + // we have a TABLE > TR situation + var tbody = parent.getElementsByTagName('tbody')[0]; + if (! tbody) { + tbody = parent.ownerDocument.createElement("tbody"); + parent.insertBefore(tbody, next || null); + } + return tbody; +}; + +var moveWithOwnersIntoTbody = function (range) { + while (range.owner) + range = range.owner.dom; + + var nodes = range.getNodes(); // causes refresh + var tbody = makeOrFindTbody(range.parentNode(), + range.end.nextSibling); + for (var i = 0; i < nodes.length; i++) + tbody.appendChild(nodes[i]); + + return tbody; +}; + +UI.DomRange = DomRange; \ No newline at end of file diff --git a/packages/ui/domrange_tests.js b/packages/ui/domrange_tests.js index 3cc7b35989..7342552478 100644 --- a/packages/ui/domrange_tests.js +++ b/packages/ui/domrange_tests.js @@ -468,6 +468,105 @@ Tinytest.add("ui - DomRange - external moves", function (test) { strip('-------------- (aXb ghiWjkl)')); }); +Tinytest.add("ui - DomRange - tables", function (test) { + var range = function (x) { + new DomRange(x); + if (x.el) { + x.dom.add(x.el); + if (x.content) + DomRange.insert(x.content, x.el); + } + return x; + }; + var tr, td; + var table = range({ + el: document.createElement('table'), + content: tr = range({ + el: document.createElement('tr'), + content: td = range({ + el: document.createElement('td') + }) + }) + }); + + // TBODY got inserted automatically. + // This tests DomRange.insert. + test.equal(table.el.childNodes.length, 1); + test.equal(table.el.firstChild.nodeName, 'TBODY'); + // TBODY contains [start, TR, end] + test.equal(table.el.firstChild.childNodes.length, 3); + test.equal(table.el.firstChild.childNodes[1], tr.el); + test.equal(tr.el.childNodes.length, 3); + test.equal(tr.el.childNodes[1], td.el); + + // start over + $(table.el).empty(); + test.equal(table.el.childNodes.length, 0); + + table.content = range({}); + DomRange.insert(table.content, table.el); + // table has two children (start/end markers), no elements + test.equal(table.el.childNodes.length, 2); + test.notEqual(table.el.firstChild.nodeType, 1); + test.notEqual(table.el.lastChild.nodeType, 1); + + // shazam, adding a TR should move the whole range + // into a TBODY. This tests range.add(node). + table.content.dom.add(document.createElement('tr')); + + test.equal(table.el.childNodes.length, 1); + test.equal(table.el.firstChild.nodeName, 'TBODY'); + test.equal(table.el.firstChild.childNodes.length, 3); + test.equal(table.el.firstChild.childNodes[1].nodeName, 'TR'); + + // start over. + $(table.el).empty(); + test.equal(table.el.childNodes.length, 0); + + table.content = range({}); + DomRange.insert(table.content, table.el); + var a1 = range({}); + var a2 = range({}); + a1.dom.add(a2); + table.content.dom.add(a1); + // 6 marker nodes in table, no elements + test.equal(table.el.childNodes.length, 6); + test.equal($(table.el).find("*").length, 0); + // shazam, adding a TR to the innermost range + // should move all the ranges into a TBODY. + a2.dom.add(document.createElement('tr')); + test.equal(table.el.childNodes.length, 1); + test.equal(table.el.firstChild.nodeName, 'TBODY'); + test.equal(table.el.firstChild.childNodes.length, 7); + test.equal(table.el.firstChild.childNodes[3].nodeName, 'TR'); + + // start over. this time test adding a range containing + // a TR. + $(table.el).empty(); + test.equal(table.el.childNodes.length, 0); + + table.content = range({}); + DomRange.insert(table.content, table.el); + var a1 = range({}); + var a2 = range({}); + table.content.dom.add(a1); + a2.dom.add(document.createElement('tr')); + // 4 marker nodes in table, no elements + test.equal(table.el.childNodes.length, 4); + test.equal($(table.el).find("*").length, 0); + // shazam, adding a2, which contains a TR, + // should move all the ranges into a TBODY. + a1.dom.add(a2); + test.equal(table.el.childNodes.length, 1); + test.equal(table.el.firstChild.nodeName, 'TBODY'); + test.equal(table.el.firstChild.childNodes.length, 7); + test.equal(table.el.firstChild.childNodes[3].nodeName, 'TR'); + + test.equal(a2.dom.parentNode().nodeName, 'TBODY'); + test.equal(a1.dom.parentNode().nodeName, 'TBODY'); + test.equal(table.content.dom.parentNode().nodeName, 'TBODY'); +}); + // TO TEST STILL: // - external remove element