shop.balmet.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

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 })()));