UNPKG

kityminder

Version:
384 lines (323 loc) 12.8 kB
var GM = KityMinder.Geometry; // 矩形的变形动画定义 var MoveToParentCommand = kity.createClass('MoveToParentCommand', { base: Command, execute: function(minder, nodes, parent) { var node; for (var i = nodes.length - 1; i >= 0; i--) { node = nodes[i]; if (node.parent) { node.parent.removeChild(node); parent.appendChild(node); node.render(); } } parent.expand(); minder.select(nodes, true); } }); var DropHinter = kity.createClass('DropHinter', { base: kity.Group, constructor: function() { this.callBase(); this.rect = new kity.Rect(); this.addShape(this.rect); }, render: function(target) { this.setVisible(!!target); if (target) { this.rect .setBox(target.getLayoutBox()) .setRadius(target.getStyle('radius') || 0) .stroke( target.getStyle('drop-hint-color') || 'yellow', target.getStyle('drop-hint-width') || 2 ); this.bringTop(); } } }); var OrderHinter = kity.createClass('OrderHinter', { base: kity.Group, constructor: function() { this.callBase(); this.area = new kity.Rect(); this.path = new kity.Path(); this.addShapes([this.area, this.path]); }, render: function(hint) { this.setVisible(!!hint); if (hint) { this.area.setBox(hint.area); this.area.fill(hint.node.getStyle('order-hint-area-color') || 'rgba(0, 255, 0, .5)'); this.path.setPathData(hint.path); this.path.stroke( hint.node.getStyle('order-hint-path-color') || '#0f0', hint.node.getStyle('order-hint-path-width') || 1); } } }); // 对拖动对象的一个替代盒子,控制整个拖放的逻辑,包括: // 1. 从节点列表计算出拖动部分 // 2. 计算可以 drop 的节点,产生 drop 交互提示 var TreeDragger = kity.createClass('TreeDragger', { constructor: function(minder) { this._minder = minder; this._dropHinter = new DropHinter(); this._orderHinter = new OrderHinter(); minder.getRenderContainer().addShapes([this._dropHinter, this._orderHinter]); }, dragStart: function(position) { // 只记录开始位置,不马上开启拖放模式 // 这个位置同时是拖放范围收缩时的焦点位置(中心) this._startPosition = position; }, dragMove: function(position) { // 启动拖放模式需要最小的移动距离 var DRAG_MOVE_THRESHOLD = 10; if (!this._startPosition) return; var movement = kity.Vector.fromPoints(this._dragPosition || this._startPosition, position); var minder = this._minder; this._dragPosition = position; if (!this._dragMode) { // 判断拖放模式是否该启动 if (GM.getDistance(this._dragPosition, this._startPosition) < DRAG_MOVE_THRESHOLD) { return; } if (!this._enterDragMode()) { return; } } for (var i = 0; i < this._dragSources.length; i++) { this._dragSources[i].setLayoutOffset(this._dragSources[i].getLayoutOffset().offset(movement)); minder.applyLayoutResult(this._dragSources[i]); } if (!this._dropTest()) { this._orderTest(); } else { this._renderOrderHint(this._orderSucceedHint = null); } }, dragEnd: function() { this._startPosition = null; this._dragPosition = null; if (!this._dragMode) { return; } this._fadeDragSources(1); if (this._dropSucceedTarget) { this._dragSources.forEach(function(source) { source.setLayoutOffset(null); }); this._minder.layout(-1); this._minder.execCommand('movetoparent', this._dragSources, this._dropSucceedTarget); } else if (this._orderSucceedHint) { var hint = this._orderSucceedHint; var index = hint.node.getIndex(); var sourceIndexes = this._dragSources.map(function(source) { // 顺便干掉布局偏移 source.setLayoutOffset(null); return source.getIndex(); }); var maxIndex = Math.max.apply(Math, sourceIndexes); var minIndex = Math.min.apply(Math, sourceIndexes); if (index < minIndex && hint.type == 'down') index++; if (index > maxIndex && hint.type == 'up') index--; hint.node.setLayoutOffset(null); this._minder.execCommand('arrange', this._dragSources, index); this._renderOrderHint(null); } else { this._minder.fire('savescene'); } this._minder.layout(300); this._leaveDragMode(); this._minder.fire('contentchange'); }, // 进入拖放模式: // 1. 计算拖放源和允许的拖放目标 // 2. 标记已启动 _enterDragMode: function() { this._calcDragSources(); if (!this._dragSources.length) { this._startPosition = null; return false; } this._fadeDragSources(0.5); this._calcDropTargets(); this._calcOrderHints(); this._dragMode = true; this._minder.setStatus('dragtree'); return true; }, // 从选中的节点计算拖放源 // 并不是所有选中的节点都作为拖放源,如果选中节点中存在 A 和 B, // 并且 A 是 B 的祖先,则 B 不作为拖放源 // // 计算过程: // 1. 将节点按照树高排序,排序后只可能是前面节点是后面节点的祖先 // 2. 从后往前枚举排序的结果,如果发现枚举目标之前存在其祖先, // 则排除枚举目标作为拖放源,否则加入拖放源 _calcDragSources: function() { this._dragSources = this._minder.getSelectedAncestors(); }, _fadeDragSources: function(opacity) { var minder = this._minder; this._dragSources.forEach(function(source) { source.getRenderContainer().setOpacity(opacity, 200); source.traverse(function(node) { if (opacity < 1) { minder.detachNode(node); } else { minder.attachNode(node); } }, true); }); }, // 计算拖放目标可以释放的节点列表(释放意味着成为其子树),存在这条限制规则: // - 不能拖放到拖放目标的子树上(允许拖放到自身,因为多选的情况下可以把其它节点加入) // // 1. 加入当前节点(初始为根节点)到允许列表 // 2. 对于当前节点的每一个子节点: // (1) 如果是拖放目标的其中一个节点,忽略(整棵子树被剪枝) // (2) 如果不是拖放目标之一,以当前子节点为当前节点,回到 1 计算 // 3. 返回允许列表 // _calcDropTargets: function() { function findAvailableParents(nodes, root) { var availables = [], i; availables.push(root); root.getChildren().forEach(function(test) { for (i = 0; i < nodes.length; i++) { if (nodes[i] == test) return; } availables = availables.concat(findAvailableParents(nodes, test)); }); return availables; } this._dropTargets = findAvailableParents(this._dragSources, this._minder.getRoot()); this._dropTargetBoxes = this._dropTargets.map(function(source) { return source.getLayoutBox(); }); }, _calcOrderHints: function() { var sources = this._dragSources; var ancestor = MinderNode.getCommonAncestor(sources); // 只有一个元素选中,公共祖先是其父 if (ancestor == sources[0]) ancestor = sources[0].parent; if (sources.length === 0 || ancestor != sources[0].parent) { this._orderHints = []; return; } var siblings = ancestor.children; this._orderHints = siblings.reduce(function(hint, sibling) { if (sources.indexOf(sibling) == -1) { hint = hint.concat(sibling.getOrderHint()); } return hint; }, []); }, _leaveDragMode: function() { this._dragMode = false; this._dropSucceedTarget = null; this._orderSucceedHint = null; this._renderDropHint(null); this._renderOrderHint(null); this._minder.rollbackStatus(); }, _drawForDragMode: function() { this._text.setContent(this._dragSources.length + ' items'); this._text.setPosition(this._startPosition.x, this._startPosition.y + 5); this._minder.getRenderContainer().addShape(this); }, _boxTest: function(targets, targetBoxMapper, judge) { var sourceBoxes = this._dragSources.map(function(source) { return source.getLayoutBox(); }); var i, j, target, sourceBox, targetBox; judge = judge || function(intersectBox, sourceBox, targetBox) { return intersectBox; }; for (i = 0; i < targets.length; i++) { target = targets[i]; targetBox = targetBoxMapper.call(this, target, i); for (j = 0; j < sourceBoxes.length; j++) { sourceBox = sourceBoxes[j]; var intersectBox = GM.getIntersectBox(sourceBox, targetBox); if (judge(intersectBox, sourceBox, targetBox)) { return target; } } } return null; }, _dropTest: function() { this._dropSucceedTarget = this._boxTest(this._dropTargets, function(target, i) { return this._dropTargetBoxes[i]; }, function(intersectBox, sourceBox, targetBox) { function area(box) { return box.width * box.height; } if (!intersectBox) return false; // 面积判断 if (area(intersectBox) > 0.5 * Math.min(area(sourceBox), area(targetBox))) return true; if (intersectBox.width + 1 >= Math.min(sourceBox.width, targetBox.width)) return true; if (intersectBox.height + 1 >= Math.min(sourceBox.height, targetBox.height)) return true; return false; }); this._renderDropHint(this._dropSucceedTarget); return !!this._dropSucceedTarget; }, _orderTest: function() { this._orderSucceedHint = this._boxTest(this._orderHints, function(hint) { return hint.area; }); this._renderOrderHint(this._orderSucceedHint); return !!this._orderSucceedHint; }, _renderDropHint: function(target) { this._dropHinter.render(target); }, _renderOrderHint: function(hint) { this._orderHinter.render(hint); }, preventDragMove: function() { this._startPosition = null; } }); KityMinder.registerModule('DragTree', function() { var dragger; return { init: function() { dragger = new TreeDragger(this); window.addEventListener('mouseup', function() { dragger.dragEnd(); }); }, events: { 'normal.mousedown inputready.mousedown': function(e) { // 单选中根节点也不触发拖拽 if (e.originEvent.button) return; if (e.getTargetNode() && e.getTargetNode() != this.getRoot()) { dragger.dragStart(e.getPosition(this.getRenderContainer())); } }, 'normal.mousemove dragtree.mousemove': function(e) { dragger.dragMove(e.getPosition(this.getRenderContainer())); }, 'normal.mouseup dragtree.beforemouseup': function(e) { dragger.dragEnd(); //e.stopPropagation(); e.preventDefault(); }, 'statuschange': function(e) { if (e.lastStatus == 'textedit' && e.currentStatus == 'normal') { dragger.preventDragMove(); } } }, commands: { 'movetoparent': MoveToParentCommand } }; });