Mercurial > defr > drupal > core
comparison misc/tabledrag.js @ 1:c1f4ac30525a 6.0
Drupal 6.0
author | Franck Deroche <webmaster@defr.org> |
---|---|
date | Tue, 23 Dec 2008 14:28:28 +0100 |
parents | |
children | fff6d4c8c043 |
comparison
equal
deleted
inserted
replaced
0:5a113a1c4740 | 1:c1f4ac30525a |
---|---|
1 // $Id: tabledrag.js,v 1.13.2.1 2008/02/08 18:54:10 goba Exp $ | |
2 | |
3 /** | |
4 * Drag and drop table rows with field manipulation. | |
5 * | |
6 * Using the drupal_add_tabledrag() function, any table with weights or parent | |
7 * relationships may be made into draggable tables. Columns containing a field | |
8 * may optionally be hidden, providing a better user experience. | |
9 * | |
10 * Created tableDrag instances may be modified with custom behaviors by | |
11 * overriding the .onDrag, .onDrop, .row.onSwap, and .row.onIndent methods. | |
12 * See blocks.js for an example of adding additional functionality to tableDrag. | |
13 */ | |
14 Drupal.behaviors.tableDrag = function(context) { | |
15 for (var base in Drupal.settings.tableDrag) { | |
16 if (!$('#' + base + '.tabledrag-processed', context).size()) { | |
17 var tableSettings = Drupal.settings.tableDrag[base]; | |
18 | |
19 $('#' + base).filter(':not(.tabledrag-processed)').each(function() { | |
20 // Create the new tableDrag instance. Save in the Drupal variable | |
21 // to allow other scripts access to the object. | |
22 Drupal.tableDrag[base] = new Drupal.tableDrag(this, tableSettings); | |
23 }); | |
24 | |
25 $('#' + base).addClass('tabledrag-processed'); | |
26 } | |
27 } | |
28 }; | |
29 | |
30 /** | |
31 * Constructor for the tableDrag object. Provides table and field manipulation. | |
32 * | |
33 * @param table | |
34 * DOM object for the table to be made draggable. | |
35 * @param tableSettings | |
36 * Settings for the table added via drupal_add_dragtable(). | |
37 */ | |
38 Drupal.tableDrag = function(table, tableSettings) { | |
39 var self = this; | |
40 | |
41 // Required object variables. | |
42 this.table = table; | |
43 this.tableSettings = tableSettings; | |
44 this.dragObject = null; // Used to hold information about a current drag operation. | |
45 this.rowObject = null; // Provides operations for row manipulation. | |
46 this.oldRowElement = null; // Remember the previous element. | |
47 this.oldY = 0; // Used to determine up or down direction from last mouse move. | |
48 this.changed = false; // Whether anything in the entire table has changed. | |
49 this.maxDepth = 0; // Maximum amount of allowed parenting. | |
50 this.rtl = $(this.table).css('direction') == 'rtl' ? -1 : 1; // Direction of the table. | |
51 | |
52 // Configure the scroll settings. | |
53 this.scrollSettings = { amount: 4, interval: 50, trigger: 70 }; | |
54 this.scrollInterval = null; | |
55 this.scrollY = 0; | |
56 this.windowHeight = 0; | |
57 | |
58 // Check this table's settings to see if there are parent relationships in | |
59 // this table. For efficiency, large sections of code can be skipped if we | |
60 // don't need to track horizontal movement and indentations. | |
61 this.indentEnabled = false; | |
62 for (group in tableSettings) { | |
63 for (n in tableSettings[group]) { | |
64 if (tableSettings[group][n]['relationship'] == 'parent') { | |
65 this.indentEnabled = true; | |
66 } | |
67 if (tableSettings[group][n]['limit'] > 0) { | |
68 this.maxDepth = tableSettings[group][n]['limit']; | |
69 } | |
70 } | |
71 } | |
72 if (this.indentEnabled) { | |
73 this.indentCount = 1; // Total width of indents, set in makeDraggable. | |
74 // Find the width of indentations to measure mouse movements against. | |
75 // Because the table doesn't need to start with any indentations, we | |
76 // manually create an empty div, check it's width, then remove. | |
77 var indent = $(Drupal.theme('tableDragIndentation')).appendTo('body'); | |
78 this.indentAmount = parseInt(indent.css('width')); | |
79 indent.remove(); | |
80 } | |
81 | |
82 // Make each applicable row draggable. | |
83 $('tr.draggable', table).each(function() { self.makeDraggable(this); }); | |
84 | |
85 // Hide columns containing affected form elements. | |
86 this.hideColumns(); | |
87 | |
88 // Add mouse bindings to the document. The self variable is passed along | |
89 // as event handlers do not have direct access to the tableDrag object. | |
90 $(document).bind('mousemove', function(event) { self.dragRow(event, self); return false; }); | |
91 $(document).bind('mouseup', function(event) { self.dropRow(event, self); }); | |
92 }; | |
93 | |
94 /** | |
95 * Hide the columns containing form elements according to the settings for | |
96 * this tableDrag instance. | |
97 */ | |
98 Drupal.tableDrag.prototype.hideColumns = function() { | |
99 for (var group in this.tableSettings) { | |
100 // Find the first field in this group. | |
101 for (var d in this.tableSettings[group]) { | |
102 if ($('.' + this.tableSettings[group][d]['target'], this.table).size()) { | |
103 var hidden = this.tableSettings[group][d]['hidden']; | |
104 var field = $('.' + this.tableSettings[group][d]['target'] + ':first', this.table); | |
105 var cell = field.parents('td:first, th:first'); | |
106 break; | |
107 } | |
108 } | |
109 // Hide the column containing this field. | |
110 if (hidden && cell[0] && cell.css('display') != 'none') { | |
111 // Add 1 to our indexes. The nth-child selector is 1 based, not 0 based. | |
112 var columnIndex = $('td', field.parents('tr:first')).index(cell.get(0)) + 1; | |
113 var headerIndex = $('td:not(:hidden)', field.parents('tr:first')).index(cell.get(0)) + 1; | |
114 $('tbody tr', this.table).each(function() { | |
115 // Find and hide the cell in the table body. | |
116 var cell = $('td:nth-child('+ columnIndex +')', this); | |
117 if (cell.size()) { | |
118 cell.css('display', 'none'); | |
119 } | |
120 // We might be dealing with a row spanning the entire table. | |
121 // Reduce the colspan on the first cell to prevent the cell from | |
122 // overshooting the table. | |
123 else { | |
124 cell = $('td:first', this); | |
125 cell.attr('colspan', cell.attr('colspan') - 1); | |
126 } | |
127 }); | |
128 $('thead tr', this.table).each(function() { | |
129 // Remove table header cells entirely (Safari doesn't hide properly). | |
130 var th = $('th:nth-child('+ headerIndex +')', this); | |
131 if (th.size()) { | |
132 th.remove(); | |
133 } | |
134 }); | |
135 } | |
136 } | |
137 }; | |
138 | |
139 /** | |
140 * Find the target used within a particular row and group. | |
141 */ | |
142 Drupal.tableDrag.prototype.rowSettings = function(group, row) { | |
143 var field = $('.' + group, row); | |
144 for (delta in this.tableSettings[group]) { | |
145 var targetClass = this.tableSettings[group][delta]['target']; | |
146 if (field.is('.' + targetClass)) { | |
147 // Return a copy of the row settings. | |
148 var rowSettings = new Object(); | |
149 for (var n in this.tableSettings[group][delta]) { | |
150 rowSettings[n] = this.tableSettings[group][delta][n]; | |
151 } | |
152 return rowSettings; | |
153 } | |
154 } | |
155 }; | |
156 | |
157 /** | |
158 * Take an item and add event handlers to make it become draggable. | |
159 */ | |
160 Drupal.tableDrag.prototype.makeDraggable = function(item) { | |
161 var self = this; | |
162 | |
163 // Create the handle. | |
164 var handle = $('<a href="#" class="tabledrag-handle"><div class="handle"> </div></a>').attr('title', Drupal.t('Drag to re-order')); | |
165 // Insert the handle after indentations (if any). | |
166 if ($('td:first .indentation:last', item).after(handle).size()) { | |
167 // Update the total width of indentation in this entire table. | |
168 self.indentCount = Math.max($('.indentation', item).size(), self.indentCount); | |
169 } | |
170 else { | |
171 $('td:first', item).prepend(handle); | |
172 } | |
173 | |
174 // Add hover action for the handle. | |
175 handle.hover(function() { | |
176 self.dragObject == null ? $(this).addClass('tabledrag-handle-hover') : null; | |
177 }, function() { | |
178 self.dragObject == null ? $(this).removeClass('tabledrag-handle-hover') : null; | |
179 }); | |
180 | |
181 // Add the mousedown action for the handle. | |
182 handle.mousedown(function(event) { | |
183 // Create a new dragObject recording the event information. | |
184 self.dragObject = new Object(); | |
185 self.dragObject.initMouseOffset = self.getMouseOffset(item, event); | |
186 self.dragObject.initMouseCoords = self.mouseCoords(event); | |
187 if (self.indentEnabled) { | |
188 self.dragObject.indentMousePos = self.dragObject.initMouseCoords; | |
189 } | |
190 | |
191 // If there's a lingering row object from the keyboard, remove its focus. | |
192 if (self.rowObject) { | |
193 $('a.tabledrag-handle', self.rowObject.element).blur(); | |
194 } | |
195 | |
196 // Create a new rowObject for manipulation of this row. | |
197 self.rowObject = new self.row(item, 'mouse', self.indentEnabled, self.maxDepth, true); | |
198 | |
199 // Save the position of the table. | |
200 self.table.topY = self.getPosition(self.table).y; | |
201 self.table.bottomY = self.table.topY + self.table.offsetHeight; | |
202 | |
203 // Add classes to the handle and row. | |
204 $(this).addClass('tabledrag-handle-hover'); | |
205 $(item).addClass('drag'); | |
206 | |
207 // Set the document to use the move cursor during drag. | |
208 $('body').addClass('drag'); | |
209 if (self.oldRowElement) { | |
210 $(self.oldRowElement).removeClass('drag-previous'); | |
211 } | |
212 | |
213 // Hack for IE6 that flickers uncontrollably if select lists are moved. | |
214 if (navigator.userAgent.indexOf('MSIE 6.') != -1) { | |
215 $('select', this.table).css('display', 'none'); | |
216 } | |
217 | |
218 // Hack for Konqueror, prevent the blur handler from firing. | |
219 // Konqueror always gives links focus, even after returning false on mousedown. | |
220 self.safeBlur = false; | |
221 | |
222 // Call optional placeholder function. | |
223 self.onDrag(); | |
224 return false; | |
225 }); | |
226 | |
227 // Prevent the anchor tag from jumping us to the top of the page. | |
228 handle.click(function() { | |
229 return false; | |
230 }); | |
231 | |
232 // Similar to the hover event, add a class when the handle is focused. | |
233 handle.focus(function() { | |
234 $(this).addClass('tabledrag-handle-hover'); | |
235 self.safeBlur = true; | |
236 }); | |
237 | |
238 // Remove the handle class on blur and fire the same function as a mouseup. | |
239 handle.blur(function(event) { | |
240 $(this).removeClass('tabledrag-handle-hover'); | |
241 if (self.rowObject && self.safeBlur) { | |
242 self.dropRow(event, self); | |
243 } | |
244 }); | |
245 | |
246 // Add arrow-key support to the handle. | |
247 handle.keydown(function(event) { | |
248 // If a rowObject doesn't yet exist and this isn't the tab key. | |
249 if (event.keyCode != 9 && !self.rowObject) { | |
250 self.rowObject = new self.row(item, 'keyboard', self.indentEnabled, self.maxDepth, true); | |
251 } | |
252 | |
253 var keyChange = false; | |
254 switch (event.keyCode) { | |
255 case 37: // Left arrow. | |
256 case 63234: // Safari left arrow. | |
257 keyChange = true; | |
258 self.rowObject.indent(-1 * self.rtl); | |
259 break; | |
260 case 38: // Up arrow. | |
261 case 63232: // Safari up arrow. | |
262 var previousRow = $(self.rowObject.element).prev('tr').get(0); | |
263 while (previousRow && $(previousRow).is(':hidden')) { | |
264 previousRow = $(previousRow).prev('tr').get(0); | |
265 } | |
266 if (previousRow) { | |
267 self.safeBlur = false; // Do not allow the onBlur cleanup. | |
268 self.rowObject.direction = 'up'; | |
269 keyChange = true; | |
270 | |
271 if ($(item).is('.tabledrag-root')) { | |
272 // Swap with the previous top-level row.. | |
273 var groupHeight = 0; | |
274 while (previousRow && $('.indentation', previousRow).size()) { | |
275 previousRow = $(previousRow).prev('tr').get(0); | |
276 groupHeight += $(previousRow).is(':hidden') ? 0 : previousRow.offsetHeight; | |
277 } | |
278 if (previousRow) { | |
279 self.rowObject.swap('before', previousRow); | |
280 // No need to check for indentation, 0 is the only valid one. | |
281 window.scrollBy(0, -groupHeight); | |
282 } | |
283 } | |
284 else if (self.table.tBodies[0].rows[0] != previousRow || $(previousRow).is('.draggable')) { | |
285 // Swap with the previous row (unless previous row is the first one | |
286 // and undraggable). | |
287 self.rowObject.swap('before', previousRow); | |
288 self.rowObject.interval = null; | |
289 self.rowObject.indent(0); | |
290 window.scrollBy(0, -parseInt(item.offsetHeight)); | |
291 } | |
292 handle.get(0).focus(); // Regain focus after the DOM manipulation. | |
293 } | |
294 break; | |
295 case 39: // Right arrow. | |
296 case 63235: // Safari right arrow. | |
297 keyChange = true; | |
298 self.rowObject.indent(1 * self.rtl); | |
299 break; | |
300 case 40: // Down arrow. | |
301 case 63233: // Safari down arrow. | |
302 var nextRow = $(self.rowObject.group).filter(':last').next('tr').get(0); | |
303 while (nextRow && $(nextRow).is(':hidden')) { | |
304 nextRow = $(nextRow).next('tr').get(0); | |
305 } | |
306 if (nextRow) { | |
307 self.safeBlur = false; // Do not allow the onBlur cleanup. | |
308 self.rowObject.direction = 'down'; | |
309 keyChange = true; | |
310 | |
311 if ($(item).is('.tabledrag-root')) { | |
312 // Swap with the next group (necessarily a top-level one). | |
313 var groupHeight = 0; | |
314 nextGroup = new self.row(nextRow, 'keyboard', self.indentEnabled, self.maxDepth, false); | |
315 if (nextGroup) { | |
316 $(nextGroup.group).each(function () {groupHeight += $(this).is(':hidden') ? 0 : this.offsetHeight}); | |
317 nextGroupRow = $(nextGroup.group).filter(':last').get(0); | |
318 self.rowObject.swap('after', nextGroupRow); | |
319 // No need to check for indentation, 0 is the only valid one. | |
320 window.scrollBy(0, parseInt(groupHeight)); | |
321 } | |
322 } | |
323 else { | |
324 // Swap with the next row. | |
325 self.rowObject.swap('after', nextRow); | |
326 self.rowObject.interval = null; | |
327 self.rowObject.indent(0); | |
328 window.scrollBy(0, parseInt(item.offsetHeight)); | |
329 } | |
330 handle.get(0).focus(); // Regain focus after the DOM manipulation. | |
331 } | |
332 break; | |
333 } | |
334 | |
335 if (self.rowObject && self.rowObject.changed == true) { | |
336 $(item).addClass('drag'); | |
337 if (self.oldRowElement) { | |
338 $(self.oldRowElement).removeClass('drag-previous'); | |
339 } | |
340 self.oldRowElement = item; | |
341 self.restripeTable(); | |
342 self.onDrag(); | |
343 } | |
344 | |
345 // Returning false if we have an arrow key to prevent scrolling. | |
346 if (keyChange) { | |
347 return false; | |
348 } | |
349 }); | |
350 | |
351 // Compatibility addition, return false on keypress to prevent unwanted scrolling. | |
352 // IE and Safari will supress scrolling on keydown, but all other browsers | |
353 // need to return false on keypress. http://www.quirksmode.org/js/keys.html | |
354 handle.keypress(function(event) { | |
355 switch (event.keyCode) { | |
356 case 37: // Left arrow. | |
357 case 38: // Up arrow. | |
358 case 39: // Right arrow. | |
359 case 40: // Down arrow. | |
360 return false; | |
361 } | |
362 }); | |
363 }; | |
364 | |
365 /** | |
366 * Mousemove event handler, bound to document. | |
367 */ | |
368 Drupal.tableDrag.prototype.dragRow = function(event, self) { | |
369 if (self.dragObject) { | |
370 self.currentMouseCoords = self.mouseCoords(event); | |
371 | |
372 var y = self.currentMouseCoords.y - self.dragObject.initMouseOffset.y; | |
373 var x = self.currentMouseCoords.x - self.dragObject.initMouseOffset.x; | |
374 | |
375 // Check for row swapping and vertical scrolling. | |
376 if (y != self.oldY) { | |
377 self.rowObject.direction = y > self.oldY ? 'down' : 'up'; | |
378 self.oldY = y; // Update the old value. | |
379 | |
380 // Check if the window should be scrolled (and how fast). | |
381 var scrollAmount = self.checkScroll(self.currentMouseCoords.y); | |
382 // Stop any current scrolling. | |
383 clearInterval(self.scrollInterval); | |
384 // Continue scrolling if the mouse has moved in the scroll direction. | |
385 if (scrollAmount > 0 && self.rowObject.direction == 'down' || scrollAmount < 0 && self.rowObject.direction == 'up') { | |
386 self.setScroll(scrollAmount); | |
387 } | |
388 | |
389 // If we have a valid target, perform the swap and restripe the table. | |
390 var currentRow = self.findDropTargetRow(x, y); | |
391 if (currentRow) { | |
392 if (self.rowObject.direction == 'down') { | |
393 self.rowObject.swap('after', currentRow, self); | |
394 } | |
395 else { | |
396 self.rowObject.swap('before', currentRow, self); | |
397 } | |
398 self.restripeTable(); | |
399 } | |
400 } | |
401 | |
402 // Similar to row swapping, handle indentations. | |
403 if (self.indentEnabled) { | |
404 var xDiff = self.currentMouseCoords.x - self.dragObject.indentMousePos.x; | |
405 // Set the number of indentations the mouse has been moved left or right. | |
406 var indentDiff = parseInt(xDiff / self.indentAmount * self.rtl); | |
407 // Indent the row with our estimated diff, which may be further | |
408 // restricted according to the rows around this row. | |
409 var indentChange = self.rowObject.indent(indentDiff); | |
410 // Update table and mouse indentations. | |
411 self.dragObject.indentMousePos.x += self.indentAmount * indentChange * self.rtl; | |
412 self.indentCount = Math.max(self.indentCount, self.rowObject.indents); | |
413 } | |
414 } | |
415 }; | |
416 | |
417 /** | |
418 * Mouseup event handler, bound to document. | |
419 * Blur event handler, bound to drag handle for keyboard support. | |
420 */ | |
421 Drupal.tableDrag.prototype.dropRow = function(event, self) { | |
422 // Drop row functionality shared between mouseup and blur events. | |
423 if (self.rowObject != null) { | |
424 var droppedRow = self.rowObject.element; | |
425 // The row is already in the right place so we just release it. | |
426 if (self.rowObject.changed == true) { | |
427 // Update the fields in the dropped row. | |
428 self.updateFields(droppedRow); | |
429 | |
430 // If a setting exists for affecting the entire group, update all the | |
431 // fields in the entire dragged group. | |
432 for (var group in self.tableSettings) { | |
433 var rowSettings = self.rowSettings(group, droppedRow); | |
434 if (rowSettings.relationship == 'group') { | |
435 for (n in self.rowObject.children) { | |
436 self.updateField(self.rowObject.children[n], group); | |
437 } | |
438 } | |
439 } | |
440 | |
441 self.rowObject.markChanged(); | |
442 if (self.changed == false) { | |
443 $(Drupal.theme('tableDragChangedWarning')).insertAfter(self.table).hide().fadeIn('slow'); | |
444 self.changed = true; | |
445 } | |
446 } | |
447 | |
448 if (self.indentEnabled) { | |
449 self.rowObject.removeIndentClasses(); | |
450 } | |
451 if (self.oldRowElement) { | |
452 $(self.oldRowElement).removeClass('drag-previous'); | |
453 } | |
454 $(droppedRow).removeClass('drag').addClass('drag-previous'); | |
455 self.oldRowElement = droppedRow; | |
456 self.onDrop(); | |
457 self.rowObject = null; | |
458 } | |
459 | |
460 // Functionality specific only to mouseup event. | |
461 if (self.dragObject != null) { | |
462 $('.tabledrag-handle', droppedRow).removeClass('tabledrag-handle-hover'); | |
463 | |
464 self.dragObject = null; | |
465 $('body').removeClass('drag'); | |
466 clearInterval(self.scrollInterval); | |
467 | |
468 // Hack for IE6 that flickers uncontrollably if select lists are moved. | |
469 if (navigator.userAgent.indexOf('MSIE 6.') != -1) { | |
470 $('select', this.table).css('display', 'block'); | |
471 } | |
472 } | |
473 }; | |
474 | |
475 /** | |
476 * Get the position of an element by adding up parent offsets in the DOM tree. | |
477 */ | |
478 Drupal.tableDrag.prototype.getPosition = function(element){ | |
479 var left = 0; | |
480 var top = 0; | |
481 // Because Safari doesn't report offsetHeight on table rows, but does on table | |
482 // cells, grab the firstChild of the row and use that instead. | |
483 // http://jacob.peargrove.com/blog/2006/technical/table-row-offsettop-bug-in-safari | |
484 if (element.offsetHeight == 0) { | |
485 element = element.firstChild; // a table cell | |
486 } | |
487 | |
488 while (element.offsetParent){ | |
489 left += element.offsetLeft; | |
490 top += element.offsetTop; | |
491 element = element.offsetParent; | |
492 } | |
493 | |
494 left += element.offsetLeft; | |
495 top += element.offsetTop; | |
496 | |
497 return {x:left, y:top}; | |
498 }; | |
499 | |
500 /** | |
501 * Get the mouse coordinates from the event (allowing for browser differences). | |
502 */ | |
503 Drupal.tableDrag.prototype.mouseCoords = function(event){ | |
504 if (event.pageX || event.pageY) { | |
505 return {x:event.pageX, y:event.pageY}; | |
506 } | |
507 return { | |
508 x:event.clientX + document.body.scrollLeft - document.body.clientLeft, | |
509 y:event.clientY + document.body.scrollTop - document.body.clientTop | |
510 }; | |
511 }; | |
512 | |
513 /** | |
514 * Given a target element and a mouse event, get the mouse offset from that | |
515 * element. To do this we need the element's position and the mouse position. | |
516 */ | |
517 Drupal.tableDrag.prototype.getMouseOffset = function(target, event) { | |
518 var docPos = this.getPosition(target); | |
519 var mousePos = this.mouseCoords(event); | |
520 return {x:mousePos.x - docPos.x, y:mousePos.y - docPos.y}; | |
521 }; | |
522 | |
523 /** | |
524 * Find the row the mouse is currently over. This row is then taken and swapped | |
525 * with the one being dragged. | |
526 * | |
527 * @param x | |
528 * The x coordinate of the mouse on the page (not the screen). | |
529 * @param y | |
530 * The y coordinate of the mouse on the page (not the screen). | |
531 */ | |
532 Drupal.tableDrag.prototype.findDropTargetRow = function(x, y) { | |
533 var rows = this.table.tBodies[0].rows; | |
534 for (var n=0; n<rows.length; n++) { | |
535 var row = rows[n]; | |
536 var indentDiff = 0; | |
537 // Safari fix see Drupal.tableDrag.prototype.getPosition() | |
538 if (row.offsetHeight == 0) { | |
539 var rowY = this.getPosition(row.firstChild).y; | |
540 var rowHeight = parseInt(row.firstChild.offsetHeight)/2; | |
541 } | |
542 // Other browsers. | |
543 else { | |
544 var rowY = this.getPosition(row).y; | |
545 var rowHeight = parseInt(row.offsetHeight)/2; | |
546 } | |
547 | |
548 // Because we always insert before, we need to offset the height a bit. | |
549 if ((y > (rowY - rowHeight)) && (y < (rowY + rowHeight))) { | |
550 if (this.indentEnabled) { | |
551 // Check that this row is not a child of the row being dragged. | |
552 for (n in this.rowObject.group) { | |
553 if (this.rowObject.group[n] == row) { | |
554 return null; | |
555 } | |
556 } | |
557 } | |
558 // Check that swapping with this row is allowed. | |
559 if (!this.rowObject.isValidSwap(row)) { | |
560 return null; | |
561 } | |
562 | |
563 // We may have found the row the mouse just passed over, but it doesn't | |
564 // take into account hidden rows. Skip backwards until we find a draggable | |
565 // row. | |
566 while ($(row).is(':hidden') && $(row).prev('tr').is(':hidden')) { | |
567 row = $(row).prev('tr').get(0); | |
568 } | |
569 return row; | |
570 } | |
571 } | |
572 return null; | |
573 }; | |
574 | |
575 /** | |
576 * After the row is dropped, update the table fields according to the settings | |
577 * set for this table. | |
578 * | |
579 * @param changedRow | |
580 * DOM object for the row that was just dropped. | |
581 */ | |
582 Drupal.tableDrag.prototype.updateFields = function(changedRow) { | |
583 for (var group in this.tableSettings) { | |
584 // Each group may have a different setting for relationship, so we find | |
585 // the source rows for each seperately. | |
586 this.updateField(changedRow, group); | |
587 } | |
588 }; | |
589 | |
590 /** | |
591 * After the row is dropped, update a single table field according to specific | |
592 * settings. | |
593 * | |
594 * @param changedRow | |
595 * DOM object for the row that was just dropped. | |
596 * @param group | |
597 * The settings group on which field updates will occur. | |
598 */ | |
599 Drupal.tableDrag.prototype.updateField = function(changedRow, group) { | |
600 var rowSettings = this.rowSettings(group, changedRow); | |
601 | |
602 // Set the row as it's own target. | |
603 if (rowSettings.relationship == 'self' || rowSettings.relationship == 'group') { | |
604 var sourceRow = changedRow; | |
605 } | |
606 // Siblings are easy, check previous and next rows. | |
607 else if (rowSettings.relationship == 'sibling') { | |
608 var previousRow = $(changedRow).prev('tr').get(0); | |
609 var nextRow = $(changedRow).next('tr').get(0); | |
610 var sourceRow = changedRow; | |
611 if ($(previousRow).is('.draggable') && $('.' + group, previousRow).length) { | |
612 if (this.indentEnabled) { | |
613 if ($('.indentations', previousRow).size() == $('.indentations', changedRow)) { | |
614 sourceRow = previousRow; | |
615 } | |
616 } | |
617 else { | |
618 sourceRow = previousRow; | |
619 } | |
620 } | |
621 else if ($(nextRow).is('.draggable') && $('.' + group, nextRow).length) { | |
622 if (this.indentEnabled) { | |
623 if ($('.indentations', nextRow).size() == $('.indentations', changedRow)) { | |
624 sourceRow = nextRow; | |
625 } | |
626 } | |
627 else { | |
628 sourceRow = nextRow; | |
629 } | |
630 } | |
631 } | |
632 // Parents, look up the tree until we find a field not in this group. | |
633 // Go up as many parents as indentations in the changed row. | |
634 else if (rowSettings.relationship == 'parent') { | |
635 var previousRow = $(changedRow).prev('tr'); | |
636 while (previousRow.length && $('.indentation', previousRow).length >= this.rowObject.indents) { | |
637 previousRow = previousRow.prev('tr'); | |
638 } | |
639 // If we found a row. | |
640 if (previousRow.length) { | |
641 sourceRow = previousRow[0]; | |
642 } | |
643 // Otherwise we went all the way to the left of the table without finding | |
644 // a parent, meaning this item has been placed at the root level. | |
645 else { | |
646 // Use the first row in the table as source, because it's garanteed to | |
647 // be at the root level. Find the first item, then compare this row | |
648 // against it as a sibling. | |
649 sourceRow = $('tr.draggable:first').get(0); | |
650 if (sourceRow == this.rowObject.element) { | |
651 sourceRow = $(this.rowObject.group[this.rowObject.group.length - 1]).next('tr.draggable').get(0); | |
652 } | |
653 var useSibling = true; | |
654 } | |
655 } | |
656 | |
657 // Because we may have moved the row from one category to another, | |
658 // take a look at our sibling and borrow its sources and targets. | |
659 this.copyDragClasses(sourceRow, changedRow, group); | |
660 rowSettings = this.rowSettings(group, changedRow); | |
661 | |
662 // In the case that we're looking for a parent, but the row is at the top | |
663 // of the tree, copy our sibling's values. | |
664 if (useSibling) { | |
665 rowSettings.relationship = 'sibling'; | |
666 rowSettings.source = rowSettings.target; | |
667 } | |
668 | |
669 var targetClass = '.' + rowSettings.target; | |
670 var targetElement = $(targetClass, changedRow).get(0); | |
671 | |
672 // Check if a target element exists in this row. | |
673 if (targetElement) { | |
674 var sourceClass = '.' + rowSettings.source; | |
675 var sourceElement = $(sourceClass, sourceRow).get(0); | |
676 switch (rowSettings.action) { | |
677 case 'depth': | |
678 // Get the depth of the target row. | |
679 targetElement.value = $('.indentation', $(sourceElement).parents('tr:first')).size(); | |
680 break; | |
681 case 'match': | |
682 // Update the value. | |
683 targetElement.value = sourceElement.value; | |
684 break; | |
685 case 'order': | |
686 var siblings = this.rowObject.findSiblings(rowSettings); | |
687 if ($(targetElement).is('select')) { | |
688 // Get a list of acceptable values. | |
689 var values = new Array(); | |
690 $('option', targetElement).each(function() { | |
691 values.push(this.value); | |
692 }); | |
693 var maxVal = values[values.length - 1]; | |
694 // Populate the values in the siblings. | |
695 $(targetClass, siblings).each(function() { | |
696 // If there are more items than possible values, assign the maximum value to the row. | |
697 if (values.length > 0) { | |
698 this.value = values.shift(); | |
699 } | |
700 else { | |
701 this.value = maxVal; | |
702 } | |
703 }); | |
704 } | |
705 else { | |
706 // Assume a numeric input field. | |
707 var weight = parseInt($(targetClass, siblings[0]).val()) || 0; | |
708 $(targetClass, siblings).each(function() { | |
709 this.value = weight; | |
710 weight++; | |
711 }); | |
712 } | |
713 break; | |
714 } | |
715 } | |
716 }; | |
717 | |
718 /** | |
719 * Copy all special tableDrag classes from one row's form elements to a | |
720 * different one, removing any special classes that the destination row | |
721 * may have had. | |
722 */ | |
723 Drupal.tableDrag.prototype.copyDragClasses = function(sourceRow, targetRow, group) { | |
724 var sourceElement = $('.' + group, sourceRow); | |
725 var targetElement = $('.' + group, targetRow); | |
726 if (sourceElement.length && targetElement.length) { | |
727 targetElement[0].className = sourceElement[0].className; | |
728 } | |
729 }; | |
730 | |
731 Drupal.tableDrag.prototype.checkScroll = function(cursorY) { | |
732 var de = document.documentElement; | |
733 var b = document.body; | |
734 | |
735 var windowHeight = this.windowHeight = window.innerHeight || (de.clientHeight && de.clientWidth != 0 ? de.clientHeight : b.offsetHeight); | |
736 var scrollY = this.scrollY = (document.all ? (!de.scrollTop ? b.scrollTop : de.scrollTop) : (window.pageYOffset ? window.pageYOffset : window.scrollY)); | |
737 var trigger = this.scrollSettings.trigger; | |
738 var delta = 0; | |
739 | |
740 // Return a scroll speed relative to the edge of the screen. | |
741 if (cursorY - scrollY > windowHeight - trigger) { | |
742 delta = trigger / (windowHeight + scrollY - cursorY); | |
743 delta = (delta > 0 && delta < trigger) ? delta : trigger; | |
744 return delta * this.scrollSettings.amount; | |
745 } | |
746 else if (cursorY - scrollY < trigger) { | |
747 delta = trigger / (cursorY - scrollY); | |
748 delta = (delta > 0 && delta < trigger) ? delta : trigger; | |
749 return -delta * this.scrollSettings.amount; | |
750 } | |
751 }; | |
752 | |
753 Drupal.tableDrag.prototype.setScroll = function(scrollAmount) { | |
754 var self = this; | |
755 | |
756 this.scrollInterval = setInterval(function() { | |
757 // Update the scroll values stored in the object. | |
758 self.checkScroll(self.currentMouseCoords.y); | |
759 var aboveTable = self.scrollY > self.table.topY; | |
760 var belowTable = self.scrollY + self.windowHeight < self.table.bottomY; | |
761 if (scrollAmount > 0 && belowTable || scrollAmount < 0 && aboveTable) { | |
762 window.scrollBy(0, scrollAmount); | |
763 } | |
764 }, this.scrollSettings.interval); | |
765 }; | |
766 | |
767 Drupal.tableDrag.prototype.restripeTable = function() { | |
768 // :even and :odd are reversed because jquery counts from 0 and | |
769 // we count from 1, so we're out of sync. | |
770 $('tr.draggable', this.table) | |
771 .filter(':odd').filter('.odd') | |
772 .removeClass('odd').addClass('even') | |
773 .end().end() | |
774 .filter(':even').filter('.even') | |
775 .removeClass('even').addClass('odd'); | |
776 }; | |
777 | |
778 /** | |
779 * Stub function. Allows a custom handler when a row begins dragging. | |
780 */ | |
781 Drupal.tableDrag.prototype.onDrag = function() { | |
782 return null; | |
783 }; | |
784 | |
785 /** | |
786 * Stub function. Allows a custom handler when a row is dropped. | |
787 */ | |
788 Drupal.tableDrag.prototype.onDrop = function() { | |
789 return null; | |
790 }; | |
791 | |
792 /** | |
793 * Constructor to make a new object to manipulate a table row. | |
794 * | |
795 * @param tableRow | |
796 * The DOM element for the table row we will be manipulating. | |
797 * @param method | |
798 * The method in which this row is being moved. Either 'keyboard' or 'mouse'. | |
799 * @param indentEnabled | |
800 * Whether the containing table uses indentations. Used for optimizations. | |
801 * @param maxDepth | |
802 * The maximum amount of indentations this row may contain. | |
803 * @param addClasses | |
804 * Whether we want to add classes to this row to indicate child relationships. | |
805 */ | |
806 Drupal.tableDrag.prototype.row = function(tableRow, method, indentEnabled, maxDepth, addClasses) { | |
807 this.element = tableRow; | |
808 this.method = method; | |
809 this.group = new Array(tableRow); | |
810 this.groupDepth = $('.indentation', tableRow).size(); | |
811 this.changed = false; | |
812 this.table = $(tableRow).parents('table:first').get(0); | |
813 this.indentEnabled = indentEnabled; | |
814 this.maxDepth = maxDepth; | |
815 this.direction = ''; // Direction the row is being moved. | |
816 | |
817 if (this.indentEnabled) { | |
818 this.indents = $('.indentation', tableRow).size(); | |
819 this.children = this.findChildren(addClasses); | |
820 this.group = $.merge(this.group, this.children); | |
821 // Find the depth of this entire group. | |
822 for (var n = 0; n < this.group.length; n++) { | |
823 this.groupDepth = Math.max($('.indentation', this.group[n]).size(), this.groupDepth); | |
824 } | |
825 } | |
826 }; | |
827 | |
828 /** | |
829 * Find all children of rowObject by indentation. | |
830 * | |
831 * @param addClasses | |
832 * Whether we want to add classes to this row to indicate child relationships. | |
833 */ | |
834 Drupal.tableDrag.prototype.row.prototype.findChildren = function(addClasses) { | |
835 var parentIndentation = this.indents; | |
836 var currentRow = $(this.element, this.table).next('tr.draggable'); | |
837 var rows = new Array(); | |
838 var child = 0; | |
839 while (currentRow.length) { | |
840 var rowIndentation = $('.indentation', currentRow).length; | |
841 // A greater indentation indicates this is a child. | |
842 if (rowIndentation > parentIndentation) { | |
843 child++; | |
844 rows.push(currentRow[0]); | |
845 if (addClasses) { | |
846 $('.indentation', currentRow).each(function(indentNum) { | |
847 if (child == 1 && (indentNum == parentIndentation)) { | |
848 $(this).addClass('tree-child-first'); | |
849 } | |
850 if (indentNum == parentIndentation) { | |
851 $(this).addClass('tree-child'); | |
852 } | |
853 else if (indentNum > parentIndentation) { | |
854 $(this).addClass('tree-child-horizontal'); | |
855 } | |
856 }); | |
857 } | |
858 } | |
859 else { | |
860 break; | |
861 } | |
862 currentRow = currentRow.next('tr.draggable'); | |
863 } | |
864 if (addClasses && rows.length) { | |
865 $('.indentation:nth-child(' + (parentIndentation + 1) + ')', rows[rows.length - 1]).addClass('tree-child-last'); | |
866 } | |
867 return rows; | |
868 }; | |
869 | |
870 /** | |
871 * Ensure that two rows are allowed to be swapped. | |
872 * | |
873 * @param row | |
874 * DOM object for the row being considered for swapping. | |
875 */ | |
876 Drupal.tableDrag.prototype.row.prototype.isValidSwap = function(row) { | |
877 if (this.indentEnabled) { | |
878 var prevRow, nextRow; | |
879 if (this.direction == 'down') { | |
880 prevRow = row; | |
881 nextRow = $(row).next('tr').get(0); | |
882 } | |
883 else { | |
884 prevRow = $(row).prev('tr').get(0); | |
885 nextRow = row; | |
886 } | |
887 this.interval = this.validIndentInterval(prevRow, nextRow); | |
888 | |
889 // We have an invalid swap if the valid indentations interval is empty. | |
890 if (this.interval.min > this.interval.max) { | |
891 return false; | |
892 } | |
893 } | |
894 | |
895 // Do not let an un-draggable first row have anything put before it. | |
896 if (this.table.tBodies[0].rows[0] == row && $(row).is(':not(.draggable)')) { | |
897 return false; | |
898 } | |
899 | |
900 return true; | |
901 }; | |
902 | |
903 /** | |
904 * Perform the swap between two rows. | |
905 * | |
906 * @param position | |
907 * Whether the swap will occur 'before' or 'after' the given row. | |
908 * @param row | |
909 * DOM element what will be swapped with the row group. | |
910 */ | |
911 Drupal.tableDrag.prototype.row.prototype.swap = function(position, row) { | |
912 $(row)[position](this.group); | |
913 this.changed = true; | |
914 this.onSwap(row); | |
915 }; | |
916 | |
917 /** | |
918 * Determine the valid indentations interval for the row at a given position | |
919 * in the table. | |
920 * | |
921 * @param prevRow | |
922 * DOM object for the row before the tested position | |
923 * (or null for first position in the table). | |
924 * @param nextRow | |
925 * DOM object for the row after the tested position | |
926 * (or null for last position in the table). | |
927 */ | |
928 Drupal.tableDrag.prototype.row.prototype.validIndentInterval = function (prevRow, nextRow) { | |
929 var minIndent, maxIndent; | |
930 | |
931 // Minimum indentation: | |
932 // Do not orphan the next row. | |
933 minIndent = nextRow ? $('.indentation', nextRow).size() : 0; | |
934 | |
935 // Maximum indentation: | |
936 if (!prevRow || $(this.element).is('.tabledrag-root')) { | |
937 // Do not indent the first row in the table or 'root' rows.. | |
938 maxIndent = 0; | |
939 } | |
940 else { | |
941 // Do not go deeper than as a child of the previous row. | |
942 maxIndent = $('.indentation', prevRow).size() + ($(prevRow).is('.tabledrag-leaf') ? 0 : 1); | |
943 // Limit by the maximum allowed depth for the table. | |
944 if (this.maxDepth) { | |
945 maxIndent = Math.min(maxIndent, this.maxDepth - (this.groupDepth - this.indents)); | |
946 } | |
947 } | |
948 | |
949 return {'min':minIndent, 'max':maxIndent}; | |
950 } | |
951 | |
952 /** | |
953 * Indent a row within the legal bounds of the table. | |
954 * | |
955 * @param indentDiff | |
956 * The number of additional indentations proposed for the row (can be | |
957 * positive or negative). This number will be adjusted to nearest valid | |
958 * indentation level for the row. | |
959 */ | |
960 Drupal.tableDrag.prototype.row.prototype.indent = function(indentDiff) { | |
961 // Determine the valid indentations interval if not available yet. | |
962 if (!this.interval) { | |
963 prevRow = $(this.element).prev('tr').get(0); | |
964 nextRow = $(this.group).filter(':last').next('tr').get(0); | |
965 this.interval = this.validIndentInterval(prevRow, nextRow); | |
966 } | |
967 | |
968 // Adjust to the nearest valid indentation. | |
969 var indent = this.indents + indentDiff; | |
970 indent = Math.max(indent, this.interval.min); | |
971 indent = Math.min(indent, this.interval.max); | |
972 indentDiff = indent - this.indents; | |
973 | |
974 for (var n = 1; n <= Math.abs(indentDiff); n++) { | |
975 // Add or remove indentations. | |
976 if (indentDiff < 0) { | |
977 $('.indentation:first', this.group).remove(); | |
978 this.indents--; | |
979 } | |
980 else { | |
981 $('td:first', this.group).prepend(Drupal.theme('tableDragIndentation')); | |
982 this.indents++; | |
983 } | |
984 } | |
985 if (indentDiff) { | |
986 // Update indentation for this row. | |
987 this.changed = true; | |
988 this.groupDepth += indentDiff; | |
989 this.onIndent(); | |
990 } | |
991 | |
992 return indentDiff; | |
993 }; | |
994 | |
995 /** | |
996 * Find all siblings for a row, either according to its subgroup or indentation. | |
997 * Note that the passed in row is included in the list of siblings. | |
998 * | |
999 * @param settings | |
1000 * The field settings we're using to identify what constitutes a sibling. | |
1001 */ | |
1002 Drupal.tableDrag.prototype.row.prototype.findSiblings = function(rowSettings) { | |
1003 var siblings = new Array(); | |
1004 var directions = new Array('prev', 'next'); | |
1005 var rowIndentation = this.indents; | |
1006 for (var d in directions) { | |
1007 var checkRow = $(this.element)[directions[d]](); | |
1008 while (checkRow.length) { | |
1009 // Check that the sibling contains a similar target field. | |
1010 if ($('.' + rowSettings.target, checkRow)) { | |
1011 // Either add immediately if this is a flat table, or check to ensure | |
1012 // that this row has the same level of indentaiton. | |
1013 if (this.indentEnabled) { | |
1014 var checkRowIndentation = $('.indentation', checkRow).length | |
1015 } | |
1016 | |
1017 if (!(this.indentEnabled) || (checkRowIndentation == rowIndentation)) { | |
1018 siblings.push(checkRow[0]); | |
1019 } | |
1020 else if (checkRowIndentation < rowIndentation) { | |
1021 // No need to keep looking for siblings when we get to a parent. | |
1022 break; | |
1023 } | |
1024 } | |
1025 else { | |
1026 break; | |
1027 } | |
1028 checkRow = $(checkRow)[directions[d]](); | |
1029 } | |
1030 // Since siblings are added in reverse order for previous, reverse the | |
1031 // completed list of previous siblings. Add the current row and continue. | |
1032 if (directions[d] == 'prev') { | |
1033 siblings.reverse(); | |
1034 siblings.push(this.element); | |
1035 } | |
1036 } | |
1037 return siblings; | |
1038 }; | |
1039 | |
1040 /** | |
1041 * Remove indentation helper classes from the current row group. | |
1042 */ | |
1043 Drupal.tableDrag.prototype.row.prototype.removeIndentClasses = function() { | |
1044 for (n in this.children) { | |
1045 $('.indentation', this.children[n]) | |
1046 .removeClass('tree-child') | |
1047 .removeClass('tree-child-first') | |
1048 .removeClass('tree-child-last') | |
1049 .removeClass('tree-child-horizontal'); | |
1050 } | |
1051 }; | |
1052 | |
1053 /** | |
1054 * Add an asterisk or other marker to the changed row. | |
1055 */ | |
1056 Drupal.tableDrag.prototype.row.prototype.markChanged = function() { | |
1057 var marker = Drupal.theme('tableDragChangedMarker'); | |
1058 var cell = $('td:first', this.element); | |
1059 if ($('span.tabledrag-changed', cell).length == 0) { | |
1060 cell.append(marker); | |
1061 } | |
1062 }; | |
1063 | |
1064 /** | |
1065 * Stub function. Allows a custom handler when a row is indented. | |
1066 */ | |
1067 Drupal.tableDrag.prototype.row.prototype.onIndent = function() { | |
1068 return null; | |
1069 }; | |
1070 | |
1071 /** | |
1072 * Stub function. Allows a custom handler when a row is swapped. | |
1073 */ | |
1074 Drupal.tableDrag.prototype.row.prototype.onSwap = function(swappedRow) { | |
1075 return null; | |
1076 }; | |
1077 | |
1078 Drupal.theme.prototype.tableDragChangedMarker = function () { | |
1079 return '<span class="warning tabledrag-changed">*</span>'; | |
1080 }; | |
1081 | |
1082 Drupal.theme.prototype.tableDragIndentation = function () { | |
1083 return '<div class="indentation"> </div>'; | |
1084 }; | |
1085 | |
1086 Drupal.theme.prototype.tableDragChangedWarning = function () { | |
1087 return '<div class="warning">' + Drupal.theme('tableDragChangedMarker') + ' ' + Drupal.t("Changes made in this table will not be saved until the form is submitted.") + '</div>'; | |
1088 }; |