From 6986a23cd7d654c2d963683f727661d9389fc1f5 Mon Sep 17 00:00:00 2001 From: fritz-c Date: Fri, 4 Aug 2017 09:06:30 +0900 Subject: [PATCH] feat(tree-to-tree): Enable tree-to-tree drag-and-drop Also makes implementation of external nodes more lightweight BREAKING CHANGE: `dropCancelled` and `addNewItem` callbacks on external nodes wrapped with `dndWrapExternalSource` are now ignored. Drag-and-drop now works without them. --- .../__snapshots__/storyshots.test.js.snap | 420 ++++++++++++++++++ examples/storybooks/external-node.js | 18 +- examples/storybooks/index.js | 4 + examples/storybooks/tree-to-tree.js | 63 +++ package-lock.json | 287 +++++++++--- package.json | 16 +- src/node-renderer-default.js | 2 + src/react-sortable-tree.js | 107 ++++- src/tree-node.js | 4 + src/utils/drag-and-drop-utils.js | 39 +- 10 files changed, 821 insertions(+), 139 deletions(-) create mode 100644 examples/storybooks/tree-to-tree.js diff --git a/examples/storybooks/__snapshots__/storyshots.test.js.snap b/examples/storybooks/__snapshots__/storyshots.test.js.snap index 2ba28338..3271086e 100644 --- a/examples/storybooks/__snapshots__/storyshots.test.js.snap +++ b/examples/storybooks/__snapshots__/storyshots.test.js.snap @@ -472,6 +472,426 @@ exports[`Storyshots Advanced Touch support (Experimental) 1`] = ` `; +exports[`Storyshots Advanced Tree-to-tree dragging 1`] = ` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + node1 + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + node2 + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + node3 + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + node4 + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + View source + +
+`; + exports[`Storyshots Basics Add and remove nodes programmatically 1`] = `
diff --git a/examples/storybooks/external-node.js b/examples/storybooks/external-node.js index 691faad6..c683991e 100644 --- a/examples/storybooks/external-node.js +++ b/examples/storybooks/external-node.js @@ -3,7 +3,6 @@ import { DragDropContext } from 'react-dnd'; import HTML5Backend from 'react-dnd-html5-backend'; import { SortableTreeWithoutDndContext as SortableTree, - insertNode, dndWrapExternalSource, } from '../../src'; @@ -47,22 +46,9 @@ class App extends Component {
{ - const { treeData } = insertNode({ - treeData: this.state.treeData, - newNode: newItem.node, - depth: newItem.depth, - minimumTreeIndex: newItem.minimumTreeIndex, - expandParent: true, - getNodeKey: ({ treeIndex }) => treeIndex, - }); - this.setState({ treeData }); - }} + addNewItem={() => {}} // Update the tree appearance post-drag - dropCancelled={() => - this.setState(state => ({ - treeData: state.treeData.concat(), - }))} + dropCancelled={() => {}} /> ↑ drag this
diff --git a/examples/storybooks/index.js b/examples/storybooks/index.js index f155d302..923b3e54 100644 --- a/examples/storybooks/index.js +++ b/examples/storybooks/index.js @@ -7,6 +7,7 @@ import BarebonesExample from './barebones'; import AddRemoveExample from './add-remove'; import ExternalNodeExample from './external-node'; import TouchSupportExample from './touch-support'; +import TreeToTreeExample from './tree-to-tree'; const wrapWithSource = (node, src) =>
@@ -36,4 +37,7 @@ storiesOf('Advanced', module) ) .add('Touch support (Experimental)', () => wrapWithSource(, 'touch-support.js') + ) + .add('Tree-to-tree dragging', () => + wrapWithSource(, 'tree-to-tree.js') ); diff --git a/examples/storybooks/tree-to-tree.js b/examples/storybooks/tree-to-tree.js new file mode 100644 index 00000000..0bb3123c --- /dev/null +++ b/examples/storybooks/tree-to-tree.js @@ -0,0 +1,63 @@ +import React, { Component } from 'react'; +// import { DragDropContext } from 'react-dnd'; +// import HTML5Backend from 'react-dnd-html5-backend'; +import SortableTree from '../../src'; +// SortableTreeWithoutDndContext as SortableTree, +// insertNode, +// dndWrapExternalSource, + +const externalNodeType = 'yourNodeType'; + +class App extends Component { + constructor(props) { + super(props); + + this.state = { + treeData1: [ + { title: 'node1', children: [{ title: 'Child node' }] }, + { title: 'node2' }, + ], + treeData2: [{ title: 'node3' }, { title: 'node4' }], + }; + } + + render() { + return ( +
+
+ this.setState({ treeData1 })} + dndType={externalNodeType} + /> +
+ +
+ this.setState({ treeData2 })} + dndType={externalNodeType} + /> +
+ +
+
+ ); + } +} + +export default App; diff --git a/package-lock.json b/package-lock.json index 18e71d12..9e004753 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,12 +5,12 @@ "requires": true, "dependencies": { "@storybook/addon-actions": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-3.1.9.tgz", - "integrity": "sha1-L7DmhnQrnWRdnGIvAeW6mayri2k=", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-3.2.0.tgz", + "integrity": "sha1-5df2P+yJyuG5i7wSRpQVO3QJMX8=", "dev": true, "requires": { - "@storybook/addons": "3.1.6", + "@storybook/addons": "3.2.0", "deep-equal": "1.0.1", "json-stringify-safe": "5.0.1", "prop-types": "15.5.10", @@ -19,39 +19,39 @@ } }, "@storybook/addon-links": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-3.1.6.tgz", - "integrity": "sha512-+mZSKyehEgS8a2e1xA32hPvoR6C+KHPpPDMc+G47BLCZbn1kMKL5gIQc8MuA14F3RT2h5qHaDbGbcoxqNqXFWQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-3.2.0.tgz", + "integrity": "sha1-KMc9H9f6N1kROfP7FuYKQxE61kM=", "dev": true, "requires": { - "@storybook/addons": "3.1.6" + "@storybook/addons": "3.2.0" } }, "@storybook/addon-notes": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@storybook/addon-notes/-/addon-notes-3.1.6.tgz", - "integrity": "sha512-bTZprF7tuHWxg11ohlmInCWV+nRYz2J9B31zyfqr/qMyyreUgFUbOTYajOpj720VQoPWPRRq48HQgdwvGrQS5Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@storybook/addon-notes/-/addon-notes-3.2.0.tgz", + "integrity": "sha1-57ZyDiYPqxHHWjeZ7l1o0bNJbyw=", "dev": true, "requires": { - "@storybook/addons": "3.1.6", + "@storybook/addons": "3.2.0", "@types/react": "15.6.0", "babel-runtime": "6.25.0", - "prop-types": "15.5.10" + "util-deprecate": "1.0.2" } }, "@storybook/addon-options": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@storybook/addon-options/-/addon-options-3.1.6.tgz", - "integrity": "sha512-n7UrUOTMyBQhdW/FpUrO/RJSG5m4j7bCKs2U30XQ+QOzIhQwyQPMPd9aDU5bqH8Np+4BUvJbKzt+1zjvg29nwQ==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@storybook/addon-options/-/addon-options-3.2.3.tgz", + "integrity": "sha1-6jdA0onUKc52fpaZvzpkfddBWS0=", "dev": true, "requires": { - "@storybook/addons": "3.1.6" + "@storybook/addons": "3.2.0" } }, "@storybook/addon-storyshots": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-storyshots/-/addon-storyshots-3.1.9.tgz", - "integrity": "sha1-U7/1t4jCieyZ78NdwUnmy/u32uU=", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@storybook/addon-storyshots/-/addon-storyshots-3.2.3.tgz", + "integrity": "sha1-deJLpbRCjzYZo3wNQoDWgqZCtqc=", "dev": true, "requires": { "babel-runtime": "6.25.0", @@ -61,39 +61,39 @@ } }, "@storybook/addons": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@storybook/addons/-/addons-3.1.6.tgz", - "integrity": "sha512-O3mMVCEAXdiMWvDSIyTBtxfw5tThzfqpP/EgAQhy9hyzsn6xME2PcNPhG/p6KkPg2FL5mP32FxfakIZIXx/jXQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@storybook/addons/-/addons-3.2.0.tgz", + "integrity": "sha1-4URsxWE68XlwFnMnYmfO5xhZv0E=", "dev": true }, "@storybook/channel-postmessage": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-3.1.6.tgz", - "integrity": "sha512-PNKsEkDUVp6o4WUyLaPlf++jpC02O1c3Q/FTcR7KqdtzEvPims+DoNLaU0yMcaEu0bdQ4nULomxbnamXfiOl2w==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-3.2.0.tgz", + "integrity": "sha1-YS/1MSC/JmZgy5MousmtZxIoovc=", "dev": true, "requires": { - "@storybook/channels": "3.1.6", + "@storybook/channels": "3.2.0", "global": "4.3.2", "json-stringify-safe": "5.0.1" } }, "@storybook/channels": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-3.1.6.tgz", - "integrity": "sha512-WPNREHS5xKuYRNTZP4aE4MrHPKqO7DsLMwmgrZTtLpT9XWV2U+MO+xmsxBk8X+o2m54qq7pdbjLxuUHlL8/QkQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-3.2.0.tgz", + "integrity": "sha1-11OVIS23a0njM19QzOW8djzwtcY=", "dev": true }, "@storybook/react": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/@storybook/react/-/react-3.1.9.tgz", - "integrity": "sha1-gO8iqm9njwkyYAXEOxa1rMMx+7I=", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-3.2.3.tgz", + "integrity": "sha1-Tlr/gLYTKCiNz546ayookOq4gL4=", "dev": true, "requires": { - "@storybook/addon-actions": "3.1.9", - "@storybook/addon-links": "3.1.6", - "@storybook/addons": "3.1.6", - "@storybook/channel-postmessage": "3.1.6", - "@storybook/ui": "3.1.9", + "@storybook/addon-actions": "3.2.0", + "@storybook/addon-links": "3.2.0", + "@storybook/addons": "3.2.0", + "@storybook/channel-postmessage": "3.2.0", + "@storybook/ui": "3.2.3", "airbnb-js-shims": "1.3.0", "autoprefixer": "7.1.2", "babel-core": "6.25.0", @@ -106,7 +106,7 @@ "babel-preset-stage-0": "6.24.1", "babel-runtime": "6.25.0", "case-sensitive-paths-webpack-plugin": "2.1.1", - "chalk": "1.1.3", + "chalk": "2.0.1", "commander": "2.11.0", "common-tags": "1.4.0", "configstore": "3.1.1", @@ -114,7 +114,7 @@ "express": "4.15.3", "file-loader": "0.11.2", "find-cache-dir": "1.0.0", - "glamor": "2.20.37", + "glamor": "2.20.39", "glamorous": "3.25.0", "global": "4.3.2", "json-loader": "0.5.7", @@ -122,7 +122,7 @@ "json5": "0.5.1", "lodash.flattendeep": "4.4.0", "lodash.pick": "4.4.0", - "postcss-flexbugs-fixes": "3.0.0", + "postcss-flexbugs-fixes": "3.2.0", "postcss-loader": "2.0.6", "prop-types": "15.5.10", "qs": "6.5.0", @@ -140,6 +140,17 @@ "webpack-hot-middleware": "2.18.2" }, "dependencies": { + "chalk": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.0.1.tgz", + "integrity": "sha512-Mp+FXEI+FrwY/XYV45b2YD3E8i3HwnEAoFcM0qlZzq/RZ9RwWitt2Y/c7cqRAz70U7hfekqx6qNYthuKFO6K0g==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.2.1" + } + }, "style-loader": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.17.0.tgz", @@ -164,9 +175,9 @@ } }, "@storybook/ui": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/@storybook/ui/-/ui-3.1.9.tgz", - "integrity": "sha1-OOmgqvIeSffSD5s5NEmon+xxV1k=", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@storybook/ui/-/ui-3.2.3.tgz", + "integrity": "sha1-lXnBkaFpXT1aqrzvk6elRW2PGa8=", "dev": true, "requires": { "@storybook/react-fuzzy": "0.4.0", @@ -183,10 +194,12 @@ "podda": "1.2.2", "prop-types": "15.5.10", "qs": "6.5.0", + "react-icons": "2.2.5", "react-inspector": "2.1.3", "react-komposer": "2.0.0", "react-modal": "1.9.7", "react-split-pane": "0.1.65", + "react-treebeard": "2.0.3", "redux": "3.7.2" } }, @@ -435,6 +448,12 @@ "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", "dev": true }, + "array-find": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-find/-/array-find-1.0.0.tgz", + "integrity": "sha1-bI4obRHtdoMn+OYuzuhzU8o+eLg=", + "dev": true + }, "array-find-index": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", @@ -2128,6 +2147,12 @@ "lazy-cache": "1.0.4" } }, + "chain-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/chain-function/-/chain-function-1.0.0.tgz", + "integrity": "sha1-DUqzfn4Y6tC9xHuSB2QRjOWHM9w=", + "dev": true + }, "chalk": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", @@ -2605,9 +2630,9 @@ } }, "cross-env": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.0.1.tgz", - "integrity": "sha1-/05y6kO0faJIa0On8gQ7JgnkSRM=", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.0.3.tgz", + "integrity": "sha1-j1Ws73Rp/tNk9AOan37OkBkeOYE=", "dev": true, "requires": { "cross-spawn": "5.1.0", @@ -4001,9 +4026,9 @@ "dev": true }, "fast-memoize": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.2.7.tgz", - "integrity": "sha1-8UXFwiA5zt8KHU/2ylkq0CaEcMo=", + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.2.8.tgz", + "integrity": "sha512-3ppTC3fZ9Vwtjslx8DkhSIbI9PH1nM4pobuTHQINOxTxchG8n3SDGZ8L6jbatGJCGLKR+gbkNWKFN4E1iUROSA==", "dev": true }, "fastparse": { @@ -5316,15 +5341,16 @@ } }, "glamor": { - "version": "2.20.37", - "resolved": "https://registry.npmjs.org/glamor/-/glamor-2.20.37.tgz", - "integrity": "sha512-+zfncOt+qhno7li2wDih/Q+F6ugtAJQ7E+V3NJ6SZ6rbkOAOa4yEvRLfC9NnNqYp0XIpH/MW6bW5x4Pua41DIQ==", + "version": "2.20.39", + "resolved": "https://registry.npmjs.org/glamor/-/glamor-2.20.39.tgz", + "integrity": "sha512-tQ25Rmuad18jybnQgnsBzjhayPaK5Izy7SApBAaRnYQ50uA96SQEAoyymA8QNan51QyK1mB7dFUCX0OyaKa0yg==", "dev": true, "requires": { "fbjs": "0.8.14", - "inline-style-prefixer": "3.0.6", + "inline-style-prefixer": "3.0.7", "object-assign": "4.1.1", - "prop-types": "15.5.10" + "prop-types": "15.5.10", + "through": "2.3.8" } }, "glamorous": { @@ -5334,7 +5360,7 @@ "dev": true, "requires": { "brcast": "2.0.2", - "fast-memoize": "2.2.7", + "fast-memoize": "2.2.8", "html-tag-names": "1.1.2", "react-html-attributes": "1.4.1", "svg-tag-names": "1.1.1" @@ -5680,9 +5706,9 @@ "dev": true }, "html-webpack-plugin": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-2.29.0.tgz", - "integrity": "sha1-6Yf0IYU9O2k4yMTIFxhC5f0XryM=", + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-2.30.1.tgz", + "integrity": "sha1-f5xCG36pHsRg9WUn1430hO51N9U=", "dev": true, "requires": { "bluebird": "3.5.0", @@ -5906,9 +5932,9 @@ "dev": true }, "inline-style-prefixer": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-3.0.6.tgz", - "integrity": "sha1-sn/jCbQWijHq84yOjGCrnnwRcx8=", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-3.0.7.tgz", + "integrity": "sha1-DMyS5ZAv5uDSjZdcQlhEP4gGFfg=", "dev": true, "requires": { "bowser": "1.7.1", @@ -9053,9 +9079,9 @@ } }, "postcss-flexbugs-fixes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-3.0.0.tgz", - "integrity": "sha1-ezHLbCfQQXo1pnkUwpX4PEA8ftQ=", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-3.2.0.tgz", + "integrity": "sha512-0AuD9HG1Ey3/3nqPWu9yqf7rL0KCPu5VgjDsjf5mzEcuo9H/z8nco/fljKgjsOUrZypa95MI0kS4xBZeBzz2lw==", "dev": true, "requires": { "postcss": "6.0.8" @@ -9991,6 +10017,36 @@ "integrity": "sha1-DPf4T5Rj/wrlHExLFC2VvjdyTZw=", "dev": true }, + "radium": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/radium/-/radium-0.19.4.tgz", + "integrity": "sha1-VqpJ/eYYHS9eH6V7RxD/0MI96CA=", + "dev": true, + "requires": { + "array-find": "1.0.0", + "exenv": "1.2.2", + "inline-style-prefixer": "2.0.5", + "prop-types": "15.5.10" + }, + "dependencies": { + "exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=", + "dev": true + }, + "inline-style-prefixer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-2.0.5.tgz", + "integrity": "sha1-wVPH6I/YT+9cYC6VqBaLJ3BnH+c=", + "dev": true, + "requires": { + "bowser": "1.7.1", + "hyphenate-style-name": "1.0.2" + } + } + } + }, "raf": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/raf/-/raf-3.3.2.tgz", @@ -10183,9 +10239,9 @@ } }, "react-dom-factories": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/react-dom-factories/-/react-dom-factories-1.0.0.tgz", - "integrity": "sha1-9DwF5QUbME8zJRYY1byFmynka20=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/react-dom-factories/-/react-dom-factories-1.0.1.tgz", + "integrity": "sha1-xQaSrF/xrbOdht/m2+NIXaz1hFU=", "dev": true }, "react-hot-loader": { @@ -10222,6 +10278,35 @@ "html-element-attributes": "1.3.0" } }, + "react-icon-base": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/react-icon-base/-/react-icon-base-2.0.7.tgz", + "integrity": "sha1-C9GHNr1s55ym1pzoOHoH+41M7/4=", + "dev": true, + "requires": { + "prop-types": "15.5.8" + }, + "dependencies": { + "prop-types": { + "version": "15.5.8", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.5.8.tgz", + "integrity": "sha1-a3suFBCDvjjIWVqlH8VXdccZk5Q=", + "dev": true, + "requires": { + "fbjs": "0.8.14" + } + } + } + }, + "react-icons": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-2.2.5.tgz", + "integrity": "sha1-+UJQHCGkzARWziu+5QMsk/YFHc8=", + "dev": true, + "requires": { + "react-icon-base": "2.0.7" + } + }, "react-inspector": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/react-inspector/-/react-inspector-2.1.3.tgz", @@ -10256,7 +10341,7 @@ "exenv": "1.2.0", "lodash.assign": "4.2.0", "prop-types": "15.5.10", - "react-dom-factories": "1.0.0" + "react-dom-factories": "1.0.1" } }, "react-proxy": { @@ -10284,7 +10369,7 @@ "integrity": "sha1-N8C16lyWCCfn82IdChFLD4yciRg=", "dev": true, "requires": { - "inline-style-prefixer": "3.0.6", + "inline-style-prefixer": "3.0.7", "prop-types": "15.5.10", "react-style-proptype": "3.0.0" } @@ -10317,6 +10402,33 @@ "object-assign": "4.1.1" } }, + "react-transition-group": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-1.2.0.tgz", + "integrity": "sha1-tR/JIbDDg1p+98Vxx5/ILHPpIE8=", + "dev": true, + "requires": { + "chain-function": "1.0.0", + "dom-helpers": "3.2.1", + "loose-envify": "1.3.1", + "prop-types": "15.5.10", + "warning": "3.0.0" + } + }, + "react-treebeard": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/react-treebeard/-/react-treebeard-2.0.3.tgz", + "integrity": "sha512-fqSmx0RPuUxvG5kQantyW89HnOCjz6e53o3AajuaXSVWL8O8tXNUCoSdxaO6xScXpujIV+iVaYUGBpOm5Pdhiw==", + "dev": true, + "requires": { + "babel-runtime": "6.25.0", + "deep-equal": "1.0.1", + "prop-types": "15.5.10", + "radium": "0.19.4", + "shallowequal": "0.2.2", + "velocity-react": "1.3.3" + } + }, "react-virtualized": { "version": "9.9.0", "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.9.0.tgz", @@ -12181,6 +12293,32 @@ "integrity": "sha1-Z1Neu2lMHVIldFeYRmUyP1h+jTc=", "dev": true }, + "velocity-animate": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/velocity-animate/-/velocity-animate-1.5.0.tgz", + "integrity": "sha1-/Idx2N/hE2/wKnB+EPuwlXxLAw8=", + "dev": true + }, + "velocity-react": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/velocity-react/-/velocity-react-1.3.3.tgz", + "integrity": "sha1-1tRyds/Ivip1Yjh5sgFArFjBuCs=", + "dev": true, + "requires": { + "lodash": "3.10.1", + "prop-types": "15.5.10", + "react-transition-group": "1.2.0", + "velocity-animate": "1.5.0" + }, + "dependencies": { + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "dev": true + } + } + }, "vendors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.1.tgz", @@ -12214,6 +12352,15 @@ "makeerror": "1.0.11" } }, + "warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", + "integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=", + "dev": true, + "requires": { + "loose-envify": "1.3.1" + } + }, "watch": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/watch/-/watch-0.10.0.tgz", diff --git a/package.json b/package.json index 43e43d5f..a6f935bf 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "dependencies": { "lodash.isequal": "^4.4.0", "prop-types": "^15.5.8", - "react-dnd": "^2.1.4", + "react-dnd": ">=2.0.0 <=2.4.0", "react-dnd-html5-backend": "^2.1.2", "react-dnd-scrollzone": "^4.0.0", "react-virtualized": "^9.9.0" @@ -60,11 +60,11 @@ "react-dom": "^15.3.0" }, "devDependencies": { - "@storybook/addon-actions": "^3.1.9", - "@storybook/addon-notes": "^3.1.6", - "@storybook/addon-options": "^3.1.6", - "@storybook/addon-storyshots": "^3.1.9", - "@storybook/react": "^3.1.9", + "@storybook/addon-actions": "^3.2.0", + "@storybook/addon-notes": "^3.2.0", + "@storybook/addon-options": "^3.2.3", + "@storybook/addon-storyshots": "^3.2.3", + "@storybook/react": "^3.2.3", "autoprefixer": "^7.1.2", "babel-cli": "^6.10.1", "babel-core": "^6.10.4", @@ -75,7 +75,7 @@ "babel-polyfill": "^6.23.0", "babel-preset-es2015": "^6.9.0", "babel-preset-react": "^6.11.1", - "cross-env": "^5.0.1", + "cross-env": "^5.0.3", "css-loader": "^0.28.4", "enzyme": "^2.9.1", "eslint": "^3.19.0", @@ -87,7 +87,7 @@ "eslint-plugin-react": "^7.1.0", "file-loader": "^0.11.2", "gh-pages": "^1.0.0", - "html-webpack-plugin": "^2.29.0", + "html-webpack-plugin": "^2.30.1", "identity-obj-proxy": "^3.0.0", "jest": "^20.0.4", "jest-enzyme": "^3.5.3", diff --git a/src/node-renderer-default.js b/src/node-renderer-default.js index 9915722d..89f95f41 100644 --- a/src/node-renderer-default.js +++ b/src/node-renderer-default.js @@ -41,6 +41,7 @@ class NodeRendererDefault extends Component { parentNode: _parentNode, // Needed for drag-and-drop utils endDrag: _endDrag, // Needed for drag-and-drop utils startDrag: _startDrag, // Needed for drag-and-drop utils + treeId: _treeId, // Needed for drag-and-drop utils /* eslint-enable no-unused-vars */ ...otherProps } = this.props; @@ -217,6 +218,7 @@ NodeRendererDefault.propTypes = { parentNode: PropTypes.shape({}), // Needed for drag-and-drop utils startDrag: PropTypes.func.isRequired, // Needed for drag-and-drop utils endDrag: PropTypes.func.isRequired, // Needed for drag-and-drop utils + treeId: PropTypes.string.isRequired, // Needed for drag-and-drop utils isDragging: PropTypes.bool.isRequired, didDrop: PropTypes.bool.isRequired, draggedNode: PropTypes.shape({}), diff --git a/src/react-sortable-tree.js b/src/react-sortable-tree.js index 7ed92a57..03735c9f 100644 --- a/src/react-sortable-tree.js +++ b/src/react-sortable-tree.js @@ -37,7 +37,7 @@ import { } from './utils/drag-and-drop-utils'; import styles from './react-sortable-tree.scss'; -let dndTypeCounter = 1; +let treeIdCounter = 1; class ReactSortableTree extends Component { constructor(props) { @@ -52,8 +52,9 @@ class ReactSortableTree extends Component { } = props; // Wrapping classes for use with react-dnd - this.dndType = dndType || `rst__${dndTypeCounter}`; - dndTypeCounter += 1; + this.treeId = `rst__${treeIdCounter}`; + treeIdCounter += 1; + this.dndType = dndType || this.treeId; this.nodeContentRenderer = dndWrapSource(nodeContentRenderer, this.dndType); this.treeNodeRenderer = dndWrapTarget(TreeNode, this.dndType); @@ -79,12 +80,21 @@ class ReactSortableTree extends Component { this.startDrag = this.startDrag.bind(this); this.dragHover = this.dragHover.bind(this); this.endDrag = this.endDrag.bind(this); + this.drop = this.drop.bind(this); + this.handleDndMonitorChange = this.handleDndMonitorChange.bind(this); } componentWillMount() { this.loadLazyChildren(); this.search(this.props, false, false); this.ignoreOneTreeUpdate = false; + + // Hook into react-dnd state changes to detect when the drag ends + // TODO: This is very brittle, so it needs to be replaced if react-dnd + // offers a more official way to detect when a drag ends + this.clearMonitorSubscription = this.context.dragDropManager + .getMonitor() + .subscribeToStateChange(this.handleDndMonitorChange); } componentWillReceiveProps(nextProps) { @@ -114,6 +124,10 @@ class ReactSortableTree extends Component { } } + componentWillUnmount() { + this.clearMonitorSubscription(); + } + getRows(treeData) { return getFlatDataFromTree({ ignoreCollapsed: true, @@ -122,6 +136,22 @@ class ReactSortableTree extends Component { }); } + handleDndMonitorChange() { + const monitor = this.context.dragDropManager.getMonitor(); + // If the drag ends and the tree is still in a mid-drag state, + // it means that the drag was canceled or the dragSource dropped + // elsewhere, and we should reset the state of this tree + if (!monitor.isDragging() && this.state.draggingTreeData) { + this.setState({ + draggingTreeData: null, + swapFrom: null, + swapLength: null, + swapDepth: null, + rows: this.getRows(this.props.treeData), + }); + } + } + toggleChildrenVisibility({ node: targetNode, path }) { const treeData = changeNodeAtPath({ treeData: this.props.treeData, @@ -141,7 +171,13 @@ class ReactSortableTree extends Component { } } - moveNode({ node, depth, minimumTreeIndex }) { + moveNode({ + node, + path: prevPath, + treeIndex: prevTreeIndex, + depth, + minimumTreeIndex, + }) { const { treeData, treeIndex, path } = insertNode({ treeData: this.state.draggingTreeData, newNode: node, @@ -154,7 +190,16 @@ class ReactSortableTree extends Component { this.props.onChange(treeData); if (this.props.onMoveNode) { - this.props.onMoveNode({ treeData, node, treeIndex, path }); + this.props.onMoveNode({ + treeData, + node, + treeIndex, + path, + nextPath: path, + nextTreeIndex: treeIndex, + prevPath, + prevTreeIndex, + }); } } @@ -273,7 +318,8 @@ class ReactSortableTree extends Component { } endDrag(dropResult) { - if (!dropResult || !dropResult.node) { + // Drop was cancelled + if (!dropResult) { this.setState({ draggingTreeData: null, swapFrom: null, @@ -281,16 +327,32 @@ class ReactSortableTree extends Component { swapDepth: null, rows: this.getRows(this.props.treeData), }); - - return; + } else if (dropResult.treeId !== this.treeId) { + const { node, path: prevPath, treeIndex: prevTreeIndex } = dropResult; + // The node was dropped in an external drop target or tree + const treeData = this.state.draggingTreeData || this.props.treeData; + this.props.onChange(treeData); + + if (this.props.onMoveNode) { + this.props.onMoveNode({ + treeData, + node, + treeIndex: null, + path: null, + nextPath: null, + nextTreeIndex: null, + prevPath, + prevTreeIndex, + }); + } } + } + drop(dropResult) { this.moveNode(dropResult); } - /** -* Load any children in the tree that are given by a function -*/ + // Load any children in the tree that are given by a function loadLazyChildren(props = this.props) { walk({ treeData: props.treeData, @@ -371,38 +433,41 @@ class ReactSortableTree extends Component { const rowCanDrag = typeof canDrag !== 'function' ? canDrag : canDrag(callbackParams); + const sharedProps = { + treeIndex, + scaffoldBlockPxWidth, + node, + path, + treeId: this.treeId, + }; + return ( @@ -615,6 +680,10 @@ ReactSortableTree.defaultProps = { style: {}, }; +ReactSortableTree.contextTypes = { + dragDropManager: PropTypes.shape({}), +}; + // Export the tree component without the react-dnd DragDropContext, // for when component is used with other components using react-dnd. // see: https://github.com/gaearon/react-dnd/issues/186 diff --git a/src/tree-node.js b/src/tree-node.js index e4082bc1..5c53af07 100644 --- a/src/tree-node.js +++ b/src/tree-node.js @@ -26,6 +26,8 @@ class TreeNode extends Component { node: _node, // Delete from otherProps path: _path, // Delete from otherProps treeData: _treeData, // Delete from otherProps + treeId: _treeId, // Delete from otherProps + drop: _drop, // Delete from otherProps /* eslint-enable no-unused-vars */ ...otherProps } = this.props; @@ -177,8 +179,10 @@ TreeNode.propTypes = { dragHover: PropTypes.func.isRequired, // used in drag-and-drop-utils getNodeKey: PropTypes.func.isRequired, // used in drag-and-drop-utils getPrevRow: PropTypes.func.isRequired, // used in drag-and-drop-utils + drop: PropTypes.func.isRequired, // used in drag-and-drop-utils maxDepth: PropTypes.number, // used in drag-and-drop-utils treeData: PropTypes.arrayOf(PropTypes.object), // used in drag-and-drop-utils + treeId: PropTypes.string.isRequired, // used in drag-and-drop-utils }; export default TreeNode; diff --git a/src/utils/drag-and-drop-utils.js b/src/utils/drag-and-drop-utils.js index 2dda476e..de1527b8 100644 --- a/src/utils/drag-and-drop-utils.js +++ b/src/utils/drag-and-drop-utils.js @@ -19,6 +19,7 @@ const nodeDragSource = { parentNode: props.parentNode, path: props.path, treeIndex: props.treeIndex, + treeId: props.treeId, }; }, @@ -41,19 +42,11 @@ const externalSource = { ...props.node, }, path: [], - type: 'rst__NewItem', + treeId: null, parentNode: null, treeIndex: -1, // Use -1 to indicate external node }; }, - - endDrag(props, monitor) { - if (!monitor.didDrop()) { - props.dropCancelled(); - } else { - props.addNewItem(monitor.getDropResult()); - } - }, }; function getTargetDepth(dropTargetProps, monitor, component) { @@ -70,8 +63,8 @@ function getTargetDepth(dropTargetProps, monitor, component) { } let blocksOffset; - if (monitor.getItem().type === 'rst__NewItem') { - // Add new node from external source + // When adding node from external source + if (monitor.getItem().treeId !== dropTargetProps.treeId) { if (component) { const relativePosition = findDOMNode(component).getBoundingClientRect(); // eslint-disable-line react/no-find-dom-node const leftShift = @@ -113,7 +106,7 @@ function getTargetDepth(dropTargetProps, monitor, component) { return targetDepth; } -function canDrop(dropTargetProps, monitor, component) { +function canDrop(dropTargetProps, monitor) { if (!monitor.isOver()) { return false; } @@ -121,7 +114,7 @@ function canDrop(dropTargetProps, monitor, component) { const rowAbove = dropTargetProps.getPrevRow(); const abovePath = rowAbove ? rowAbove.path : []; const aboveNode = rowAbove ? rowAbove.node : {}; - const targetDepth = getTargetDepth(dropTargetProps, monitor, component); + const targetDepth = getTargetDepth(dropTargetProps, monitor, null); // Cannot drop if we're adding to the children of the row above and // the row above is a function @@ -159,12 +152,18 @@ function canDrop(dropTargetProps, monitor, component) { const nodeDropTarget = { drop(dropTargetProps, monitor, component) { - return { + const result = { node: monitor.getItem().node, path: monitor.getItem().path, + treeIndex: monitor.getItem().treeIndex, + treeId: dropTargetProps.treeId, minimumTreeIndex: dropTargetProps.treeIndex, depth: getTargetDepth(dropTargetProps, monitor, component), }; + + dropTargetProps.drop(result); + + return result; }, hover(dropTargetProps, monitor, component) { @@ -229,23 +228,11 @@ export function dndWrapExternalSource(UserComponent, type) { } } - // these defaultProps must be passed to the custom external node component as props - DndWrapExternalSource.defaultProps = { - dropCancelled() { - throw new Error('External Nodes must define dropCancelled prop function'); - }, - addNewItem() { - throw new Error('External Nodes must define addNewItem prop function'); - }, - }; - DndWrapExternalSource.propTypes = { connectDragSource: PropTypes.func.isRequired, /* eslint-disable react/no-unused-prop-types */ // The following are used within the react-dnd lifecycle hooks node: PropTypes.shape({}).isRequired, - dropCancelled: PropTypes.func.isRequired, - addNewItem: PropTypes.func.isRequired, /* eslint-enable react/no-unused-prop-types */ };