Greasy Fork is available in English.

Table Sort for TorrentsMD

A script for making table sortable

//
// ==UserScript==
// @name          Table Sort for TorrentsMD
// @namespace     Table Sorter
// @description   A script for making table sortable
// @include     *torrentsmd.*/browse.php*
// @include     *torrentsmoldova.*/browse.php*
// @include     *torrentsmd.*/search.php*
// @include     *torrentsmoldova.*/search.php*

// @version 0.0.1.20140607094600
// ==/UserScript==

if (CODEBOX===undefined){
    var CODEBOX={};
}

CODEBOX.tabsorter = {
    'processTable' : function(tableElement){
        if ( !checkTableStructureOk(tableElement) ){
            return null;
        }

     // State is initialised here
        var rowObjects           = [];
        var currentSortColIndex  = null;
        var currentSortAscending = null;
        var linkTds              = [];
        var rowWithArrows, rowWithOptions;
        var colCount             = 0;
        var sortTypes = {
            'NONE' : {
                'text' : 'none',
                'fn'   : sortRestore
            },
            'ALPHA' : {
                'text' : 'alpha',
                'fn'   : sortColByAlpha
            },
            'NUM' : {
                'text' : 'num',
                'fn'   : sortColByNum
            }
        };
        var currentSortType = sortTypes.NONE;


        function sortRestore(){
            rowObjects.sort(function(a,b){
                return a.getOriginalIndex()    - b.getOriginalIndex();
            });
            redraw();
        }

        function sortColByAlpha(col, ascending){
            if (col<colCount){
                var multiplier = ascending ? 1 : -1;
                rowObjects.sort(function(a,b){
                    var aItem = a.getCellValue(col);
                    aItem = aItem ? aItem.toLowerCase() : aItem;

                    var bItem = b.getCellValue(col);
                    bItem = bItem ? bItem.toLowerCase() : bItem;

                    if (aItem){
                        if (bItem){
                            if (aItem > bItem){
                                return 1 * multiplier;
                            } else if (aItem < bItem){
                                return -1 * multiplier;
                            } else {
                                return 0;
                            }
                        } else {
                            return 1 * multiplier;
                        }
                    } else {
                        if (bItem){
                            return -1 * multiplier;
                        } else {
                            return 0;
                        }
                    }
                    CODEBOX.assert(false);
                });
            }
            redraw();
        }

        function sortColByNum(col, ascending){
            function makeNumFromText(txt){
                return parseFloat(txt.replace(/,/g, '').replace(/[^0-9\.+\-]+/, ' '));
            }
            if (col<colCount){
                var multiplier = ascending ? 1 : -1;
                rowObjects.sort(function(a,b){
                    var aNum = makeNumFromText(a.getCellValue(col));
                    if (isNaN(aNum)){
                     // Make non-numeric values go to the bottom of the list
                        return Infinity;
                    }
                    var bNum = makeNumFromText(b.getCellValue(col));
                    if (isNaN(bNum)){
                     // Make non-numeric values go to the bottom of the list
                        return -Infinity;
                    }
                    return (aNum - bNum) * multiplier;
                });
            }
            redraw();
        }

     // Populate rowObjects array
        CODEBOX.forEach(tableElement.getElementsByTagName("TR"), function(item, i){
            var rowObject = buildRowObject(item, i);
            if (rowObject){
                rowObjects.push(rowObject);
                colCount = ( (colCount < rowObject.getCellCount()) ? rowObject.getCellCount() : colCount);
            }
        });

        setupSortRows();


        function checkTableStructureOk(tableElement){
            CODEBOX.assert(tableElement);

         // tables that contain other tables are not processed...
             var nestedTables = tableElement.getElementsByTagName("TABLE");
             if (nestedTables.length > 0){
                 return false;
             }

         // tables that have less than 2 rows are not processed...
             var rows = tableElement.getElementsByTagName("TR");
             if (rows.length < 2){
                 return false;
             }

         // tables that don't have any text nodes are not processed...
              function checkForTextNodeChildren(item){
                  CODEBOX.assert(item);
                  var childNodes = item.childNodes;

                  if (!childNodes){
                      return false;
                  }

                  var childNode;
                  var TEXT_NODE_TYPE = 3;
                  var TRIM_REGEX     = /^\s+|\s+$/;
                  for (var i=0, l=childNodes.length; i<l; i++){
                      childNode = childNodes[i];
                      if (childNode.nodeType===TEXT_NODE_TYPE){
                          var text = childNode.nodeValue;
                          text = text.replace(TRIM_REGEX, '');
                          if (text.length > 0){
                              return true;
                          }
                      } else {
                          if ( checkForTextNodeChildren(childNode) ){
                              return true;
                          }
                      }
                  }
                  return false;
              }
              if (!checkForTextNodeChildren(tableElement)){
                  return false;
              }

            return true;
        }

        function buildRowObject(trElement, index){
            if (trElement.parentNode.tagName==='THEAD'){
             // Any rows inside a header are ignored
                return null;
            }

            var tdElements = trElement.getElementsByTagName("TD");
            if (tdElements.length===0){
             // Rows without any TDs are ignored, these are usually headers with THs instead
                return null;
            }

            var obj = {};

            obj.getTr = function(){
                return trElement;
            };

            var parent = trElement.parentNode;
            var cellToElementMappingArray = [];
            var cellIndex=0;

         /* Produces a mapping from 'virtual' cells to TD elements
            so if the third cell has colspan='3' then virtual cells 2,3 and 4 all map to TD 2
             |A|B|  C  |
             [0,1,2,2,2] */
             CODEBOX.forEach(tdElements, function(item, i){
                var colSpan = +(item.colSpan);
                if (isNaN(colSpan) || colSpan===0){
                    colSpan = 1;
                }
                CODEBOX.assert(colSpan);
                for(var j=0; j<colSpan; j++){
                    cellToElementMappingArray[cellIndex++] = i;
                }
             });

            obj.getCellCount = function(){
                return cellToElementMappingArray.length;
            };

            obj.getCellValue = function(index){
                if (index >= cellToElementMappingArray.length){
                    return null;
                } else {
                    var tdIndex = cellToElementMappingArray[index];
                    CODEBOX.assert( tdIndex < tdElements.length );

                    var cellContent = tdElements[tdIndex].innerHTML;
                 // This might contain markup, eg <span class='tdText>cell text</span>, so clean it out
                     var STRIP_MARKUP_REGEX = /<[^<]*>/g;
                    cellContent = cellContent.replace(STRIP_MARKUP_REGEX, '');

                    return cellContent;
                }
            };

            obj.getOriginalIndex = function(){
                return index;
            };

            obj.removeFromParent = function(){
                parent.removeChild(trElement);
            };
            obj.attachToParent = function(){
                parent.appendChild(trElement);
            };

            return obj;
        }


        function setSortType(sortType){
            currentSortType = sortType;

         // Adjust the styles of the sort-type links
            CODEBOX.forEachObject(sortTypes, function(thisSortType){
                var isSelectedSortType = (thisSortType===currentSortType);
                
                thisSortType.link.style.color          = (isSelectedSortType ? 'black' : 'gray');
                thisSortType.link.style.textDecoration = (isSelectedSortType ? '' : 'underline');
                thisSortType.link.style.cursor         = (isSelectedSortType ? '' : 'pointer'  );
            });
            
         // Hide the sort arrows if we aren't sorting
            rowWithArrows.style.display = (sortType===sortTypes.NONE) ? 'none' : '';
            
         // Reset any 'selected' arrow from the previous sort
            if (currentSortColIndex !== null){
                updateLinkCell(currentSortColIndex, false, false);
            }
        }

        function setupSortRows(){
            var firstRow = rowObjects[0].getTr();

         // Add a new row for the options
             rowWithOptions = document.createElement("TR");
             firstRow.parentNode.insertBefore(rowWithOptions, firstRow);

            var optionsTd = document.createElement("TD");
            optionsTd.style.fontSize = '0.7em';
            optionsTd.colSpan        = colCount;

            var optionsSpan = document.createElement("SPAN");
            optionsSpan.style.paddingLeft = '1em';
            optionsSpan.innerHTML         = 'Sort Type: ';

            var optionAlphaSortLink = document.createElement("A");
            optionAlphaSortLink.style.paddingLeft    = '0.5em';
            optionAlphaSortLink.style.textDecoration = 'underline';
            optionAlphaSortLink.style.cursor         = 'pointer';
            optionAlphaSortLink.innerHTML            = 'alpha';
            optionAlphaSortLink.addEventListener('click',
                    function(e){
                        setSortType( sortTypes.ALPHA );
                    }, false);
            sortTypes.ALPHA.link = optionAlphaSortLink;

            var optionNoSortLink = document.createElement("A");
            optionNoSortLink.style.paddingLeft    = '0.5em';
            optionNoSortLink.style.textDecoration = 'underline';
            optionNoSortLink.style.cursor         = 'pointer';
            optionNoSortLink.innerHTML            = 'none';
            optionNoSortLink.addEventListener('click',
                    function(e){
                        setSortType( sortTypes.NONE );
                        sortTypes.NONE.fn();
                    }, false);
            sortTypes.NONE.link = optionNoSortLink;

            var optionNumSortLink = document.createElement("A");
            optionNumSortLink.style.paddingLeft    = '0.5em';
            optionNumSortLink.style.textDecoration = 'underline';
            optionNumSortLink.style.cursor         = 'pointer';
            optionNumSortLink.innerHTML            = 'num';
            optionNumSortLink.addEventListener('click',
                    function(e){
                        setSortType( sortTypes.NUM );
                    }, false);
            sortTypes.NUM.link = optionNumSortLink;

            optionsTd.appendChild(optionsSpan);
            optionsTd.appendChild(optionAlphaSortLink);
            optionsTd.appendChild(optionNumSortLink);
            optionsTd.appendChild(optionNoSortLink);
            rowWithOptions.appendChild(optionsTd);

         // Add a new row for the sort arrows
             rowWithArrows = document.createElement("TR");
             firstRow.parentNode.insertBefore(rowWithArrows, firstRow);
             var linkTd, linkElement;
             for(var i=0; i<colCount; i++){
                linkTd = document.createElement("TD");
                linkTd.style.fontSize   = '0.7em';
                linkTd.style.fontWeight = 'bold';
                linkTd.style.textAlign  = 'center';

                linkTds.push(linkTd);

                linkElement = document.createElement("A");
                linkTd.appendChild(linkElement);

                rowWithArrows.appendChild(linkTd);

                setCellImages(i, false, false);
             }

             setSortType(sortTypes.NONE);
        }

        function updateLinkCell(colIndex, isAscending, isDescending){
            if (currentSortColIndex === null){
             // No column is currently selected, so we don't need to reset anything
                setCellImages(colIndex, isAscending, isDescending);

            } else {
             // There is already a column selected
                if ( colIndex == currentSortColIndex){
                 // The new column is the same as the current one...
                    if (currentSortAscending != null && ((currentSortAscending && isAscending) || (!currentSortAscending && isDescending))){
                         // The new sort order the the same as the current one, so do nothing
                    } else {
                     // The column is the same, but the order is different
                        setCellImages(colIndex, isAscending, isDescending);
                    }
                } else {
                 // The new column is different to the current one so reset the current one
                    setCellImages(currentSortColIndex, false, false);
                 // Set up the new one
                     setCellImages(colIndex, isAscending, isDescending);
                }
            }
        }

        function setCellImages(colIndex, isAscending, isDescending){
            var DATA_URL_UP         = "data:image/gif;base64,R0lGODlhCAAEAPcAAAAAAPfvc/8Avf///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////yH5BAEAAAIALAAAAAAIAAQAAAgSAAUIBABAoEGCBA8iTLhwYUAAADs=";
            var DATA_URL_UP_FADED   = "data:image/gif;base64,R0lGODlhCAAEAPcAAAAAAP8Avf///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////yH5BAEAAAEALAAAAAAIAAQAAAgWAAMIBABAoEEAAgQUHJgwYUGCECEGBAA7";
            var DATA_URL_DOWN       = "data:image/gif;base64,R0lGODlhCAAEAPcAAAAAAPfvc/8Avf///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////yH5BAEAAAIALAAAAAAIAAQAAAgTAAEIHChQAMGCAgwOTMgQoYCAAAA7";
            var DATA_URL_DOWN_FADED = "data:image/gif;base64,R0lGODlhCAAEAPcAAAAAAP8Avf///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////yH5BAEAAAEALAAAAAAIAAQAAAgXAAEIHCgwAAABCAUACMDwoEKGEAsyDAgAOw==";

            var linkCell = linkTds[colIndex];

         // Remove the current images...
            while(linkCell.firstChild){
                linkCell.removeChild(linkCell.firstChild);
            }

            var upArrowImage = document.createElement('IMG');
            upArrowImage.src = (isAscending ? DATA_URL_UP : DATA_URL_UP_FADED);
            upArrowImage.style.cursor = 'pointer';
            upArrowImage.addEventListener('click',
                    function(e){
                        updateLinkCell(colIndex, true, false);
                        currentSortColIndex  = colIndex;
                        currentSortAscending = true;
                        currentSortType.fn(colIndex, true);
                    }, false);

            var downArrowImage = document.createElement('IMG');
            downArrowImage.src = (isDescending ? DATA_URL_DOWN : DATA_URL_DOWN_FADED);
            downArrowImage.style.cursor = 'pointer';
            downArrowImage.addEventListener('click',
                    function(e){
                        updateLinkCell(colIndex, false, true);
                        currentSortColIndex  = colIndex;
                        currentSortAscending = false;
                        currentSortType.fn(colIndex, false);
                    }, false);

            linkCell.appendChild(upArrowImage);
            linkCell.appendChild(downArrowImage);
        }


        function redraw(){
            CODEBOX.forEach(rowObjects, function(item){
                item.removeFromParent();
            });

            CODEBOX.forEach(rowObjects, function(item){
                item.attachToParent();
            });
        }

    }
};


CODEBOX.addNamespace = function (namespaceName){
    function addNamespaceToObject(obj, namespace){
        if (!obj){
            obj = CODEBOX;
        }
        if (obj[namespace]===undefined){
            obj[namespace] = {};
        }
        return obj[namespace];
    }

    var namespaceParts = namespaceName.split('.');
    var parentNamespace = null;
    var thisPart;

    for( var i=0; i<namespaceParts.length; i++) {
        thisPart = namespaceParts[i];
        parentNamespace = addNamespaceToObject(parentNamespace, thisPart);
    }
};

CODEBOX.addNamespace('consts');
CODEBOX.consts.assert = true;
CODEBOX.consts.dialogOnAssert = true;

CODEBOX.assert = function( expression, msg, location ){
    if (CODEBOX.consts.assert){
        if (!expression){
            var errMsg = 'ASSERTION FAILED';
            if (msg){
                errMsg += ': ' + msg;
                if (location){
                    errMsg += ' at ' + location;
                }
            }
            if (CODEBOX.consts.dialogOnAssert){
                alert(errMsg);
            }
            throw new Error(errMsg);
        }
    }
};

CODEBOX.forEach = function(arr, fnDoThis){
    var len = arr.length;
    for (var i=0; i < len; i++){
        fnDoThis(arr[i], i);
    }
};

CODEBOX.forEachObject = function(obj, fnDoThis){
    for (var item in obj){
        if (obj.hasOwnProperty(item)){
            fnDoThis(obj[item]);
        }
    }
};

var allTables = window.document.documentElement.getElementsByTagName("TABLE");
CODEBOX.forEach(allTables, function(table){
    CODEBOX.tabsorter.processTable(table);
});