├── .gitignore ├── CHANGES ├── Gruntfile.js ├── LICENSE ├── README.md ├── TODO ├── bower.json ├── dist └── RowSorter.js ├── examples ├── basic.html ├── basic2.html ├── big_table.php ├── handler1.html ├── handler2.html ├── jquery.html ├── revert.html ├── sticky.html ├── style.html └── touch_test.html ├── package.json └── src └── RowSorter.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | Changes in version 2.1.0 2 | - New method: revert (reverts last dragged row to old position) 3 | 4 | Changes in version 2.0.0 5 | - jQuery is not required anymore. But if jQuery is defined in browser, RowSorter script registers itself as a jquery plugin. 6 | - Dropped "disabledClass" option. use handler option instead. 7 | - Dropped "onBeforeMove" option. 8 | - New option "stickTopRows": the count of top sticky rows. 9 | - New option "stickBottomRows": the count of bottom sticky rows. 10 | - New option "tbody": when true, only tbody rows will be sortable. Rows of thead and tfoot wont be sortable. 11 | - Renamed option "tableDragClass" to "tableClass". 12 | - Renamed option "disabledRowClass" to "disabledClass". 13 | - The "handler" option accepts null. 14 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.initConfig({ 3 | uglify: { 4 | dist: { 5 | files: { 6 | 'dist/RowSorter.js': ['src/RowSorter.js'] 7 | } 8 | } 9 | } 10 | }); 11 | 12 | grunt.loadNpmTasks('grunt-contrib-uglify'); 13 | grunt.registerTask('default', ['uglify']); 14 | }; 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Gökhan Bora 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | RowSorter.js 2 | ============ 3 | ## Drag & drop row sorter plugin. 4 | * Works on Touch devices. 5 | * Supports IE8+ and all other modern browsers. 6 | * No framework dependency (But registers itself as a jquery plugin if exists.) 7 | 8 | ### Install 9 | bower install rowsorter 10 | ```html 11 | 12 | ``` 13 | 14 | ### Usage 15 | ```javascript 16 | 33 | ``` 34 | 35 | ### Options: 36 | 37 | @string handler : drag handler selector (default: null) 38 | @boolean tbody : pass true if want to sort only tbody > tr. (default: true) 39 | @string tableClass : adds this class name to table while rows are sorting (default: "sorting-table") 40 | @string dragClass : dragging row's class name (default: "sorting-row"). 41 | @number stickTopRows : count of top sticky rows (default: 0), 42 | @number stickBottomRows : count of bottom sticky rows (default: 0), 43 | @function onDragStart : (default: null) 44 | @function onDragEnd : (default: null) 45 | @function onDrop : (default: null) 46 | 47 | #### Handling Events 48 | ```javascript 49 | onDragStart: function(tbody, row, old_index) { 50 | // find the table 51 | // if options.tbody is true, this param will be tbody element 52 | // otherwise it will be table element 53 | var table = tbody.tagName === "TBODY" ? tbody.parentNode : tbody; 54 | 55 | // old_index is zero-based index of row in tbody (or table if tbody not exists) 56 | console.log(table, row, old_index); 57 | }, 58 | 59 | // if new_index === old_index, this function won't be called. 60 | onDrop: function(tbody, row, new_index, old_index) { 61 | // find the table 62 | // if options.tbody is true, this param will be tbody element 63 | // otherwise it will be table element 64 | var table = tbody.tagName === "TBODY" ? tbody.parentNode : tbody; 65 | 66 | // old_index is stored index of row in table/tbody before start the dragging. 67 | // new_index is index of row in table/tbody after the row has been dragged. 68 | console.log(table, row, new_index, old_index); 69 | }, 70 | 71 | // if new_index === old_index, this function will be called. 72 | onDragEnd: function(tbody, row, current_index) { 73 | console.log('Dragging the ' + current_index + '. row canceled.'); 74 | } 75 | ``` 76 | 77 | ### Samples 78 | 79 | * [Basic Usage][basic] 80 | * [Custom Handler 1][handler1] 81 | * [Custom Handler 2][handler2] 82 | * [Sticky Top & Bottom][sticky] 83 | * [jQuery Plugin][jquery] 84 | * [Custom CSS][style] 85 | * [Big Table][bigtable] 86 | * [Mobile Sample][touchtest] 87 | * [Revert][revert] 88 | 89 | ### File Sizes 90 | 91 | * Minified: ~7kb 92 | * Minified and gzipped: < 3kb 93 | 94 | [basic]: http://borayazilim.com/projects/rowsorter/examples/basic.html 95 | [handler1]: http://borayazilim.com/projects/rowsorter/examples/handler1.html 96 | [handler2]: http://borayazilim.com/projects/rowsorter/examples/handler2.html 97 | [sticky]: http://borayazilim.com/projects/rowsorter/examples/sticky.html 98 | [jquery]: http://borayazilim.com/projects/rowsorter/examples/jquery.html 99 | [style]: http://borayazilim.com/projects/rowsorter/examples/style.html 100 | [bigtable]: http://borayazilim.com/projects/rowsorter/examples/big_table.php 101 | [touchtest]: http://borayazilim.com/projects/rowsorter/examples/touch_test.html 102 | [revert]: http://borayazilim.com/projects/rowsorter/examples/revert.html 103 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | merge onDrop and onDragEnd handlers and rename to onDragEnd. 2 | onDragEnd should be called too if old_index and new_index is same value. 3 | - let user to control the equality of old and new index. 4 | rename minified file to rowsorter.min.js 5 | remove RowSorter.revert method. use RowSorter.undo 6 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rowsorter", 3 | "version": "2.1.1", 4 | "description": "Drag & drop table row sorter pluging with touch support for Vanilla JS and jQuery.", 5 | "main": "dist/RowSorter.js", 6 | "keywords": [ 7 | "row sorting", 8 | "table sort" 9 | ], 10 | "authors": [ 11 | "Gökhan Bora" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "git@github.com:borayazilim/rowsorter.git" 16 | }, 17 | "license": "MIT", 18 | "homepage": "https://github.com/borayazilim/rowsorter", 19 | "ignore": [ 20 | "**/.*", 21 | "node_modules", 22 | "bower_components", 23 | "samples" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /dist/RowSorter.js: -------------------------------------------------------------------------------- 1 | !function(a,b){"use strict";"function"==typeof define&&define.amd?define("RowSorter",b):"object"==typeof exports?module.exports=b():a.RowSorter=b()}(this,function(){"use strict";function a(h,k){if(!(this instanceof a))return new a(h,k);if("string"==typeof h&&(h=i(h)),j(h,"table")===!1)throw new Error("Table not found.");return h[A]instanceof a?h[A]:(this._options=t(B,k),this._table=h,this._tbody=h,this._rows=[],this._lastY=!1,this._draggingRow=null,this._firstTouch=!0,this._lastSort=null,this._ended=!0,this._mousedown=s(b,this),this._mousemove=s(d,this),this._mouseup=s(f,this),this._touchstart=s(c,this),this._touchmove=s(e,this),this._touchend=s(g,this),this._touchId=null,this._table[A]=this,void this.init())}function b(a){return a=a||window.event,this._start(a.target||a.srcElement,a.clientY)?(a.preventDefault?a.preventDefault():a.returnValue=!1,!1):!0}function c(a){if(1===a.touches.length){var b=a.touches[0],c=document.elementFromPoint(b.clientX,b.clientY);if(this._touchId=b.identifier,this._start(c,b.clientY))return a.preventDefault?a.preventDefault():a.returnValue=!1,!1}return!0}function d(a){return a=a||window.event,this._move(a.target||a.srcElement,a.clientY),!0}function e(a){if(1===a.touches.length){var b=a.touches[0],c=document.elementFromPoint(b.clientX,b.clientY);this._touchId===b.identifier&&this._move(c,b.clientY)}return!0}function f(){this._end()}function g(a){a.changedTouches.length>0&&this._touchId===a.changedTouches[0].identifier&&this._end()}function h(b){return b instanceof a?b:("string"==typeof b&&(b=i(b)),j(b,"table")&&A in b&&b[A]instanceof a?b[A]:null)}function i(a){var b=u(document,a);return b.length>0&&j(b[0],"table")?b[0]:null}function j(a,b){return a&&"object"==typeof a&&"nodeName"in a&&a.nodeName===b.toUpperCase()}function k(a,b,c){var d=a.parentNode;1===c?b.nextSibling?d.insertBefore(a,b.nextSibling):d.appendChild(a):-1===c&&d.insertBefore(a,b)}function l(a,b){for(var c=a.rows,d=c.length,e=0;d>e;e++)if(b===c[e])return e;return-1}function m(a,b,c){a.attachEvent?a.attachEvent("on"+b,c):a.addEventListener(b,c,!1)}function n(a,b,c){a.detachEvent?a.detachEvent("on"+b,c):a.removeEventListener(b,c,!1)}function o(a){return a.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")}function p(a,b){if(b=o(b),""===b)return!1;if(-1!==b.indexOf(" ")){for(var c=b.replace(/\s+/g," ").split(" "),d=0,e=c.length;e>d;d++)if(p(a,c[d])===!1)return!1;return!0}return a.classList?!!a.classList.contains(b):!!a.className.match(new RegExp("(\\s|^)"+b+"(\\s|$)"))}function q(a,b){if(b=o(b),""!==b)if(-1===b.indexOf(" "))p(a,b)===!1&&(a.classList?a.classList.add(b):a.className+=" "+b);else for(var c=b.replace(/\s+/g," ").split(" "),d=0,e=c.length;e>d;d++)q(a,c[d])}function r(a,b){if(b=o(b),""!==b)if(-1===b.indexOf(" "))p(a,b)&&(a.classList?a.classList.remove(b):a.className=a.className.replace(new RegExp("(\\s|^)"+b+"(\\s|$)")," "));else for(var c=b.replace(/\s+/g," ").split(" "),d=0,e=c.length;e>d;d++)r(a,c[d])}function s(a,b){return Function.prototype.bind?a.bind(b):function(){a.apply(b,y.slice.call(arguments))}}function t(a,b){if(x)return x.extend({},a,b);var c,d={};for(c in a)a.hasOwnProperty(c)&&(d[c]=a[c]);if(b&&"[object Object]"===Object.prototype.toString.call(b))for(c in b)b.hasOwnProperty(c)&&(d[c]=b[c]);return d}function u(a,b){return x?x.makeArray(x(a).find(b)):a.querySelectorAll(b)}function v(a,b){var c=1,d=20,e=a;for(b=b.toLowerCase();e.tagName&&e.tagName.toLowerCase()!==b;){if(c>d||!e.parentNode)return null;e=e.parentNode,c++}return e}function w(a,b){if(y.indexOf)return y.indexOf.call(a,b);for(var c=0,d=a.length;d>c;c++)if(b===a[c])return c;return-1}var x=window.jQuery||!1,y=Array.prototype,z=!!("ontouchstart"in document),A="data-rowsorter",B={handler:null,tbody:!0,tableClass:"sorting-table",dragClass:"sorting-row",stickTopRows:0,stickBottomRows:0,onDragStart:null,onDragEnd:null,onDrop:null};return a.prototype.init=function(){if(this._options.tbody){var a=this._table.getElementsByTagName("tbody");a.length>0&&(this._tbody=a[0])}if("function"!=typeof this._options.onDragStart&&(this._options.onDragStart=null),"function"!=typeof this._options.onDrop&&(this._options.onDrop=null),"function"!=typeof this._options.onDragEnd&&(this._options.onDragEnd=null),("number"!=typeof this._options.stickTopRows||this._options.stickTopRows<0)&&(this._options.stickTopRows=0),("number"!=typeof this._options.stickBottomRows||this._options.stickBottomRows<0)&&(this._options.stickBottomRows=0),m(this._table,"mousedown",this._mousedown),m(document,"mouseup",this._mouseup),z&&(m(this._table,"touchstart",this._touchstart),m(this._table,"touchend",this._touchend)),"onselectstart"in document){var b=this;m(document,"selectstart",function(a){var c=a||window.event;return null!==b._draggingRow?(c.preventDefault?c.preventDefault():c.returnValue=!1,!1):void 0})}},a.prototype._start=function(a,b){if(this._draggingRow&&this._end(),this._rows=this._tbody.rows,this._rows.length<2)return!1;if(this._options.handler){var c=u(this._table,this._options.handler);if(!c||-1===w(c,a))return!1}var d=v(a,"tr"),e=l(this._tbody,d);return-1===e||this._options.stickTopRows>0&&e0&&e>=this._rows.length-this._options.stickBottomRows?!1:(this._draggingRow=d,this._options.tableClass&&q(this._table,this._options.tableClass),this._options.dragClass&&q(this._draggingRow,this._options.dragClass),this._oldIndex=e,this._options.onDragStart&&this._options.onDragStart(this._tbody,this._draggingRow,this._oldIndex),this._lastY=b,this._ended=!1,m(this._table,"mousemove",this._mousemove),z&&m(this._table,"touchmove",this._touchmove),!0)},a.prototype._move=function(a,b){if(this._draggingRow){var c=b>this._lastY?1:b0||this._options.stickBottomRows>0){var f=l(this._tbody,d);(this._options.stickTopRows>0&&f0&&f>=this._rows.length-this._options.stickBottomRows)&&(e=!1)}e&&k(this._draggingRow,d,c),this._lastY=b}}}},a.prototype._end=function(){if(!this._draggingRow)return!0;this._options.tableClass&&r(this._table,this._options.tableClass),this._options.dragClass&&r(this._draggingRow,this._options.dragClass);var a=l(this._tbody,this._draggingRow);if(a!==this._oldIndex){var b=this._lastSort;this._lastSort={previous:b,newIndex:a,oldIndex:this._oldIndex},this._options.onDrop&&this._options.onDrop(this._tbody,this._draggingRow,a,this._oldIndex)}else this._options.onDragEnd&&this._options.onDragEnd(this._tbody,this._draggingRow,this._oldIndex);this._draggingRow=null,this._lastY=!1,this._touchId=null,this._ended=!0,n(this._table,"mousemove",this._mousemove),z&&n(this._table,"touchmove",this._touchmove)},a.prototype.revert=function(){if(null!==this._lastSort){var a=this._lastSort,b=a.oldIndex,c=a.newIndex,d=this._tbody.rows,e=d.length-1;d.length>1&&(e>b?this._tbody.insertBefore(d[c],d[b+(c>b?0:1)]):this._tbody.appendChild(d[c])),this._lastSort=a.previous}},a.prototype.undo=a.prototype.revert,a.prototype.destroy=function(){this._table[A]=null,this._ended===!1&&this._end(),n(this._table,"mousedown",this._mousedown),n(document,"mouseup",this._mouseup),z&&(n(this._table,"touchstart",this._touchstart),n(this._table,"touchend",this._touchend))},a.revert=function(a,b){var c=h(a);if(null===c&&b===!1)throw new Error("Table not found.");c&&c.revert()},a.undo=a.revert,a.destroy=function(a,b){var c=h(a);if(null===c&&b===!1)throw new Error("Table not found.");c&&c.destroy()},x&&(x.fn.extend({rowSorter:function(b){var c=[];return this.each(function(d,e){c.push(new a(e,b))}),1===c.length?c[0]:c}}),x.rowSorter={undo:a.undo,revert:a.revert,destroy:a.destroy}),a}); -------------------------------------------------------------------------------- /examples/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | rowsorter.js 6 | 7 | 8 | 9 | 10 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 |
Basic Sorting
Row 1Sample Content 1Sample Content 1
Row 2Sample Content 2Sample Content 2
Row 3Sample Content 3Sample Content 3
Row 4Sample Content 4Sample Content 4
Row 5Sample Content 5Sample Content 5
Row 6Sample Content 6Sample Content 6
Row 7Sample Content 7Sample Content 7
Row 8Sample Content 8Sample Content 8
Row 9Sample Content 9Sample Content 9
Row 10Sample Content 10Sample Content 10
83 | 84 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /examples/basic2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | rowsorter.js 6 | 7 | 8 | 9 | 10 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 |
Basic Sorting
Row 1Sample Content 1Sample Content 1
Row 2Sample Content 2Sample Content 2
Row 3Sample Content 3Sample Content 3
Row 4Sample Content 4Sample Content 4
Row 5Sample Content 5Sample Content 5
Row 6Sample Content 6Sample Content 6
Row 7Sample Content 7Sample Content 7
Row 8Sample Content 8Sample Content 8
Row 9Sample Content 9Sample Content 9
Row 10Sample Content 10Sample Content 10
85 | 86 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /examples/big_table.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | rowsorter.js - big table 6 | 7 | 8 | 9 | 10 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
Big Table Sorting - 1000 Rows
Row Sample Content
Inner Elements
Sample Content Sample Content Sample Content Sample Content Sample Content Sample Content Sample Content Sample Content
45 | 46 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /examples/handler1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | rowsorter.js - handler 1 6 | 7 | 8 | 9 | 10 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
handler: 'td.sorter'
Row 1Sample Content 1Sample Content 1
Row 2Sample Content 2Sample Content 2
Row 3Sample Content 3Sample Content 3
Row 4Sample Content 4Sample Content 4
Row 5Sample Content 5Sample Content 5
 
67 | 68 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /examples/handler2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | rowsorter.js - handler 2 6 | 7 | 8 | 9 | 10 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
handler: 'span.sort-handler'
Row 1Sample Content 1 Sample Content 1
Row 2Sample Content 2 Sample Content 2
Row 3Sample Content 3 Sample Content 3
Row 4Sample Content 4 Sample Content 4
Row 5Sample Content 5 Sample Content 5
 
62 | 63 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /examples/jquery.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | rowsorter.js - jquery plugin 6 | 7 | 8 | 9 | 10 | 11 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 |
Basic Sorting - jQuery Plugin
Row 1Sample Content 1Sample Content 1
Row 2Sample Content 2Sample Content 2
Row 3Sample Content 3Sample Content 3
Row 4Sample Content 4Sample Content 4
Row 5Sample Content 5Sample Content 5
Row 6Sample Content 6Sample Content 6
Row 7Sample Content 7Sample Content 7
Row 8Sample Content 8Sample Content 8
Row 9Sample Content 9Sample Content 9
Row 10Sample Content 10Sample Content 10
 
87 | 88 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /examples/revert.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | rowsorter.js - revert 6 | 7 | 8 | 9 | 10 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 83 | 84 | 85 |
Revert Last Operation
Row 1Sample Content 1Sample Content 1
Row 2Sample Content 2Sample Content 2
Row 3Sample Content 3Sample Content 3
Row 4Sample Content 4Sample Content 4
Row 5Sample Content 5Sample Content 5
Row 6Sample Content 6Sample Content 6
Row 7Sample Content 7Sample Content 7
Row 8Sample Content 8Sample Content 8
Row 9Sample Content 9Sample Content 9
Row 10Sample Content 10Sample Content 10
80 | 81 | 82 |
86 | 87 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /examples/sticky.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | rowsorter.js - sticky 6 | 7 | 8 | 9 | 10 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 |
First row is sticky
Row 1Sample Content 1Sample Content 1
Row 2Sample Content 2Sample Content 2
Row 3Sample Content 3Sample Content 3
Row 4Sample Content 4Sample Content 4
Row 5Sample Content 5Sample Content 5
Row 6Sample Content 6Sample Content 6
Row 7Sample Content 7Sample Content 7
Row 8Sample Content 8Sample Content 8
Row 9Sample Content 9Sample Content 9
Row 10Sample Content 10Sample Content 10
83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 |
Last row is sticky
Row 1Sample Content 1Sample Content 1
Row 2Sample Content 2Sample Content 2
Row 3Sample Content 3Sample Content 3
Row 4Sample Content 4Sample Content 4
Row 5Sample Content 5Sample Content 5
Row 6Sample Content 6Sample Content 6
Row 7Sample Content 7Sample Content 7
Row 8Sample Content 8Sample Content 8
Row 9Sample Content 9Sample Content 9
Row 10Sample Content 10Sample Content 10
148 | 149 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /examples/style.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | rowsorter.js - styling 6 | 7 | 8 | 9 | 10 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
handler: null
Row 1Record 1Record 1
Row 2Record 2Record 2
Row 3Record 3Recrod 3
Row 4Record 4Record 4
Row 5Record 5Record 5
 
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 |
handler: "td.sorter"
Row 1Record 1Record 1
Row 2Record 2Record 2
Row 3Record 3Recrod 3
Row 4Record 4Record 4
Row 5Record 5Record 5
 
111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 |
handler: "span.sort-handler"
Row 1Record 1 Record 1
Row 2Record 2 Record 2
Row 3Record 3 Recrod 3
Row 4Record 4 Record 4
Row 5Record 5 Record 5
 
151 | 152 | 183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /examples/touch_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | rowsorter.js - mobile 7 | 8 | 9 | 10 | 11 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 |
Basic Sorting
Row 1Sample Content 1 Sample Content 1
Row 2Sample Content 2 Sample Content 2
Row 3Sample Content 3 Sample Content 3
Row 4Sample Content 4 Sample Content 4
Row 5Sample Content 5 Sample Content 5
Row 6Sample Content 6 Sample Content 6
Row 7Sample Content 7 Sample Content 7
70 | 71 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@borayazilim/rowsorter", 3 | "description": "Drag & drop table row sorter pluging with touch support for Vanilla JS and jQuery.", 4 | "version": "2.1.0", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "git@github.com:borayazilim/rowsorter.git" 9 | }, 10 | "devDependencies": { 11 | "grunt": "^0.4.5", 12 | "grunt-contrib-uglify": "^0.9.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/RowSorter.js: -------------------------------------------------------------------------------- 1 | (function(root, factory) { 2 | 'use strict'; 3 | if (typeof define === 'function' && define.amd) { 4 | define('RowSorter', factory); 5 | } else if (typeof exports === 'object') { 6 | module.exports = factory(); 7 | } else { 8 | root.RowSorter = factory(); 9 | } 10 | })(this, function() { 11 | 'use strict'; 12 | 13 | var $ = window.jQuery||false, 14 | arrProto = Array.prototype, 15 | touchSupport = !!('ontouchstart' in document), 16 | helperAttrName = 'data-rowsorter', 17 | defaults = { 18 | handler : null, 19 | tbody : true, 20 | tableClass : 'sorting-table', 21 | dragClass : 'sorting-row', 22 | stickTopRows : 0, 23 | stickBottomRows : 0, 24 | onDragStart : null, 25 | onDragEnd : null, 26 | onDrop : null 27 | }; 28 | 29 | function RowSorter(table, opts) 30 | { 31 | if (!(this instanceof RowSorter)) { 32 | return new RowSorter(table, opts); 33 | } 34 | 35 | if (typeof table === 'string') { 36 | table = findTable(table); 37 | } 38 | 39 | if (is(table, 'table') === false) { 40 | throw new Error('Table not found.'); 41 | } 42 | 43 | if (table[ helperAttrName ] instanceof RowSorter) { 44 | return table[ helperAttrName ]; 45 | } 46 | 47 | this._options = extend(defaults, opts); 48 | this._table = table; 49 | this._tbody = table; 50 | this._rows = []; 51 | this._lastY = false; 52 | this._draggingRow = null; 53 | this._firstTouch = true; 54 | this._lastSort = null; 55 | this._ended = true; 56 | 57 | this._mousedown = bind(mousedown, this); 58 | this._mousemove = bind(mousemove, this); 59 | this._mouseup = bind(mouseup, this); 60 | 61 | this._touchstart = bind(touchstart, this); 62 | this._touchmove = bind(touchmove, this); 63 | this._touchend = bind(touchend, this); 64 | this._touchId = null; 65 | 66 | this._table[ helperAttrName ] = this; 67 | this.init(); 68 | } 69 | 70 | RowSorter.prototype.init = function() 71 | { 72 | if (this._options.tbody) { 73 | var bodies = this._table.getElementsByTagName('tbody'); 74 | if (bodies.length > 0) { 75 | this._tbody = bodies[0]; 76 | } 77 | } 78 | 79 | // pre-check handlers 80 | if (typeof this._options.onDragStart !== 'function') { 81 | this._options.onDragStart = null; 82 | } 83 | 84 | if (typeof this._options.onDrop !== 'function') { 85 | this._options.onDrop = null; 86 | } 87 | 88 | if (typeof this._options.onDragEnd !== 'function') { 89 | this._options.onDragEnd = null; 90 | } 91 | 92 | if (typeof this._options.stickTopRows !== 'number' || this._options.stickTopRows < 0) { 93 | this._options.stickTopRows = 0; 94 | } 95 | 96 | if (typeof this._options.stickBottomRows !== 'number' || this._options.stickBottomRows < 0) { 97 | this._options.stickBottomRows = 0; 98 | } 99 | 100 | addEvent(this._table, 'mousedown', this._mousedown); 101 | addEvent(document, 'mouseup', this._mouseup); 102 | 103 | if (touchSupport) { 104 | addEvent(this._table, 'touchstart', this._touchstart); 105 | addEvent(this._table, 'touchend', this._touchend); 106 | } 107 | 108 | // if document has onselectstart event (old-ie) 109 | if ('onselectstart' in document) { 110 | var that = this; 111 | addEvent(document, 'selectstart', function(e) { 112 | var ev = e||window.event; 113 | // if dragging status is true 114 | if (that._draggingRow !== null) { 115 | // prevent default 116 | if (ev.preventDefault) { 117 | ev.preventDefault(); 118 | } else { 119 | ev.returnValue = false; 120 | } 121 | return false; 122 | } 123 | }); 124 | } 125 | }; 126 | 127 | function mousedown(ev) 128 | { 129 | ev = ev || window.event; 130 | if (this._start(ev.target || ev.srcElement, ev.clientY)) { 131 | if (ev.preventDefault) { 132 | ev.preventDefault(); 133 | } else { 134 | ev.returnValue = false; 135 | } 136 | return false; 137 | } 138 | return true; 139 | } 140 | 141 | function touchstart(ev) 142 | { 143 | if (ev.touches.length === 1) { 144 | var touch = ev.touches[0], 145 | target = document.elementFromPoint(touch.clientX, touch.clientY); 146 | 147 | this._touchId = touch.identifier; 148 | if (this._start(target, touch.clientY)) { 149 | if (ev.preventDefault) { 150 | ev.preventDefault(); 151 | } else { 152 | ev.returnValue = false; 153 | } 154 | return false; 155 | } 156 | } 157 | return true; 158 | } 159 | 160 | RowSorter.prototype._start = function(target, clientY) 161 | { 162 | if (this._draggingRow) { 163 | this._end(); 164 | } 165 | 166 | // read rows 167 | this._rows = this._tbody.rows; 168 | if (this._rows.length < 2) { 169 | return false; 170 | } 171 | 172 | // if handler options is specified 173 | if (this._options.handler) { 174 | // find the handlers 175 | var handlers = qsa(this._table, this._options.handler); 176 | // check targeted element in handlers 177 | if (!handlers || inArray(handlers, target) === -1) { 178 | return false; 179 | } 180 | } 181 | 182 | // find the closest row element. 183 | var draggingRow = closest(target, 'tr'); 184 | 185 | // find current index 186 | var current_index = rowIndex(this._tbody, draggingRow); 187 | 188 | if ( 189 | // if not found any tr element or not valid 190 | current_index === -1 || 191 | // if stickTopRows > 0 and active row is in top sticky rows 192 | (this._options.stickTopRows > 0 && current_index < this._options.stickTopRows) || 193 | // if stickBottomRows > 0 and active row is in bottom sticky rows 194 | (this._options.stickBottomRows > 0 && current_index >= this._rows.length - this._options.stickBottomRows) 195 | ) { 196 | return false; 197 | } 198 | 199 | this._draggingRow = draggingRow; 200 | 201 | // add tableClass to table 202 | if (this._options.tableClass) { 203 | addClass(this._table, this._options.tableClass); 204 | } 205 | 206 | // add dragClass to active row 207 | if (this._options.dragClass) { 208 | addClass(this._draggingRow, this._options.dragClass); 209 | } 210 | 211 | // store current index as old index 212 | this._oldIndex = current_index; 213 | 214 | // call onDragStart 215 | if (this._options.onDragStart) { 216 | this._options.onDragStart(this._tbody, this._draggingRow, this._oldIndex); 217 | } 218 | 219 | // store last mouse position 220 | this._lastY = clientY; 221 | this._ended = false; 222 | 223 | // attach events 224 | addEvent(this._table, 'mousemove', this._mousemove); 225 | 226 | if (touchSupport) { 227 | addEvent(this._table, 'touchmove', this._touchmove); 228 | } 229 | 230 | return true; 231 | }; 232 | 233 | function mousemove(ev) 234 | { 235 | ev = ev || window.event; 236 | this._move(ev.target || ev.srcElement, ev.clientY); 237 | return true; 238 | } 239 | 240 | function touchmove(ev) 241 | { 242 | if (ev.touches.length === 1) { 243 | var touch = ev.touches[0], 244 | target = document.elementFromPoint(touch.clientX, touch.clientY); 245 | 246 | if (this._touchId === touch.identifier) { 247 | this._move(target, touch.clientY); 248 | } 249 | } 250 | return true; 251 | } 252 | 253 | RowSorter.prototype._move = function(target, clientY) 254 | { 255 | // if there is not a draggingRow, kill the event. 256 | if (!this._draggingRow) { 257 | return; 258 | } 259 | 260 | // find direction by last stored position 261 | var direction = clientY > this._lastY ? 1 : (clientY < this._lastY ? -1 : 0); 262 | 263 | // if direction is not zero (when first mouse-drag, it can be zero) 264 | if (direction !== 0) { 265 | 266 | // search the hovered row 267 | var hoveredRow = closest(target, 'tr'); 268 | 269 | // if found any row 270 | // and hovered row is not the dragging row 271 | // and the hovered row is valid 272 | if (hoveredRow && hoveredRow !== this._draggingRow && inArray(this._rows, hoveredRow) !== -1) { 273 | 274 | var move = true; 275 | if (this._options.stickTopRows > 0 || this._options.stickBottomRows > 0) { 276 | // find new position 277 | var new_index = rowIndex(this._tbody, hoveredRow); 278 | 279 | if ( 280 | (this._options.stickTopRows > 0 && new_index < this._options.stickTopRows) || 281 | (this._options.stickBottomRows > 0 && new_index >= this._rows.length - this._options.stickBottomRows) 282 | ) { 283 | move = false; 284 | } 285 | } 286 | 287 | // move row 288 | if (move) { 289 | moveRow(this._draggingRow, hoveredRow, direction); 290 | } 291 | 292 | // store last mouse position 293 | this._lastY = clientY; 294 | } 295 | } 296 | }; 297 | 298 | function mouseup() 299 | { 300 | this._end(); 301 | } 302 | 303 | function touchend(ev) 304 | { 305 | if (ev.changedTouches.length > 0 && this._touchId === ev.changedTouches[0].identifier) { 306 | this._end(); 307 | } 308 | } 309 | 310 | RowSorter.prototype._end = function() 311 | { 312 | // if there is not a draggingRow, kill the event. 313 | if (!this._draggingRow) { 314 | return true; 315 | } 316 | 317 | // remove table class 318 | if (this._options.tableClass) { 319 | removeClass(this._table, this._options.tableClass); 320 | } 321 | 322 | // remove draggingRow class 323 | if (this._options.dragClass) { 324 | removeClass(this._draggingRow, this._options.dragClass); 325 | } 326 | 327 | // find the dragging row's new index 328 | var new_index = rowIndex(this._tbody, this._draggingRow); 329 | 330 | // if new index is not the old index 331 | if (new_index !== this._oldIndex) { 332 | // backup previous sort operation 333 | var previous = this._lastSort; 334 | // store current sort operation and backup data 335 | this._lastSort = { 336 | previous: previous, 337 | newIndex: new_index, 338 | oldIndex: this._oldIndex 339 | }; 340 | 341 | if (this._options.onDrop) { 342 | this._options.onDrop(this._tbody, this._draggingRow, new_index, this._oldIndex); 343 | } 344 | } else if (this._options.onDragEnd) { 345 | this._options.onDragEnd(this._tbody, this._draggingRow, this._oldIndex); 346 | } 347 | 348 | // remove stored active row 349 | this._draggingRow = null; 350 | this._lastY = false; 351 | this._touchId = null; 352 | this._ended = true; 353 | 354 | // attach events 355 | removeEvent(this._table, 'mousemove', this._mousemove); 356 | 357 | if (touchSupport) { 358 | removeEvent(this._table, 'touchmove', this._touchmove); 359 | } 360 | }; 361 | 362 | // @deprecated 363 | // bad method name, use undo instead 364 | RowSorter.prototype.revert = function() 365 | { 366 | if (this._lastSort !== null) { 367 | var lastSort = this._lastSort, 368 | old_index = lastSort.oldIndex, 369 | new_index = lastSort.newIndex, 370 | rows = this._tbody.rows, 371 | max_index = rows.length - 1; 372 | 373 | if (rows.length > 1) { 374 | if (old_index < max_index) { 375 | this._tbody.insertBefore(rows[ new_index ], rows[ old_index + (new_index > old_index ? 0 : 1) ]); 376 | } else { 377 | this._tbody.appendChild(rows[ new_index ]); 378 | } 379 | } 380 | 381 | this._lastSort = lastSort.previous; 382 | } 383 | }; 384 | 385 | RowSorter.prototype.undo = RowSorter.prototype.revert; 386 | 387 | RowSorter.prototype.destroy = function() 388 | { 389 | this._table[ helperAttrName ] = null; 390 | 391 | if (this._ended === false) { 392 | this._end(); 393 | } 394 | 395 | removeEvent(this._table, 'mousedown', this._mousedown); 396 | removeEvent(document, 'mouseup', this._mouseup); 397 | 398 | if (touchSupport) { 399 | removeEvent(this._table, 'touchstart', this._touchstart); 400 | removeEvent(this._table, 'touchend', this._touchend); 401 | } 402 | }; 403 | 404 | // not necessary 405 | /* 406 | RowSorter.get = function(table) 407 | { 408 | if (helperAttrName in table && table[ helperAttrName ] instanceof RowSorter) { 409 | return table[ helperAttrName ]; 410 | } 411 | return null; 412 | };*/ 413 | 414 | // @deprecated 415 | // bad method name, use undo instead 416 | RowSorter.revert = function(table, suppressError) 417 | { 418 | var sorter = getSorterObject(table); 419 | 420 | if (sorter === null && suppressError === false) { 421 | throw new Error('Table not found.'); 422 | } 423 | 424 | if (sorter) { 425 | sorter.revert(); 426 | } 427 | }; 428 | 429 | RowSorter.undo = RowSorter.revert; 430 | 431 | RowSorter.destroy = function(table, suppressError) 432 | { 433 | var sorter = getSorterObject(table); 434 | 435 | if (sorter === null && suppressError === false) { 436 | throw new Error('Table not found.'); 437 | } 438 | 439 | if (sorter) { 440 | sorter.destroy(); 441 | } 442 | }; 443 | 444 | /** 445 | * Returns RowSorter object by table element. 446 | * 447 | * @param {Element} table 448 | * @return {RowSorter|null} 449 | */ 450 | function getSorterObject(table) 451 | { 452 | if (table instanceof RowSorter) { 453 | return table; 454 | } 455 | 456 | if (typeof table === 'string') { 457 | table = findTable(table); 458 | } 459 | 460 | if (is(table, 'table') && helperAttrName in table && table[ helperAttrName ] instanceof RowSorter) { 461 | return table[ helperAttrName ]; 462 | } 463 | return null; 464 | } 465 | 466 | /** 467 | * Searchs table by specified query. 468 | * 469 | * @param {string} query 470 | * @return {mixed|null} 471 | */ 472 | function findTable(query) 473 | { 474 | var elements = qsa(document, query); 475 | if (elements.length > 0 && is(elements[0], 'table')) { 476 | return elements[0]; 477 | } 478 | return null; 479 | } 480 | 481 | /** 482 | * Is specified object an html element? 483 | * 484 | * @param {object} obj 485 | * @param {string} tag 486 | * @return {boolean} 487 | */ 488 | function is(obj, tag) 489 | { 490 | return obj && typeof obj === 'object' && 491 | 'nodeName' in obj && obj.nodeName === tag.toUpperCase(); 492 | } 493 | 494 | /** 495 | * Moves row by direction. 496 | * 497 | * @param {Element} row 498 | * @param {Element} reference 499 | * @param {Number} direction 500 | */ 501 | function moveRow(row, reference, direction) 502 | { 503 | var parent = row.parentNode; 504 | // 1 = down, -1 = up 505 | if (direction === 1) { 506 | if (reference.nextSibling) { 507 | parent.insertBefore(row, reference.nextSibling); 508 | } else { 509 | parent.appendChild(row); 510 | } 511 | } else if (direction === -1) { 512 | parent.insertBefore(row, reference); 513 | } 514 | } 515 | 516 | /** 517 | * Finds row index. 518 | * 519 | * @param {Element} tbody 520 | * @param {Element} row 521 | * @return {Number} 522 | */ 523 | function rowIndex(tbody, row) 524 | { 525 | var rows = tbody.rows, length = rows.length, i = 0; 526 | for (; i < length; i++) { 527 | if (row === rows[ i ]) { 528 | return i; 529 | } 530 | } 531 | return -1; 532 | } 533 | 534 | /** 535 | * Attachs an event to an element. 536 | * 537 | * @param {Element} element 538 | * @param {string} type 539 | * @param {Function} fn 540 | */ 541 | function addEvent(obj, type, fn) 542 | { 543 | if (obj.attachEvent) { 544 | obj.attachEvent('on' + type, fn); 545 | } else { 546 | obj.addEventListener(type, fn, false); 547 | } 548 | } 549 | 550 | /** 551 | * Detachs an event from an element. 552 | * 553 | * @param {Element} element 554 | * @param {string} type 555 | * @param {Function} fn 556 | */ 557 | function removeEvent(obj, type, fn) 558 | { 559 | if (obj.detachEvent) { 560 | obj.detachEvent('on' + type, fn); 561 | } else { 562 | obj.removeEventListener(type, fn, false); 563 | } 564 | } 565 | 566 | function trim(str) 567 | { 568 | return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); 569 | } 570 | 571 | /** 572 | * Checks an element has a class. 573 | * 574 | * @param {Element} element 575 | * @param {string} cls 576 | * @return {boolean} 577 | */ 578 | function hasClass(element, cls) 579 | { 580 | cls = trim(cls); 581 | if (cls === '') { 582 | return false; 583 | } 584 | 585 | if (cls.indexOf(' ') !== -1) { 586 | var classes = cls.replace(/\s+/g, ' ').split(' '), 587 | i = 0, len = classes.length; 588 | for (; i < len; i++) { 589 | if (hasClass(element, classes[ i ]) === false) { 590 | return false; 591 | } 592 | } 593 | return true; 594 | } 595 | 596 | if (element.classList) { 597 | return !!element.classList.contains(cls); 598 | } else { 599 | return !!element.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)')); 600 | } 601 | } 602 | 603 | /** 604 | * Adds a class name to an element. 605 | * 606 | * @param {Element} element 607 | * @param {string} cls 608 | */ 609 | function addClass(element, cls) 610 | { 611 | cls = trim(cls); 612 | if (cls === '') { 613 | return; 614 | } 615 | 616 | if (cls.indexOf(' ') !== -1) { 617 | var classes = cls.replace(/\s+/g, ' ').split(' '), 618 | i = 0, len = classes.length; 619 | for (; i < len; i++) { 620 | addClass(element, classes[ i ]); 621 | } 622 | return; 623 | } 624 | 625 | if (hasClass(element, cls) === false) { 626 | if (element.classList) { 627 | element.classList.add(cls); 628 | } else { 629 | element.className += ' ' + cls; 630 | } 631 | } 632 | } 633 | 634 | /** 635 | * Removes a class name from an element if exists. 636 | * 637 | * @param {Element} element 638 | * @param {string} cls 639 | */ 640 | function removeClass(element, cls) 641 | { 642 | cls = trim(cls); 643 | if (cls === '') { 644 | return; 645 | } 646 | 647 | if (cls.indexOf(' ') !== -1) { 648 | var classes = cls.replace(/\s+/g, ' ').split(' '), 649 | i = 0, len = classes.length; 650 | for (; i < len; i++) { 651 | removeClass(element, classes[ i ]); 652 | } 653 | return; 654 | } 655 | 656 | if (hasClass(element, cls)) { 657 | if (element.classList) { 658 | element.classList.remove(cls); 659 | } else { 660 | element.className = element.className.replace(new RegExp('(\\s|^)' + cls + '(\\s|$)'), ' '); 661 | } 662 | } 663 | } 664 | 665 | function bind(fn, context) 666 | { 667 | if (Function.prototype.bind) { 668 | return fn.bind(context); 669 | } 670 | return function () { 671 | fn.apply(context, arrProto.slice.call(arguments)); 672 | }; 673 | } 674 | 675 | /** 676 | * Extends an object. 677 | * 678 | * @param {Object} base Main Object 679 | * @param {Object} from Extender Object 680 | * @return {Object} New extended Object 681 | */ 682 | function extend(base, from) 683 | { 684 | if ($) { 685 | return $.extend({}, base, from); 686 | } 687 | 688 | var obj = {}, key; 689 | for (key in base) { 690 | if (base.hasOwnProperty(key)) { 691 | obj[ key ] = base[ key ]; 692 | } 693 | } 694 | 695 | if (from && '[object Object]' === Object.prototype.toString.call(from)) { 696 | for (key in from) { 697 | if (from.hasOwnProperty(key)) { 698 | obj[ key ] = from[ key ]; 699 | } 700 | } 701 | } 702 | return obj; 703 | } 704 | 705 | /** 706 | * Dom Query 707 | * 708 | * @param {Element} element 709 | * @param {string} query 710 | * @return {Array} 711 | */ 712 | function qsa(element, query) 713 | { 714 | if ($) { 715 | return $.makeArray($(element).find(query)); 716 | } 717 | return element.querySelectorAll(query); 718 | } 719 | 720 | /** 721 | * Searchs up the closest element. 722 | * 723 | * @param {Element} element 724 | * @param {string} tag 725 | * @return {Element|null} 726 | */ 727 | function closest(element, tag) 728 | { 729 | var c = 1, max = 20, found = element; 730 | tag = tag.toLowerCase(); 731 | while (found.tagName && found.tagName.toLowerCase() !== tag) { 732 | if (c > max || !found.parentNode) { 733 | return null; 734 | } 735 | found = found.parentNode; 736 | c++; 737 | } 738 | return found; 739 | } 740 | 741 | /** 742 | * Search in array 743 | * 744 | * @param {Array} arr 745 | * @param {mixed} search 746 | * @return {Number} 747 | */ 748 | function inArray(arr, search) 749 | { 750 | if (arrProto.indexOf) { 751 | return arrProto.indexOf.call(arr, search); 752 | } 753 | 754 | for (var i = 0, len = arr.length; i < len; i++) { 755 | if (search === arr[ i ]) { 756 | return i; 757 | } 758 | } 759 | return -1; 760 | } 761 | 762 | // if jQuery, register plugin. 763 | if ($) { 764 | $.fn.extend({ 765 | rowSorter: function(options) { 766 | var sorters = []; 767 | this.each(function(index, element) { 768 | sorters.push(new RowSorter(element, options)); 769 | }); 770 | return sorters.length === 1 ? sorters[0] : sorters; 771 | } 772 | }); 773 | $.rowSorter = {undo: RowSorter.undo, revert: RowSorter.revert, destroy: RowSorter.destroy}; 774 | } 775 | 776 | return RowSorter; 777 | }); 778 | --------------------------------------------------------------------------------