Updated the Bootstrap theme.
[yaffs-website] / web / themes / contrib / bootstrap / js / misc / tabledrag.js
1 /**
2  * @file
3  * Extends methods from core/misc/tabledrag.js.
4  */
5 (function ($) {
6
7   // Save the original prototype.
8   var prototype = Drupal.tableDrag.prototype;
9
10   /**
11    * Provides table and field manipulation.
12    *
13    * @constructor
14    *
15    * @param {HTMLElement} table
16    *   DOM object for the table to be made draggable.
17    * @param {object} tableSettings
18    *   Settings for the table added via drupal_add_dragtable().
19    */
20   Drupal.tableDrag = function (table, tableSettings) {
21     var self = this;
22     var $table = $(table);
23
24     /**
25      * @type {jQuery}
26      */
27     this.$table = $(table);
28
29     /**
30      *
31      * @type {HTMLElement}
32      */
33     this.table = table;
34
35     /**
36      * @type {object}
37      */
38     this.tableSettings = tableSettings;
39
40     /**
41      * Used to hold information about a current drag operation.
42      *
43      * @type {?HTMLElement}
44      */
45     this.dragObject = null;
46
47     /**
48      * Provides operations for row manipulation.
49      *
50      * @type {?HTMLElement}
51      */
52     this.rowObject = null;
53
54     /**
55      * Remember the previous element.
56      *
57      * @type {?HTMLElement}
58      */
59     this.oldRowElement = null;
60
61     /**
62      * Used to determine up or down direction from last mouse move.
63      *
64      * @type {number}
65      */
66     this.oldY = 0;
67
68     /**
69      * Whether anything in the entire table has changed.
70      *
71      * @type {bool}
72      */
73     this.changed = false;
74
75     /**
76      * Maximum amount of allowed parenting.
77      *
78      * @type {number}
79      */
80     this.maxDepth = 0;
81
82     /**
83      * Direction of the table.
84      *
85      * @type {number}
86      */
87     this.rtl = $(this.table).css('direction') === 'rtl' ? -1 : 1;
88
89     /**
90      *
91      * @type {bool}
92      */
93     this.striping = $(this.table).data('striping') === 1;
94
95     /**
96      * Configure the scroll settings.
97      *
98      * @type {object}
99      *
100      * @prop {number} amount
101      * @prop {number} interval
102      * @prop {number} trigger
103      */
104     this.scrollSettings = {amount: 4, interval: 50, trigger: 70};
105
106     /**
107      *
108      * @type {?number}
109      */
110     this.scrollInterval = null;
111
112     /**
113      *
114      * @type {number}
115      */
116     this.scrollY = 0;
117
118     /**
119      *
120      * @type {number}
121      */
122     this.windowHeight = 0;
123
124     /**
125      * Check this table's settings to see if there are parent relationships in
126      * this table. For efficiency, large sections of code can be skipped if we
127      * don't need to track horizontal movement and indentations.
128      *
129      * @type {bool}
130      */
131     this.indentEnabled = false;
132     for (var group in tableSettings) {
133       if (tableSettings.hasOwnProperty(group)) {
134         for (var n in tableSettings[group]) {
135           if (tableSettings[group].hasOwnProperty(n)) {
136             if (tableSettings[group][n].relationship === 'parent') {
137               this.indentEnabled = true;
138             }
139             if (tableSettings[group][n].limit > 0) {
140               this.maxDepth = tableSettings[group][n].limit;
141             }
142           }
143         }
144       }
145     }
146     if (this.indentEnabled) {
147
148       /**
149        * Total width of indents, set in makeDraggable.
150        *
151        * @type {number}
152        */
153       this.indentCount = 1;
154       // Find the width of indentations to measure mouse movements against.
155       // Because the table doesn't need to start with any indentations, we
156       // manually append 2 indentations in the first draggable row, measure
157       // the offset, then remove.
158       var indent = Drupal.theme('tableDragIndentation');
159       var testRow = $('<tr/>').addClass('draggable').appendTo(table);
160       var testCell = $('<td/>').appendTo(testRow).prepend(indent).prepend(indent);
161       var $indentation = testCell.find('.js-indentation');
162
163       /**
164        *
165        * @type {number}
166        */
167       this.indentAmount = $indentation.get(1).offsetLeft - $indentation.get(0).offsetLeft;
168       testRow.remove();
169     }
170
171     // Make each applicable row draggable.
172     // Match immediate children of the parent element to allow nesting.
173     $table.find('> tr.draggable, > tbody > tr.draggable').each(function () { self.makeDraggable(this); });
174
175     // Add a link before the table for users to show or hide weight columns.
176     var $button = $(Drupal.theme('btn-sm', {
177       'class': ['tabledrag-toggle-weight'],
178       title: Drupal.t('Re-order rows by numerical weight instead of dragging.'),
179       'data-toggle': 'tooltip'
180     }));
181
182     $button
183       .on('click', $.proxy(function (e) {
184         e.preventDefault();
185         this.toggleColumns();
186       }, this))
187       .wrap('<div class="tabledrag-toggle-weight-wrapper"></div>')
188       .parent()
189     ;
190     $table.before($button);
191
192     // Initialize the specified columns (for example, weight or parent columns)
193     // to show or hide according to user preference. This aids accessibility
194     // so that, e.g., screen reader users can choose to enter weight values and
195     // manipulate form elements directly, rather than using drag-and-drop..
196     self.initColumns();
197
198     // Add event bindings to the document. The self variable is passed along
199     // as event handlers do not have direct access to the tableDrag object.
200     $(document).on('touchmove', function (event) { return self.dragRow(event.originalEvent.touches[0], self); });
201     $(document).on('touchend', function (event) { return self.dropRow(event.originalEvent.touches[0], self); });
202     $(document).on('mousemove pointermove', function (event) { return self.dragRow(event, self); });
203     $(document).on('mouseup pointerup', function (event) { return self.dropRow(event, self); });
204
205     // React to localStorage event showing or hiding weight columns.
206     $(window).on('storage', $.proxy(function (e) {
207       // Only react to 'Drupal.tableDrag.showWeight' value change.
208       if (e.originalEvent.key === 'Drupal.tableDrag.showWeight') {
209         // This was changed in another window, get the new value for this
210         // window.
211         showWeight = JSON.parse(e.originalEvent.newValue);
212         this.displayColumns(showWeight);
213       }
214     }, this));
215   };
216
217   // Restore the original prototype.
218   Drupal.tableDrag.prototype = prototype;
219
220   /**
221    * Take an item and add event handlers to make it become draggable.
222    *
223    * @param {HTMLElement} item
224    */
225   Drupal.tableDrag.prototype.makeDraggable = function (item) {
226     var self = this;
227     var $item = $(item);
228
229     // Add a class to the title link
230     $item.find('td:first-of-type').find('a').addClass('menu-item__link');
231
232     // Create the handle.
233     var handle = $('<a href="#" class="tabledrag-handle"/>');
234
235     // Insert the handle after indentations (if any).
236     var $indentationLast = $item.find('td:first-of-type').find('.js-indentation').eq(-1);
237     if ($indentationLast.length) {
238       $indentationLast.after(handle);
239       // Update the total width of indentation in this entire table.
240       self.indentCount = Math.max($item.find('.js-indentation').length, self.indentCount);
241     }
242     else {
243       $item.find('td').eq(0).prepend(handle);
244     }
245
246     // Add the glyphicon to the handle.
247     handle
248       .attr('title', Drupal.t('Drag to re-order'))
249       .attr('data-toggle', 'tooltip')
250       .append(Drupal.theme('bootstrapIcon', 'move'))
251     ;
252
253     handle.on('mousedown touchstart pointerdown', function (event) {
254       event.preventDefault();
255       if (event.originalEvent.type === 'touchstart') {
256         event = event.originalEvent.touches[0];
257       }
258       self.dragStart(event, self, item);
259     });
260
261     // Prevent the anchor tag from jumping us to the top of the page.
262     handle.on('click', function (e) {
263       e.preventDefault();
264     });
265
266     // Set blur cleanup when a handle is focused.
267     handle.on('focus', function () {
268       self.safeBlur = true;
269     });
270
271     // On blur, fire the same function as a touchend/mouseup. This is used to
272     // update values after a row has been moved through the keyboard support.
273     handle.on('blur', function (event) {
274       if (self.rowObject && self.safeBlur) {
275         self.dropRow(event, self);
276       }
277     });
278
279     // Add arrow-key support to the handle.
280     handle.on('keydown', function (event) {
281       // If a rowObject doesn't yet exist and this isn't the tab key.
282       if (event.keyCode !== 9 && !self.rowObject) {
283         self.rowObject = new self.row(item, 'keyboard', self.indentEnabled, self.maxDepth, true);
284       }
285
286       var keyChange = false;
287       var groupHeight;
288       switch (event.keyCode) {
289         // Left arrow.
290         case 37:
291         // Safari left arrow.
292         case 63234:
293           keyChange = true;
294           self.rowObject.indent(-1 * self.rtl);
295           break;
296
297         // Up arrow.
298         case 38:
299         // Safari up arrow.
300         case 63232:
301           var $previousRow = $(self.rowObject.element).prev('tr:first-of-type');
302           var previousRow = $previousRow.get(0);
303           while (previousRow && $previousRow.is(':hidden')) {
304             $previousRow = $(previousRow).prev('tr:first-of-type');
305             previousRow = $previousRow.get(0);
306           }
307           if (previousRow) {
308             // Do not allow the onBlur cleanup.
309             self.safeBlur = false;
310             self.rowObject.direction = 'up';
311             keyChange = true;
312
313             if ($(item).is('.tabledrag-root')) {
314               // Swap with the previous top-level row.
315               groupHeight = 0;
316               while (previousRow && $previousRow.find('.js-indentation').length) {
317                 $previousRow = $(previousRow).prev('tr:first-of-type');
318                 previousRow = $previousRow.get(0);
319                 groupHeight += $previousRow.is(':hidden') ? 0 : previousRow.offsetHeight;
320               }
321               if (previousRow) {
322                 self.rowObject.swap('before', previousRow);
323                 // No need to check for indentation, 0 is the only valid one.
324                 window.scrollBy(0, -groupHeight);
325               }
326             }
327             else if (self.table.tBodies[0].rows[0] !== previousRow || $previousRow.is('.draggable')) {
328               // Swap with the previous row (unless previous row is the first
329               // one and undraggable).
330               self.rowObject.swap('before', previousRow);
331               self.rowObject.interval = null;
332               self.rowObject.indent(0);
333               window.scrollBy(0, -parseInt(item.offsetHeight, 10));
334             }
335             // Regain focus after the DOM manipulation.
336             handle.trigger('focus');
337           }
338           break;
339
340         // Right arrow.
341         case 39:
342         // Safari right arrow.
343         case 63235:
344           keyChange = true;
345           self.rowObject.indent(self.rtl);
346           break;
347
348         // Down arrow.
349         case 40:
350         // Safari down arrow.
351         case 63233:
352           var $nextRow = $(self.rowObject.group).eq(-1).next('tr:first-of-type');
353           var nextRow = $nextRow.get(0);
354           while (nextRow && $nextRow.is(':hidden')) {
355             $nextRow = $(nextRow).next('tr:first-of-type');
356             nextRow = $nextRow.get(0);
357           }
358           if (nextRow) {
359             // Do not allow the onBlur cleanup.
360             self.safeBlur = false;
361             self.rowObject.direction = 'down';
362             keyChange = true;
363
364             if ($(item).is('.tabledrag-root')) {
365               // Swap with the next group (necessarily a top-level one).
366               groupHeight = 0;
367               var nextGroup = new self.row(nextRow, 'keyboard', self.indentEnabled, self.maxDepth, false);
368               if (nextGroup) {
369                 $(nextGroup.group).each(function () {
370                   groupHeight += $(this).is(':hidden') ? 0 : this.offsetHeight;
371                 });
372                 var nextGroupRow = $(nextGroup.group).eq(-1).get(0);
373                 self.rowObject.swap('after', nextGroupRow);
374                 // No need to check for indentation, 0 is the only valid one.
375                 window.scrollBy(0, parseInt(groupHeight, 10));
376               }
377             }
378             else {
379               // Swap with the next row.
380               self.rowObject.swap('after', nextRow);
381               self.rowObject.interval = null;
382               self.rowObject.indent(0);
383               window.scrollBy(0, parseInt(item.offsetHeight, 10));
384             }
385             // Regain focus after the DOM manipulation.
386             handle.trigger('focus');
387           }
388           break;
389       }
390
391       if (self.rowObject && self.rowObject.changed === true) {
392         $(item).addClass('drag');
393         if (self.oldRowElement) {
394           $(self.oldRowElement).removeClass('drag-previous');
395         }
396         self.oldRowElement = item;
397         if (self.striping === true) {
398           self.restripeTable();
399         }
400         self.onDrag();
401       }
402
403       // Returning false if we have an arrow key to prevent scrolling.
404       if (keyChange) {
405         return false;
406       }
407     });
408
409     // Compatibility addition, return false on keypress to prevent unwanted
410     // scrolling. IE and Safari will suppress scrolling on keydown, but all
411     // other browsers need to return false on keypress.
412     // http://www.quirksmode.org/js/keys.html
413     handle.on('keypress', function (event) {
414       switch (event.keyCode) {
415         // Left arrow.
416         case 37:
417         // Up arrow.
418         case 38:
419         // Right arrow.
420         case 39:
421         // Down arrow.
422         case 40:
423           return false;
424       }
425     });
426   };
427
428   /**
429    * Add an asterisk or other marker to the changed row.
430    */
431   Drupal.tableDrag.prototype.row.prototype.markChanged = function () {
432     var $cell = $('td:first', this.element);
433     // Find the first appropriate place to insert the marker.
434     var $target = $($cell.find('.file-size').get(0) || $cell.find('.file').get(0) || $cell.find('.tabledrag-handle').get(0));
435     if (!$cell.find('.tabledrag-changed').length) {
436       $target.after(' ' + Drupal.theme('tableDragChangedMarker') + ' ');
437     }
438   };
439
440   $.extend(Drupal.theme, /** @lends Drupal.theme */{
441
442     /**
443      * @return {string}
444      */
445     tableDragChangedMarker: function () {
446       return Drupal.theme('bootstrapIcon', 'warning-sign', {'class': ['tabledrag-changed', 'text-warning']});
447     },
448
449     /**
450      * @return {string}
451      */
452     tableDragChangedWarning: function () {
453       return '<div class="tabledrag-changed-warning alert alert-sm alert-warning messages warning">' + Drupal.theme('tableDragChangedMarker') + ' ' + Drupal.t('You have unsaved changes.') + '</div>';
454     }
455   });
456
457 })(jQuery);