/** * tablednd plug-in for jquery, allows you to drag and drop table rows * you can set up various options to control how the system will work * copyright (c) denis howlett * licensed like jquery, see http://docs.jquery.com/license. * download by http://www.codefans.net * configuration options: * * ondragstyle * this is the style that is assigned to the row during drag. there are limitations to the styles that can be * associated with a row (such as you can't assign a border--well you can, but it won't be * displayed). (so instead consider using ondragclass.) the css style to apply is specified as * a map (as used in the jquery css(...) function). * ondropstyle * this is the style that is assigned to the row when it is dropped. as for ondragstyle, there are limitations * to what you can do. also this replaces the original style, so again consider using ondragclass which * is simply added and then removed on drop. * ondragclass * this class is added for the duration of the drag and then removed when the row is dropped. it is more * flexible than using ondragstyle since it can be inherited by the row cells and other content. the default * is class is tdnd_whiledrag. so to use the default, simply customise this css class in your * stylesheet. * ondrop * pass a function that will be called when the row is dropped. the function takes 2 parameters: the table * and the row that was dropped. you can work out the new order of the rows by using * table.rows. * ondragstart * pass a function that will be called when the user starts dragging. the function takes 2 parameters: the * table and the row which the user has started to drag. * onallowdrop * pass a function that will be called as a row is over another row. if the function returns true, allow * dropping on that row, otherwise not. the function takes 2 parameters: the dragged row and the row under * the cursor. it returns a boolean: true allows the drop, false doesn't allow it. * scrollamount * this is the number of pixels to scroll if the user moves the mouse cursor to the top or bottom of the * window. the page should automatically scroll up or down as appropriate (tested in ie6, ie7, safari, ff2, * ff3 beta * draghandle * this is the name of a class that you assign to one or more cells in each row that is draggable. if you * specify this class, then you are responsible for setting cursor: move in the css and only these cells * will have the drag behaviour. if you do not specify a draghandle, then you get the old behaviour where * the whole row is draggable. * * other ways to control behaviour: * * add class="nodrop" to any rows for which you don't want to allow dropping, and class="nodrag" to any rows * that you don't want to be draggable. * * inside the ondrop method you can also call $.tablednd.serialize() this returns a string of the form * []=&[]= so that you can send this back to the server. the table must have * an id as must all the rows. * * other methods: * * $("...").tabledndupdate() * will update all the matching tables, that is it will reapply the mousedown method to the rows (or handle cells). * this is useful if you have updated the table rows using ajax and you want to make the table draggable again. * the table maintains the original configuration (so you don't have to specify it again). * * $("...").tabledndserialize() * will serialize and return the serialized string as above, but for each of the matching tables--so it can be * called from anywhere and isn't dependent on the currenttable being set up correctly before calling * * known problems: * - auto-scoll has some problems with ie7 (it scrolls even when it shouldn't), work-around: set scrollamount to 0 * * version 0.2: 2008-02-20 first public version * version 0.3: 2008-02-07 added ondragstart option * made the scroll amount configurable (default is 5 as before) * version 0.4: 2008-03-15 changed the nodrag/nodrop attributes to nodrag/nodrop classes * added onallowdrop to control dropping * fixed a bug which meant that you couldn't set the scroll amount in both directions * added serialize method * version 0.5: 2008-05-16 changed so that if you specify a draghandle class it doesn't make the whole row * draggable * improved the serialize method to use a default (and settable) regular expression. * added tabledndupate() and tabledndserialize() to be called when you are outside the table */ jquery.tablednd = { /** keep hold of the current table being dragged */ currenttable : null, /** keep hold of the current drag object if any */ dragobject: null, /** the current mouse offset */ mouseoffset: null, /** remember the old value of y so that we don't do too much processing */ oldy: 0, /** actually build the structure */ build: function(options) { // set up the defaults if any this.each(function() { // this is bound to each matching table, set up the defaults and override with user options this.tabledndconfig = jquery.extend({ ondragstyle: null, ondropstyle: null, // add in the default class for whiledragging ondragclass: "tdnd_whiledrag", ondrop: null, ondragstart: null, scrollamount: 5, serializeregexp: /[^\-]*$/, // the regular expression to use to trim row ids serializeparamname: null, // if you want to specify another parameter name instead of the table id draghandle: null // if you give the name of a class here, then only cells with this class will be draggable }, options || {}); // now make the rows draggable jquery.tablednd.makedraggable(this); }); // now we need to capture the mouse up and mouse move event // we can use bind so that we don't interfere with other event handlers jquery(document) .bind('mousemove', jquery.tablednd.mousemove) .bind('mouseup', jquery.tablednd.mouseup); // don't break the chain return this; }, /** this function makes all the rows on the table draggable apart from those marked as "nodrag" */ makedraggable: function(table) { var config = table.tabledndconfig; if (table.tabledndconfig.draghandle) { // we only need to add the event to the specified cells var cells = jquery("td."+table.tabledndconfig.draghandle, table); cells.each(function() { // the cell is bound to "this" jquery(this).mousedown(function(ev) { jquery.tablednd.dragobject = this.parentnode; jquery.tablednd.currenttable = table; jquery.tablednd.mouseoffset = jquery.tablednd.getmouseoffset(this, ev); if (config.ondragstart) { // call the ondrop method if there is one config.ondragstart(table, this); } return false; }); }) } else { // for backwards compatibility, we add the event to the whole row var rows = jquery("tr", table); // get all the rows as a wrapped set rows.each(function() { // iterate through each row, the row is bound to "this" var row = jquery(this); if (! row.hasclass("nodrag")) { row.mousedown(function(ev) { if (ev.target.tagname == "td") { jquery.tablednd.dragobject = this; jquery.tablednd.currenttable = table; jquery.tablednd.mouseoffset = jquery.tablednd.getmouseoffset(this, ev); if (config.ondragstart) { // call the ondrop method if there is one config.ondragstart(table, this); } return false; } }).css("cursor", "move"); // store the tablednd object } }); } }, updatetables: function() { this.each(function() { // this is now bound to each matching table if (this.tabledndconfig) { jquery.tablednd.makedraggable(this); } }) }, /** get the mouse coordinates from the event (allowing for browser differences) */ mousecoords: function(ev){ if(ev.pagex || ev.pagey){ return {x:ev.pagex, y:ev.pagey}; } return { x:ev.clientx + document.body.scrollleft - document.body.clientleft, y:ev.clienty + document.body.scrolltop - document.body.clienttop }; }, /** given a target element and a mouse event, get the mouse offset from that element. to do this we need the element's position and the mouse position */ getmouseoffset: function(target, ev) { ev = ev || window.event; var docpos = this.getposition(target); var mousepos = this.mousecoords(ev); return {x:mousepos.x - docpos.x, y:mousepos.y - docpos.y}; }, /** get the position of an element by going up the dom tree and adding up all the offsets */ getposition: function(e){ var left = 0; var top = 0; /** safari fix -- thanks to luis chato for this! */ if (e.offsetheight == 0) { /** safari 2 doesn't correctly grab the offsettop of a table row this is detailed here: http://jacob.peargrove.com/blog/2006/technical/table-row-offsettop-bug-in-safari/ the solution is likewise noted there, grab the offset of a table cell in the row - the firstchild. note that firefox will return a text node as a first child, so designing a more thorough solution may need to take that into account, for now this seems to work in firefox, safari, ie */ e = e.firstchild; // a table cell } if (e && e.offsetparent) { while (e.offsetparent){ left += e.offsetleft; top += e.offsettop; e = e.offsetparent; } left += e.offsetleft; top += e.offsettop; } return {x:left, y:top}; }, mousemove: function(ev) { if (jquery.tablednd.dragobject == null) { return; } var dragobj = jquery(jquery.tablednd.dragobject); var config = jquery.tablednd.currenttable.tabledndconfig; var mousepos = jquery.tablednd.mousecoords(ev); var y = mousepos.y - jquery.tablednd.mouseoffset.y; //auto scroll the window var yoffset = window.pageyoffset; if (document.all) { // windows version //yoffset=document.body.scrolltop; if (typeof document.compatmode != 'undefined' && document.compatmode != 'backcompat') { yoffset = document.documentelement.scrolltop; } else if (typeof document.body != 'undefined') { yoffset=document.body.scrolltop; } } if (mousepos.y-yoffset < config.scrollamount) { window.scrollby(0, -config.scrollamount); } else { var windowheight = window.innerheight ? window.innerheight : document.documentelement.clientheight ? document.documentelement.clientheight : document.body.clientheight; if (windowheight-(mousepos.y-yoffset) < config.scrollamount) { window.scrollby(0, config.scrollamount); } } if (y != jquery.tablednd.oldy) { // work out if we're going up or down... var movingdown = y > jquery.tablednd.oldy; // update the old value jquery.tablednd.oldy = y; // update the style to show we're dragging if (config.ondragclass) { dragobj.addclass(config.ondragclass); } else { dragobj.css(config.ondragstyle); } // if we're over a row then move the dragged row to there so that the user sees the // effect dynamically var currentrow = jquery.tablednd.finddroptargetrow(dragobj, y); if (currentrow) { // todo worry about what happens when there are multiple tbodies if (movingdown && jquery.tablednd.dragobject != currentrow) { jquery.tablednd.dragobject.parentnode.insertbefore(jquery.tablednd.dragobject, currentrow.nextsibling); } else if (! movingdown && jquery.tablednd.dragobject != currentrow) { jquery.tablednd.dragobject.parentnode.insertbefore(jquery.tablednd.dragobject, currentrow); } } } return false; }, /** we're only worried about the y position really, because we can only move rows up and down */ finddroptargetrow: function(draggedrow, y) { var rows = jquery.tablednd.currenttable.rows; for (var i=0; i rowy - rowheight) && (y < (rowy + rowheight))) { // that's the row we're over // if it's the same as the current row, ignore it if (row == draggedrow) {return null;} var config = jquery.tablednd.currenttable.tabledndconfig; if (config.onallowdrop) { if (config.onallowdrop(draggedrow, row)) { return row; } else { return null; } } else { // if a row has nodrop class, then don't allow dropping (inspired by john tarr and famic) var nodrop = jquery(row).hasclass("nodrop"); if (! nodrop) { return row; } else { return null; } } return row; } } return null; }, mouseup: function(e) { if (jquery.tablednd.currenttable && jquery.tablednd.dragobject) { var droppedrow = jquery.tablednd.dragobject; var config = jquery.tablednd.currenttable.tabledndconfig; // if we have a dragobject, then we need to release it, // the row will already have been moved to the right place so we just reset stuff if (config.ondragclass) { jquery(droppedrow).removeclass(config.ondragclass); } else { jquery(droppedrow).css(config.ondropstyle); } jquery.tablednd.dragobject = null; if (config.ondrop) { // call the ondrop method if there is one config.ondrop(jquery.tablednd.currenttable, droppedrow); } jquery.tablednd.currenttable = null; // let go of the table too } }, serialize: function() { if (jquery.tablednd.currenttable) { return jquery.tablednd.serializetable(jquery.tablednd.currenttable); } else { return "error: no table id set, you need to set an id on your table and every row"; } }, serializetable: function(table) { var result = ""; var tableid = table.id; var rows = table.rows; for (var i=0; i 0) result += "&"; var rowid = rows[i].id; if (rowid && rowid && table.tabledndconfig && table.tabledndconfig.serializeregexp) { rowid = rowid.match(table.tabledndconfig.serializeregexp)[0]; } result += tableid + '[]=' + rowid; } return result; }, serializetables: function() { var result = ""; this.each(function() { // this is now bound to each matching table result += jquery.tablednd.serializetable(this); }); return result; } } jquery.fn.extend( { tablednd : jquery.tablednd.build, tabledndupdate : jquery.tablednd.updatetables, tabledndserialize: jquery.tablednd.serializetables } );