jquery.treeview.sortable.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. /*
  2. * jQuery UI Sortable
  3. *
  4. * Copyright (c) 2008 Paul Bakaus
  5. * Dual licensed under the MIT (MIT-LICENSE.txt)
  6. * and GPL (GPL-LICENSE.txt) licenses.
  7. *
  8. * http://docs.jquery.com/UI/Sortables
  9. *
  10. * Depends:
  11. * ui.base.js
  12. *
  13. * Revision: $Id: ui.sortable.js 5262 2008-04-17 13:13:51Z paul.bakaus $
  14. */
  15. ;(function($) {
  16. if (window.Node && Node.prototype && !Node.prototype.contains) {
  17. Node.prototype.contains = function (arg) {
  18. return !!(this.compareDocumentPosition(arg) & 16);
  19. };
  20. }
  21. $.widget("ui.sortableTree", $.extend($.ui.mouse, {
  22. init: function() {
  23. //Initialize needed constants
  24. var self = this, o = this.options;
  25. this.containerCache = {};
  26. this.element.addClass("ui-sortableTree");
  27. //Get the items
  28. this.refresh();
  29. //Let's determine the parent's offset
  30. if(!(/(relative|absolute|fixed)/).test(this.element.css('position'))) this.element.css('position', 'relative');
  31. this.offset = this.element.offset();
  32. //Initialize mouse events for interaction
  33. this.mouseInit();
  34. //Prepare cursorAt
  35. if(o.cursorAt && o.cursorAt.constructor == Array)
  36. o.cursorAt = { left: o.cursorAt[0], top: o.cursorAt[1] };
  37. },
  38. plugins: {},
  39. ui: function(inst) {
  40. return {
  41. helper: (inst || this)["helper"],
  42. position: (inst || this)["position"].current,
  43. absolutePosition: (inst || this)["position"].absolute,
  44. instance: this,
  45. options: this.options,
  46. element: this.element,
  47. item: (inst || this)["currentItem"],
  48. sender: inst ? inst.element : null
  49. };
  50. },
  51. propagate: function(n,e,inst) {
  52. $.ui.plugin.call(this, n, [e, this.ui(inst)]);
  53. this.element.triggerHandler(n == "sort" ? n : "sort"+n, [e, this.ui(inst)], this.options[n]);
  54. },
  55. serialize: function(o) {
  56. var items = $(this.options.items, this.element).not('.ui-sortableTree-helper'); //Only the items of the sortable itself
  57. var str = []; o = o || {};
  58. items.each(function() {
  59. var res = ($(this).attr(o.attribute || 'id') || '').match(o.expression || (/(.+)[-=_](.+)/));
  60. if(res) str.push((o.key || res[1])+'[]='+(o.key ? res[1] : res[2]));
  61. });
  62. return str.join('&');
  63. },
  64. toArray: function(attr) {
  65. var items = $(this.options.items, this.element).not('.ui-sortableTree-helper'); //Only the items of the sortable itself
  66. var ret = [];
  67. items.each(function() { ret.push($(this).attr(attr || 'id')); });
  68. return ret;
  69. },
  70. enable: function() {
  71. this.element.removeClass("ui-sortableTree-disabled");
  72. this.options.disabled = false;
  73. },
  74. disable: function() {
  75. this.element.addClass("ui-sortableTree-disabled");
  76. this.options.disabled = true;
  77. },
  78. /* Be careful with the following core functions */
  79. intersectsWith: function(item) {
  80. var x1 = this.position.absolute.left - 10, x2 = x1 + 10,
  81. y1 = this.position.absolute.top - 10, y2 = y1 + 10;
  82. var l = item.left, r = l + item.width,
  83. t = item.top, b = t + item.height;
  84. return ( l < x1 + (this.helperProportions.width / 2) // Right Half
  85. && x2 - (this.helperProportions.width / 2) < r // Left Half
  86. && t < y1 + (this.helperProportions.height / 2) // Bottom Half
  87. && y2 - (this.helperProportions.height / 2) < b ); // Top Half
  88. },
  89. intersectsWithEdge: function(item) {
  90. var y1 = this.position.absolute.top - 10, y2 = y1 + 10;
  91. var t = item.top, b = t + item.height;
  92. if(!this.intersectsWith(item.item.parents(".ui-sortableTree").data("sortableTree").containerCache)) return false;
  93. if (!( t < y1 + (this.helperProportions.height / 2) // Bottom Half
  94. && y2 - (this.helperProportions.height / 2) < b )) return false; // Top Half
  95. if(y2 > t && y1 < t) return 1; //Crosses top edge
  96. if(y1 < b && y2 > b) return 2; //Crosses bottom edge
  97. return false;
  98. },
  99. refresh: function() {
  100. this.refreshItems();
  101. this.refreshPositions();
  102. },
  103. refreshItems: function() {
  104. this.items = [];
  105. this.containers = [this];
  106. var items = this.items;
  107. var queries = [$(this.options.items, this.element)];
  108. if(this.options.connectWith) {
  109. for (var i = this.options.connectWith.length - 1; i >= 0; i--){
  110. var cur = $(this.options.connectWith[i]);
  111. for (var j = cur.length - 1; j >= 0; j--){
  112. var inst = $.data(cur[j], 'sortableTree');
  113. if(inst && !inst.options.disabled) {
  114. queries.push($(inst.options.items, inst.element));
  115. this.containers.push(inst);
  116. }
  117. };
  118. };
  119. }
  120. for (var i = queries.length - 1; i >= 0; i--){
  121. queries[i].each(function() {
  122. $.data(this, 'sortableTree-item', true); // Data for target checking (mouse manager)
  123. items.push({
  124. item: $(this),
  125. width: 0, height: 0,
  126. left: 0, top: 0
  127. });
  128. });
  129. };
  130. },
  131. refreshPositions: function(fast) {
  132. for (var i = this.items.length - 1; i >= 0; i--){
  133. if(!fast) this.items[i].height = this.items[i].item.outerHeight();
  134. this.items[i].top = this.items[i].item.offset().top;
  135. };
  136. for (var i = this.containers.length - 1; i >= 0; i--){
  137. var p =this.containers[i].element.offset();
  138. this.containers[i].containerCache.left = p.left;
  139. this.containers[i].containerCache.top = p.top;
  140. this.containers[i].containerCache.width = this.containers[i].element.outerWidth();
  141. this.containers[i].containerCache.height= this.containers[i].element.outerHeight();
  142. };
  143. },
  144. destroy: function() {
  145. this.element
  146. .removeClass("ui-sortableTree ui-sortableTree-disabled")
  147. .removeData("sortableTree")
  148. .unbind(".sortableTree");
  149. this.mouseDestroy();
  150. for ( var i = this.items.length - 1; i >= 0; i-- )
  151. this.items[i].item.removeData("sortableTree-item");
  152. },
  153. contactContainers: function(e) {
  154. for (var i = this.containers.length - 1; i >= 0; i--){
  155. if(this.intersectsWith(this.containers[i].containerCache)) {
  156. if(!this.containers[i].containerCache.over) {
  157. if(this.currentContainer != this.containers[i]) {
  158. //When entering a new container, we will find the item with the least distance and append our item near it
  159. var dist = 10000; var itemWithLeastDistance = null; var base = this.position.absolute.top;
  160. for (var j = this.items.length - 1; j >= 0; j--) {
  161. if(!this.containers[i].element[0].contains(this.items[j].item[0])) continue;
  162. var cur = this.items[j].top;
  163. if(Math.abs(cur - base) < dist) {
  164. dist = Math.abs(cur - base); itemWithLeastDistance = this.items[j];
  165. }
  166. }
  167. itemWithLeastDistance ? this.rearrange(e, itemWithLeastDistance) : this.rearrange(e, null, this.containers[i].element);
  168. this.propagate("change", e); //Call plugins and callbacks
  169. this.containers[i].propagate("change", e, this); //Call plugins and callbacks
  170. this.currentContainer = this.containers[i];
  171. }
  172. this.containers[i].propagate("over", e, this);
  173. this.containers[i].containerCache.over = 1;
  174. }
  175. } else {
  176. if(this.containers[i].containerCache.over) {
  177. this.containers[i].propagate("out", e, this);
  178. this.containers[i].containerCache.over = 0;
  179. }
  180. }
  181. };
  182. },
  183. mouseStart: function(e,el) {
  184. if(this.options.disabled || this.options.type == 'static') return false;
  185. //Find out if the clicked node (or one of its parents) is a actual item in this.items
  186. var currentItem = null, nodes = $(e.target).parents().each(function() {
  187. if($.data(this, 'sortableTree-item')) {
  188. currentItem = $(this);
  189. return false;
  190. }
  191. });
  192. if($.data(e.target, 'sortableTree-item')) currentItem = $(e.target);
  193. if(!currentItem) return false;
  194. if(this.options.handle) {
  195. var validHandle = false;
  196. $(this.options.handle, currentItem).each(function() { if(this == e.target) validHandle = true; });
  197. if(!validHandle) return false;
  198. }
  199. this.currentItem = currentItem;
  200. var o = this.options;
  201. this.currentContainer = this;
  202. this.refresh();
  203. //Create and append the visible helper
  204. this.helper = typeof o.helper == 'function' ? $(o.helper.apply(this.element[0], [e, this.currentItem])) : this.currentItem.clone();
  205. if(!this.helper.parents('body').length) this.helper.appendTo("body"); //Add the helper to the DOM if that didn't happen already
  206. this.helper.css({ position: 'absolute', clear: 'both' }).addClass('ui-sortableTree-helper'); //Position it absolutely and add a helper class
  207. //Prepare variables for position generation
  208. $.extend(this, {
  209. offsetParent: this.helper.offsetParent(),
  210. offsets: { absolute: this.currentItem.offset() }
  211. });
  212. //Save the first time position
  213. $.extend(this, {
  214. position: {
  215. current: { left: e.pageX, top: e.pageY },
  216. absolute: { left: e.pageX, top: e.pageY },
  217. dom: this.currentItem.prev()[0]
  218. },
  219. clickOffset: { left: -5, top: -5 }
  220. });
  221. this.propagate("start", e); //Call plugins and callbacks
  222. this.helperProportions = { width: this.helper.outerWidth(), height: this.helper.outerHeight() }; //Save and store the helper proportions
  223. for (var i = this.containers.length - 1; i >= 0; i--) {
  224. this.containers[i].propagate("activate", e, this);
  225. } //Post 'activate' events to possible containers
  226. //Prepare possible droppables
  227. if($.ui.ddmanager) $.ui.ddmanager.current = this;
  228. if ($.ui.ddmanager && !o.dropBehaviour) $.ui.ddmanager.prepareOffsets(this, e);
  229. this.dragging = true;
  230. return true;
  231. },
  232. mouseStop: function(e) {
  233. if(this.newPositionAt) this.options.sortIndication.remove.call(this.currentItem, this.newPositionAt); //remove sort indicator
  234. this.propagate("stop", e); //Call plugins and trigger callbacks
  235. //If we are using droppables, inform the manager about the drop
  236. var dropped = ($.ui.ddmanager && !this.options.dropBehaviour) ? $.ui.ddmanager.drop(this, e) : false;
  237. if(!dropped && this.newPositionAt) this.newPositionAt[this.direction == 'down' ? 'before' : 'after'](this.currentItem); //Append to element to its new position
  238. if(this.position.dom != this.currentItem.prev()[0]) this.propagate("update", e); //Trigger update callback if the DOM position has changed
  239. if(!this.element[0].contains(this.currentItem[0])) { //Node was moved out of the current element
  240. this.propagate("remove", e);
  241. for (var i = this.containers.length - 1; i >= 0; i--){
  242. if(this.containers[i].element[0].contains(this.currentItem[0])) {
  243. this.containers[i].propagate("update", e, this);
  244. this.containers[i].propagate("receive", e, this);
  245. }
  246. };
  247. };
  248. //Post events to containers
  249. for (var i = this.containers.length - 1; i >= 0; i--){
  250. this.containers[i].propagate("deactivate", e, this);
  251. if(this.containers[i].containerCache.over) {
  252. this.containers[i].propagate("out", e, this);
  253. this.containers[i].containerCache.over = 0;
  254. }
  255. }
  256. this.dragging = false;
  257. if(this.cancelHelperRemoval) return false;
  258. this.helper.remove();
  259. return false;
  260. },
  261. mouseDrag: function(e) {
  262. //Compute the helpers position
  263. this.position.current = { top: e.pageY + 5, left: e.pageX + 5 };
  264. this.position.absolute = { left: e.pageX + 5, top: e.pageY + 5 };
  265. //Interconnect with droppables
  266. if($.ui.ddmanager) $.ui.ddmanager.drag(this, e);
  267. var intersectsWithDroppable = false;
  268. $.each($.ui.ddmanager.droppables, function() {
  269. if(this.isover) intersectsWithDroppable = true;
  270. });
  271. //Rearrange
  272. if(intersectsWithDroppable) {
  273. if(this.newPositionAt) this.options.sortIndication.remove.call(this.currentItem, this.newPositionAt);
  274. } else {
  275. for (var i = this.items.length - 1; i >= 0; i--) {
  276. if(this.currentItem[0].contains(this.items[i].item[0])) continue;
  277. var intersection = this.intersectsWithEdge(this.items[i]);
  278. if(!intersection) continue;
  279. this.direction = intersection == 1 ? "down" : "up";
  280. this.rearrange(e, this.items[i]);
  281. this.propagate("change", e); //Call plugins and callbacks
  282. break;
  283. }
  284. }
  285. //Post events to containers
  286. this.contactContainers(e);
  287. this.propagate("sort", e); //Call plugins and callbacks
  288. this.helper.css({ left: this.position.current.left+'px', top: this.position.current.top+'px' }); // Stick the helper to the cursor
  289. return false;
  290. },
  291. rearrange: function(e, i, a) {
  292. if(i) {
  293. if(this.newPositionAt) this.options.sortIndication.remove.call(this.currentItem, this.newPositionAt);
  294. this.newPositionAt = i.item;
  295. this.options.sortIndication[this.direction].call(this.currentItem, this.newPositionAt);
  296. } else {
  297. //Append
  298. }
  299. }
  300. }));
  301. $.extend($.ui.sortableTree, {
  302. defaults: {
  303. items: '> *',
  304. zIndex: 1000,
  305. distance: 1
  306. },
  307. getter: "serialize toArray"
  308. });
  309. })(jQuery);