1 |
/** |
2 |
* Change the order of the table |
3 |
* @param {object} oSettings dataTables settings object |
4 |
* @param {bool} bApplyClasses optional - should we apply classes or not |
5 |
* @memberof DataTable#oApi |
6 |
*/ |
7 |
function _fnSort ( oSettings, bApplyClasses ) |
8 |
{ |
9 |
var |
10 |
i, iLen, j, jLen, k, kLen, |
11 |
sDataType, nTh, |
12 |
aaSort = [], |
13 |
aiOrig = [], |
14 |
oSort = DataTable.ext.oSort, |
15 |
aoData = oSettings.aoData, |
16 |
aoColumns = oSettings.aoColumns, |
17 |
oAria = oSettings.oLanguage.oAria; |
18 |
|
19 |
/* No sorting required if server-side or no sorting array */ |
20 |
if ( !oSettings.oFeatures.bServerSide && |
21 |
(oSettings.aaSorting.length !== 0 || oSettings.aaSortingFixed !== null) ) |
22 |
{ |
23 |
aaSort = ( oSettings.aaSortingFixed !== null ) ? |
24 |
oSettings.aaSortingFixed.concat( oSettings.aaSorting ) : |
25 |
oSettings.aaSorting.slice(); |
26 |
|
27 |
/* If there is a sorting data type, and a function belonging to it, then we need to |
28 |
* get the data from the developer's function and apply it for this column |
29 |
*/ |
30 |
for ( i=0 ; i<aaSort.length ; i++ ) |
31 |
{ |
32 |
var iColumn = aaSort[i][0]; |
33 |
var iVisColumn = _fnColumnIndexToVisible( oSettings, iColumn ); |
34 |
sDataType = oSettings.aoColumns[ iColumn ].sSortDataType; |
35 |
if ( DataTable.ext.afnSortData[sDataType] ) |
36 |
{ |
37 |
var aData = DataTable.ext.afnSortData[sDataType].call( |
38 |
oSettings.oInstance, oSettings, iColumn, iVisColumn |
39 |
); |
40 |
if ( aData.length === aoData.length ) |
41 |
{ |
42 |
for ( j=0, jLen=aoData.length ; j<jLen ; j++ ) |
43 |
{ |
44 |
_fnSetCellData( oSettings, j, iColumn, aData[j] ); |
45 |
} |
46 |
} |
47 |
else |
48 |
{ |
49 |
_fnLog( oSettings, 0, "Returned data sort array (col "+iColumn+") is the wrong length" ); |
50 |
} |
51 |
} |
52 |
} |
53 |
|
54 |
/* Create a value - key array of the current row positions such that we can use their |
55 |
* current position during the sort, if values match, in order to perform stable sorting |
56 |
*/ |
57 |
for ( i=0, iLen=oSettings.aiDisplayMaster.length ; i<iLen ; i++ ) |
58 |
{ |
59 |
aiOrig[ oSettings.aiDisplayMaster[i] ] = i; |
60 |
} |
61 |
|
62 |
/* Build an internal data array which is specific to the sort, so we can get and prep |
63 |
* the data to be sorted only once, rather than needing to do it every time the sorting |
64 |
* function runs. This make the sorting function a very simple comparison |
65 |
*/ |
66 |
var iSortLen = aaSort.length; |
67 |
var fnSortFormat, aDataSort; |
68 |
for ( i=0, iLen=aoData.length ; i<iLen ; i++ ) |
69 |
{ |
70 |
for ( j=0 ; j<iSortLen ; j++ ) |
71 |
{ |
72 |
aDataSort = aoColumns[ aaSort[j][0] ].aDataSort; |
73 |
|
74 |
for ( k=0, kLen=aDataSort.length ; k<kLen ; k++ ) |
75 |
{ |
76 |
sDataType = aoColumns[ aDataSort[k] ].sType; |
77 |
fnSortFormat = oSort[ (sDataType ? sDataType : 'string')+"-pre" ]; |
78 |
|
79 |
aoData[i]._aSortData[ aDataSort[k] ] = fnSortFormat ? |
80 |
fnSortFormat( _fnGetCellData( oSettings, i, aDataSort[k], 'sort' ) ) : |
81 |
_fnGetCellData( oSettings, i, aDataSort[k], 'sort' ); |
82 |
} |
83 |
} |
84 |
} |
85 |
|
86 |
/* Do the sort - here we want multi-column sorting based on a given data source (column) |
87 |
* and sorting function (from oSort) in a certain direction. It's reasonably complex to |
88 |
* follow on it's own, but this is what we want (example two column sorting): |
89 |
* fnLocalSorting = function(a,b){ |
90 |
* var iTest; |
91 |
* iTest = oSort['string-asc']('data11', 'data12'); |
92 |
* if (iTest !== 0) |
93 |
* return iTest; |
94 |
* iTest = oSort['numeric-desc']('data21', 'data22'); |
95 |
* if (iTest !== 0) |
96 |
* return iTest; |
97 |
* return oSort['numeric-asc']( aiOrig[a], aiOrig[b] ); |
98 |
* } |
99 |
* Basically we have a test for each sorting column, if the data in that column is equal, |
100 |
* test the next column. If all columns match, then we use a numeric sort on the row |
101 |
* positions in the original data array to provide a stable sort. |
102 |
*/ |
103 |
oSettings.aiDisplayMaster.sort( function ( a, b ) { |
104 |
var k, l, lLen, iTest, aDataSort, sDataType; |
105 |
for ( k=0 ; k<iSortLen ; k++ ) |
106 |
{ |
107 |
aDataSort = aoColumns[ aaSort[k][0] ].aDataSort; |
108 |
|
109 |
for ( l=0, lLen=aDataSort.length ; l<lLen ; l++ ) |
110 |
{ |
111 |
sDataType = aoColumns[ aDataSort[l] ].sType; |
112 |
|
113 |
iTest = oSort[ (sDataType ? sDataType : 'string')+"-"+aaSort[k][1] ]( |
114 |
aoData[a]._aSortData[ aDataSort[l] ], |
115 |
aoData[b]._aSortData[ aDataSort[l] ] |
116 |
); |
117 |
|
118 |
if ( iTest !== 0 ) |
119 |
{ |
120 |
return iTest; |
121 |
} |
122 |
} |
123 |
} |
124 |
|
125 |
return oSort['numeric-asc']( aiOrig[a], aiOrig[b] ); |
126 |
} ); |
127 |
} |
128 |
|
129 |
/* Alter the sorting classes to take account of the changes */ |
130 |
if ( (bApplyClasses === undefined || bApplyClasses) && !oSettings.oFeatures.bDeferRender ) |
131 |
{ |
132 |
_fnSortingClasses( oSettings ); |
133 |
} |
134 |
|
135 |
for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ ) |
136 |
{ |
137 |
var sTitle = aoColumns[i].sTitle.replace( /<.*?>/g, "" ); |
138 |
nTh = aoColumns[i].nTh; |
139 |
nTh.removeAttribute('aria-sort'); |
140 |
nTh.removeAttribute('aria-label'); |
141 |
|
142 |
/* In ARIA only the first sorting column can be marked as sorting - no multi-sort option */ |
143 |
if ( aoColumns[i].bSortable ) |
144 |
{ |
145 |
if ( aaSort.length > 0 && aaSort[0][0] == i ) |
146 |
{ |
147 |
nTh.setAttribute('aria-sort', aaSort[0][1]=="asc" ? "ascending" : "descending" ); |
148 |
|
149 |
var nextSort = (aoColumns[i].asSorting[ aaSort[0][2]+1 ]) ? |
150 |
aoColumns[i].asSorting[ aaSort[0][2]+1 ] : aoColumns[i].asSorting[0]; |
151 |
nTh.setAttribute('aria-label', sTitle+ |
152 |
(nextSort=="asc" ? oAria.sSortAscending : oAria.sSortDescending) ); |
153 |
} |
154 |
else |
155 |
{ |
156 |
nTh.setAttribute('aria-label', sTitle+ |
157 |
(aoColumns[i].asSorting[0]=="asc" ? oAria.sSortAscending : oAria.sSortDescending) ); |
158 |
} |
159 |
} |
160 |
else |
161 |
{ |
162 |
nTh.setAttribute('aria-label', sTitle); |
163 |
} |
164 |
} |
165 |
|
166 |
/* Tell the draw function that we have sorted the data */ |
167 |
oSettings.bSorted = true; |
168 |
$(oSettings.oInstance).trigger('sort', oSettings); |
169 |
|
170 |
/* Copy the master data into the draw array and re-draw */ |
171 |
if ( oSettings.oFeatures.bFilter ) |
172 |
{ |
173 |
/* _fnFilter() will redraw the table for us */ |
174 |
_fnFilterComplete( oSettings, oSettings.oPreviousSearch, 1 ); |
175 |
} |
176 |
else |
177 |
{ |
178 |
oSettings.aiDisplay = oSettings.aiDisplayMaster.slice(); |
179 |
oSettings._iDisplayStart = 0; /* reset display back to page 0 */ |
180 |
_fnCalculateEnd( oSettings ); |
181 |
_fnDraw( oSettings ); |
182 |
} |
183 |
} |
184 |
|
185 |
|
186 |
/** |
187 |
* Attach a sort handler (click) to a node |
188 |
* @param {object} oSettings dataTables settings object |
189 |
* @param {node} nNode node to attach the handler to |
190 |
* @param {int} iDataIndex column sorting index |
191 |
* @param {function} [fnCallback] callback function |
192 |
* @memberof DataTable#oApi |
193 |
*/ |
194 |
function _fnSortAttachListener ( oSettings, nNode, iDataIndex, fnCallback ) |
195 |
{ |
196 |
_fnBindAction( nNode, {}, function (e) { |
197 |
/* If the column is not sortable - don't to anything */ |
198 |
if ( oSettings.aoColumns[iDataIndex].bSortable === false ) |
199 |
{ |
200 |
return; |
201 |
} |
202 |
|
203 |
/* |
204 |
* This is a little bit odd I admit... I declare a temporary function inside the scope of |
205 |
* _fnBuildHead and the click handler in order that the code presented here can be used |
206 |
* twice - once for when bProcessing is enabled, and another time for when it is |
207 |
* disabled, as we need to perform slightly different actions. |
208 |
* Basically the issue here is that the Javascript engine in modern browsers don't |
209 |
* appear to allow the rendering engine to update the display while it is still executing |
210 |
* it's thread (well - it does but only after long intervals). This means that the |
211 |
* 'processing' display doesn't appear for a table sort. To break the js thread up a bit |
212 |
* I force an execution break by using setTimeout - but this breaks the expected |
213 |
* thread continuation for the end-developer's point of view (their code would execute |
214 |
* too early), so we only do it when we absolutely have to. |
215 |
*/ |
216 |
var fnInnerSorting = function () { |
217 |
var iColumn, iNextSort; |
218 |
|
219 |
/* If the shift key is pressed then we are multiple column sorting */ |
220 |
if ( e.shiftKey ) |
221 |
{ |
222 |
/* Are we already doing some kind of sort on this column? */ |
223 |
var bFound = false; |
224 |
for ( var i=0 ; i<oSettings.aaSorting.length ; i++ ) |
225 |
{ |
226 |
if ( oSettings.aaSorting[i][0] == iDataIndex ) |
227 |
{ |
228 |
bFound = true; |
229 |
iColumn = oSettings.aaSorting[i][0]; |
230 |
iNextSort = oSettings.aaSorting[i][2]+1; |
231 |
|
232 |
if ( !oSettings.aoColumns[iColumn].asSorting[iNextSort] ) |
233 |
{ |
234 |
/* Reached the end of the sorting options, remove from multi-col sort */ |
235 |
oSettings.aaSorting.splice( i, 1 ); |
236 |
} |
237 |
else |
238 |
{ |
239 |
/* Move onto next sorting direction */ |
240 |
oSettings.aaSorting[i][1] = oSettings.aoColumns[iColumn].asSorting[iNextSort]; |
241 |
oSettings.aaSorting[i][2] = iNextSort; |
242 |
} |
243 |
break; |
244 |
} |
245 |
} |
246 |
|
247 |
/* No sort yet - add it in */ |
248 |
if ( bFound === false ) |
249 |
{ |
250 |
oSettings.aaSorting.push( [ iDataIndex, |
251 |
oSettings.aoColumns[iDataIndex].asSorting[0], 0 ] ); |
252 |
} |
253 |
} |
254 |
else |
255 |
{ |
256 |
/* If no shift key then single column sort */ |
257 |
if ( oSettings.aaSorting.length == 1 && oSettings.aaSorting[0][0] == iDataIndex ) |
258 |
{ |
259 |
iColumn = oSettings.aaSorting[0][0]; |
260 |
iNextSort = oSettings.aaSorting[0][2]+1; |
261 |
if ( !oSettings.aoColumns[iColumn].asSorting[iNextSort] ) |
262 |
{ |
263 |
iNextSort = 0; |
264 |
} |
265 |
oSettings.aaSorting[0][1] = oSettings.aoColumns[iColumn].asSorting[iNextSort]; |
266 |
oSettings.aaSorting[0][2] = iNextSort; |
267 |
} |
268 |
else |
269 |
{ |
270 |
oSettings.aaSorting.splice( 0, oSettings.aaSorting.length ); |
271 |
oSettings.aaSorting.push( [ iDataIndex, |
272 |
oSettings.aoColumns[iDataIndex].asSorting[0], 0 ] ); |
273 |
} |
274 |
} |
275 |
|
276 |
/* Run the sort */ |
277 |
_fnSort( oSettings ); |
278 |
}; /* /fnInnerSorting */ |
279 |
|
280 |
if ( !oSettings.oFeatures.bProcessing ) |
281 |
{ |
282 |
fnInnerSorting(); |
283 |
} |
284 |
else |
285 |
{ |
286 |
_fnProcessingDisplay( oSettings, true ); |
287 |
setTimeout( function() { |
288 |
fnInnerSorting(); |
289 |
if ( !oSettings.oFeatures.bServerSide ) |
290 |
{ |
291 |
_fnProcessingDisplay( oSettings, false ); |
292 |
} |
293 |
}, 0 ); |
294 |
} |
295 |
|
296 |
/* Call the user specified callback function - used for async user interaction */ |
297 |
if ( typeof fnCallback == 'function' ) |
298 |
{ |
299 |
fnCallback( oSettings ); |
300 |
} |
301 |
} ); |
302 |
} |
303 |
|
304 |
|
305 |
/** |
306 |
* Set the sorting classes on the header, Note: it is safe to call this function |
307 |
* when bSort and bSortClasses are false |
308 |
* @param {object} oSettings dataTables settings object |
309 |
* @memberof DataTable#oApi |
310 |
*/ |
311 |
function _fnSortingClasses( oSettings ) |
312 |
{ |
313 |
var i, iLen, j, jLen, iFound; |
314 |
var aaSort, sClass; |
315 |
var iColumns = oSettings.aoColumns.length; |
316 |
var oClasses = oSettings.oClasses; |
317 |
|
318 |
for ( i=0 ; i<iColumns ; i++ ) |
319 |
{ |
320 |
if ( oSettings.aoColumns[i].bSortable ) |
321 |
{ |
322 |
$(oSettings.aoColumns[i].nTh).removeClass( oClasses.sSortAsc +" "+ oClasses.sSortDesc + |
323 |
" "+ oSettings.aoColumns[i].sSortingClass ); |
324 |
} |
325 |
} |
326 |
|
327 |
if ( oSettings.aaSortingFixed !== null ) |
328 |
{ |
329 |
aaSort = oSettings.aaSortingFixed.concat( oSettings.aaSorting ); |
330 |
} |
331 |
else |
332 |
{ |
333 |
aaSort = oSettings.aaSorting.slice(); |
334 |
} |
335 |
|
336 |
/* Apply the required classes to the header */ |
337 |
for ( i=0 ; i<oSettings.aoColumns.length ; i++ ) |
338 |
{ |
339 |
if ( oSettings.aoColumns[i].bSortable ) |
340 |
{ |
341 |
sClass = oSettings.aoColumns[i].sSortingClass; |
342 |
iFound = -1; |
343 |
for ( j=0 ; j<aaSort.length ; j++ ) |
344 |
{ |
345 |
if ( aaSort[j][0] == i ) |
346 |
{ |
347 |
sClass = ( aaSort[j][1] == "asc" ) ? |
348 |
oClasses.sSortAsc : oClasses.sSortDesc; |
349 |
iFound = j; |
350 |
break; |
351 |
} |
352 |
} |
353 |
$(oSettings.aoColumns[i].nTh).addClass( sClass ); |
354 |
|
355 |
if ( oSettings.bJUI ) |
356 |
{ |
357 |
/* jQuery UI uses extra markup */ |
358 |
var jqSpan = $("span."+oClasses.sSortIcon, oSettings.aoColumns[i].nTh); |
359 |
jqSpan.removeClass(oClasses.sSortJUIAsc +" "+ oClasses.sSortJUIDesc +" "+ |
360 |
oClasses.sSortJUI +" "+ oClasses.sSortJUIAscAllowed +" "+ oClasses.sSortJUIDescAllowed ); |
361 |
|
362 |
var sSpanClass; |
363 |
if ( iFound == -1 ) |
364 |
{ |
365 |
sSpanClass = oSettings.aoColumns[i].sSortingClassJUI; |
366 |
} |
367 |
else if ( aaSort[iFound][1] == "asc" ) |
368 |
{ |
369 |
sSpanClass = oClasses.sSortJUIAsc; |
370 |
} |
371 |
else |
372 |
{ |
373 |
sSpanClass = oClasses.sSortJUIDesc; |
374 |
} |
375 |
|
376 |
jqSpan.addClass( sSpanClass ); |
377 |
} |
378 |
} |
379 |
else |
380 |
{ |
381 |
/* No sorting on this column, so add the base class. This will have been assigned by |
382 |
* _fnAddColumn |
383 |
*/ |
384 |
$(oSettings.aoColumns[i].nTh).addClass( oSettings.aoColumns[i].sSortingClass ); |
385 |
} |
386 |
} |
387 |
|
388 |
/* |
389 |
* Apply the required classes to the table body |
390 |
* Note that this is given as a feature switch since it can significantly slow down a sort |
391 |
* on large data sets (adding and removing of classes is always slow at the best of times..) |
392 |
* Further to this, note that this code is admittedly fairly ugly. It could be made a lot |
393 |
* simpler using jQuery selectors and add/removeClass, but that is significantly slower |
394 |
* (on the order of 5 times slower) - hence the direct DOM manipulation here. |
395 |
* Note that for deferred drawing we do use jQuery - the reason being that taking the first |
396 |
* row found to see if the whole column needs processed can miss classes since the first |
397 |
* column might be new. |
398 |
*/ |
399 |
sClass = oClasses.sSortColumn; |
400 |
|
401 |
if ( oSettings.oFeatures.bSort && oSettings.oFeatures.bSortClasses ) |
402 |
{ |
403 |
var nTds = _fnGetTdNodes( oSettings ); |
404 |
|
405 |
/* Determine what the sorting class for each column should be */ |
406 |
var iClass, iTargetCol; |
407 |
var asClasses = []; |
408 |
for (i = 0; i < iColumns; i++) |
409 |
{ |
410 |
asClasses.push(""); |
411 |
} |
412 |
for (i = 0, iClass = 1; i < aaSort.length; i++) |
413 |
{ |
414 |
iTargetCol = parseInt( aaSort[i][0], 10 ); |
415 |
asClasses[iTargetCol] = sClass + iClass; |
416 |
|
417 |
if ( iClass < 3 ) |
418 |
{ |
419 |
iClass++; |
420 |
} |
421 |
} |
422 |
|
423 |
/* Make changes to the classes for each cell as needed */ |
424 |
var reClass = new RegExp(sClass + "[123]"); |
425 |
var sTmpClass, sCurrentClass, sNewClass; |
426 |
for ( i=0, iLen=nTds.length; i<iLen; i++ ) |
427 |
{ |
428 |
/* Determine which column we're looking at */ |
429 |
iTargetCol = i % iColumns; |
430 |
|
431 |
/* What is the full list of classes now */ |
432 |
sCurrentClass = nTds[i].className; |
433 |
/* What sorting class should be applied? */ |
434 |
sNewClass = asClasses[iTargetCol]; |
435 |
/* What would the new full list be if we did a replacement? */ |
436 |
sTmpClass = sCurrentClass.replace(reClass, sNewClass); |
437 |
|
438 |
if ( sTmpClass != sCurrentClass ) |
439 |
{ |
440 |
/* We changed something */ |
441 |
nTds[i].className = $.trim( sTmpClass ); |
442 |
} |
443 |
else if ( sNewClass.length > 0 && sCurrentClass.indexOf(sNewClass) == -1 ) |
444 |
{ |
445 |
/* We need to add a class */ |
446 |
nTds[i].className = sCurrentClass + " " + sNewClass; |
447 |
} |
448 |
} |
449 |
} |
450 |
} |