No Description

draggable-admin.js 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. /* global django */
  2. // IE<9 lacks Array.prototype.indexOf
  3. if (!Array.prototype.indexOf) {
  4. Array.prototype.indexOf = function(needle) {
  5. for (var i=0, l=this.length; i<l; ++i) {
  6. if (this[i] === needle) return i;
  7. }
  8. return -1;
  9. };
  10. }
  11. // https://github.com/jquery/jquery-ui/blob/master/ui/disable-selection.js
  12. django.jQuery.fn.extend({
  13. disableSelection: (function() {
  14. var eventType = 'onselectstart' in document.createElement('div') ? 'selectstart' : 'mousedown';
  15. return function() {
  16. return this.on(eventType + '.ui-disableSelection', function(event) {
  17. event.preventDefault();
  18. });
  19. };
  20. })(),
  21. enableSelection: function() {
  22. return this.off('.ui-disableSelection');
  23. }
  24. });
  25. django.jQuery(function($){
  26. // We are not on a changelist it seems.
  27. if (!document.getElementById('result_list')) return;
  28. var DraggableMPTTAdmin = null;
  29. function isExpandedNode(id) {
  30. return DraggableMPTTAdmin.collapsedNodes.indexOf(id) == -1;
  31. }
  32. function markNodeAsExpanded(id) {
  33. // remove itemId from array of collapsed nodes
  34. var idx = DraggableMPTTAdmin.collapsedNodes.indexOf(id);
  35. if(idx >= 0)
  36. DraggableMPTTAdmin.collapsedNodes.splice(idx, 1);
  37. }
  38. function markNodeAsCollapsed(id) {
  39. if(isExpandedNode(id))
  40. DraggableMPTTAdmin.collapsedNodes.push(id);
  41. }
  42. function treeNode(pk) {
  43. return $('.tree-node[data-pk="' + pk + '"]');
  44. }
  45. // toggle children
  46. function doToggle(id, show) {
  47. var children = DraggableMPTTAdmin.treeStructure[id] || [];
  48. for (var i=0; i<children.length; ++i) {
  49. var childId = children[i];
  50. if(show) {
  51. treeNode(childId).closest('tr').show();
  52. // only reveal children if current node is not collapsed
  53. if(isExpandedNode(childId)) {
  54. doToggle(childId, show);
  55. }
  56. } else {
  57. treeNode(childId).closest('tr').hide();
  58. // always recursively hide children
  59. doToggle(childId, show);
  60. }
  61. }
  62. }
  63. function rowLevel($row) {
  64. try {
  65. return $row.find('.tree-node').data('level') || 0;
  66. } catch (e) {
  67. return 0;
  68. }
  69. }
  70. /*
  71. * FeinCMS Drag-n-drop tree reordering.
  72. * Based upon code by bright4 for Radiant CMS, rewritten for
  73. * FeinCMS by Bjorn Post.
  74. *
  75. * September 2010
  76. */
  77. $.extend($.fn.feinTree = function() {
  78. $.each(DraggableMPTTAdmin.treeStructure, function(key, value) {
  79. treeNode(key).addClass('children');
  80. });
  81. $('div.drag-handle').bind('mousedown', function(event) {
  82. var BEFORE = 'before';
  83. var AFTER = 'after';
  84. var CHILD = 'child';
  85. var CHILD_PAD = DraggableMPTTAdmin.levelIndent;
  86. var originalRow = $(event.target).closest('tr');
  87. var rowHeight = originalRow.height();
  88. var moveTo = new Object();
  89. var resultListWidth = $('#result_list').width();
  90. $('body').addClass('dragging').disableSelection().bind('mousemove', function(event) {
  91. // Remove focus
  92. originalRow.blur();
  93. // attach dragged item to mouse
  94. var cloned = originalRow.html();
  95. if($('#ghost').length == 0) {
  96. $('<div id="ghost"></div>').appendTo('body');
  97. }
  98. $('#ghost').html(cloned).css({
  99. 'opacity': .8,
  100. 'position': 'absolute',
  101. 'top': event.pageY,
  102. 'left': event.pageX - 30,
  103. 'width': 600
  104. });
  105. // check on edge of screen
  106. if(event.pageY+100 > $(window).height()+$(window).scrollTop()) {
  107. $('html,body').stop().animate({scrollTop: $(window).scrollTop()+250 }, 500);
  108. } else if(event.pageY-50 < $(window).scrollTop()) {
  109. $('html,body').stop().animate({scrollTop: $(window).scrollTop()-250 }, 500);
  110. }
  111. // check if drag-line element already exists, else append
  112. if($('#drag-line').length < 1) {
  113. $('body').append('<div id="drag-line"><span></span></div>');
  114. }
  115. // loop trough all rows
  116. $('tr', originalRow.parent()).each(function(index, el) {
  117. var element = $(el),
  118. top = element.offset().top,
  119. next;
  120. // check if mouse is over a row
  121. if (event.pageY >= top && event.pageY < top + rowHeight) {
  122. var targetRow = null,
  123. targetLoc = null,
  124. elementLevel = rowLevel(element);
  125. if (event.pageY >= top && event.pageY < top + rowHeight / 3) {
  126. targetRow = element;
  127. targetLoc = BEFORE;
  128. } else if (event.pageY >= top + rowHeight / 3 && event.pageY < top + rowHeight * 2 / 3) {
  129. next = element.next();
  130. // there's no point in allowing adding children when there are some already
  131. // better move the items to the correct place right away
  132. if (!next.length || rowLevel(next) <= elementLevel) {
  133. targetRow = element;
  134. targetLoc = CHILD;
  135. }
  136. } else if (event.pageY >= top + rowHeight * 2 / 3 && event.pageY < top + rowHeight) {
  137. next = element.next();
  138. if (!next.length || rowLevel(next) <= elementLevel) {
  139. targetRow = element;
  140. targetLoc = AFTER;
  141. }
  142. }
  143. if(targetRow) {
  144. // Positioning relative to cell containing the link
  145. var offset = targetRow.find('th').offset();
  146. var left = offset.left
  147. + rowLevel(targetRow) * CHILD_PAD
  148. + (targetLoc == CHILD ? CHILD_PAD : 0)
  149. + 5; // Center of the circle aligns with start of link text (cell padding!)
  150. $('#drag-line').css({
  151. 'width': resultListWidth - left,
  152. 'left': left,
  153. 'top': offset.top + (targetLoc == BEFORE ? 0 : rowHeight)
  154. }).find('span').text(DraggableMPTTAdmin.messages[targetLoc] || '');
  155. // Store the found row and options
  156. moveTo.hovering = element;
  157. moveTo.relativeTo = targetRow;
  158. moveTo.side = targetLoc;
  159. return true;
  160. }
  161. }
  162. });
  163. });
  164. $('body').keydown(function(event) {
  165. if (event.which == '27') {
  166. $('#drag-line').remove();
  167. $('#ghost').remove();
  168. $('body').removeClass('dragging').enableSelection().unbind('mousemove').unbind('mouseup');
  169. event.preventDefault();
  170. }
  171. });
  172. $('body').bind('mouseup', function() {
  173. if(moveTo.relativeTo) {
  174. var cutItem = originalRow.find('.tree-node').data('pk');
  175. var pastedOn = moveTo.relativeTo.find('.tree-node').data('pk');
  176. // get out early if items are the same
  177. if(cutItem != pastedOn) {
  178. var isParent = (
  179. rowLevel(moveTo.relativeTo.next()) >
  180. rowLevel(moveTo.relativeTo));
  181. var position = '';
  182. // determine position
  183. if(moveTo.side == CHILD && !isParent) {
  184. position = 'last-child';
  185. } else if (moveTo.side == BEFORE) {
  186. position = 'left';
  187. } else {
  188. position = 'right';
  189. }
  190. $.ajax({
  191. complete: function() {
  192. window.location.reload();
  193. },
  194. data: {
  195. cmd: 'move_node',
  196. position: position,
  197. cut_item: cutItem,
  198. pasted_on: pastedOn
  199. },
  200. headers: {
  201. 'X-CSRFToken': $('input[type=hidden][name=csrfmiddlewaretoken]').val()
  202. },
  203. method: 'POST'
  204. });
  205. } else {
  206. $('#drag-line').remove();
  207. $('#ghost').remove();
  208. }
  209. $('body').removeClass('dragging').enableSelection().unbind('mousemove').unbind('mouseup');
  210. }
  211. });
  212. });
  213. return this;
  214. });
  215. /* Every time the user expands or collapses a part of the tree, we remember
  216. the current state of the tree so we can restore it on a reload. */
  217. function storeCollapsedNodes(nodes) {
  218. window.localStorage && window.localStorage.setItem(
  219. DraggableMPTTAdmin.storageName,
  220. JSON.stringify(nodes)
  221. );
  222. }
  223. function retrieveCollapsedNodes() {
  224. try {
  225. return JSON.parse(window.localStorage.getItem(
  226. DraggableMPTTAdmin.storageName
  227. ));
  228. } catch(e) {
  229. return null;
  230. }
  231. }
  232. function expandOrCollapseNode(item) {
  233. var show = true;
  234. if (!item.hasClass('children'))
  235. return;
  236. var itemId = item.data('pk');
  237. if (!isExpandedNode(itemId)) {
  238. item.removeClass('closed');
  239. markNodeAsExpanded(itemId);
  240. } else {
  241. item.addClass('closed');
  242. show = false;
  243. markNodeAsCollapsed(itemId);
  244. }
  245. storeCollapsedNodes(DraggableMPTTAdmin.collapsedNodes);
  246. doToggle(itemId, show);
  247. }
  248. function collapseTree() {
  249. var rlist = $("#result_list");
  250. rlist.hide();
  251. $('tbody tr', rlist).each(function(i, el) {
  252. var marker = $('.tree-node', el);
  253. if (marker.hasClass('children')) {
  254. var itemId = marker.data('pk');
  255. doToggle(itemId, false);
  256. marker.addClass('closed');
  257. markNodeAsCollapsed(itemId);
  258. }
  259. });
  260. storeCollapsedNodes(DraggableMPTTAdmin.collapsedNodes);
  261. rlist.show();
  262. return false;
  263. }
  264. function expandTree() {
  265. var rlist = $("#result_list");
  266. rlist.hide();
  267. $('tbody tr', rlist).each(function(i, el) {
  268. var marker = $('.tree-node', el);
  269. if (marker.hasClass('children')) {
  270. var itemId = $('.tree-node', el).data('pk');
  271. doToggle(itemId, true);
  272. marker.removeClass('closed');
  273. markNodeAsExpanded(itemId);
  274. }
  275. });
  276. storeCollapsedNodes([]);
  277. rlist.show();
  278. return false;
  279. }
  280. var changelistTab = function(elem, event, direction) {
  281. event.preventDefault();
  282. elem = $(elem);
  283. var ne = (direction > 0) ? elem.nextAll(':visible:first') : elem.prevAll(':visible:first');
  284. if(ne) {
  285. elem.attr('tabindex', -1);
  286. ne.attr('tabindex', '0');
  287. ne.focus();
  288. }
  289. };
  290. function keyboardNavigationHandler(event) {
  291. // On form element? Ignore.
  292. if (/textarea|select|input/i.test(event.target.nodeName))
  293. return;
  294. // console.log('keydown', this, event.keyCode);
  295. switch (event.keyCode) {
  296. case 40: // down
  297. changelistTab(this, event, 1);
  298. break;
  299. case 38: // up
  300. changelistTab(this, event, -1);
  301. break;
  302. case 37: // left
  303. case 39: // right
  304. expandOrCollapseNode($(this).find('.tree-node'));
  305. break;
  306. case 13: // return
  307. document.location = $('a', this).attr('href');
  308. break;
  309. default:
  310. break;
  311. }
  312. }
  313. function addObjectTool(title, handler) {
  314. var $a = $('<a href/>');
  315. $a.click(handler);
  316. $a.text(title);
  317. $a.prependTo('.object-tools').wrap('<li>');
  318. }
  319. // Some old browsers do not support JSON.parse (the only thing we require)
  320. var jsonParse = JSON.parse || function jsonParse(sJSON) { return eval('(' + sJSON + ')'); };
  321. DraggableMPTTAdmin = jsonParse(
  322. document.getElementById('draggable-admin-context').getAttribute('data-context'));
  323. addObjectTool(DraggableMPTTAdmin.messages.collapseTree, collapseTree);
  324. addObjectTool(DraggableMPTTAdmin.messages.expandTree, expandTree);
  325. // fire!
  326. var rlist = $("#result_list"),
  327. rlist_tbody = rlist.find('tbody');
  328. if ($('tbody tr', rlist).length > 1) {
  329. rlist_tbody.feinTree();
  330. rlist.find('.tree-node').on('click', function(event) {
  331. event.preventDefault();
  332. event.stopPropagation();
  333. expandOrCollapseNode($(this));
  334. });
  335. /* Enable focussing, put focus on first result, add handler for keyboard navigation */
  336. $('tr', rlist).attr('tabindex', -1);
  337. $('tbody tr:first', rlist).attr('tabindex', 0).focus();
  338. $('tr', rlist).keydown(keyboardNavigationHandler);
  339. DraggableMPTTAdmin.collapsedNodes = [];
  340. var storedNodes = retrieveCollapsedNodes();
  341. if (storedNodes) {
  342. for(var i=0; i<storedNodes.length; i++) {
  343. expandOrCollapseNode(treeNode(storedNodes[i]));
  344. }
  345. } else {
  346. if (!DraggableMPTTAdmin.expandTreeByDefault) {
  347. collapseTree();
  348. }
  349. }
  350. }
  351. });