From 7ba6e427354ee8e6fa3ae59d0df21d0746757da3 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2020 18:23:08 +0800 Subject: [PATCH 001/243] chore(deps-dev): bump rc-trigger from 4.4.3 to 5.0.7 (#383) Bumps [rc-trigger](https://github.com/react-component/trigger) from 4.4.3 to 5.0.7. - [Release notes](https://github.com/react-component/trigger/releases) - [Changelog](https://github.com/react-component/trigger/blob/master/HISTORY.md) - [Commits](https://github.com/react-component/trigger/compare/v4.4.3...v5.0.7) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c2cb2e6c..8b24bd92 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "np": "^6.0.0", "rc-dialog": "^8.1.0", "rc-tooltip": "4.x", - "rc-trigger": "^4.0.0", + "rc-trigger": "^5.0.7", "react": "^16.8.0", "react-dom": "^16.8.0", "typescript": "^4.0.2" From 205f152f40b641dc74b07eb2676f2f486584a1d1 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Fri, 23 Oct 2020 10:59:27 +0800 Subject: [PATCH 002/243] refactor: drag behavior & allowDrop (#355) * refactor: optimize rerender caused by dragenter & leave * chore * chore * docs: draggable demo * chore: wip * refactor: drag style * refactor: drag, wip * chore * fix: test * chore * chore * chore * chore: clean code * refactor: drag behavior * chore: remove unused vars * refactor: extract dropIndicatorRender * refactor: dropPosition logic * chore: update test * refactor: optimize drag logic * fix: lint * refactor: drag logic * chore: use offset * perf * feat: allow-drop * fix: lint * docs: allow drop * docs: optimize perf by remove logger * chore * chore * chore * feat: dropTarget & dropContainer * chore: vertical indicator * chore * chore: snapshot update & lint fix * refactor: clean codes * chore: offset * feat: function draggable * fix: test * test: coverage * chore * fix: lint * test: cov * test: coverage * test: cov * feat: first node top half * fix: lint * test: cov * test: cov * test: cov * test: cov * test: cov * docs: allowDrop & draggable * chore * chore * test: fulfill test cases * docs: props.indent * test: cov * refactor: drop area * chore * fix: lint * fix: LGTM * fix: drag * fix: drag over indicator * fix: lint * fix: expand logic * chore: do not allow drag left on non-last child * refactor: don't allow dropPosition != 0 on node with children * feat: use css indent * refactor: move indent to state * docs: remove indent prop * fix: dropIndicatorRender type * refactor: default indicator renderer * fix: test * test: snapshot * fix: test case `without motionNodes` * fix: scrollTo test * fix: cov * test: use domSpy * refactor: extract drop indicator render * refactor: use single indent measurer node * test: snapshot * refactor: allowDrop parameter api * chore: add comment for tree state * refactor: remove nodeInstances by create props manually * chore: add comment * chore * test: update snapshot * refactor: remove abstract-drag-over-node-key * refactor: allow first child to drop on its parent * chore: trigger ci --- README.md | 4 +- assets/index.less | 50 +- examples/draggable-allow-drop.jsx | 115 +++++ examples/draggable.jsx | 46 +- package.json | 1 + src/DropIndicator.tsx | 34 ++ src/Indent.tsx | 6 +- src/NodeList.tsx | 21 +- src/Tree.tsx | 383 +++++++++++--- src/TreeNode.tsx | 54 +- src/contextTypes.ts | 16 +- src/util.tsx | 183 ++++++- src/utils/treeUtil.ts | 2 + tests/Accessibility.spec.js | 5 +- tests/Tree.spec.js | 482 +++++++++++++++++- tests/TreeMotion.spec.js | 8 +- tests/__snapshots__/Tree.spec.js.snap | 127 +++++ .../__snapshots__/TreeNodeProps.spec.js.snap | 165 ++++++ tests/__snapshots__/TreeProps.spec.js.snap | 433 ++++++++++++++++ tests/util.spec.js | 12 +- 20 files changed, 1946 insertions(+), 201 deletions(-) create mode 100644 examples/draggable-allow-drop.jsx create mode 100644 src/DropIndicator.tsx diff --git a/README.md b/README.md index 5da49ae2..2184bca8 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ see examples | defaultExpandParent | auto expand parent treeNodes when init | bool | true | | defaultSelectedKeys | default selected treeNodes | String[] | [] | | disabled | whether disabled the tree | bool | false | -| draggable | whether can drag treeNode. (drag events are not supported in Internet Explorer 8 and earlier versions or Safari 5.1 and earlier versions.) | bool | false | +| draggable | whether can drag treeNode. (drag events are not supported in Internet Explorer 8 and earlier versions or Safari 5.1 and earlier versions.) | bool \| ({ node }) => boolean | false | | expandedKeys | Controlled expand specific treeNodes | String[] | - | | filterTreeNode | filter some treeNodes as you need. it should return true | function(node) | - | | icon | customize icon. When you pass component, whose render will receive full TreeNode props as component props | element/Function(props) | - | @@ -94,6 +94,7 @@ see examples | onSelect | click the treeNode to fire | function(selectedKeys, e:{selected: bool, selectedNodes, node, event, nativeEvent}) | - | | switcherIcon | specific the switcher icon. | ReactNode / (props: TreeNodeAttribute) => ReactNode | - | | virtual | Disable virtual scroll when `false` | boolean | - | +| allowDrop | whether to allow drop on node | ({ dropNode, dropPosition }) => boolean | - | ### TreeNode props @@ -143,3 +144,4 @@ rc-tree is released under the MIT license. - [jqTree](http://mbraak.github.io/jqTree/) - [jquery.treeselect](http://travistidwell.com/jquery.treeselect.js/) - [angular Select Tree](http://a5hik.github.io/angular-multi-select-tree/) + diff --git a/assets/index.less b/assets/index.less index b7b778d3..47eb0586 100644 --- a/assets/index.less +++ b/assets/index.less @@ -13,7 +13,7 @@ .@{treeNodePrefixCls} { margin: 0; padding: 0; - line-height: 20px; + line-height: 24px; white-space: nowrap; list-style: none; outline: 0; @@ -27,22 +27,25 @@ -khtml-user-drag: element; -webkit-user-drag: element; } - &.drag-over { - > .draggable { - color: white; - background-color: #316ac5; - border: 1px #316ac5 solid; - opacity: 0.8; + + &.drop-container { + > .draggable::after { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + box-shadow: inset 0 0 0 2px red; + content: ""; } - } - &.drag-over-gap-top { - > .draggable { - border-top: 2px blue solid; + & ~ .@{treeNodePrefixCls} { + border-left: 2px solid chocolate; } } - &.drag-over-gap-bottom { - > .draggable { - border-bottom: 2px blue solid; + &.drop-target { + background-color: yellowgreen; + & ~ .@{treeNodePrefixCls} { + border-left: none; } } &.filter-node { @@ -56,10 +59,11 @@ padding: 0 0 0 18px; } .@{treePrefixCls}-node-content-wrapper { + position: relative; display: inline-block; - height: 17px; + height: 24px; margin: 0; - padding: 1px 3px 0 0; + padding: 0; text-decoration: none; vertical-align: top; cursor: pointer; @@ -73,7 +77,7 @@ height: 16px; margin-right: 2px; line-height: 16px; - vertical-align: middle; + vertical-align: -.125em; background-color: transparent; background-image: url('data:image/png;base64,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'); background-repeat: no-repeat; @@ -187,7 +191,7 @@ } &-node-selected { background-color: #ffe6b0; - border: 1px #ffb951 solid; + box-shadow: 0 0 0 1px #ffb951; opacity: 0.8; } &-icon__open { @@ -209,8 +213,16 @@ margin-right: 2px; vertical-align: top; } + &-title { + display: inline-block; + } + &-indent { + display: inline-block; + vertical-align: bottom; + height: 0; + } &-indent-unit { + width: 24px; display: inline-block; - padding-left: 18px; } } diff --git a/examples/draggable-allow-drop.jsx b/examples/draggable-allow-drop.jsx new file mode 100644 index 00000000..8570e395 --- /dev/null +++ b/examples/draggable-allow-drop.jsx @@ -0,0 +1,115 @@ +/* eslint-disable no-console, react/no-access-state-in-setstate */ +import React from 'react'; +import { gData } from './utils/dataUtil'; +import './draggable.less'; +import '../assets/index.less'; +import Tree from '../src'; + +function allowDrop({ dropNode, dropPosition }) { + if (!dropNode.children) { + if (dropPosition === 0) return false; + } + return true; +} + +class Demo extends React.Component { + state = { + gData, + autoExpandParent: true, + expandedKeys: ['0-0-key', '0-0-0-key', '0-0-0-0-key'], + }; + + onDragStart = info => { + console.log('start', info); + }; + + onDragEnter = () => { + console.log('enter'); + }; + + onDrop = info => { + console.log('drop', info); + const dropKey = info.node.key; + const dragKey = info.dragNode.key; + const dropPos = info.node.pos.split('-'); + const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]); + + const loop = (data, key, callback) => { + data.forEach((item, index, arr) => { + if (item.key === key) { + callback(item, index, arr); + return; + } + if (item.children) { + loop(item.children, key, callback); + } + }); + }; + const data = [...this.state.gData]; + + // Find dragObject + let dragObj; + loop(data, dragKey, (item, index, arr) => { + arr.splice(index, 1); + dragObj = item; + }); + + if (dropPosition === 0) { + // Drop on the content + loop(data, dropKey, item => { + // eslint-disable-next-line no-param-reassign + item.children = item.children || []; + // where to insert 示例添加到尾部,可以是随意位置 + item.children.unshift(dragObj); + }); + } else { + // Drop on the gap (insert before or insert after) + let ar; + let i; + loop(data, dropKey, (item, index, arr) => { + ar = arr; + i = index; + }); + if (dropPosition === -1) { + ar.splice(i, 0, dragObj); + } else { + ar.splice(i + 1, 0, dragObj); + } + } + + this.setState({ + gData: data, + }); + }; + + onExpand = expandedKeys => { + console.log('onExpand', expandedKeys); + this.setState({ + expandedKeys, + autoExpandParent: false, + }); + }; + + render() { + return ( +
+

draggable with allow drop

+

node can not be dropped inside a leaf node

+
+ +
+
+ ); + } +} + +export default Demo; diff --git a/examples/draggable.jsx b/examples/draggable.jsx index b933d408..7ff85c2e 100644 --- a/examples/draggable.jsx +++ b/examples/draggable.jsx @@ -3,7 +3,7 @@ import React from 'react'; import { gData } from './utils/dataUtil'; import './draggable.less'; import '../assets/index.less'; -import Tree, { TreeNode } from '../src'; +import Tree from '../src'; class Demo extends React.Component { state = { @@ -16,18 +16,15 @@ class Demo extends React.Component { console.log('start', info); }; - onDragEnter = info => { - console.log('enter', info); - this.setState({ - expandedKeys: info.expandedKeys, - }); + onDragEnter = () => { + console.log('enter'); }; onDrop = info => { console.log('drop', info); - const dropKey = info.node.props.eventKey; - const dragKey = info.dragNode.props.eventKey; - const dropPos = info.node.props.pos.split('-'); + const dropKey = info.node.key; + const dragKey = info.dragNode.key; + const dropPos = info.node.pos.split('-'); const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]); const loop = (data, key, callback) => { @@ -50,19 +47,8 @@ class Demo extends React.Component { dragObj = item; }); - if (!info.dropToGap) { + if (dropPosition === 0) { // Drop on the content - loop(data, dropKey, item => { - // eslint-disable-next-line no-param-reassign - item.children = item.children || []; - // where to insert 示例添加到尾部,可以是随意位置 - item.children.push(dragObj); - }); - } else if ( - (info.node.props.children || []).length > 0 && // Has children - info.node.props.expanded && // Is expanded - dropPosition === 1 // On the bottom gap - ) { loop(data, dropKey, item => { // eslint-disable-next-line no-param-reassign item.children = item.children || []; @@ -70,7 +56,7 @@ class Demo extends React.Component { item.children.unshift(dragObj); }); } else { - // Drop on the gap + // Drop on the gap (insert before or insert after) let ar; let i; loop(data, dropKey, (item, index, arr) => { @@ -98,17 +84,6 @@ class Demo extends React.Component { }; render() { - const loop = data => - data.map(item => { - if (item.children && item.children.length) { - return ( - - {loop(item.children)} - - ); - } - return ; - }); return (

draggable

@@ -122,9 +97,8 @@ class Demo extends React.Component { onDragStart={this.onDragStart} onDragEnter={this.onDragEnter} onDrop={this.onDrop} - > - {loop(this.state.gData)} - + treeData={this.state.gData} + />
); diff --git a/package.json b/package.json index 8b24bd92..f15908e0 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "postpublish": "npm run gh-pages", "lint": "eslint src/ examples/ --ext .tsx,.ts,.jsx,.js", "test": "father test", + "coverage": "father test --coverage", "gh-pages": "npm run build && father doc deploy", "now-build": "npm run build" }, diff --git a/src/DropIndicator.tsx b/src/DropIndicator.tsx new file mode 100644 index 00000000..3f519fe1 --- /dev/null +++ b/src/DropIndicator.tsx @@ -0,0 +1,34 @@ +import * as React from 'react' + +export default function DropIndicator ({ + dropPosition, + dropLevelOffset, + indent, +}: { + dropPosition: -1 | 0 | 1, + dropLevelOffset: number, + indent: number, +}) { + const style: React.CSSProperties = { + pointerEvents: 'none', + position: 'absolute', + right: 0, + backgroundColor: 'red', + height: 2, + }; + switch (dropPosition) { + case -1: + style.top = 0; + style.left = -dropLevelOffset * indent; + break; + case 1: + style.bottom = 0; + style.left = -dropLevelOffset * indent; + break; + case 0: + style.bottom = 0; + style.left = indent; + break; + } + return
; +} diff --git a/src/Indent.tsx b/src/Indent.tsx index 11f4cfcc..3cce26c2 100644 --- a/src/Indent.tsx +++ b/src/Indent.tsx @@ -8,11 +8,7 @@ interface IndentProps { isEnd: boolean[]; } -const Indent: React.FC = ({ prefixCls, level, isStart, isEnd }) => { - if (!level) { - return null; - } - +const Indent = ({ prefixCls, level, isStart, isEnd }: IndentProps) => { const baseClassName = `${prefixCls}-indent-unit`; const list: React.ReactElement[] = []; for (let i = 0; i < level; i += 1) { diff --git a/src/NodeList.tsx b/src/NodeList.tsx index 0e6880d2..ca4a014b 100644 --- a/src/NodeList.tsx +++ b/src/NodeList.tsx @@ -48,6 +48,7 @@ const MotionFlattenData: FlattenNode = { export interface NodeListRef { scrollTo: ScrollTo; + getIndentWidth: () => number; } interface NodeListProps { @@ -167,10 +168,12 @@ const RefNodeList: React.RefForwardingComponent = (p // =============================== Ref ================================ const listRef = React.useRef(null); + const indentMeasurerRef = React.useRef(null); React.useImperativeHandle(ref, () => ({ scrollTo: scroll => { listRef.current.scrollTo(scroll); }, + getIndentWidth: () => indentMeasurerRef.current.offsetWidth, })); // ============================== Motion ============================== @@ -278,6 +281,22 @@ const RefNodeList: React.RefForwardingComponent = (p />
+
+
+
+
+
+ {...domProps} data={mergedData} @@ -305,7 +324,7 @@ const RefNodeList: React.RefForwardingComponent = (p boolean; + export interface TreeProps { prefixCls: string; className?: string; @@ -72,7 +75,7 @@ export interface TreeProps { multiple?: boolean; checkable?: boolean | React.ReactNode; checkStrictly?: boolean; - draggable?: boolean; + draggable?: ((node: DataNode) => boolean) | boolean; defaultExpandParent?: boolean; autoExpandParent?: boolean; defaultExpandAll?: boolean; @@ -82,7 +85,14 @@ export interface TreeProps { checkedKeys?: Key[] | { checked: Key[]; halfChecked: Key[] }; defaultSelectedKeys?: Key[]; selectedKeys?: Key[]; + allowDrop?: AllowDrop; titleRender?: (node: DataNode) => React.ReactNode; + dropIndicatorRender?: (props: { + dropPosition: -1 | 0 | 1; + dropLevelOffset: number; + indent: number; + prefixCls: string; + }) => React.ReactNode; onFocus?: React.FocusEventHandler; onBlur?: React.FocusEventHandler; onKeyDown?: React.KeyboardEventHandler; @@ -151,6 +161,8 @@ export interface TreeProps { interface TreeState { keyEntities: Record; + indent: number | null; + selectedKeys: Key[]; checkedKeys: Key[]; halfCheckedKeys: Key[]; @@ -159,9 +171,16 @@ interface TreeState { expandedKeys: Key[]; dragging: boolean; - dragNodesKeys: Key[]; - dragOverNodeKey: Key; - dropPosition: number; + dragChildrenKeys: Key[]; + + // for details see comment in Tree.state + dropPosition: -1 | 0 | 1 | null; + dropLevelOffset: number | null; + dropContainerKey: Key | null; + dropTargetKey: Key | null; + dropTargetPos: string | null; + dropAllowed: boolean; + dragOverNodeKey: Key | null; treeData: DataNode[]; flattenNodes: FlattenNode[]; @@ -192,6 +211,8 @@ class Tree extends React.Component { defaultExpandedKeys: [], defaultCheckedKeys: [], defaultSelectedKeys: [], + dropIndicatorRender: DropIndicator, + allowDrop: () => true, }; static TreeNode = TreeNode; @@ -203,6 +224,8 @@ class Tree extends React.Component { state: TreeState = { keyEntities: {}, + indent: null, + selectedKeys: [], checkedKeys: [], halfCheckedKeys: [], @@ -211,9 +234,21 @@ class Tree extends React.Component { expandedKeys: [], dragging: false, - dragNodesKeys: [], + dragChildrenKeys: [], + + // dropTargetKey is the key of abstract-drop-node + // the abstract-drop-node is the real drop node when drag and drop + // not the DOM drag over node + dropTargetKey: null, + dropPosition: null, // the drop position of abstract-drop-node, inside 0, top -1, bottom 1 + dropContainerKey: null, // the container key of abstract-drop-node if dropPosition is -1 or 1 + dropLevelOffset: null, // the drop level offset of abstract-drag-over-node + dropTargetPos: null, // the pos of abstract-drop-node + dropAllowed: true, // if drop to abstract-drop-node is allowed + // the abstract-drag-over-node + // if mouse is on the bottom of top dom node or no the top of the bottom dom node + // abstract-drag-over-node is the top node dragOverNodeKey: null, - dropPosition: null, treeData: [], flattenNodes: [], @@ -226,10 +261,17 @@ class Tree extends React.Component { prevProps: null, }; + dragStartMousePosition = null; + dragNode: NodeInstance; listRef = React.createRef(); + componentWillUnmount() { + window.removeEventListener('dragend', this.onWindowDragEnd); + this.destroyed = true; + } + static getDerivedStateFromProps(props: TreeProps, prevState: TreeState) { const { prevProps } = prevState; const newState: Partial = { @@ -344,117 +386,246 @@ class Tree extends React.Component { return newState; } - componentWillUnmount() { - this.destroyed = true; - } - onNodeDragStart: NodeDragEventHandler = (event, node) => { const { expandedKeys, keyEntities } = this.state; const { onDragStart } = this.props; const { eventKey } = node.props; this.dragNode = node; + this.dragStartMousePosition = { + x: event.clientX, + y: event.clientY, + }; const newExpandedKeys = arrDel(expandedKeys, eventKey); this.setState({ dragging: true, - dragNodesKeys: getDragNodesKeys(eventKey, keyEntities), + dragChildrenKeys: getDragChildrenKeys(eventKey, keyEntities), + indent: this.listRef.current.getIndentWidth(), }); this.setExpandedKeys(newExpandedKeys); + window.addEventListener('dragend', this.onWindowDragEnd); + if (onDragStart) { onDragStart({ event, node: convertNodePropsToEventData(node.props) }); } }; /** - * [Legacy] Select handler is less small than node, + * [Legacy] Select handler is smaller than node, * so that this will trigger when drag enter node or select handler. * This is a little tricky if customize css without padding. * Better for use mouse move event to refresh drag state. * But let's just keep it to avoid event trigger logic change. */ - onNodeDragEnter: NodeDragEventHandler = (event, node) => { - const { expandedKeys, keyEntities, dragNodesKeys } = this.state; - const { onDragEnter } = this.props; - const { pos, eventKey } = node.props; - - if (!this.dragNode || dragNodesKeys.indexOf(eventKey) !== -1) return; + onNodeDragEnter = (event: React.MouseEvent, node: NodeInstance) => { + const { + expandedKeys, + keyEntities, + dragChildrenKeys, + flattenNodes, + indent, + } = this.state; + const { onDragEnter, onExpand, allowDrop } = this.props; + const { pos } = node.props; + const { dragNode } = this; - const dropPosition = calcDropPosition(event, node); + const { + dropPosition, + dropLevelOffset, + dropTargetKey, + dropContainerKey, + dropTargetPos, + dropAllowed, + dragOverNodeKey, + } = calcDropPosition( + event, + node, + indent, + this.dragStartMousePosition, + allowDrop, + flattenNodes, + keyEntities, + expandedKeys, + ); - // Skip if drag node is self - if (this.dragNode.props.eventKey === eventKey && dropPosition === 0) { + if ( + !dragNode || + // don't allow drop inside its children + dragChildrenKeys.indexOf(dropTargetKey) !== -1 || + // don't allow drop when drop is not allowed caculated by calcDropPosition + !dropAllowed + ) { this.setState({ - dragOverNodeKey: '', + dragOverNodeKey: null, dropPosition: null, + dropLevelOffset: null, + dropTargetKey: null, + dropContainerKey: null, + dropTargetPos: null, + dropAllowed: false, }); return; } - // Ref: https://github.com/react-component/tree/issues/132 - // Add timeout to let onDragLevel fire before onDragEnter, - // so that we can clean drag props for onDragLeave node. - // Macro task for this: - // https://html.spec.whatwg.org/multipage/webappapis.html#clean-up-after-running-script - setTimeout(() => { - // Update drag over node - this.setState({ - dragOverNodeKey: eventKey, - dropPosition, - }); + // Side effect for delay drag + if (!this.delayedDragEnterLogic) { + this.delayedDragEnterLogic = {}; + } + Object.keys(this.delayedDragEnterLogic).forEach(key => { + clearTimeout(this.delayedDragEnterLogic[key]); + }); - // Side effect for delay drag - if (!this.delayedDragEnterLogic) { - this.delayedDragEnterLogic = {}; - } - Object.keys(this.delayedDragEnterLogic).forEach(key => { - clearTimeout(this.delayedDragEnterLogic[key]); - }); + if (dragNode.props.eventKey !== node.props.eventKey) { + // hoist expand logic here + // since if logic is on the bottom + // it will be blocked by abstract dragover node check + // => if you dragenter from top, you mouse will still be consider as in the top node + event.persist(); this.delayedDragEnterLogic[pos] = window.setTimeout(() => { if (!this.state.dragging) return; let newExpandedKeys = [...expandedKeys]; - const entity = keyEntities[eventKey]; + const entity = keyEntities[node.props.eventKey]; if (entity && (entity.children || []).length) { - newExpandedKeys = arrAdd(expandedKeys, eventKey); + newExpandedKeys = arrAdd(expandedKeys, node.props.eventKey); } if (!('expandedKeys' in this.props)) { this.setExpandedKeys(newExpandedKeys); } - if (onDragEnter) { - onDragEnter({ - event, + if (onExpand) { + onExpand(newExpandedKeys, { node: convertNodePropsToEventData(node.props), - expandedKeys: newExpandedKeys, + expanded: true, + nativeEvent: event.nativeEvent, }); } - }, 400); - }, 0); + }, 800); + } + + // Skip if drag node is self + if (dragNode.props.eventKey === dropTargetKey && dropLevelOffset === 0) { + this.setState({ + dragOverNodeKey: null, + dropPosition: null, + dropLevelOffset: null, + dropTargetKey: null, + dropContainerKey: null, + dropTargetPos: null, + dropAllowed: false, + }); + return; + } + + // Update drag over node and drag state + this.setState({ + dragOverNodeKey, + dropPosition, + dropLevelOffset, + dropTargetKey, + dropContainerKey, + dropTargetPos, + dropAllowed, + }); + + if (onDragEnter) { + onDragEnter({ + event, + node: convertNodePropsToEventData(node.props), + expandedKeys, + }); + } }; - onNodeDragOver: NodeDragEventHandler = (event, node) => { - const { dragNodesKeys } = this.state; - const { onDragOver } = this.props; - const { eventKey } = node.props; + onNodeDragOver = (event: React.MouseEvent, node: NodeInstance) => { + const { + dragChildrenKeys, + flattenNodes, + keyEntities, + expandedKeys, + indent, + } = this.state; + const { onDragOver, allowDrop } = this.props; + const { dragNode } = this; - if (dragNodesKeys.indexOf(eventKey) !== -1) { + const { + dropPosition, + dropLevelOffset, + dropTargetKey, + dropContainerKey, + dropAllowed, + dropTargetPos, + dragOverNodeKey, + } = calcDropPosition( + event, + node, + indent, + this.dragStartMousePosition, + allowDrop, + flattenNodes, + keyEntities, + expandedKeys, + ); + + if ( + !dragNode || + dragChildrenKeys.indexOf(dropTargetKey) !== -1 || + !dropAllowed + ) { + // don't allow drop inside its children + // don't allow drop when drop is not allowed caculated by calcDropPosition return; } // Update drag position - if (this.dragNode && eventKey === this.state.dragOverNodeKey) { - const dropPosition = calcDropPosition(event, node); - - if (dropPosition === this.state.dropPosition) return; + if (dragNode.props.eventKey === dropTargetKey && dropLevelOffset === 0) { + if ( + !( + this.state.dropPosition === null && + this.state.dropLevelOffset === null && + this.state.dropTargetKey === null && + this.state.dropContainerKey === null && + this.state.dropTargetPos === null && + this.state.dropAllowed === false && + this.state.dragOverNodeKey === null + ) + ) { + this.setState({ + dropPosition: null, + dropLevelOffset: null, + dropTargetKey: null, + dropContainerKey: null, + dropTargetPos: null, + dropAllowed: false, + dragOverNodeKey: null, + }); + } + } else if ( + !( + dropPosition === this.state.dropPosition && + dropLevelOffset === this.state.dropLevelOffset && + dropTargetKey === this.state.dropTargetKey && + dropContainerKey === this.state.dropContainerKey && + dropTargetPos === this.state.dropTargetPos && + dropAllowed === this.state.dropAllowed && + dragOverNodeKey === this.state.dragOverNodeKey + ) + ) { this.setState({ dropPosition, + dropLevelOffset, + dropTargetKey, + dropContainerKey, + dropTargetPos, + dropAllowed, + dragOverNodeKey, }); } @@ -466,60 +637,81 @@ class Tree extends React.Component { onNodeDragLeave: NodeDragEventHandler = (event, node) => { const { onDragLeave } = this.props; - this.setState({ - dragOverNodeKey: '', - }); - if (onDragLeave) { onDragLeave({ event, node: convertNodePropsToEventData(node.props) }); } }; - onNodeDragEnd: NodeDragEventHandler = (event, node) => { + // since stopPropagation() is called in treeNode + // if onWindowDrag is called, whice means state is keeped, drag state should be cleared + onWindowDragEnd = event => { + this.onNodeDragEnd(event, null, true); + window.removeEventListener('dragend', this.onWindowDragEnd); + }; + + // if onNodeDragEnd is called, onWindowDragEnd won't be called since stopPropagation() is called + onNodeDragEnd: NodeDragEventHandler = (event, node, outsideTree = false) => { const { onDragEnd } = this.props; this.setState({ - dragOverNodeKey: '', + dragOverNodeKey: null, }); + this.cleanDragState(); - if (onDragEnd) { + if (onDragEnd && !outsideTree) { onDragEnd({ event, node: convertNodePropsToEventData(node.props) }); } this.dragNode = null; }; - onNodeDrop: NodeDragEventHandler = (event, node) => { - const { dragNodesKeys = [], dropPosition } = this.state; + onNodeDrop = (event: React.MouseEvent, node, outsideTree: boolean = false) => { + const { + dragChildrenKeys, + dropPosition, + dropTargetKey, + dropTargetPos, + dropAllowed, + } = this.state; + + if (!dropAllowed) return; + const { onDrop } = this.props; - const { eventKey, pos } = node.props; this.setState({ - dragOverNodeKey: '', + dragOverNodeKey: null, }); this.cleanDragState(); - if (dragNodesKeys.indexOf(eventKey) !== -1) { - warning(false, "Can not drop to dragNode(include it's children node)"); - return; + if (dropTargetKey === null) return; + + const abstractDropNodeProps = { + ...getTreeNodeProps( + dropTargetKey, + this.getTreeNodeRequiredProps(), + ), + active: this.getActiveItem()?.data.key === dropTargetKey, + data: this.state.keyEntities[dropTargetKey].node, } + const dropToChild = dragChildrenKeys.indexOf(dropTargetKey) !== -1; - const posArr = posToArr(pos); + warning( + !dropToChild, + "Can not drop to dragNode's children node. This is a bug of rc-tree. Please report an issue.", + ); + + const posArr = posToArr(dropTargetPos); const dropResult = { event, - node: convertNodePropsToEventData(node.props), + node: convertNodePropsToEventData(abstractDropNodeProps), dragNode: this.dragNode ? convertNodePropsToEventData(this.dragNode.props) : null, - dragNodesKeys: dragNodesKeys.slice(), + dragNodesKeys: [this.dragNode.props.eventKey].concat(dragChildrenKeys), + dropToGap: dropPosition !== 0, dropPosition: dropPosition + Number(posArr[posArr.length - 1]), - dropToGap: false, }; - if (dropPosition !== 0) { - dropResult.dropToGap = true; - } - - if (onDrop) { + if (onDrop && !outsideTree) { onDrop(dropResult); } @@ -531,8 +723,15 @@ class Tree extends React.Component { if (dragging) { this.setState({ dragging: false, + dropPosition: null, + dropContainerKey: null, + dropTargetKey: null, + dropLevelOffset: null, + dropAllowed: true, + dragOverNodeKey: null, }); } + this.dragStartMousePosition = null; }; onNodeClick: NodeMouseEventHandler = (e, treeNode) => { @@ -1035,7 +1234,19 @@ class Tree extends React.Component { }; render() { - const { focused, flattenNodes, keyEntities, dragging, activeKey } = this.state; + const { + focused, + flattenNodes, + keyEntities, + dragging, + activeKey, + dropLevelOffset, + dropContainerKey, + dropTargetKey, + dropPosition, + dragOverNodeKey, + indent, + } = this.state; const { prefixCls, className, @@ -1058,6 +1269,7 @@ class Tree extends React.Component { itemHeight, virtual, titleRender, + dropIndicatorRender, onContextMenu, } = this.props; const domProps: React.HTMLAttributes = getDataAndAria(this.props); @@ -1075,6 +1287,13 @@ class Tree extends React.Component { checkStrictly, disabled, keyEntities, + dropLevelOffset, + dropContainerKey, + dropTargetKey, + dropPosition, + dragOverNodeKey, + indent, + dropIndicatorRender, loadData, filterTreeNode, diff --git a/src/TreeNode.tsx b/src/TreeNode.tsx index 8e434028..c3b4df7e 100644 --- a/src/TreeNode.tsx +++ b/src/TreeNode.tsx @@ -401,6 +401,7 @@ class InternalTreeNode extends React.Component {$icon} {$title} + {this.renderDropIndicator()} ); }; + renderDropIndicator = () => { + const { disabled, eventKey } = this.props; + const { + context: { + draggable, + dropLevelOffset, + dropPosition, + prefixCls, + indent, + dropIndicatorRender, + dragOverNodeKey, + }, + } = this.props; + const mergedDraggable = draggable !== false; + // allowDrop is calculated in Tree.tsx, there is no need for calc it here + const showIndicator = !disabled && mergedDraggable && dragOverNodeKey === eventKey; + return showIndicator + ? dropIndicatorRender({ dropPosition, dropLevelOffset, indent, prefixCls }) + : null; + }; + render() { const { eventKey, @@ -476,16 +499,25 @@ class InternalTreeNode extends React.Component diff --git a/src/contextTypes.ts b/src/contextTypes.ts index f880b01b..ce2e5ae2 100644 --- a/src/contextTypes.ts +++ b/src/contextTypes.ts @@ -21,6 +21,7 @@ export type NodeMouseEventHandler = ( export type NodeDragEventHandler = ( e: React.MouseEvent, node: NodeInstance, + outsideTree?: boolean, ) => void; export interface TreeContextProps { @@ -29,11 +30,24 @@ export interface TreeContextProps { showIcon: boolean; icon: IconType; switcherIcon: IconType; - draggable: boolean; + draggable: ((node: DataNode) => boolean) | boolean; checkable: boolean | React.ReactNode; checkStrictly: boolean; disabled: boolean; keyEntities: Record; + // for details see comment in Tree.state (Tree.tsx) + dropLevelOffset?: number; + dropContainerKey: Key | null; + dropTargetKey: Key | null; + dropPosition: -1 | 0 | 1 | null; + indent: number | null; + dropIndicatorRender: (props: { + dropPosition: -1 | 0 | 1; + dropLevelOffset: number; + indent; + prefixCls; + }) => React.ReactNode; + dragOverNodeKey: Key | null; loadData: (treeNode: EventDataNode) => Promise; filterTreeNode: (treeNode: EventDataNode) => boolean; diff --git a/src/util.tsx b/src/util.tsx index 227a5ec6..3c5c0d1c 100644 --- a/src/util.tsx +++ b/src/util.tsx @@ -1,3 +1,4 @@ +/* eslint-disable no-lonely-if */ /** * Legacy code. Should avoid to use if you are new to import these code. */ @@ -5,11 +6,8 @@ import React from 'react'; import warning from 'rc-util/lib/warning'; import TreeNode, { TreeNodeProps } from './TreeNode'; -import { NodeElement, Key, DataNode, Entity, DataEntity, NodeInstance } from './interface'; -import { TreeProps } from './Tree'; - -const DRAG_SIDE_RANGE = 0.25; -const DRAG_MIN_GAP = 2; +import { NodeElement, Key, DataNode, Entity, DataEntity, NodeInstance, FlattenNode } from './interface'; +import { TreeProps, AllowDrop } from './Tree'; export function arrDel(list: Key[], value: Key) { const clone = list.slice(); @@ -40,36 +38,185 @@ export function isTreeNode(node: NodeElement) { return node && node.type && node.type.isTreeNode; } -export function getDragNodesKeys(dragNodeKey: Key, keyEntities: Record): Key[] { - const dragNodesKeys = [dragNodeKey]; +export function getDragChildrenKeys(dragNodeKey: Key, keyEntities: Record): Key[] { + // not contains self + // self for left or right drag + const dragChildrenKeys = []; const entity = keyEntities[dragNodeKey]; function dig(list: DataEntity[] = []) { list.forEach(({ key, children }) => { - dragNodesKeys.push(key); + dragChildrenKeys.push(key); dig(children); }); } dig(entity.children); - return dragNodesKeys; + return dragChildrenKeys; +} + +export function isLastChild (treeNodeEntity: DataEntity) { + if (treeNodeEntity.parent) { + const posArr = posToArr(treeNodeEntity.pos); + return Number(posArr[posArr.length - 1]) === treeNodeEntity.parent.children.length - 1; + } + return false; +} + +export function isFirstChild (treeNodeEntity: DataEntity) { + const posArr = posToArr(treeNodeEntity.pos); + return Number(posArr[posArr.length - 1]) === 0; } // Only used when drag, not affect SSR. -export function calcDropPosition(event: React.MouseEvent, treeNode: NodeInstance) { - const { clientY } = event; - const { top, bottom, height } = treeNode.selectHandle.getBoundingClientRect(); - const des = Math.max(height * DRAG_SIDE_RANGE, DRAG_MIN_GAP); +export function calcDropPosition( + event: React.MouseEvent, + targetNode: NodeInstance, + indent: number, + startMousePosition: { + x: number, + y: number, + }, + allowDrop: AllowDrop, + flattenedNodes: FlattenNode[], + keyEntities: Record, + expandKeys: Key[], +) : { + dropPosition: -1 | 0 | 1, + dropLevelOffset: number, + dropTargetKey: Key, + dropTargetPos: string, + dropContainerKey: Key, + dragOverNodeKey: Key, + dropAllowed: boolean, +} { + const { clientX, clientY } = event; + const { top, height } = (event.target as HTMLElement).getBoundingClientRect(); + // optional chain for testing + const horizontalMouseOffset = (startMousePosition?.x || 0) - clientX; + const rawDropLevelOffset = (horizontalMouseOffset - 12) / indent; + + // find abstract drop node by horizontal offset + let abstractDropNodeEntity: DataEntity = keyEntities[targetNode.props.eventKey]; + + if (clientY < top + height / 2) { + // first half, set abstract drop node to previous node + const nodeIndex = flattenedNodes.findIndex( + flattenedNode => flattenedNode.data.key === abstractDropNodeEntity.key, + ); + const prevNodeIndex = nodeIndex <= 0 ? 0 : nodeIndex - 1; + const prevNodeKey = flattenedNodes[prevNodeIndex].data.key; + abstractDropNodeEntity = keyEntities[prevNodeKey]; + } - if (clientY <= top + des) { - return -1; + const abstractDragOverEntity = abstractDropNodeEntity; + const dragOverNodeKey = abstractDropNodeEntity.key; + + let dropPosition: -1 | 0 | 1 = 0; + let dropLevelOffset = 0; + for (let i = 0; i < rawDropLevelOffset; i += 1) { + if ( + isLastChild(abstractDropNodeEntity) + ) { + abstractDropNodeEntity = abstractDropNodeEntity.parent; + dropLevelOffset += 1; + } else { + break; + } } - if (clientY >= bottom - des) { - return 1; + + const abstractDropDataNode = abstractDropNodeEntity.node + let dropAllowed = true; + if ( + isFirstChild(abstractDropNodeEntity) && + abstractDropNodeEntity.level === 0 && + clientY < top + height / 2 && + allowDrop({ + dropNode: abstractDropDataNode, + dropPosition: -1, + }) && + abstractDropNodeEntity.key === targetNode.props.eventKey + ) { + // first half of first node in first level + dropPosition = -1 + } else if ( + (abstractDragOverEntity.children || []).length && + expandKeys.includes(dragOverNodeKey) + ) { + // drop on expanded node + // only allow drop inside + if (allowDrop({ + dropNode: abstractDropDataNode, + dropPosition: 0, + })) { + dropPosition = 0; + } else { + dropAllowed = false + } + } else if ( + dropLevelOffset === 0 + ) { + if (rawDropLevelOffset > -1.5) { + // | Node | <- abstractDropNode + // | -^-===== | <- mousePosition + // 1. try drop after + // 2. do not allow drop + if (allowDrop({ + dropNode: abstractDropDataNode, + dropPosition: 1, + })) { + dropPosition = 1; + } else { + dropAllowed = false; + } + } else { + // | Node | <- abstractDropNode + // | ---==^== | <- mousePosition + // whether it has children or doesn't has children + // always + // 1. try drop inside + // 2. try drop after + // 3. do not allow drop + if (allowDrop({ + dropNode: abstractDropDataNode, + dropPosition: 0, + })) { + dropPosition = 0; + } else if (allowDrop({ + dropNode: abstractDropDataNode, + dropPosition: 1, + })) { + dropPosition = 1; + } else { + dropAllowed = false; + } + } + } else { + // | Node1 | <- abstractDropNode + // | Node2 | + // --^--|----=====| <- mousePosition + // 1. try insert after Node1 + // 2. do not allow drop + if (allowDrop({ + dropNode: abstractDropDataNode, + dropPosition: 1, + })) { + dropPosition = 1; + } else { + dropAllowed = false; + } } - return 0; + return { + dropPosition, + dropLevelOffset, + dropTargetKey: abstractDropNodeEntity.key, + dropTargetPos: abstractDropNodeEntity.pos, + dragOverNodeKey, + dropContainerKey: dropPosition === 0 ? null : (abstractDropNodeEntity.parent?.key || null), + dropAllowed, + }; } /** diff --git a/src/utils/treeUtil.ts b/src/utils/treeUtil.ts index 980132c6..6b066dec 100644 --- a/src/utils/treeUtil.ts +++ b/src/utils/treeUtil.ts @@ -301,6 +301,8 @@ export function getTreeNodeProps( pos: String(entity ? entity.pos : ''), // [Legacy] Drag props + // Since the interaction of drag is changed, the semantic of the props are + // not accuracy, I think it should be finally removed dragOver: dragOverNodeKey === key && dropPosition === 0, dragOverGapTop: dragOverNodeKey === key && dropPosition === -1, dragOverGapBottom: dragOverNodeKey === key && dropPosition === 1, diff --git a/tests/Accessibility.spec.js b/tests/Accessibility.spec.js index 91e71bff..efcb4d4e 100644 --- a/tests/Accessibility.spec.js +++ b/tests/Accessibility.spec.js @@ -207,7 +207,10 @@ describe('Tree Accessibility', () => { expect(wrapper.find(InternalTreeNode).find('.rc-tree-treenode-active').length).toBeTruthy(); // Mouse move - wrapper.find('.rc-tree-treenode').simulate('mouseMove'); + wrapper + .find('.rc-tree-treenode') + .at(1) + .simulate('mouseMove'); expect(wrapper.find(InternalTreeNode).find('.rc-tree-treenode-active').length).toBeFalsy(); }); }); diff --git a/tests/Tree.spec.js b/tests/Tree.spec.js index 5842c225..1c6b31f3 100644 --- a/tests/Tree.spec.js +++ b/tests/Tree.spec.js @@ -1054,16 +1054,16 @@ describe('Tree Basic', () => { // Not trigger self wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragEnter'); - await delay(500); + await delay(900); expect(onDragEnter).not.toHaveBeenCalled(); wrapper .find('.dropTarget') .at(0) .simulate('dragEnter'); - expect(onDragEnter).not.toHaveBeenCalled(); + expect(onDragEnter).toHaveBeenCalled(); - await delay(500); + await delay(900); wrapper.update(); const node = convertNodePropsToEventData( wrapper @@ -1072,7 +1072,7 @@ describe('Tree Basic', () => { .props(), ); const event = onDragEnter.mock.calls[0][0]; - expect(event.node).toEqual(node); + expect(event.node.key).toEqual(node.key); expect(event.expandedKeys).toEqual(['0-0', '0-0-0-1']); expect(onDragEnter).toHaveBeenCalledTimes(1); }); @@ -1086,11 +1086,11 @@ describe('Tree Basic', () => { .at(2) .props(), ); + wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragStart'); wrapper .find('.dropTarget') .at(0) .simulate('dragOver'); - const event = onDragOver.mock.calls[0][0]; expect(event.node).toEqual(node); }); @@ -1116,6 +1116,8 @@ describe('Tree Basic', () => { const onDrop = jest.fn(); const wrapper = mount(createTree({ onDrop })); wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragStart'); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragEnter'); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragOver'); const dropNode = convertNodePropsToEventData( wrapper .find(InternalTreeNode) @@ -1182,13 +1184,16 @@ describe('Tree Basic', () => { function dropTarget(targetSelector) { return new Promise(resolve => { const wrapper = mount( - + true} defaultExpandAll onExpand={() => {}}> - + + + + , ); @@ -1206,37 +1211,474 @@ describe('Tree Basic', () => { wrapper.find(targetSelector).simulate('dragEnter', { clientY: 0 }); setTimeout(() => { wrapper.find(targetSelector).simulate('dragOver', { clientY: 999 }); - // 4. Drop wrapper.find(targetSelector).simulate('drop'); wrapper.find('div.dragTarget').simulate('dragEnd'); resolve(); - }, 500); + }, 1000); }, 10); }); } - - const { getBoundingClientRect } = Element.prototype; + let domSpy; beforeEach(() => { - Element.prototype.getBoundingClientRect = jest.fn(() => ({ - width: 100, - height: 20, - top: 0, - left: 0, - bottom: 20, - right: 100, - })); + domSpy = spyElementPrototypes(HTMLElement, { + offsetWidth: { + get() { + return 24; + }, + }, + getBoundingClientRect: jest.fn(() => ({ + width: 100, + height: 20, + top: 0, + left: 0, + bottom: 20, + right: 100, + })), + }); + // Object.defineProperties(window.HTMLElement.prototype, { + // // mock indent as 24 + // // no need for clearing it, since jest make each file a independent env + // offsetWidth: { + // get() { + // return 24; + // }, + // }, + // }); }); afterEach(() => { - Element.prototype.getBoundingClientRect = getBoundingClientRect; + domSpy.mockRestore(); }); it('self', () => dropTarget('div.dragTarget')); it('target', () => dropTarget('div.dropTarget')); }); + + describe('new drop logic', () => { + let domSpy; + beforeEach(() => { + domSpy = spyElementPrototypes(HTMLElement, { + getBoundingClientRect: () => ({ + width: 100, + height: 20, + top: 0, + left: 0, + bottom: 20, + right: 100, + }), + offsetWidth: { + get() { + return 24; + }, + }, + }); + }); + afterEach(() => { + domSpy.mockRestore(); + }); + it('allowDrop all nodes', () => { + const onDrop = jest.fn(); + const wrapper = mount( + + + + + + + + + + + + + , + ); + wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragStart', { + clientX: 500, + clientY: 500, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragEnter', { + clientX: 400, + clientY: 600, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragOver', { + clientX: 400, + clientY: 600, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('drop'); + expect(onDrop.mock.calls[0][0].node.key).toEqual('0-1'); + expect(onDrop.mock.calls[0][0].dropPosition).toEqual(2); + wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragStart', { + clientX: 500, + clientY: 500, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragEnter', { + clientX: 500, + clientY: 600, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragOver', { + clientX: 500, + clientY: 600, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('drop'); + expect(onDrop.mock.calls[1][0].node.key).toEqual('0-1-0-0'); + expect(onDrop.mock.calls[1][0].dropPosition).toEqual(1); + wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragStart', { + clientX: 500, + clientY: 500, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragEnter', { + clientX: 550, + clientY: 600, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragOver', { + clientX: 550, + clientY: 600, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('drop'); + expect(onDrop.mock.calls[2][0].node.key).toEqual('0-1-0-0'); + expect(onDrop.mock.calls[2][0].dropPosition).toEqual(0); + wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragStart', { + clientX: 500, + clientY: 500, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragEnter', { + clientX: 600, + clientY: 600, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragOver', { + clientX: 600, + clientY: 600, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('drop'); + expect(onDrop.mock.calls[2][0].node.key).toEqual('0-1-0-0'); + expect(onDrop.mock.calls[2][0].dropPosition).toEqual(0); + }); + it('allowDrop no node', () => { + const onDrop = jest.fn(); + const wrapper = mount( + false}> + + + + + + + + + + + + , + ); + wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragStart', { + clientX: 500, + clientY: 500, + }); + wrapper.find('.dropTargetParent > .rc-tree-node-content-wrapper').simulate('dragEnter', { + clientX: 400, + clientY: 600, + }); + wrapper.find('.dropTargetParent > .rc-tree-node-content-wrapper').simulate('dragOver', { + clientX: 400, + clientY: 600, + }); + wrapper.find('.dropTargetParent > .rc-tree-node-content-wrapper').simulate('drop'); + // not allow any dropPosition except 0 on expanded node + expect(onDrop).not.toHaveBeenCalled(); + wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragStart', { + clientX: 500, + clientY: 500, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragEnter', { + clientX: 400, + clientY: 600, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragOver', { + clientX: 400, + clientY: 600, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('drop'); + expect(onDrop).not.toHaveBeenCalled(); + wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragStart', { + clientX: 500, + clientY: 500, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragEnter', { + clientX: 500, + clientY: 600, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragOver', { + clientX: 500, + clientY: 600, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('drop'); + expect(onDrop).not.toHaveBeenCalled(); + wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragStart', { + clientX: 500, + clientY: 500, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragEnter', { + clientX: 550, + clientY: 600, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragOver', { + clientX: 550, + clientY: 600, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('drop'); + expect(onDrop).not.toHaveBeenCalled(); + wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragStart', { + clientX: 500, + clientY: 500, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragEnter', { + clientX: 600, + clientY: 600, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragOver', { + clientX: 600, + clientY: 600, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('drop'); + expect(onDrop).not.toHaveBeenCalled(); + }); + it('not allowDrop on node which has children', () => { + const onDrop = jest.fn(); + const allowDrop = ({ dropNode, dropPosition }) => { + if (!dropNode.children) { + if (dropPosition === 0) return false; + } + return true; + }; + const wrapper = mount( + + + + + + + + + + + , + ); + wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragStart', { + clientX: 500, + clientY: 500, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragEnter', { + clientX: 400, + clientY: 600, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragOver', { + clientX: 400, + clientY: 600, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('drop'); + expect(onDrop.mock.calls[0][0].node.key).toEqual('0-1'); + expect(onDrop.mock.calls[0][0].dropPosition).toEqual(2); + wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragStart', { + clientX: 500, + clientY: 500, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragEnter', { + clientX: 500, + clientY: 600, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragOver', { + clientX: 500, + clientY: 600, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('drop'); + expect(onDrop.mock.calls[1][0].node.key).toEqual('0-1-0-0'); + expect(onDrop.mock.calls[1][0].dropPosition).toEqual(1); + wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragStart', { + clientX: 500, + clientY: 500, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragEnter', { + clientX: 550, + clientY: 600, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragOver', { + clientX: 550, + clientY: 600, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('drop'); + expect(onDrop.mock.calls[2][0].node.key).toEqual('0-1-0-0'); + expect(onDrop.mock.calls[2][0].dropPosition).toEqual(1); + wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragStart', { + clientX: 500, + clientY: 500, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragEnter', { + clientX: 600, + clientY: 600, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragOver', { + clientX: 600, + clientY: 600, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('drop'); + expect(onDrop.mock.calls[2][0].node.key).toEqual('0-1-0-0'); + expect(onDrop.mock.calls[2][0].dropPosition).toEqual(1); + }); + it('drop to top half of first node', () => { + const onDrop = jest.fn(); + const wrapper = mount( + + + + + + + + + + + + , + ); + wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragStart', { + clientX: 500, + clientY: 500, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragEnter', { + clientX: 400, + clientY: 0, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragOver', { + clientX: 400, + clientY: -1000, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('drop'); + expect(onDrop.mock.calls[0][0].node.key).toEqual('0-1'); + expect(onDrop.mock.calls[0][0].dropPosition).toEqual(-1); + }); + it('can drop on its direct parent', () => { + const onDrop = jest.fn(); + const wrapper = mount( + + + + + + + + + + + + , + ); + wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragStart', { + clientX: 500, + clientY: 500, + }); + wrapper.find('.dragTargetParent > .rc-tree-node-content-wrapper').simulate('dragEnter', { + clientX: 500, + clientY: 500, + }); + wrapper.find('.dragTargetParent > .rc-tree-node-content-wrapper').simulate('dragOver', { + clientX: 500, + clientY: 500, + }); + wrapper.find('.dragTargetParent > .rc-tree-node-content-wrapper').simulate('drop'); + expect(onDrop).toHaveBeenCalled(); + }); + it('cover window dragend & componentWillUnmount', () => { + const wrapper = mount( + + + + + + + + + + + + , + ); + wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragStart', { + clientX: 500, + clientY: 500, + }); + window.dispatchEvent(new Event('dragend')); + expect(wrapper.instance().state.dragging).toEqual(false); + wrapper.unmount(); + }); + it('dragover self', () => { + const onDrop = jest.fn(); + const wrapper = mount( + + + + + + + + + + + + , + ); + wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragStart', { + clientX: 500, + clientY: 500, + }); + wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragEnter', { + clientX: 400, + clientY: 500, + }); + wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragOver', { + clientX: 600, + clientY: 500, + }); + wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragOver', { + clientX: 600, + clientY: 500, + }); + wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('drop'); + expect(onDrop).not.toHaveBeenCalled(); + }); + it('dragover first half of non-first child', () => { + const onDrop = jest.fn(); + const wrapper = mount( + + + + + + + + + , + ); + wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragStart', { + clientX: 500, + clientY: 500, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragEnter', { + clientX: 500, + clientY: 1, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragOver', { + clientX: 500, + clientY: 1, + }); + wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('drop'); + expect(onDrop.mock.calls[0][0].node.key).toEqual('0-0-1'); + expect(onDrop.mock.calls[0][0].dropPosition).toEqual(2); + }); + }); }); it('renders without errors', () => { diff --git a/tests/TreeMotion.spec.js b/tests/TreeMotion.spec.js index f82da868..0bf462d9 100644 --- a/tests/TreeMotion.spec.js +++ b/tests/TreeMotion.spec.js @@ -129,7 +129,13 @@ describe('Tree Motion', () => { const onMotionStart = jest.fn(); const onMotionEnd = jest.fn(); const wrapper = mount( - + null, + }} + > , ); diff --git a/tests/__snapshots__/Tree.spec.js.snap b/tests/__snapshots__/Tree.spec.js.snap index 42e7d8c1..a2eec8d7 100644 --- a/tests/__snapshots__/Tree.spec.js.snap +++ b/tests/__snapshots__/Tree.spec.js.snap @@ -13,6 +13,19 @@ exports[`Tree Basic check basic render 1`] = ` value="" />
+