Added updateAll; plus reorganization & cleanup

This commit is contained in:
Mottie 2013-03-25 09:04:00 -05:00
parent c9d4d12aa5
commit 5df7f77819
5 changed files with 459 additions and 227 deletions

View File

@ -134,7 +134,7 @@
<div class="next-up">
<hr />
Next up: <a href="example-update-cell.html">Update the table after cell content has changed &rsaquo;&rsaquo;</a>
Next up: <a href="example-update-all.html">Update an entire table column (thead and tbody) &rsaquo;&rsaquo;</a>
</div>
</div>

View File

@ -0,0 +1,114 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>jQuery plugin: Tablesorter 2.0 - Update headers &amp; table body (updateAll)</title>
<!-- jQuery -->
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7/jquery.min.js"></script>
<!-- Demo stuff -->
<link rel="stylesheet" href="css/jq.css">
<link href="css/prettify.css" rel="stylesheet">
<script src="js/prettify.js"></script>
<script src="js/docs.js"></script>
<!-- Tablesorter: required -->
<link rel="stylesheet" href="../css/theme.blue.css">
<script src="../js/jquery.tablesorter.js"></script>
<script id="js">$(function() {
// Set up empty table with second and first columns pre-sorted
$("table").tablesorter({ theme : 'blue', sortList: [[2,0]] });
var indx = 0;
$("#update").on('click', function() {
// prevent link from removing all data from the column
if (indx++ < 2) {
// append new html to thead & tbody
$("table thead th:eq(2)").html("Age");
$("table tbody").find('td:nth-child(3)').html(function(i,h){
return h.substring(1); // remove x & y prefix
});
var resort = true, // re-apply the current sort
callback = function(){
// do something after the updateAll method has completed
};
// let the plugin know that we made a update, then the plugin will
// automatically sort the table based on the header settings
$("table").trigger("updateAll", [ resort, callback ]);
}
return false;
});
});</script>
</head>
<body>
<div id="banner">
<h1>table<em>sorter</em></h1>
<h2>Update headers &amp; table body (updateAll)</h2>
<h3>Flexible client-side table sorting</h3>
<a href="index.html">Back to documentation</a>
</div>
<div id="main">
<p class="tip">
<em>NOTE!</em>
<ul>
<li>This demo uses the <code>updateAll</code> method. <span class="tip"><em>New</em></span> v2.8.</li>
<li>This method allows you to update the cache with data from both the <code>thead</code> and <code>tbody</code> of the table.</li>
<li>The <code>update</code> method only updates the cache from the <code>tbody</code>.</li>
</ul>
</p>
<h1>Demo</h1>
<div id="demo"><table class="tablesorter">
<thead>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Value</th>
<th>Total</th>
<th>Discount</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<tr><td>Peter</td><td>Parker</td><td>x28</td><td>$9.99</td><td>20%</td><td>Jul 6, 2006 8:14 AM</td></tr>
<tr><td>John</td><td>Hood</td><td>y33</td><td>$19.99</td><td>25%</td><td>Dec 10, 2002 5:14 AM</td></tr>
<tr><td>Clark</td><td>Kent</td><td>y18</td><td>$15.89</td><td>44%</td><td>Jan 12, 2003 11:14 AM</td></tr>
<tr><td>Bruce</td><td>Almighty</td><td>x45</td><td>$153.19</td><td>44%</td><td>Jan 18, 2001 9:12 AM</td></tr>
</tbody>
</table>
<a href="#" id="update">Modify the entire value column</a>
</div>
<h1>Javascript</h1>
<div id="javascript">
<pre class="prettyprint lang-javascript"></pre>
</div>
<h1>HTML</h1>
<div id="html">
<pre class="prettyprint lang-html"></pre>
<div class="next-up">
<hr />
Next up: <a href="example-update-cell.html">Update the table after cell content has changed &rsaquo;&rsaquo;</a>
</div>
</div>
</div>
</body>
</html>

View File

@ -278,13 +278,49 @@
<li><a href="example-option-show-processing.html">Show a processing icon during sorting/filtering</a> (v2.4)</li>
</ul>
<h4>Parsers / Extracting Content</h4>
<h4>Using Parsers / Extracting Content</h4>
<ul>
<li><a href="example-option-digits.html">Dealing with digits!</a></li>
<li><a href="example-options-headers-digits-strings.html">Dealing with text strings in numerical sorts</a> (v2.0.10)</li>
<li><a href="example-parsers-class-name.html">Disable or set the column parser using class names</a> (v2.0.11)</li>
<li><a href="example-parsers-jquery-data.html">Disable or set the column parser using jQuery Data</a> (v2.3)</li>
<li><a href="example-option-date-format.html">Changing the date format</a> (v2.0.23)</li>
</ul>
<h4 id="extras">Plugins / Custom Widgets / Custom Parsers</h4>
<ul>
<li><a href="example-pager.html">Pager plugin</a></li>
<br>
<li><a href="example-widget-columns.html">Columns widget</a> (v2.0.17)</li>
<li>Filter Widget:
<ul>
<li><a href="example-widget-filter.html">basic</a> (v2.0.18)</li>
<li><a href="example-widget-filter-custom.html">custom</a> (v2.3.6)</li>
<li>formatter (<a href="example-widget-filter-formatter-1.html">jQuery UI widgets</a> and <a href="">HTML5 Elements</a>) <span class="tip"><em>New</em></span> v2.7.7.</li>
</ul>
</li>
<li><a href="example-widget-grouping.html">Grouping rows Widget</a> <span class="tip"><em>New</em></span> v2.8.</li>
<li><a href="example-widget-resizable.html">Resizable Columns widget</a> (v2.0.23.1; <span class="tip"><em>Modified</em></span> v2.7.4)</li>
<li><a href="example-widget-savesort.html">Save sort widget</a> (v2.0.27)</li>
<li><a href="example-widget-sticky-header.html">Sticky header widget</a> (v2.0.21.1)</li>
<li>UITheme widget:
<ul>
<li><a href="example-widget-ui-theme.html">jQuery UI theme</a> (v2.0.9; Modified v2.4)</li>
<li><a href="example-widget-bootstrap-theme.html">Bootstrap</a> (v2.4)</li>
</ul>
</li>
<li><a href="example-widget-zebra.html">Zebra stripe widget</a></li>
<br>
<li><a href="example-parsers-dates.html">Date parsers</a></li>
</ul>
</div>
<div class="box">
<h3>Advanced</h3>
<h4>Parsers / Extracting Content</h4>
<ul>
<li><a href="example-parsers.html">Parser, writing your own</a></li>
<li><a href="example-parsers-advanced.html">Parser, writing your own, advanced use</a> (v2.1)</li>
<li><a href="example-option-text-extraction.html">Dealing with markup inside cells (textExtraction function)</a></li>
@ -293,30 +329,17 @@
<h4>Widgets / Plugins</h4>
<ul>
<li><a href="example-apply-widget.html">Applying widgets</a></li>
<li><a href="example-widget-columns.html">Columns widget</a> (v2.0.17)</li>
<li><a href="example-widget-filter.html">Filter widget</a> (v2.0.18)</li>
<li><a href="example-widget-filter-custom.html">Filter widget, custom</a> (v2.3.6)</li>
<li>Filter widget, formatter (<a href="example-widget-filter-formatter-1.html">part 1</a> and <a href="">part 2</a>) <span class="tip"><em>New</em></span> v2.7.7.</li>
<li><a href="example-widget-ui-theme.html">UITheme widget - jQuery UI theme</a> (v2.0.9; Modified v2.4)</li>
<li><a href="example-widget-bootstrap-theme.html">UITheme widget - Bootstrap</a> (v2.4)</li>
<li><a href="example-widget-resizable.html">Resizable Columns widget</a> (v2.0.23.1; <span class="tip"><em>Modified</em></span> v2.7.4)</li>
<li><a href="example-widget-savesort.html">Save sort widget</a> (v2.0.27)</li>
<li><a href="example-widget-sticky-header.html">Sticky header widget</a> (v2.0.21.1)</li>
<li><a href="example-widget-zebra.html">Zebra stripe widget</a></li>
<li><a href="example-widgets.html">Widgets, writing your own</a></li>
<li><a href="example-pager.html">Pager plugin</a></li>
<li><a href="example-pager-ajax.html">Pager plugin - ajax</a> (v2.1)</li>
<li><a href="example-pager-filtered.html">Pager plugin + filter widget</a> (v2.4)</li>
</ul>
</div>
<div class="box">
<h3>Advanced</h3>
<h4>Adding / Removing Content</h4>
<ul>
<li><a href="example-empty-table.html">Initializing tablesorter on a empty table</a></li>
<li><a href="example-ajax.html">Appending table data with ajax</a></li>
<li><a href="example-add-rows.html">Adding a table row</a> (v2.0.16)</li>
<li><a href="example-update-all.html">Update an entire table column (<code>thead</code> and <code>tbody</code>)</a> <span class="tip"><em>New!</em></span> v2.8</li>
<li><a href="example-update-cell.html">Update the table after cell content has changed</a></li>
<li><a href="example-pager.html">Pager plugin</a> - examples of how to add and remove rows</li>
</ul>
@ -328,7 +351,7 @@
<li><a href="example-option-render-template.html">Modifying the Header using a template</a>. <span class="tip"><em>New!</em></span> v2.7</li>
</ul>
<br>
<br/>
<h3>Other</h3>
<h4>Options &amp; Events</h4>
@ -399,7 +422,7 @@
<td><a href="#" class="toggle2">cssAsc</a></td>
<td>String</td>
<td>"tablesorter-headerAsc"</td>
<td>The CSS style used to style the header when sorting ascending. Default value <span class="tip"><em>Changed!</em></span> v2.5.
<td>The CSS style used to style the header when sorting ascending. Default value (changed v2.5).
<div class="collapsible">
Example from the blue theme:
<pre class="prettyprint lang-css">.tablesorter-blue .tablesorter-headerAsc {
@ -469,7 +492,7 @@
<td><a href="#" class="toggle2">cssDesc</a></td>
<td>String</td>
<td>"tablesorter-headerDesc"</td>
<td>The CSS style used to style the header when sorting descending. Default value <span class="tip"><em>Changed!</em></span> v2.5.
<td>The CSS style used to style the header when sorting descending. Default value (changed v2.5).
<div class="collapsible">
Example from the blue theme:
<pre class="prettyprint lang-css">.tablesorter-blue .tablesorter-headerDesc {
@ -827,7 +850,7 @@ From the example function above, you'll end up with something similar to this HT
<tr><th><code>sorter: "isoDate"</code></th><td>Sort by ISO date (YYYY-MM-DD or YYYY/MM/DD; these formats can be followed by a time).</td></tr>
<tr><th><code>sorter: "percent"</code></th><td>Sort by percent.</td></tr>
<tr><th><code>sorter: "usLongDate"</code></th><td>Sort by date (U.S. Standard, e.g. Jan 18, 2001 9:12 AM or 18 Jan 2001 9:12 AM (new in v2.7.4)).</td></tr>
<tr><th><code>sorter: "shortDate"</code></th><td>Sort by a shorten date (see <a href="#dateformat"><code>dateFormat</code></a>).</td></tr>
<tr><th><code>sorter: "shortDate"</code></th><td>Sort by a shortened date (see <a href="#dateformat"><code>dateFormat</code></a>; these formats can also be followed by a time).</td></tr>
<tr><th><code>sorter: "time"</code></th><td>Sort by time (23:59 or 12:59 pm).</td></tr>
<tr><th><code>sorter: "metadata"</code></th><td>Sort by the sorter value in the metadata - requires the metadata plugin.</td></tr>
</tbody></table><br>
@ -962,7 +985,7 @@ From the example function above, you'll end up with something similar to this HT
<td>String</td>
<td>"asc"</td>
<td>
The direction a column sorts when clicking the header for the first time. Valid arguments are <code>"asc"</code> for Ascending or <code>"desc"</code> for Descending.<br>
This sets the direction a column will sort when clicking on the header for the first time. Valid arguments are <code>"asc"</code> for Ascending or <code>"desc"</code> for Descending.<br>
<div class="collapsible">
<br>
This order can also be set by desired column using the <a href="#headers"><code>headers</code></a> option (Added in v2.0.8).<br>
@ -2156,7 +2179,7 @@ $("table").trigger("sorton", [sort, callback]);</pre></div>
<tr id="sortreset-method">
<td><a href="#" class="toggle2">sortReset</a></td>
<td>Use this method to reset the table to it's initial unsorted state. <span class="tip"><em>New</em></span> v2.4.7.
<td>Use this method to reset the table to it's initial unsorted state (v2.4.7).
<div class="collapsible">
Don't confuse this method with the <a href="#sortreset"><code>sortReset</code> option</a>. This method immediately resets the entire table sort, while the option only resets the column sort after a third click.
<pre class="prettyprint lang-javascript">// Reset the table (make it unsorted)
@ -2167,7 +2190,7 @@ $("table").trigger("sortReset");</pre></div>
<tr id="update">
<td><a href="#" class="toggle2">update</a></td>
<td>Update the stored tablesorter data and the table.
<td>Update the <code>tbody</code>'s stored data (<code>updateRow</code> does exactly the same thing)
<div class="collapsible">
<pre class="prettyprint lang-javascript">// Add new content
$("table tbody").append(html);
@ -2192,6 +2215,30 @@ $("table").trigger("sorton", [sorting]);</pre></div>
<td><a href="example-ajax.html">Example</a></td>
</tr>
<tr id="updateall">
<td><a href="#" class="toggle2">updateAll</a></td>
<td>Update a column of cells (<code>thead</code> and <code>tbody</code>) <span class="tip"><em>New!</em></span> v2.8.
<div class="collapsible">
<pre class="prettyprint lang-javascript">// Change thead & tbody column of cells
// remember, "eq()" is zero based & "nth-child()" is 1 based
$("table thead th:eq(2)").html("Number");
// add some random numbers to the table cell
$("table tbody").find('td:nth-child(3)').html(function(i,h){
return Math.floor(Math.random()*10) + 1; // random number from 0 to 10
});
var resort = true, // re-apply the current sort
callback = function(){
alert('table updated!');
};
// let the plugin know that we made a update, then the plugin will
// automatically sort the table based on the header settings
$("table").trigger("updateAll", [ resort, callback ]);</pre></div>
</td>
<td><a href="example-update-all.html">Example</a></td>
</tr>
<tr id="appendcache">
<td><a href="#" class="toggle2">appendCache</a></td>
<td>Update a table that has had its data dynamically changed; used in conjunction with "update".<br>
@ -2290,14 +2337,15 @@ $('table').trigger('applyWidgets');
$("table").trigger("destroy");
// Remove tablesorter and all classes but the "tablesorter" class on the table
$("table").trigger("destroy", [false];</pre></div>
// callback is a function
$("table").trigger("destroy", [false, callback]);</pre></div>
</td>
<td></td>
</tr>
<tr id="refreshwidgets">
<td><a href="#" class="toggle2">refreshWidgets</a></td>
<td>Refresh the currently applied widgets. Depending on the options, it will completely remove all widgets, then re-initialize the current widgets or just remove all non-current widgets. <span class="tip"><em>New</em></span> v2.4.
<td>Refresh the currently applied widgets. Depending on the options, it will completely remove all widgets, then re-initialize the current widgets or just remove all non-current widgets (v2.4).
<div class="collapsible"><br>
Trigger this method using either of the following methods (they are equivalent):
<pre class="prettyprint lang-javascript">// trigger a refresh widget event
@ -2324,7 +2372,7 @@ $.tablesorter.refreshWidgets(table, doAll, dontapply)</pre>
<tbody>
<tr id="search">
<td><a href="#" class="toggle2">search</a></td>
<td>Trigger the filter widget to update the search from current inputs and/or selections. <span class="tip"><em>Updated!</em></span> v2.4.
<td>Trigger the filter widget to update the search from current inputs and/or selections (v2.4).
<div class="collapsible">
This first method sends an array with the search strings to the filter widget.<pre class="prettyprint lang-javascript">$(function(){
// apply "2?%" filter to the fifth column (zero-based index)
@ -2509,7 +2557,7 @@ or, directly add the search string to the filter input as follows:<pre class="pr
<td><a href="#" class="toggle2">updateComplete</a></td>
<td>This event fires after tablesorter has completed updating. (v.2.3.9)
<div class="collapsible">
This occurs after an "update", "updateCell" or "addRows" method was called, but before any callback functions are executed.
This occurs after an "update", "updateAll", "updateCell" or "addRows" method was called, but before any callback functions are executed.
<pre class="prettyprint lang-javascript">$(function(){
// initialize the tablesorter plugin
@ -2588,7 +2636,7 @@ or, directly add the search string to the filter input as follows:<pre class="pr
<tr id="pagerbeforeinitialized">
<td><a href="#" class="toggle2">pagerBeforeInitialized</a></td>
<td>This event fires after all pager controls have been bound and set up but before the pager formats the table or loads any ajax data. <span class="tip"><em>New</em></span> v2.4.4.
<td>This event fires after all pager controls have been bound and set up but before the pager formats the table or loads any ajax data (v2.4.4).
<div class="collapsible">
<pre class="prettyprint lang-javascript">$(function(){
@ -2614,7 +2662,7 @@ or, directly add the search string to the filter input as follows:<pre class="pr
<tr id="pagerinitialized">
<td><a href="#" class="toggle2">pagerInitialized</a></td>
<td>This event fires when the pager plugin has completed initialization. <span class="tip"><em>New</em></span> v2.4.4.
<td>This event fires when the pager plugin has completed initialization (v2.4.4).
<div class="collapsible">
<pre class="prettyprint lang-javascript">$(function(){
@ -2641,7 +2689,7 @@ or, directly add the search string to the filter input as follows:<pre class="pr
<tr id="pagemoved">
<td><a href="#" class="toggle2">pageMoved</a></td>
<td>This event fires when the pager plugin begins to change to the selected page. <span class="tip"><em>New</em></span> v2.4.4.
<td>This event fires when the pager plugin begins to change to the selected page (v2.4.4).
<div class="collapsible">
This event may fire before the <code>pagerComplete</code> event when ajax processing is involved, or after the <code>pagerComplete</code> on normal use.
See <a href="https://github.com/Mottie/tablesorter/pull/153">issue #153</a>.
@ -2681,7 +2729,7 @@ or, directly add the search string to the filter input as follows:<pre class="pr
<tr id="filterinit">
<td><a href="#" class="toggle2">filterInit</a></td>
<td>Event triggered when the filter widget has finished initializing. <span class="tip"><em>New</em></span> v2.4.
<td>Event triggered when the filter widget has finished initializing (v2.4).
<div class="collapsible">
You can use this event to modify the filter elements (row, inputs and/or selects) as desired. Use it as follows:<pre class="prettyprint lang-javascript">$(function(){
$('table').bind('filterInit', function(){
@ -2694,7 +2742,7 @@ or, directly add the search string to the filter input as follows:<pre class="pr
<tr id="filterstart">
<td><a href="#" class="toggle2">filterStart</a></td>
<td>Event triggered when the filter widget has started processing the search. <span class="tip"><em>New</em></span> v2.4.
<td>Event triggered when the filter widget has started processing the search (v2.4).
<div class="collapsible">
You can use this event to do something like add a class to the filter row. Use it as follows:<pre class="prettyprint lang-javascript">$(function(){
$('table').bind('filterStart', function(){
@ -2707,7 +2755,7 @@ or, directly add the search string to the filter input as follows:<pre class="pr
<tr id="filterend">
<td><a href="#" class="toggle2">filterEnd</a></td>
<td>Event triggered when the filter widget has finished processing the search. <span class="tip"><em>New</em></span> v2.4.
<td>Event triggered when the filter widget has finished processing the search (v2.4).
<div class="collapsible">
You can use this event to do something like remove the class added to the filter row when the filtering started. Use it as follows:<pre class="prettyprint lang-javascript">$(function(){
$('table').bind('filterEnd', function(){
@ -2766,7 +2814,7 @@ or, directly add the search string to the filter input as follows:<pre class="pr
<p>If you would like to contribute, <a class="external" href="https://github.com/Mottie/tablesorter">fork a copy on github</a>.</p>
<p>Some basic <a href="../test.html">unit testing</a> has been added. If you would like to add more or report a problem, please use the appropriate link above. <span class="tip"><em>New!</em></span> v2.6.</p>
<p>Some basic <a href="../test.html">unit testing</a> has been added. If you would like to add more or report a problem, please use the appropriate link above (v2.6).</p>
<p>Support is also available from <a class="external" href="http://stackoverflow.com/questions/tagged/tablesorter">stackoverflow</a>.</p>

View File

@ -131,9 +131,9 @@
text = $(node).text();
}
} else {
if (typeof(t) === "function") {
if (typeof t === "function") {
text = t(node, table, cellIndex);
} else if (typeof(t) === "object" && t.hasOwnProperty(cellIndex)) {
} else if (typeof t === "object" && t.hasOwnProperty(cellIndex)) {
text = t[cellIndex](node, table, cellIndex);
} else {
text = c.supportsTextContent ? node.textContent : $(node).text();
@ -208,7 +208,7 @@
if (c.debug) {
log(parsersDebug);
}
return list;
c.parsers = list;
}
/* utils */
@ -374,7 +374,8 @@
function buildHeaders(table) {
var header_index = computeThIndexes(table), ch, $t,
h, i, t, lock, time, c = table.config;
c.headerList = [], c.headerContent = [];
c.headerList = [];
c.headerContent = [];
if (c.debug) {
time = new Date();
}
@ -398,7 +399,7 @@
this.count = -1; // set to -1 because clicking on the header automatically adds one
this.lockedOrder = false;
lock = ts.getData($t, ch, 'lockedOrder') || false;
if (typeof(lock) !== 'undefined' && lock !== false) {
if (typeof lock !== 'undefined' && lock !== false) {
this.order = this.lockedOrder = formatSortingOrder(lock) ? [1,1,1] : [0,0,0];
}
$t.addClass(c.cssHeader);
@ -415,13 +416,23 @@
}
}
function updateHeader(table) {
var s, th,
function commonUpdate(table, resort, callback) {
var $t = $(table),
c = table.config;
// remove rows/elements before update
$t.find(c.selectorRemove).remove();
// rebuild parsers
buildParserCache(table);
// rebuild the cache map
buildCache(table);
checkResort($t, resort, callback);
}
function updateHeader(table) {
var s, c = table.config;
c.$headers.each(function(index, th){
s = ts.getData( th, c.headers[index], 'sorter' ) === 'false';
th.sortDisabled = s;
// $t.toggleClass('sorter-false', this.sortDisabled); not supported until jQuery 1.3
$(th)[ s ? 'addClass' : 'removeClass' ]('sorter-false');
});
}
@ -590,9 +601,9 @@
// sort multiple columns
function multisort(table) { /*jshint loopfunc:true */
var dynamicExp, sortWrapper, col, mx = 0, dir = 0, tc = table.config,
var dir = 0, tc = table.config,
sortList = tc.sortList, l = sortList.length, bl = table.tBodies.length,
sortTime, i, j, k, c, colMax, cache, lc, s, e, order, orgOrderCol;
sortTime, i, k, c, colMax, cache, lc, s, order, orgOrderCol;
if (tc.serverSideSorting || !tc.cache[0]) { // empty table - fixes #206
return;
}
@ -646,6 +657,149 @@
}
}
function bindEvents(table){
var c = table.config,
$this = $(table),
j, downTime;
// apply event handling to headers
c.$headers
// http://stackoverflow.com/questions/5312849/jquery-find-self; andSelf() deprecated in jQuery 1.8
.find('*')[ $.fn.addBack ? 'addBack': 'andSelf' ]().filter(c.selectorSort)
.unbind('mousedown.tablesorter mouseup.tablesorter')
.bind('mousedown.tablesorter mouseup.tablesorter', function(e, external) {
// jQuery v1.2.6 doesn't have closest()
var $cell = this.tagName.match('TH|TD') ? $(this) : $(this).parents('th, td').filter(':last'), cell = $cell[0];
// only recognize left clicks
if ((e.which || e.button) !== 1) { return false; }
// set timer on mousedown
if (e.type === 'mousedown') {
downTime = new Date().getTime();
return e.target.tagName === "INPUT" ? '' : !c.cancelSelection;
}
// ignore long clicks (prevents resizable widget from initializing a sort)
if (external !== true && (new Date().getTime() - downTime > 250)) { return false; }
if (c.delayInit && !c.cache) { buildCache(table); }
if (!cell.sortDisabled) {
initSort(table, cell, e);
}
});
if (c.cancelSelection) {
// cancel selection
c.$headers.each(function() {
this.onselectstart = function() {
return false;
};
});
}
// apply easy methods that trigger bound events
$this
.unbind('sortReset update updateRows updateCell updateAll addRows sorton appendCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave '.split(' ').join('.tablesorter '))
.bind("sortReset.tablesorter", function(e){
e.stopPropagation();
c.sortList = [];
setHeadersCss(table);
multisort(table);
appendToTable(table);
})
.bind("updateAll.tablesorter", function(e, resort, callback){
e.stopPropagation();
ts.restoreHeaders(table);
buildHeaders(table);
bindEvents(table);
commonUpdate(table, resort, callback);
})
.bind("update.tablesorter updateRows.tablesorter", function(e, resort, callback) {
e.stopPropagation();
// update sorting (if enabled/disabled)
updateHeader(table);
commonUpdate(table, resort, callback);
})
.bind("updateCell.tablesorter", function(e, cell, resort, callback) {
e.stopPropagation();
$this.find(c.selectorRemove).remove();
// get position from the dom
var l, row, icell,
$tb = $this.find('tbody'),
// update cache - format: function(s, table, cell, cellIndex)
// no closest in jQuery v1.2.6 - tbdy = $tb.index( $(cell).closest('tbody') ),$row = $(cell).closest('tr');
tbdy = $tb.index( $(cell).parents('tbody').filter(':last') ),
$row = $(cell).parents('tr').filter(':last');
cell = $(cell)[0]; // in case cell is a jQuery object
// tbody may not exist if update is initialized while tbody is removed for processing
if ($tb.length && tbdy >= 0) {
row = $tb.eq(tbdy).find('tr').index( $row );
icell = cell.cellIndex;
l = c.cache[tbdy].normalized[row].length - 1;
c.cache[tbdy].row[table.config.cache[tbdy].normalized[row][l]] = $row;
c.cache[tbdy].normalized[row][icell] = c.parsers[icell].format( getElementText(table, cell, icell), table, cell, icell );
checkResort($this, resort, callback);
}
})
.bind("addRows.tablesorter", function(e, $row, resort, callback) {
e.stopPropagation();
var i, rows = $row.filter('tr').length,
dat = [], l = $row[0].cells.length,
tbdy = $this.find('tbody').index( $row.closest('tbody') );
// fixes adding rows to an empty table - see issue #179
if (!c.parsers) {
buildParserCache(table);
}
// add each row
for (i = 0; i < rows; i++) {
// add each cell
for (j = 0; j < l; j++) {
dat[j] = c.parsers[j].format( getElementText(table, $row[i].cells[j], j), table, $row[i].cells[j], j );
}
// add the row index to the end
dat.push(c.cache[tbdy].row.length);
// update cache
c.cache[tbdy].row.push([$row[i]]);
c.cache[tbdy].normalized.push(dat);
dat = [];
}
// resort using current settings
checkResort($this, resort, callback);
})
.bind("sorton.tablesorter", function(e, list, callback, init) {
e.stopPropagation();
$this.trigger("sortStart", this);
// update header count index
updateHeaderSortCount(table, list);
// set css for headers
setHeadersCss(table);
// sort the table and append it to the dom
multisort(table);
appendToTable(table, init);
if (typeof callback === "function") {
callback(table);
}
})
.bind("appendCache.tablesorter", function(e, callback, init) {
e.stopPropagation();
appendToTable(table, init);
if (typeof callback === "function") {
callback(table);
}
})
.bind("applyWidgetId.tablesorter", function(e, id) {
e.stopPropagation();
ts.getWidgetById(id).format(table, c, c.widgetOptions);
})
.bind("applyWidgets.tablesorter", function(e, init) {
e.stopPropagation();
// apply widgets
ts.applyWidget(table, init);
})
.bind("refreshWidgets.tablesorter", function(e, all, dontapply){
e.stopPropagation();
ts.refreshWidgets(table, all, dontapply);
})
.bind("destroy.tablesorter", function(e, c, cb){
e.stopPropagation();
ts.destroy(table, c, cb);
});
}
/* public methods */
ts.construct = function(settings) {
return this.each(function() {
@ -654,8 +808,8 @@
return (this.config && this.config.debug) ? log('stopping initialization! No thead, tbody or tablesorter has already been initialized') : '';
}
// declare
var $cell, $this = $(this), table = this,
c, i, j, k = '', a, s, o, downTime,
var $this = $(this), table = this,
c, k = '',
m = $.metadata;
// initialization flag
table.hasInitialized = false;
@ -685,148 +839,12 @@
// do this after theme has been applied
fixColumnWidth(table);
// try to auto detect column type, and store in tables config
c.parsers = buildParserCache(table);
buildParserCache(table);
// build the cache for the tbody cells
// delayInit will delay building the cache until the user starts a sort
if (!c.delayInit) { buildCache(table); }
// apply event handling to headers
// this is to big, perhaps break it out?
c.$headers
// http://stackoverflow.com/questions/5312849/jquery-find-self; andSelf() deprecated in jQuery 1.8
.find('*')[ $.fn.addBack ? 'addBack': 'andSelf' ]().filter(c.selectorSort)
.unbind('mousedown.tablesorter mouseup.tablesorter')
.bind('mousedown.tablesorter mouseup.tablesorter', function(e, external) {
// jQuery v1.2.6 doesn't have closest()
var $cell = this.tagName.match('TH|TD') ? $(this) : $(this).parents('th, td').filter(':last'), cell = $cell[0];
// only recognize left clicks
if ((e.which || e.button) !== 1) { return false; }
// set timer on mousedown
if (e.type === 'mousedown') {
downTime = new Date().getTime();
return e.target.tagName === "INPUT" ? '' : !c.cancelSelection;
}
// ignore long clicks (prevents resizable widget from initializing a sort)
if (external !== true && (new Date().getTime() - downTime > 250)) { return false; }
if (c.delayInit && !c.cache) { buildCache(table); }
if (!cell.sortDisabled) {
initSort(table, cell, e);
}
});
if (c.cancelSelection) {
// cancel selection
c.$headers.each(function() {
this.onselectstart = function() {
return false;
};
});
}
// apply easy methods that trigger bound events
$this
.unbind('sortReset update updateRows updateCell addRows sorton appendCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave '.split(' ').join('.tablesorter '))
.bind("sortReset.tablesorter", function(){
e.stopPropagation();
c.sortList = [];
setHeadersCss(table);
multisort(table);
appendToTable(table);
})
.bind("update.tablesorter updateRows.tablesorter", function(e, resort, callback) {
e.stopPropagation();
// remove rows/elements before update
$this.find(c.selectorRemove).remove();
// update sorting
updateHeader(table);
// rebuild parsers
c.parsers = buildParserCache(table);
// rebuild the cache map
buildCache(table);
checkResort($this, resort, callback);
})
.bind("updateCell.tablesorter", function(e, cell, resort, callback) {
e.stopPropagation();
$this.find(c.selectorRemove).remove();
// get position from the dom
var l, row, icell,
$tb = $this.find('tbody'),
// update cache - format: function(s, table, cell, cellIndex)
// no closest in jQuery v1.2.6 - tbdy = $tb.index( $(cell).closest('tbody') ),$row = $(cell).closest('tr');
tbdy = $tb.index( $(cell).parents('tbody').filter(':last') ),
$row = $(cell).parents('tr').filter(':last');
cell = $(cell)[0]; // in case cell is a jQuery object
// tbody may not exist if update is initialized while tbody is removed for processing
if ($tb.length && tbdy >= 0) {
row = $tb.eq(tbdy).find('tr').index( $row );
icell = cell.cellIndex;
l = c.cache[tbdy].normalized[row].length - 1;
c.cache[tbdy].row[table.config.cache[tbdy].normalized[row][l]] = $row;
c.cache[tbdy].normalized[row][icell] = c.parsers[icell].format( getElementText(table, cell, icell), table, cell, icell );
checkResort($this, resort, callback);
}
})
.bind("addRows.tablesorter", function(e, $row, resort, callback) {
e.stopPropagation();
var i, rows = $row.filter('tr').length,
dat = [], l = $row[0].cells.length,
tbdy = $this.find('tbody').index( $row.closest('tbody') );
// fixes adding rows to an empty table - see issue #179
if (!c.parsers) {
c.parsers = buildParserCache(table);
}
// add each row
for (i = 0; i < rows; i++) {
// add each cell
for (j = 0; j < l; j++) {
dat[j] = c.parsers[j].format( getElementText(table, $row[i].cells[j], j), table, $row[i].cells[j], j );
}
// add the row index to the end
dat.push(c.cache[tbdy].row.length);
// update cache
c.cache[tbdy].row.push([$row[i]]);
c.cache[tbdy].normalized.push(dat);
dat = [];
}
// resort using current settings
checkResort($this, resort, callback);
})
.bind("sorton.tablesorter", function(e, list, callback, init) {
e.stopPropagation();
$this.trigger("sortStart", this);
// update header count index
updateHeaderSortCount(table, list);
// set css for headers
setHeadersCss(table);
// sort the table and append it to the dom
multisort(table);
appendToTable(table, init);
if (typeof callback === "function") {
callback(table);
}
})
.bind("appendCache.tablesorter", function(e, callback, init) {
e.stopPropagation();
appendToTable(table, init);
if (typeof callback === "function") {
callback(table);
}
})
.bind("applyWidgetId.tablesorter", function(e, id) {
e.stopPropagation();
ts.getWidgetById(id).format(table, c, c.widgetOptions);
})
.bind("applyWidgets.tablesorter", function(e, init) {
e.stopPropagation();
// apply widgets
ts.applyWidget(table, init);
})
.bind("refreshWidgets.tablesorter", function(e, all, dontapply){
e.stopPropagation();
ts.refreshWidgets(table, all, dontapply);
})
.bind("destroy.tablesorter", function(e, c, cb){
e.stopPropagation();
ts.destroy(table, c, cb);
});
// bind all header events and methods
bindEvents(table);
// get sort list from jQuery data or metadata
// in jQuery < 1.4, an error occurs when calling $this.data()
if (c.supportsDataObject && typeof $this.data().sortlist !== 'undefined') {
@ -888,7 +906,7 @@
// detach tbody but save the position
// don't use tbody because there are portions that look for a tbody index (updateCell)
ts.processTbody = function(table, $tb, getIt){
var t, holdr;
var holdr;
if (getIt) {
table.isProcessing = true;
$tb.before('<span class="tablesorter-savemyplace"/>');
@ -905,6 +923,18 @@
$(table)[0].config.$tbodies.empty();
};
// restore headers
ts.restoreHeaders = function(table){
var c = table.config;
c.$headers.each(function(i){
// only restore header cells if it is wrapped
// because this is also used by the updateAll method
if ($(this).find('.tablesorter-header-inner').length){
$(this).html( c.headerContent[i] );
}
});
};
ts.destroy = function(table, removeClasses, callback){
table = $(table)[0];
if (!table.hasInitialized) { return; }
@ -919,15 +949,12 @@
// disable tablesorter
$t
.removeData('tablesorter')
.unbind('sortReset update updateRows updateCell addRows sorton appendCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave sortBegin sortEnd '.split(' ').join('.tablesorter '));
.unbind('sortReset update updateAll updateRows updateCell addRows sorton appendCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave sortBegin sortEnd '.split(' ').join('.tablesorter '));
c.$headers.add($f)
.removeClass(c.cssHeader + ' ' + c.cssAsc + ' ' + c.cssDesc)
.removeAttr('data-column');
$r.find(c.selectorSort).unbind('mousedown.tablesorter mouseup.tablesorter');
// restore headers
$r.children().each(function(i){
$(this).html( c.headerContent[i] );
});
ts.restoreHeaders(table);
if (removeClasses !== false) {
$t.removeClass(c.tableClass + ' tablesorter-' + c.theme);
}
@ -951,8 +978,8 @@
if (a === b) { return 0; }
var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ],
r = ts.regex, xN, xD, yN, yD, xF, yF, i, mx;
if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : -e || -1; }
if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : e || 1; }
if (a === '' && e !== 0) { return typeof e === 'boolean' ? (e ? -1 : 1) : -e || -1; }
if (b === '' && e !== 0) { return typeof e === 'boolean' ? (e ? 1 : -1) : e || 1; }
if (typeof c.textSorter === 'function') { return c.textSorter(a, b, table, col); }
// chunk/tokenize
xN = a.replace(r[0], '\\0$1\\0').replace(/\\0$/, '').replace(/^\\0/, '').split('\\0');
@ -987,8 +1014,8 @@
ts.sortTextDesc = function(table, a, b, col) {
if (a === b) { return 0; }
var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ];
if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : e || 1; }
if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : -e || -1; }
if (a === '' && e !== 0) { return typeof e === 'boolean' ? (e ? -1 : 1) : e || 1; }
if (b === '' && e !== 0) { return typeof e === 'boolean' ? (e ? 1 : -1) : -e || -1; }
if (typeof c.textSorter === 'function') { return c.textSorter(b, a, table, col); }
return ts.sortText(table, b, a);
};
@ -1011,8 +1038,8 @@
ts.sortNumeric = function(table, a, b, col, mx, d) {
if (a === b) { return 0; }
var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ];
if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : -e || -1; }
if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : e || 1; }
if (a === '' && e !== 0) { return typeof e === 'boolean' ? (e ? -1 : 1) : -e || -1; }
if (b === '' && e !== 0) { return typeof e === 'boolean' ? (e ? 1 : -1) : e || 1; }
if (isNaN(a)) { a = ts.getTextValue(a, mx, d); }
if (isNaN(b)) { b = ts.getTextValue(b, mx, d); }
return a - b;
@ -1021,8 +1048,8 @@
ts.sortNumericDesc = function(table, a, b, col, mx, d) {
if (a === b) { return 0; }
var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ];
if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : e || 1; }
if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : -e || -1; }
if (a === '' && e !== 0) { return typeof e === 'boolean' ? (e ? -1 : 1) : e || 1; }
if (b === '' && e !== 0) { return typeof e === 'boolean' ? (e ? 1 : -1) : -e || -1; }
if (isNaN(a)) { a = ts.getTextValue(a, mx, d); }
if (isNaN(b)) { b = ts.getTextValue(b, mx, d); }
return b - a;
@ -1187,7 +1214,7 @@
};
ts.formatFloat = function(s, table) {
if (typeof(s) !== 'string' || s === '') { return s; }
if (typeof s !== 'string' || s === '') { return s; }
// allow using formatFloat without a table; defaults to US number format
var i,
t = table && table.config ? table.config.usNumberFormat !== false :
@ -1228,10 +1255,10 @@
// add default parsers
ts.addParser({
id: "text",
is: function(s, table, node) {
is: function() {
return true;
},
format: function(s, table, cell, cellIndex) {
format: function(s, table) {
var c = table.config;
if (s) {
s = $.trim( c.ignoreCase ? s.toLocaleLowerCase() : s );
@ -1331,7 +1358,7 @@
id: "shortDate", // "mmddyyyy", "ddmmyyyy" or "yyyymmdd"
is: function(s) {
// testing for ##-##-#### or ####-##-##, so it's not perfect; time can be included
return (/(^\d{1,2}[\/\s]\d{1,2}[\/\s]\d{4})|(^\d{4}[\/\s]\d{1,2}[\/\s]\d{1,2})/).test((s || '').replace(/\s+/g," ").replace(/[-.,]/g, "/"));
return (/(^\d{1,2}[\/\s]\d{1,2}[\/\s]\d{4})|(^\d{4}[\/\s]\d{1,2}[\/\s]\d{1,2})/).test((s || '').replace(/\s+/g," ").replace(/[\-.,]/g, "/"));
},
format: function(s, table, cell, cellIndex) {
if (s) {
@ -1341,7 +1368,7 @@
// cache header formatting so it doesn't getData for every cell in the column
format = ci.shortDateFormat = ts.getData( ci, c.headers[cellIndex], 'dateFormat') || c.dateFormat;
}
s = s.replace(/\s+/g," ").replace(/[-.,]/g, "/");
s = s.replace(/\s+/g," ").replace(/[\-.,]/g, "/"); // escaped - because JSHint in Firefox was showing it as an error
if (format === "mmddyyyy") {
s = s.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/, "$3/$1/$2");
} else if (format === "ddmmyyyy") {
@ -1368,7 +1395,7 @@
ts.addParser({
id: "metadata",
is: function(s) {
is: function() {
return false;
},
format: function(s, table, cell) {
@ -1414,7 +1441,7 @@
remove: function(table, c, wo){
var k, $tb,
b = c.$tbodies,
rmv = (c.widgetOptions.zebra || [ "even", "odd" ]).join(' ');
rmv = (wo.zebra || [ "even", "odd" ]).join(' ');
for (k = 0; k < b.length; k++ ){
$tb = $.tablesorter.processTbody(table, b.eq(k), true); // remove tbody
$tb.children().removeClass(rmv);

View File

@ -24,15 +24,15 @@
table2 = $table2[0],
th0 = $table1.find('th')[0], // first table header cell
init = false,
undef, c1, c2, i, l, t;
undef, c1, c2, e, i, l, t;
$('.tester:eq(0)')
$table1
.bind('tablesorter-initialized', function(){
init = true;
})
.tablesorter();
$('.tester:eq(1)').tablesorter({
$table2.tablesorter({
headers: {
0: { sorter: 'text' },
1: { sorter: 'text' },
@ -223,38 +223,61 @@
************************************************/
test( "parser cache; sorton methods", function() {
expect(3);
$table1.trigger('sortReset');
// lower case because table was parsed before c1.ignoreCase was changed
tester.cacheCompare( table1, [ 'test2', 2, 'test1', 3, 'test3', 1, '', '', 'testb', 5, 'testc', 4, 'testa', 6 ], 'unsorted' );
tester.cacheCompare( table1, [ 'test2', 'x2', 'test1', 'x3', 'test3', 'x1', '', '', 'testb', 'x5', 'testc', 'x4', 'testa', 'x6' ], 'unsorted' );
$table1.trigger('sorton', [[[ 0,0 ]]]);
tester.cacheCompare( table1, [ 'test1', 3, 'test2', 2, 'test3', 1, '', '', 'testa', 6, 'testb', 5, 'testc', 4 ], 'ascending sort' );
tester.cacheCompare( table1, [ 'test1', 'x3', 'test2', 'x2', 'test3', 'x1', '', '', 'testa', 'x6', 'testb', 'x5', 'testc', 'x4' ], 'ascending sort' );
$table1.trigger('sorton', [[[ 0,1 ]]]);
tester.cacheCompare( table1, [ 'test3', 1, 'test2', 2, 'test1', 3, '', '', 'testc', 4, 'testb', 5, 'testa', 6 ], 'descending sort' );
tester.cacheCompare( table1, [ 'test3', 'x1', 'test2', 'x2', 'test1', 'x3', '', '', 'testc', 'x4', 'testb', 'x5', 'testa', 'x6' ], 'descending sort' );
});
/************************************************
test update methods
************************************************/
test( "parser cache; update methods", function() {
expect(3);
expect(5);
c1.ignoreCase = true;
// updateAll
$table1
.find('th:eq(1)').removeAttr('class').html('num').end()
.find('td:nth-child(2)').html(function(i,h){
return h.substring(1);
});
$table1.trigger('updateAll', [false, function(){
c1 = $table1[0].config;
var nw = $table1.find('th:eq(1)')[0],
hc = c1.headerContent[1] === 'num',
hd = c1.$headers[1] === nw,
hl = c1.headerList[1] === nw,
p1 = c1.parsers[1].id === 'digit';
equal(hc && hd && hl && p1, true, 'testing header cache: updateAll - thead');
tester.cacheCompare( table1, [ 'test3', 1, 'test2', 2, 'test1', 3, '', '', 'testc', 4, 'testb', 5, 'testa', 6 ], 'updateAll - tbody' );
}]);
// addRows
t = $('<tr class="temp"><td>testd</td><td>7</td></tr>');
$table1.find('tbody:last').append(t);
$table1.trigger('addRows', [t, true]);
tester.cacheCompare( table1, [ 'test3', 1, 'test2', 2, 'test1', 3, '', '', 'testd', 7, 'testc', 4, 'testb', 5, 'testa', 6 ], 'addRows method' );
$table1.trigger('addRows', [t, true, function(){
tester.cacheCompare( table1, [ 'test3', 1, 'test2', 2, 'test1', 3, '', '', 'testd', 7, 'testc', 4, 'testb', 5, 'testa', 6 ], 'addRows method' );
}]);
// updateCell
t = $table1.find('td:contains("testd")');
t.html('texte');
$table1.trigger('updateCell', [t[0], true]);
tester.cacheCompare( table1, [ 'test3', 1, 'test2', 2, 'test1', 3, '', '', 'texte', 7, 'testc', 4, 'testb', 5, 'testa', 6 ], 'updateCell method' );
$table1.trigger('updateCell', [t[0], true, function(){
tester.cacheCompare( table1, [ 'test3', 1, 'test2', 2, 'test1', 3, '', '', 'texte', 7, 'testc', 4, 'testb', 5, 'testa', 6 ], 'updateCell method' );
}]);
// update
$table1.find('tr.temp').remove();
c1.ignoreCase = true;
$table1.trigger('update', [true]);
tester.cacheCompare( table1, [ 'test3', 1, 'test2', 2, 'test1', 3, '', '', 'testc', 4, 'testb', 5, 'testa', 6 ], 'update method' );
$table1.trigger('update', [true, function(){
tester.cacheCompare( table1, [ 'test3', 1, 'test2', 2, 'test1', 3, '', '', 'testc', 4, 'testb', 5, 'testa', 6 ], 'update method' );
}]);
});
@ -291,6 +314,26 @@
equal( zebra(), true, 'zebra is applied' );
});
/************************************************
check destroy method
************************************************/
test("testing destroy method", function(){
$table2.trigger('sorton', [[[ 0,1 ]]] );
$table2.trigger('destroy');
expect(7);
t = $table2.find('th:first');
e = jQuery._data(table2, 'events'); // get a list of all bound events
equal( $.isEmptyObject(e), true, 'no events applied' );
equal( $table2.data().hasOwnProperty('tablesorter'), false, 'Data removed' );
equal( $table2.attr('class'), 'tester', 'All table classes removed' );
equal( $table2.find('tr:first').attr('class'), '', 'Header row class removed' );
equal( t.attr('class').match('tablesorter'), null, 'Header classes removed' );
equal( t.children().length, 0, 'Inner wrapper removed' );
equal( typeof (t.data().column) , 'undefined', 'data-column removed');
$table2.tablesorter();
});
});
</script>
@ -320,20 +363,20 @@
<tr><th class="{sortValue:'zzz', poe:'nevermore'}">test-head</th><th>num</th></tr>
</thead>
<tfoot>
<tr><th>test-foot</th><th>num</th></tr>
<tr><th>test-foot</th><th>txt</th></tr>
</tfoot>
<tbody>
<tr><td>test2</td><td>2</td></tr>
<tr><td>test1</td><td>3</td></tr>
<tr><td>test3</td><td>1</td></tr>
<tr><td>test2</td><td>x2</td></tr>
<tr><td>test1</td><td>x3</td></tr>
<tr><td>test3</td><td>x1</td></tr>
</tbody>
<tbody class="tablesorter-infoOnly">
<tr><td colspan="3">Info</td></tr>
</tbody>
<tbody>
<tr><td>testB</td><td>5</td></tr>
<tr><td>testC</td><td>4</td></tr>
<tr><td>testA</td><td>6</td></tr>
<tr><td>testB</td><td>x5</td></tr>
<tr><td>testC</td><td>x4</td></tr>
<tr><td>testA</td><td>x6</td></tr>
</tbody>
</table>