dragtree.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. define(function(require, exports, module) {
  2. var kity = require('../core/kity');
  3. var utils = require('../core/utils');
  4. var MinderNode = require('../core/node');
  5. var Command = require('../core/command');
  6. var Module = require('../core/module');
  7. // 矩形的变形动画定义
  8. var MoveToParentCommand = kity.createClass('MoveToParentCommand', {
  9. base: Command,
  10. execute: function(minder, nodes, parent) {
  11. var node;
  12. for (var i = 0; i < nodes.length; i++) {
  13. node = nodes[i];
  14. if (node.parent) {
  15. node.parent.removeChild(node);
  16. parent.appendChild(node);
  17. node.render();
  18. }
  19. }
  20. parent.expand();
  21. minder.select(nodes, true);
  22. }
  23. });
  24. var DropHinter = kity.createClass('DropHinter', {
  25. base: kity.Group,
  26. constructor: function() {
  27. this.callBase();
  28. this.rect = new kity.Rect();
  29. this.addShape(this.rect);
  30. },
  31. render: function(target) {
  32. this.setVisible(!!target);
  33. if (target) {
  34. this.rect
  35. .setBox(target.getLayoutBox())
  36. .setRadius(target.getStyle('radius') || 0)
  37. .stroke(
  38. target.getStyle('drop-hint-color') || 'yellow',
  39. target.getStyle('drop-hint-width') || 2
  40. );
  41. this.bringTop();
  42. }
  43. }
  44. });
  45. var OrderHinter = kity.createClass('OrderHinter', {
  46. base: kity.Group,
  47. constructor: function() {
  48. this.callBase();
  49. this.area = new kity.Rect();
  50. this.path = new kity.Path();
  51. this.addShapes([this.area, this.path]);
  52. },
  53. render: function(hint) {
  54. this.setVisible(!!hint);
  55. if (hint) {
  56. this.area.setBox(hint.area);
  57. this.area.fill(hint.node.getStyle('order-hint-area-color') || 'rgba(0, 255, 0, .5)');
  58. this.path.setPathData(hint.path);
  59. this.path.stroke(
  60. hint.node.getStyle('order-hint-path-color') || '#0f0',
  61. hint.node.getStyle('order-hint-path-width') || 1);
  62. }
  63. }
  64. });
  65. // 对拖动对象的一个替代盒子,控制整个拖放的逻辑,包括:
  66. // 1. 从节点列表计算出拖动部分
  67. // 2. 计算可以 drop 的节点,产生 drop 交互提示
  68. var TreeDragger = kity.createClass('TreeDragger', {
  69. constructor: function(minder) {
  70. this._minder = minder;
  71. this._dropHinter = new DropHinter();
  72. this._orderHinter = new OrderHinter();
  73. minder.getRenderContainer().addShapes([this._dropHinter, this._orderHinter]);
  74. },
  75. dragStart: function(position) {
  76. // 只记录开始位置,不马上开启拖放模式
  77. // 这个位置同时是拖放范围收缩时的焦点位置(中心)
  78. this._startPosition = position;
  79. },
  80. dragMove: function(position) {
  81. // 启动拖放模式需要最小的移动距离
  82. var DRAG_MOVE_THRESHOLD = 10;
  83. if (!this._startPosition) return;
  84. var movement = kity.Vector.fromPoints(this._dragPosition || this._startPosition, position);
  85. var minder = this._minder;
  86. this._dragPosition = position;
  87. if (!this._dragMode) {
  88. // 判断拖放模式是否该启动
  89. if (kity.Vector.fromPoints(this._dragPosition, this._startPosition).length() < DRAG_MOVE_THRESHOLD) {
  90. return;
  91. }
  92. if (!this._enterDragMode()) {
  93. return;
  94. }
  95. }
  96. for (var i = 0; i < this._dragSources.length; i++) {
  97. this._dragSources[i].setLayoutOffset(this._dragSources[i].getLayoutOffset().offset(movement));
  98. minder.applyLayoutResult(this._dragSources[i]);
  99. }
  100. if (!this._dropTest()) {
  101. this._orderTest();
  102. } else {
  103. this._renderOrderHint(this._orderSucceedHint = null);
  104. }
  105. },
  106. dragEnd: function() {
  107. this._startPosition = null;
  108. this._dragPosition = null;
  109. if (!this._dragMode) {
  110. return;
  111. }
  112. this._fadeDragSources(1);
  113. if (this._dropSucceedTarget) {
  114. this._dragSources.forEach(function(source) {
  115. source.setLayoutOffset(null);
  116. });
  117. this._minder.layout(-1);
  118. this._minder.execCommand('movetoparent', this._dragSources, this._dropSucceedTarget);
  119. } else if (this._orderSucceedHint) {
  120. var hint = this._orderSucceedHint;
  121. var index = hint.node.getIndex();
  122. var sourceIndexes = this._dragSources.map(function(source) {
  123. // 顺便干掉布局偏移
  124. source.setLayoutOffset(null);
  125. return source.getIndex();
  126. });
  127. var maxIndex = Math.max.apply(Math, sourceIndexes);
  128. var minIndex = Math.min.apply(Math, sourceIndexes);
  129. if (index < minIndex && hint.type == 'down') index++;
  130. if (index > maxIndex && hint.type == 'up') index--;
  131. hint.node.setLayoutOffset(null);
  132. this._minder.execCommand('arrange', index);
  133. this._renderOrderHint(null);
  134. } else {
  135. this._minder.fire('savescene');
  136. }
  137. this._minder.layout(300);
  138. this._leaveDragMode();
  139. this._minder.fire('contentchange');
  140. },
  141. // 进入拖放模式:
  142. // 1. 计算拖放源和允许的拖放目标
  143. // 2. 标记已启动
  144. _enterDragMode: function() {
  145. this._calcDragSources();
  146. if (!this._dragSources.length) {
  147. this._startPosition = null;
  148. return false;
  149. }
  150. this._fadeDragSources(0.5);
  151. this._calcDropTargets();
  152. this._calcOrderHints();
  153. this._dragMode = true;
  154. this._minder.setStatus('dragtree');
  155. return true;
  156. },
  157. // 从选中的节点计算拖放源
  158. // 并不是所有选中的节点都作为拖放源,如果选中节点中存在 A 和 B,
  159. // 并且 A 是 B 的祖先,则 B 不作为拖放源
  160. //
  161. // 计算过程:
  162. // 1. 将节点按照树高排序,排序后只可能是前面节点是后面节点的祖先
  163. // 2. 从后往前枚举排序的结果,如果发现枚举目标之前存在其祖先,
  164. // 则排除枚举目标作为拖放源,否则加入拖放源
  165. _calcDragSources: function() {
  166. this._dragSources = this._minder.getSelectedAncestors();
  167. },
  168. _fadeDragSources: function(opacity) {
  169. var minder = this._minder;
  170. this._dragSources.forEach(function(source) {
  171. source.getRenderContainer().setOpacity(opacity, 200);
  172. source.traverse(function(node) {
  173. if (opacity < 1) {
  174. minder.detachNode(node);
  175. } else {
  176. minder.attachNode(node);
  177. }
  178. }, true);
  179. });
  180. },
  181. // 计算拖放目标可以释放的节点列表(释放意味着成为其子树),存在这条限制规则:
  182. // - 不能拖放到拖放目标的子树上(允许拖放到自身,因为多选的情况下可以把其它节点加入)
  183. //
  184. // 1. 加入当前节点(初始为根节点)到允许列表
  185. // 2. 对于当前节点的每一个子节点:
  186. // (1) 如果是拖放目标的其中一个节点,忽略(整棵子树被剪枝)
  187. // (2) 如果不是拖放目标之一,以当前子节点为当前节点,回到 1 计算
  188. // 3. 返回允许列表
  189. //
  190. _calcDropTargets: function() {
  191. function findAvailableParents(nodes, root) {
  192. var availables = [],
  193. i;
  194. availables.push(root);
  195. root.getChildren().forEach(function(test) {
  196. for (i = 0; i < nodes.length; i++) {
  197. if (nodes[i] == test) return;
  198. }
  199. availables = availables.concat(findAvailableParents(nodes, test));
  200. });
  201. return availables;
  202. }
  203. this._dropTargets = findAvailableParents(this._dragSources, this._minder.getRoot());
  204. this._dropTargetBoxes = this._dropTargets.map(function(source) {
  205. return source.getLayoutBox();
  206. });
  207. },
  208. _calcOrderHints: function() {
  209. var sources = this._dragSources;
  210. var ancestor = MinderNode.getCommonAncestor(sources);
  211. // 只有一个元素选中,公共祖先是其父
  212. if (ancestor == sources[0]) ancestor = sources[0].parent;
  213. if (sources.length === 0 || ancestor != sources[0].parent) {
  214. this._orderHints = [];
  215. return;
  216. }
  217. var siblings = ancestor.children;
  218. this._orderHints = siblings.reduce(function(hint, sibling) {
  219. if (sources.indexOf(sibling) == -1) {
  220. hint = hint.concat(sibling.getOrderHint());
  221. }
  222. return hint;
  223. }, []);
  224. },
  225. _leaveDragMode: function() {
  226. this._dragMode = false;
  227. this._dropSucceedTarget = null;
  228. this._orderSucceedHint = null;
  229. this._renderDropHint(null);
  230. this._renderOrderHint(null);
  231. this._minder.rollbackStatus();
  232. },
  233. _drawForDragMode: function() {
  234. this._text.setContent(this._dragSources.length + ' items');
  235. this._text.setPosition(this._startPosition.x, this._startPosition.y + 5);
  236. this._minder.getRenderContainer().addShape(this);
  237. },
  238. /**
  239. * 通过 judge 函数判断 targetBox 和 sourceBox 的位置交叉关系
  240. * @param targets -- 目标节点
  241. * @param targetBoxMapper -- 目标节点与对应 Box 的映射关系
  242. * @param judge -- 判断函数
  243. * @returns {*}
  244. * @private
  245. */
  246. _boxTest: function(targets, targetBoxMapper, judge) {
  247. var sourceBoxes = this._dragSources.map(function(source) {
  248. return source.getLayoutBox();
  249. });
  250. var i, j, target, sourceBox, targetBox;
  251. judge = judge || function(intersectBox, sourceBox, targetBox) {
  252. return intersectBox && !intersectBox.isEmpty();
  253. };
  254. for (i = 0; i < targets.length; i++) {
  255. target = targets[i];
  256. targetBox = targetBoxMapper.call(this, target, i);
  257. for (j = 0; j < sourceBoxes.length; j++) {
  258. sourceBox = sourceBoxes[j];
  259. var intersectBox = sourceBox.intersect(targetBox);
  260. if (judge(intersectBox, sourceBox, targetBox)) {
  261. return target;
  262. }
  263. }
  264. }
  265. return null;
  266. },
  267. _dropTest: function() {
  268. this._dropSucceedTarget = this._boxTest(this._dropTargets, function(target, i) {
  269. return this._dropTargetBoxes[i];
  270. }, function(intersectBox, sourceBox, targetBox) {
  271. function area(box) {
  272. return box.width * box.height;
  273. }
  274. if (!intersectBox) return false;
  275. /*
  276. * Added by zhangbobell, 2015.9.8
  277. *
  278. * 增加了下面一行判断,修复了循环比较中 targetBox 为折叠节点时,intersetBox 面积为 0,
  279. * 而 targetBox 的 width 和 height 均为 0
  280. * 此时造成了满足以下的第二个条件而返回 true
  281. * */
  282. if (!area(intersectBox)) return false;
  283. // 面积判断,交叉面积大于其中的一半
  284. if (area(intersectBox) > 0.5 * Math.min(area(sourceBox), area(targetBox))) return true;
  285. // 有一个边完全重合的情况,也认为两个是交叉的
  286. if (intersectBox.width + 1 >= Math.min(sourceBox.width, targetBox.width)) return true;
  287. if (intersectBox.height + 1 >= Math.min(sourceBox.height, targetBox.height)) return true;
  288. return false;
  289. });
  290. this._renderDropHint(this._dropSucceedTarget);
  291. return !!this._dropSucceedTarget;
  292. },
  293. _orderTest: function() {
  294. this._orderSucceedHint = this._boxTest(this._orderHints, function(hint) {
  295. return hint.area;
  296. });
  297. this._renderOrderHint(this._orderSucceedHint);
  298. return !!this._orderSucceedHint;
  299. },
  300. _renderDropHint: function(target) {
  301. this._dropHinter.render(target);
  302. },
  303. _renderOrderHint: function(hint) {
  304. this._orderHinter.render(hint);
  305. },
  306. preventDragMove: function() {
  307. this._startPosition = null;
  308. }
  309. });
  310. Module.register('DragTree', function() {
  311. var dragger;
  312. return {
  313. init: function() {
  314. dragger = new TreeDragger(this);
  315. window.addEventListener('mouseup', function() {
  316. dragger.dragEnd();
  317. });
  318. },
  319. events: {
  320. 'normal.mousedown inputready.mousedown': function(e) {
  321. // 单选中根节点也不触发拖拽
  322. if (e.originEvent.button) return;
  323. if (e.getTargetNode() && e.getTargetNode() != this.getRoot()) {
  324. dragger.dragStart(e.getPosition());
  325. }
  326. },
  327. 'normal.mousemove dragtree.mousemove': function(e) {
  328. dragger.dragMove(e.getPosition());
  329. },
  330. 'normal.mouseup dragtree.beforemouseup': function(e) {
  331. dragger.dragEnd();
  332. //e.stopPropagation();
  333. e.preventDefault();
  334. },
  335. 'statuschange': function(e) {
  336. if (e.lastStatus == 'textedit' && e.currentStatus == 'normal') {
  337. dragger.preventDragMove();
  338. }
  339. }
  340. },
  341. commands: {
  342. 'movetoparent': MoveToParentCommand
  343. }
  344. };
  345. });
  346. });