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.
Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.greasyfork.org/scripts/545962/1642423/tinysort.js
// ==UserScript==
// @name tinysort
// @version 3.2.7
// @namespace http://www.ronvalstar.nl/
// @description 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.
// @author Ron Valstar (http://www.ronvalstar.nl/)
// @license MIT
// @exclude *
// @grant none
// ==/UserScript==
(function(root,tinysort){
typeof define==='function'&&define.amd?define('tinysort',()=>tinysort):(root.tinysort = tinysort)
}(window||module||{},(_undef=>{
const fls = !1
const undef = _undef
const nll = null
const win = window
const doc = win.document
const parsefloat = parseFloat
const regexLastNr = /(-?\d+\.?\d*)\s*$/g // regex for testing strings ending on numbers
const regexLastNrNoDash = /(\d+\.?\d*)\s*$/g // regex for testing strings ending on numbers ignoring dashes
const plugins = []
const stringFromCharCode = i=>String.fromCharCode(i)
const charsFrom = from=>Array.from(new Array(3),(o,i)=>stringFromCharCode(from+i))
const charLow = charsFrom(0)
const charHigh = charsFrom(0xFFF)
/**{options}*/
const defaults = { // default settings
selector: nll // CSS selector to select the element to sort to
,order: 'asc' // order: asc, desc or rand
,attr: nll // order by attribute value
,data: nll // use the data attribute for sorting
,useVal: fls // use element value instead of text
,place: 'org' // place ordered elements at position: start, end, org (original position), first, last
,returns: fls // return all elements or only the sorted ones (true/false)
,cases: fls // a case sensitive sort orders [aB,aa,ab,bb]
,natural: fls // use natural sort order
,forceStrings:fls // if false the string '2' will sort with the value 2, not the string '2'
,ignoreDashes:fls // ignores dashes when looking for numerals
,sortFunction: nll // override the default sort function
,useFlex:fls
,emptyEnd:fls
,console
}
let numCriteria = 0
let criteriumIndex = 0
/**
* Options object
* @typedef {object} options
* @property {string} [selector] A CSS selector to select the element to sort to.
* @property {string} [order='asc'] The order of the sorting method. Possible values are 'asc', 'desc' and 'rand'.
* @property {string} [attr=null] Order by attribute value (ie title, href, class)
* @property {string} [data=null] Use the data attribute for sorting.
* @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'.
* @property {boolean} [useVal=false] Use element value instead of text.
* @property {boolean} [cases=false] A case sensitive sort (orders [aB,aa,ab,bb])
* @property {boolean} [natural=false] Use natural sort order.
* @property {boolean} [forceStrings=false] If false the string '2' will sort with the value 2, not the string '2'.
* @property {boolean} [ignoreDashes=false] Ignores dashes when looking for numerals.
* @property {function} [sortFunction=null] Override the default sort function. The parameters are of a type {elementObject}.
* @property {boolean} [useFlex=true] If one parent and display flex, ordering is done by CSS (instead of DOM)
* @property {boolean} [emptyEnd=true] Sort empty values to the end instead of the start
* @property {object|boolean} [console] - an optional console implementation to prevent output to console
*/
/**
* 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.
* @memberof tinysort
* @public
* @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.
* @param {options[]} [...optionsList] A list of options.
* @returns {HTMLElement[]}
*/
function tinysort(nodeList,...optionsList){
const options = optionsList[0]||{}
isString(nodeList) && (nodeList = doc.querySelectorAll(nodeList))
const {console} = Object.assign({},defaults,options||{})
nodeList.length===0 && console && console.warn && console.warn('No elements to sort')
const fragment = doc.createDocumentFragment()
/** both sorted and unsorted elements
* @type {elementObject[]} */
const elmObjsAll = []
/** sorted elements
* @type {elementObject[]} */
const elmObjsSorted = []
/** unsorted elements
* @type {elementObject[]} */
const elmObjsUnsorted = []
/** sorted elements before sort
* @type {elementObject[]} */
const elmObjsSortedInitial = []
/** @type {criteriumIndex[]} */
const criteria = []
/** @type {HTMLElement} */
let parentNode
let isSameParent = true
let firstParent = nodeList.length&&nodeList[0].parentNode
let isFragment = (firstParent && firstParent != undefined) ? firstParent.rootNode!==document : false;
let isFlex = nodeList.length&&(options===undef||options.useFlex!==false)&&!isFragment&&getComputedStyle(firstParent,null).display.indexOf('flex')!==-1
numCriteria = addCriteria(optionsList)
initSortList()
elmObjsSorted.sort(options.sortFunction||sortFunction)
applyToDOM()
/**
* Create criteria list
* @param {object[]} optionsList
* @returns {number}
*/
function addCriteria(optionsList){
return optionsList.length===0
&&addCriterium({}) // have at least one criterium
||loop(optionsList,param=>addCriterium(isString(param)?{selector:param}:param)).length
}
/**
* A criterium is a combination of the selector, the options and the default options
* @typedef {options} criterium
* @property {boolean} hasSelector - options has a selector
* @property {boolean} hasFilter - options has a filter
* @property {boolean} hasAttr - options has an attribute selector
* @property {boolean} hasData - options has a data selector
* @property {number} sortReturnNumber - the sort function return number determined by options.order
*/
/**
* Adds a criterium
* @memberof tinysort
* @private
* @param {Object} [options]
* @returns {number}
*/
function addCriterium(options){
const hasSelector = !!options.selector
const hasFilter = hasSelector&&options.selector[0]===':'
const allOptions = extend(options||{},defaults)
return criteria.push(extend({
// has find, attr or data
hasSelector
,hasAttr: !(allOptions.attr===nll||allOptions.attr==='')
,hasData: allOptions.data!==nll
// filter
,hasFilter
,sortReturnNumber: allOptions.order==='asc'?1:-1
},allOptions))
}
/**
* The element object.
* @typedef {Object} elementObject
* @property {HTMLElement} elm - The element
* @property {number} pos - original position
* @property {number} posn - original position on the partial list
*/
/**
* Creates an elementObject and adds to lists.
* Also checks if has one or more parents.
* @memberof tinysort
* @private
*/
function initSortList(){
loop(nodeList,(elm,pos)=>{
if (!parentNode) parentNode = elm.parentNode
else if (parentNode!==elm.parentNode) isSameParent = false
const {hasFilter,selector} = criteria[0]
const isPartial = !selector||(hasFilter&&elm.matches(selector))||(selector&&elm.querySelector(selector))
const listPartial = isPartial?elmObjsSorted:elmObjsUnsorted
const posn = listPartial.length
const elementObject = {elm,pos,posn}
elmObjsAll.push(elementObject)
listPartial.push(elementObject)
})
elmObjsSortedInitial.splice(0,Number.MAX_SAFE_INTEGER,...elmObjsSorted)
}
/**
* Compare strings using natural sort order
* http://web.archive.org/web/20130826203933/http://my.opera.com/GreyWyvern/blog/show.dml/1671288
*/
function naturalCompare(a, b, chunkify) {
const aa = chunkify(a.toString())
const bb = chunkify(b.toString())
for (let x = 0; aa[x] && bb[x]; x++) {
if (aa[x]!==bb[x]) {
const c = Number(aa[x])
,d = Number(bb[x])
if (c == aa[x] && d == bb[x]) {
return c - d
} else return aa[x]>bb[x]?1:-1
}
}
return aa.length - bb.length
}
/**
* Split a string into an array by type: numeral or string
* @memberof tinysort
* @private
* @param {string} t
* @returns {Array}
*/
function chunkify(t) {
const tz = []
let x = 0, y = -1, n = 0, i, j
while (i = (j = t.charAt(x++)).charCodeAt(0)) { // eslint-disable-line no-cond-assign
const m = (i === 46 || (i >=48 && i <= 57))
if (m !== n) {
tz[++y] = ''
n = m
}
tz[y] += j
}
return tz
}
/**
* Sort all the things
* @memberof tinysort
* @private
* @param {elementObject} a
* @param {elementObject} b
* @returns {number}
*/
function sortFunction(a,b){
let sortReturnNumber = 0
if (criteriumIndex!==0) criteriumIndex = 0
while (sortReturnNumber===0&&criteriumIndex<numCriteria) {
/** @type {criterium} */
const criterium = criteria[criteriumIndex]
const regexLast = criterium.ignoreDashes?regexLastNrNoDash:regexLastNr
//
loop(plugins,plugin=>plugin.prepare && plugin.prepare(criterium))
//
let isNumeric = fls
// prepare sort elements
let valueA = getSortBy(a,criterium)
let valueB = getSortBy(b,criterium)
if (criterium.sortFunction) { // custom sort
sortReturnNumber = criterium.sortFunction(a,b)
} else if (criterium.order==='rand') { // random sort
sortReturnNumber = Math.random()<0.5?1:-1
} else { // regular sort
if (valueA===valueB) {
sortReturnNumber = 0
} else {
if (!criterium.forceStrings) {
// cast to float if both strings are numeral (or end numeral)
let valuesA = isString(valueA)?valueA&&valueA.match(regexLast):fls// todo: isString superfluous because getSortBy returns string|undefined
let valuesB = isString(valueB)?valueB&&valueB.match(regexLast):fls
if (valuesA&&valuesB) {
const previousA = valueA.substr(0,valueA.length-valuesA[0].length)
const previousB = valueB.substr(0,valueB.length-valuesB[0].length)
if (previousA==previousB) {
isNumeric = !fls
valueA = parsefloat(valuesA[0])
valueB = parsefloat(valuesB[0])
}
}
}
if (!criterium.natural||(!isNaN(valueA)&&!isNaN(valueB))) {
sortReturnNumber = valueA<valueB?-1:(valueA>valueB?1:0)
} else {
sortReturnNumber = naturalCompare(valueA, valueB, chunkify)
}
}
}
loop(plugins,({sort})=>sort && (sortReturnNumber = sort(criterium,isNumeric,valueA,valueB,sortReturnNumber)))
sortReturnNumber *= criterium.sortReturnNumber // lastly assign asc/desc
sortReturnNumber===0 && criteriumIndex++
}
sortReturnNumber===0 && (sortReturnNumber = a.pos>b.pos?1:-1)
return sortReturnNumber
}
/**
* Applies the sorted list to the DOM
* @memberof tinysort
* @private
*/
function applyToDOM(){
const numSorted = elmObjsSorted.length
const hasSortedAll = numSorted===elmObjsAll.length
const hasSortedAllSiblings = (parentNode && parentNode != undefined) ? numSorted===parentNode.children.length : false;
const {place,console} = criteria[0]
if (isSameParent&&hasSortedAll&&hasSortedAllSiblings) {
if (isFlex) {
elmObjsSorted.forEach((elmObj,i)=>elmObj.elm.style.order = i)
} else {
if (parentNode) parentNode.appendChild(sortedIntoFragment())
else console && console.warn && console.warn('parentNode has been removed')
}
} else {
const isPlaceOrg = place==='org'
const isPlaceStart = place==='start'
const isPlaceEnd = place==='end'
const isPlaceFirst = place==='first'
const isPlaceLast = place==='last'
if (isPlaceOrg) {
elmObjsSorted.forEach(addGhost)
elmObjsSorted.forEach((elmObj,i)=>replaceGhost(elmObjsSortedInitial[i],elmObj.elm))
} else if (isPlaceStart||isPlaceEnd) {
let startElmObj = elmObjsSortedInitial[isPlaceStart?0:elmObjsSortedInitial.length-1]
const startParent = startElmObj&&startElmObj.elm.parentNode
const startElm = startParent&&(isPlaceStart&&startParent.firstChild||startParent.lastChild)
if (startElm) {
startElm!==startElmObj.elm && (startElmObj = {elm:startElm})
addGhost(startElmObj)
isPlaceEnd&&startParent.appendChild(startElmObj.ghost)
replaceGhost(startElmObj,sortedIntoFragment())
}
} else if (isPlaceFirst||isPlaceLast) {
const firstElmObj = elmObjsSortedInitial[isPlaceFirst?0:elmObjsSortedInitial.length-1]
replaceGhost(addGhost(firstElmObj),sortedIntoFragment())
}
}
}
/**
* Adds all sorted elements to the document fragment and returns it.
* @memberof tinysort
* @private
* @returns {DocumentFragment}
*/
function sortedIntoFragment(){
elmObjsSorted.forEach(elmObj=>fragment.appendChild(elmObj.elm))
return fragment
}
/**
* Adds a temporary element before an element before reordering.
* @memberof tinysort
* @private
* @param {elementObject} elmObj
* @returns {elementObject}
*/
function addGhost(elmObj){
const element = elmObj.elm
,ghost = doc.createElement('div')
elmObj.ghost = ghost
element.parentNode.insertBefore(ghost,element)
return elmObj
}
/**
* Inserts an element before a ghost element and removes the ghost.
* @memberof tinysort
* @private
* @param {elementObject} elmObjGhost
* @param {HTMLElement} elm
*/
function replaceGhost(elmObjGhost,elm){
const ghost = elmObjGhost.ghost
,ghostParent = ghost.parentNode
ghostParent.insertBefore(elm,ghost)
ghostParent.removeChild(ghost)
delete elmObjGhost.ghost
}
/**
* Get the string/number to be sorted by checking the elementObject with the criterium.
* @memberof tinysort
* @private
* @param {elementObject} elementObject
* @param {criterium} criterium
* @returns {String}
* @todo memoize
*/
function getSortBy(elementObject,criterium){
let sortBy
,element = elementObject.elm
,{selector} = criterium
// element
if (selector) {
if (criterium.hasFilter) {
if (!element.matches(selector)) element = nll
} else {
element = element.querySelector(selector)
}
}
// value
if (criterium.hasAttr) sortBy = element.getAttribute(criterium.attr)
else if (criterium.useVal) sortBy = element.value||element.getAttribute('value')
else if (criterium.hasData) sortBy = element.getAttribute('data-'+criterium.data)
else if (element) sortBy = element.textContent
// strings should be ordered in lowercase (unless specified)
if (isString(sortBy)) {
if (!criterium.cases) sortBy = sortBy.toLowerCase()
sortBy = sortBy.replace(/\s+/g,' ') // spaces/newlines
}
const noIndex = [undef,nll,''].indexOf(sortBy)
if (noIndex!==-1) sortBy = (criterium.emptyEnd?charHigh:charLow)[noIndex]
return sortBy
}
/*function memoize(fnc) {
var oCache = {}
, sKeySuffix = 0;
return function () {
var sKey = sKeySuffix + JSON.stringify(arguments); // todo: circular dependency on Nodes
return (sKey in oCache)?oCache[sKey]:oCache[sKey] = fnc.apply(fnc,arguments);
};
}*/
/**
* Test if an object is a string
* @memberOf tinysort
* @method
* @private
* @param o
* @returns {boolean}
*/
function isString(o){
return typeof o==='string'
}
return elmObjsSorted.map(o=>o.elm)
}
/**
* Traverse an array, or array-like object
* @memberOf tinysort
* @method
* @private
* @param {Array} array The object or array
* @param {Function} func Callback function with the parameters value and key.
* @returns {Array}
*/
function loop(array,func){
const l = array.length
let i = l
while (i--) {
const j = l-i-1
func(array[j],j)
}
return array
}
/**
* Extend an object
* @memberOf tinysort
* @method
* @private
* @param {Object} obj Subject.
* @param {Object} fns Property object.
* @param {boolean} [overwrite=false] Overwrite properties.
* @returns {Object} Subject.
*/
function extend(obj,fns,overwrite){
for (let s in fns) {
if (overwrite||obj[s]===undef) {
obj[s] = fns[s]
}
}
return obj
}
/**
* Public API method for plugins
* @param {Function} prepare
* @param {Function} sort
* @param {object} sortBy
* @returns {number}
*/
function plugin(prepare,sort,sortBy){
return plugins.push({prepare,sort,sortBy})
}
// Element.prototype.matches IE
win.Element&&(elementPrototype=>elementPrototype.matches = elementPrototype.matches||elementPrototype.msMatchesSelector)(Element.prototype)
// extend the plugin to expose stuff
extend(plugin,{loop})
return extend(tinysort,{plugin,defaults})
})()));