tinysort.js (19367B)
1 /** 2 * TinySort is a small script that sorts HTML elements. It sorts by text- or attribute value, or by that of one of it's children. 3 * @summary A nodeElement sorting script. 4 * @version 2.3.6 5 * @license MIT 6 * @author Ron Valstar <ron@ronvalstar.nl> 7 * @copyright Ron Valstar <ron@ronvalstar.nl> 8 * @namespace tinysort 9 */ 10 (function (root,tinysort) { 11 'use strict'; 12 13 if (typeof define==='function'&&define.amd) { 14 define('tinysort',singleton); 15 } else { 16 root.tinysort = tinysort; 17 } 18 function singleton(){ 19 return tinysort; 20 } 21 }(this,(function() { 22 'use strict'; 23 24 var fls = !1 25 ,undef 26 ,nll = null 27 ,win = window 28 ,doc = win.document 29 ,parsefloat = parseFloat 30 ,regexLastNr = /(-?\d+\.?\d*)\s*$/g // regex for testing strings ending on numbers 31 ,regexLastNrNoDash = /(\d+\.?\d*)\s*$/g // regex for testing strings ending on numbers ignoring dashes 32 ,plugins = [] 33 ,numCriteria = 0 34 ,criteriumIndex = 0 35 ,largeChar = String.fromCharCode(0xFFF) 36 ,/**options*/defaults = { // default settings 37 38 selector: nll // CSS selector to select the element to sort to 39 40 ,order: 'asc' // order: asc, desc or rand 41 42 ,attr: nll // order by attribute value 43 ,data: nll // use the data attribute for sorting 44 ,useVal: fls // use element value instead of text 45 46 ,place: 'org' // place ordered elements at position: start, end, org (original position), first, last 47 ,returns: fls // return all elements or only the sorted ones (true/false) 48 49 ,cases: fls // a case sensitive sort orders [aB,aa,ab,bb] 50 51 ,natural: fls // use natural sort order 52 53 ,forceStrings:fls // if false the string '2' will sort with the value 2, not the string '2' 54 55 ,ignoreDashes:fls // ignores dashes when looking for numerals 56 57 ,sortFunction: nll // override the default sort function 58 59 ,useFlex:fls 60 ,emptyEnd:fls 61 } 62 ; 63 64 /** 65 * Options object 66 * @typedef {Object} options 67 * @property {String} [selector] A CSS selector to select the element to sort to. 68 * @property {String} [order='asc'] The order of the sorting method. Possible values are 'asc', 'desc' and 'rand'. 69 * @property {String} [attr=null] Order by attribute value (ie title, href, class) 70 * @property {String} [data=null] Use the data attribute for sorting. 71 * @property {String} [place='org'] Determines the placement of the ordered elements in respect to the unordered elements. Possible values 'start', 'end', 'first', 'last' or 'org'. 72 * @property {Boolean} [useVal=false] Use element value instead of text. 73 * @property {Boolean} [cases=false] A case sensitive sort (orders [aB,aa,ab,bb]) 74 * @property {Boolean} [natural=false] Use natural sort order. 75 * @property {Boolean} [forceStrings=false] If false the string '2' will sort with the value 2, not the string '2'. 76 * @property {Boolean} [ignoreDashes=false] Ignores dashes when looking for numerals. 77 * @property {Function} [sortFunction=null] Override the default sort function. The parameters are of a type {elementObject}. 78 * @property {Boolean} [useFlex=true] If one parent and display flex, ordering is done by CSS (instead of DOM) 79 * @property {Boolean} [emptyEnd=true] Sort empty values to the end instead of the start 80 */ 81 82 /** 83 * TinySort is a small and simple script that will sort any nodeElement by it's text- or attribute value, or by that of one of it's children. 84 * @memberof tinysort 85 * @public 86 * @param {NodeList|HTMLElement[]|String} nodeList The nodelist or array of elements to be sorted. If a string is passed it should be a valid CSS selector. 87 * @param {options} [options] A list of options. 88 * @returns {HTMLElement[]} 89 */ 90 function tinysort(nodeList,options){ 91 if (isString(nodeList)) nodeList = doc.querySelectorAll(nodeList); 92 if (nodeList.length===0) { 93 console.warn('No elements to sort'); 94 } 95 96 var fragment = doc.createDocumentFragment() 97 /** both sorted and unsorted elements 98 * @type {elementObject[]} */ 99 ,elmObjsAll = [] 100 /** sorted elements 101 * @type {elementObject[]} */ 102 ,elmObjsSorted = [] 103 /** unsorted elements 104 * @type {elementObject[]} */ 105 ,elmObjsUnsorted = [] 106 /** sorted elements before sort 107 * @type {elementObject[]} */ 108 ,elmObjsSortedInitial 109 /** @type {criteriumIndex[]} */ 110 ,criteria = [] 111 /** @type {HTMLElement} */ 112 ,parentNode 113 ,isSameParent = true 114 ,firstParent = nodeList.length&&nodeList[0].parentNode 115 ,isFragment = firstParent.rootNode!==document 116 ,isFlex = nodeList.length&&(options===undef||options.useFlex!==false)&&!isFragment&&getComputedStyle(firstParent,null).display.indexOf('flex')!==-1 117 ; 118 119 initCriteria.apply(nll,Array.prototype.slice.call(arguments,1)); 120 initSortList(); 121 elmObjsSorted.sort(sortFunction); 122 applyToDOM(); 123 124 /** 125 * Create criteria list 126 */ 127 function initCriteria(){ 128 if (arguments.length===0) { 129 addCriterium({}); // have at least one criterium 130 } else { 131 loop(arguments,function(param){ 132 addCriterium(isString(param)?{selector:param}:param); 133 }); 134 } 135 numCriteria = criteria.length; 136 } 137 138 /** 139 * A criterium is a combination of the selector, the options and the default options 140 * @typedef {Object} criterium 141 * @property {String} selector - a valid CSS selector 142 * @property {String} order - order: asc, desc or rand 143 * @property {String} attr - order by attribute value 144 * @property {String} data - use the data attribute for sorting 145 * @property {Boolean} useVal - use element value instead of text 146 * @property {String} place - place ordered elements at position: start, end, org (original position), first 147 * @property {Boolean} returns - return all elements or only the sorted ones (true/false) 148 * @property {Boolean} cases - a case sensitive sort orders [aB,aa,ab,bb] 149 * @property {Boolean} natural - use natural sort order 150 * @property {Boolean} forceStrings - if false the string '2' will sort with the value 2, not the string '2' 151 * @property {Boolean} ignoreDashes - ignores dashes when looking for numerals 152 * @property {Function} sortFunction - override the default sort function 153 * @property {boolean} hasSelector - options has a selector 154 * @property {boolean} hasFilter - options has a filter 155 * @property {boolean} hasAttr - options has an attribute selector 156 * @property {boolean} hasData - options has a data selector 157 * @property {number} sortReturnNumber - the sort function return number determined by options.order 158 */ 159 160 /** 161 * Adds a criterium 162 * @memberof tinysort 163 * @private 164 * @param {Object} [options] 165 */ 166 function addCriterium(options){ 167 var hasSelector = !!options.selector 168 ,hasFilter = hasSelector&&options.selector[0]===':' 169 ,allOptions = extend(options||{},defaults) 170 ; 171 criteria.push(extend({ 172 // has find, attr or data 173 hasSelector: hasSelector 174 ,hasAttr: !(allOptions.attr===nll||allOptions.attr==='') 175 ,hasData: allOptions.data!==nll 176 // filter 177 ,hasFilter: hasFilter 178 ,sortReturnNumber: allOptions.order==='asc'?1:-1 179 },allOptions)); 180 } 181 182 /** 183 * The element object. 184 * @typedef {Object} elementObject 185 * @property {HTMLElement} elm - The element 186 * @property {number} pos - original position 187 * @property {number} posn - original position on the partial list 188 */ 189 190 /** 191 * Creates an elementObject and adds to lists. 192 * Also checks if has one or more parents. 193 * @memberof tinysort 194 * @private 195 */ 196 function initSortList(){ 197 loop(nodeList,function(elm,i){ 198 if (!parentNode) parentNode = elm.parentNode; 199 else if (parentNode!==elm.parentNode) isSameParent = false; 200 var criterium = criteria[0] 201 ,hasFilter = criterium.hasFilter 202 ,selector = criterium.selector 203 ,isPartial = !selector||(hasFilter&&elm.matchesSelector(selector))||(selector&&elm.querySelector(selector)) 204 ,listPartial = isPartial?elmObjsSorted:elmObjsUnsorted 205 ,elementObject = { 206 elm: elm 207 ,pos: i 208 ,posn: listPartial.length 209 } 210 ; 211 elmObjsAll.push(elementObject); 212 listPartial.push(elementObject); 213 }); 214 elmObjsSortedInitial = elmObjsSorted.slice(0); 215 } 216 217 /** 218 * Compare strings using natural sort order 219 * http://web.archive.org/web/20130826203933/http://my.opera.com/GreyWyvern/blog/show.dml/1671288 220 */ 221 function naturalCompare(a, b, chunkify) { 222 var aa = chunkify(a.toString()) 223 ,bb = chunkify(b.toString()); 224 for (var x = 0; aa[x] && bb[x]; x++) { 225 if (aa[x]!==bb[x]) { 226 var c = Number(aa[x]) 227 ,d = Number(bb[x]); 228 if (c == aa[x] && d == bb[x]) { 229 return c - d; 230 } else return aa[x]>bb[x]?1:-1; 231 } 232 } 233 return aa.length - bb.length; 234 } 235 236 /** 237 * Split a string into an array by type: numeral or string 238 * @memberof tinysort 239 * @private 240 * @param {string} t 241 * @returns {Array} 242 */ 243 function chunkify(t) { 244 var tz = [], x = 0, y = -1, n = 0, i, j; 245 while (i = (j = t.charAt(x++)).charCodeAt(0)) { 246 var m = (i == 46 || (i >=48 && i <= 57)); 247 if (m !== n) { 248 tz[++y] = ''; 249 n = m; 250 } 251 tz[y] += j; 252 } 253 return tz; 254 } 255 256 /** 257 * Sort all the things 258 * @memberof tinysort 259 * @private 260 * @param {elementObject} a 261 * @param {elementObject} b 262 * @returns {number} 263 */ 264 function sortFunction(a,b){ 265 var sortReturnNumber = 0; 266 if (criteriumIndex!==0) criteriumIndex = 0; 267 while (sortReturnNumber===0&&criteriumIndex<numCriteria) { 268 /** @type {criterium} */ 269 var criterium = criteria[criteriumIndex] 270 ,regexLast = criterium.ignoreDashes?regexLastNrNoDash:regexLastNr; 271 // 272 loop(plugins,function(plugin){ 273 var pluginPrepare = plugin.prepare; 274 if (pluginPrepare) pluginPrepare(criterium); 275 }); 276 // 277 if (criterium.sortFunction) { // custom sort 278 sortReturnNumber = criterium.sortFunction(a,b); 279 } else if (criterium.order=='rand') { // random sort 280 sortReturnNumber = Math.random()<0.5?1:-1; 281 } else { // regular sort 282 var isNumeric = fls 283 // prepare sort elements 284 ,valueA = getSortBy(a,criterium) 285 ,valueB = getSortBy(b,criterium) 286 ,noA = valueA===''||valueA===undef 287 ,noB = valueB===''||valueB===undef 288 ; 289 if (valueA===valueB) { 290 sortReturnNumber = 0; 291 } else if (criterium.emptyEnd&&(noA||noB)) { 292 sortReturnNumber = noA&&noB?0:noA?1:-1; 293 } else { 294 if (!criterium.forceStrings) { 295 // cast to float if both strings are numeral (or end numeral) 296 var valuesA = isString(valueA)?valueA&&valueA.match(regexLast):fls// todo: isString superfluous because getSortBy returns string|undefined 297 ,valuesB = isString(valueB)?valueB&&valueB.match(regexLast):fls 298 ; 299 if (valuesA&&valuesB) { 300 var previousA = valueA.substr(0,valueA.length-valuesA[0].length) 301 ,previousB = valueB.substr(0,valueB.length-valuesB[0].length); 302 if (previousA==previousB) { 303 isNumeric = !fls; 304 valueA = parsefloat(valuesA[0]); 305 valueB = parsefloat(valuesB[0]); 306 } 307 } 308 } 309 if (valueA===undef||valueB===undef) { 310 sortReturnNumber = 0; 311 } else { 312 // todo: check here 313 if (!criterium.natural||(!isNaN(valueA)&&!isNaN(valueB))) { 314 sortReturnNumber = valueA<valueB?-1:(valueA>valueB?1:0); 315 } else { 316 sortReturnNumber = naturalCompare(valueA, valueB, chunkify); 317 } 318 } 319 } 320 } 321 loop(plugins,function(o){ 322 var pluginSort = o.sort; 323 if (pluginSort) sortReturnNumber = pluginSort(criterium,isNumeric,valueA,valueB,sortReturnNumber); 324 }); 325 sortReturnNumber *= criterium.sortReturnNumber; // lastly assign asc/desc 326 if (sortReturnNumber===0) criteriumIndex++; 327 } 328 if (sortReturnNumber===0) sortReturnNumber = a.pos>b.pos?1:-1; 329 //console.log('sfn',a.pos,b.pos,valueA,valueB,sortReturnNumber); // todo: remove log; 330 return sortReturnNumber; 331 } 332 333 /** 334 * Applies the sorted list to the DOM 335 * @memberof tinysort 336 * @private 337 */ 338 function applyToDOM(){ 339 var hasSortedAll = elmObjsSorted.length===elmObjsAll.length; 340 if (isSameParent&&hasSortedAll) { 341 if (isFlex) { 342 elmObjsSorted.forEach(function(elmObj,i){ 343 elmObj.elm.style.order = i; 344 }); 345 } else { 346 if (parentNode) parentNode.appendChild(sortedIntoFragment()); 347 else console.warn('parentNode has been removed'); 348 } 349 } else { 350 var criterium = criteria[0] 351 ,place = criterium.place 352 ,placeOrg = place==='org' 353 ,placeStart = place==='start' 354 ,placeEnd = place==='end' 355 ,placeFirst = place==='first' 356 ,placeLast = place==='last' 357 ; 358 if (placeOrg) { 359 elmObjsSorted.forEach(addGhost); 360 elmObjsSorted.forEach(function(elmObj,i) { 361 replaceGhost(elmObjsSortedInitial[i],elmObj.elm); 362 }); 363 } else if (placeStart||placeEnd) { 364 var startElmObj = elmObjsSortedInitial[placeStart?0:elmObjsSortedInitial.length-1] 365 ,startParent = startElmObj&&startElmObj.elm.parentNode 366 ,startElm = startParent&&(placeStart&&startParent.firstChild||startParent.lastChild); 367 if (startElm) { 368 if (startElm!==startElmObj.elm) startElmObj = {elm:startElm}; 369 addGhost(startElmObj); 370 placeEnd&&startParent.appendChild(startElmObj.ghost); 371 replaceGhost(startElmObj,sortedIntoFragment()); 372 } 373 } else if (placeFirst||placeLast) { 374 var firstElmObj = elmObjsSortedInitial[placeFirst?0:elmObjsSortedInitial.length-1]; 375 replaceGhost(addGhost(firstElmObj),sortedIntoFragment()); 376 } 377 } 378 } 379 380 /** 381 * Adds all sorted elements to the document fragment and returns it. 382 * @memberof tinysort 383 * @private 384 * @returns {DocumentFragment} 385 */ 386 function sortedIntoFragment(){ 387 elmObjsSorted.forEach(function(elmObj){ 388 fragment.appendChild(elmObj.elm); 389 }); 390 return fragment; 391 } 392 393 /** 394 * Adds a temporary element before an element before reordering. 395 * @memberof tinysort 396 * @private 397 * @param {elementObject} elmObj 398 * @returns {elementObject} 399 */ 400 function addGhost(elmObj){ 401 var element = elmObj.elm 402 ,ghost = doc.createElement('div') 403 ; 404 elmObj.ghost = ghost; 405 element.parentNode.insertBefore(ghost,element); 406 return elmObj; 407 } 408 409 /** 410 * Inserts an element before a ghost element and removes the ghost. 411 * @memberof tinysort 412 * @private 413 * @param {elementObject} elmObjGhost 414 * @param {HTMLElement} elm 415 */ 416 function replaceGhost(elmObjGhost,elm){ 417 var ghost = elmObjGhost.ghost 418 ,ghostParent = ghost.parentNode; 419 ghostParent.insertBefore(elm,ghost); 420 ghostParent.removeChild(ghost); 421 delete elmObjGhost.ghost; 422 } 423 424 /** 425 * Get the string/number to be sorted by checking the elementObject with the criterium. 426 * @memberof tinysort 427 * @private 428 * @param {elementObject} elementObject 429 * @param {criterium} criterium 430 * @returns {String} 431 * @todo memoize 432 */ 433 function getSortBy(elementObject,criterium){ 434 var sortBy 435 ,element = elementObject.elm; 436 // element 437 if (criterium.selector) { 438 if (criterium.hasFilter) { 439 if (!element.matchesSelector(criterium.selector)) element = nll; 440 } else { 441 element = element.querySelector(criterium.selector); 442 } 443 } 444 // value 445 if (criterium.hasAttr) sortBy = element.getAttribute(criterium.attr); 446 else if (criterium.useVal) sortBy = element.value||element.getAttribute('value'); 447 else if (criterium.hasData) sortBy = element.getAttribute('data-'+criterium.data); 448 else if (element) sortBy = element.textContent; 449 // strings should be ordered in lowercase (unless specified) 450 if (isString(sortBy)) { 451 if (!criterium.cases) sortBy = sortBy.toLowerCase(); 452 sortBy = sortBy.replace(/\s+/g,' '); // spaces/newlines 453 } 454 if (sortBy===null) sortBy = largeChar; 455 return sortBy; 456 } 457 458 /*function memoize(fnc) { 459 var oCache = {} 460 , sKeySuffix = 0; 461 return function () { 462 var sKey = sKeySuffix + JSON.stringify(arguments); // todo: circular dependency on Nodes 463 return (sKey in oCache)?oCache[sKey]:oCache[sKey] = fnc.apply(fnc,arguments); 464 }; 465 }*/ 466 467 /** 468 * Test if an object is a string 469 * @memberOf tinysort 470 * @method 471 * @private 472 * @param o 473 * @returns {boolean} 474 */ 475 function isString(o){ 476 return typeof o==='string'; 477 } 478 479 return elmObjsSorted.map(function(o) { 480 return o.elm; 481 }); 482 } 483 484 /** 485 * Traverse an array, or array-like object 486 * @memberOf tinysort 487 * @method 488 * @private 489 * @param {Array} array The object or array 490 * @param {Function} func Callback function with the parameters value and key. 491 */ 492 function loop(array,func){ 493 var l = array.length 494 ,i = l 495 ,j; 496 while (i--) { 497 j = l-i-1; 498 func(array[j],j); 499 } 500 } 501 502 /** 503 * Extend an object 504 * @memberOf tinysort 505 * @method 506 * @private 507 * @param {Object} obj Subject. 508 * @param {Object} fns Property object. 509 * @param {boolean} [overwrite=false] Overwrite properties. 510 * @returns {Object} Subject. 511 */ 512 function extend(obj,fns,overwrite){ 513 for (var s in fns) { 514 if (overwrite||obj[s]===undef) { 515 obj[s] = fns[s]; 516 } 517 } 518 return obj; 519 } 520 521 function plugin(prepare,sort,sortBy){ 522 plugins.push({prepare:prepare,sort:sort,sortBy:sortBy}); 523 } 524 525 // matchesSelector shim 526 win.Element&&(function(ElementPrototype) { 527 ElementPrototype.matchesSelector = ElementPrototype.matchesSelector 528 ||ElementPrototype.mozMatchesSelector 529 ||ElementPrototype.msMatchesSelector 530 ||ElementPrototype.oMatchesSelector 531 ||ElementPrototype.webkitMatchesSelector 532 ||function (selector) { 533 var that = this, nodes = (that.parentNode || that.document).querySelectorAll(selector), i = -1; 534 //jscs:disable requireCurlyBraces 535 while (nodes[++i] && nodes[i] != that); 536 //jscs:enable requireCurlyBraces 537 return !!nodes[i]; 538 }; 539 })(Element.prototype); 540 541 // extend the plugin to expose stuff 542 extend(plugin,{ 543 loop: loop 544 }); 545 546 return extend(tinysort,{ 547 plugin: plugin 548 ,defaults: defaults 549 }); 550 })()));