├── .babelrc
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── demo
├── app.css
├── app.js
├── index.html
└── index.js
├── index.js
├── node-content-renderer.js
├── node-content-renderer.scss
├── package-lock.json
├── package.json
├── tree-node-renderer.js
├── tree-node-renderer.scss
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["env", {
4 | "targets": {
5 | "browsers": ["last 2 versions", "ie >= 9"]
6 | }
7 | }],
8 | "react"
9 | ],
10 | "plugins": [
11 | "transform-object-rest-spread",
12 | "react-hot-loader/babel"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist
2 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["airbnb", "prettier", "prettier/react"],
3 | "env": {
4 | "browser": true,
5 | "jest": true
6 | },
7 | "rules": {
8 | "react/jsx-filename-extension": 0,
9 | "react/prefer-stateless-function": 0
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | yarn.lock
4 | coverage
5 | cc-test-reporter
6 |
7 | # Editor and other tmp files
8 | *.swp
9 | *.un~
10 | *.iml
11 | *.ipr
12 | *.iws
13 | *.sublime-*
14 | .idea/
15 | *.DS_Store
16 |
17 | # Build directories (Will be preserved by npm)
18 | dist
19 | build
20 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 |
6 | # [2.0.0](https://github.com/frontend-collective/react-sortable-tree-theme-file-explorer/compare/v1.1.3...v2.0.0) (2018-09-04)
7 |
8 |
9 | ### Styles
10 |
11 | * run prettier ([a6aa65e](https://github.com/frontend-collective/react-sortable-tree-theme-file-explorer/commit/a6aa65e))
12 |
13 |
14 | ### BREAKING CHANGES
15 |
16 | * now uses react-sortable-tree@2
17 |
18 |
19 |
20 |
21 | ## [1.1.3](https://github.com/frontend-collective/react-sortable-tree-theme-file-explorer/compare/v1.1.2...v1.1.3) (2018-09-04)
22 |
23 |
24 |
25 |
26 | ## [1.1.2](https://github.com/frontend-collective/react-sortable-tree-theme-file-explorer/compare/v1.1.1...v1.1.2) (2017-11-28)
27 |
28 |
29 | ### Bug Fixes
30 |
31 | * silence warning on latest react-sortable-tree ([7c81d55](https://github.com/frontend-collective/react-sortable-tree-theme-file-explorer/commit/7c81d55))
32 |
33 |
34 |
35 |
36 | ## [1.1.1](https://github.com/frontend-collective/react-sortable-tree-theme-file-explorer/compare/v1.1.0...v1.1.1) (2017-11-01)
37 |
38 |
39 | ### Bug Fixes
40 |
41 | * make canDrag work. Fixes [#5](https://github.com/frontend-collective/react-sortable-tree-theme-file-explorer/issues/5) ([f82d6c1](https://github.com/frontend-collective/react-sortable-tree-theme-file-explorer/commit/f82d6c1))
42 |
43 |
44 |
45 |
46 | # 1.1.0 (2017-10-29)
47 |
48 |
49 | ### Features
50 |
51 | * Complete basic appearance ([98a8d09](https://github.com/frontend-collective/react-sortable-tree/commit/98a8d09))
52 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Fritz
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Sortable Tree File Explorer Theme
2 |
3 | 
4 |
5 | ## Features
6 |
7 | - You can click anywhere on a node to drag it.
8 | - More compact design, with indentation alone used to represent tree depth.
9 |
10 | ## Usage
11 |
12 | ```sh
13 | npm install --save react-sortable-tree-theme-file-explorer
14 | ```
15 |
16 | ```jsx
17 | import React, { Component } from 'react';
18 | import SortableTree from 'react-sortable-tree';
19 | import FileExplorerTheme from 'react-sortable-tree-theme-file-explorer';
20 |
21 | export default class Tree extends Component {
22 | constructor(props) {
23 | super(props);
24 |
25 | this.state = {
26 | treeData: [{ title: 'src/', children: [{ title: 'index.js' }] }],
27 | };
28 | }
29 |
30 | render() {
31 | return (
32 |
33 | this.setState({ treeData })}
36 | theme={FileExplorerTheme}
37 | />
38 |
39 | );
40 | }
41 | }
42 | ```
43 |
--------------------------------------------------------------------------------
/demo/app.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 0;
3 | margin: 0;
4 | }
5 |
--------------------------------------------------------------------------------
/demo/app.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import SortableTree, { toggleExpandedForAll } from 'react-sortable-tree';
3 | import FileExplorerTheme from '../index';
4 | import './app.css';
5 |
6 | class App extends Component {
7 | constructor(props) {
8 | super(props);
9 |
10 | this.state = {
11 | searchString: '',
12 | searchFocusIndex: 0,
13 | searchFoundCount: null,
14 | treeData: [
15 | { title: '.gitignore' },
16 | { title: 'package.json' },
17 | {
18 | title: 'src',
19 | isDirectory: true,
20 | expanded: true,
21 | children: [
22 | { title: 'styles.css' },
23 | { title: 'index.js' },
24 | { title: 'reducers.js' },
25 | { title: 'actions.js' },
26 | { title: 'utils.js' },
27 | ],
28 | },
29 | {
30 | title: 'tmp',
31 | isDirectory: true,
32 | children: [
33 | { title: '12214124-log' },
34 | { title: 'drag-disabled-file', dragDisabled: true },
35 | ],
36 | },
37 | {
38 | title: 'build',
39 | isDirectory: true,
40 | children: [{ title: 'react-sortable-tree.js' }],
41 | },
42 | {
43 | title: 'public',
44 | isDirectory: true,
45 | },
46 | {
47 | title: 'node_modules',
48 | isDirectory: true,
49 | },
50 | ],
51 | };
52 |
53 | this.updateTreeData = this.updateTreeData.bind(this);
54 | this.expandAll = this.expandAll.bind(this);
55 | this.collapseAll = this.collapseAll.bind(this);
56 | }
57 |
58 | updateTreeData(treeData) {
59 | this.setState({ treeData });
60 | }
61 |
62 | expand(expanded) {
63 | this.setState({
64 | treeData: toggleExpandedForAll({
65 | treeData: this.state.treeData,
66 | expanded,
67 | }),
68 | });
69 | }
70 |
71 | expandAll() {
72 | this.expand(true);
73 | }
74 |
75 | collapseAll() {
76 | this.expand(false);
77 | }
78 |
79 | render() {
80 | const {
81 | treeData,
82 | searchString,
83 | searchFocusIndex,
84 | searchFoundCount,
85 | } = this.state;
86 |
87 | const alertNodeInfo = ({ node, path, treeIndex }) => {
88 | const objectString = Object.keys(node)
89 | .map(k => (k === 'children' ? 'children: Array' : `${k}: '${node[k]}'`))
90 | .join(',\n ');
91 |
92 | global.alert(
93 | 'Info passed to the icon and button generators:\n\n' +
94 | `node: {\n ${objectString}\n},\n` +
95 | `path: [${path.join(', ')}],\n` +
96 | `treeIndex: ${treeIndex}`
97 | );
98 | };
99 |
100 | const selectPrevMatch = () =>
101 | this.setState({
102 | searchFocusIndex:
103 | searchFocusIndex !== null
104 | ? (searchFoundCount + searchFocusIndex - 1) % searchFoundCount
105 | : searchFoundCount - 1,
106 | });
107 |
108 | const selectNextMatch = () =>
109 | this.setState({
110 | searchFocusIndex:
111 | searchFocusIndex !== null
112 | ? (searchFocusIndex + 1) % searchFoundCount
113 | : 0,
114 | });
115 |
116 | return (
117 |
120 |
121 |
File Explorer Theme
122 |
123 |
124 |
125 |
166 |
167 |
168 |
169 |
176 | this.setState({
177 | searchFoundCount: matches.length,
178 | searchFocusIndex:
179 | matches.length > 0 ? searchFocusIndex % matches.length : 0,
180 | })
181 | }
182 | canDrag={({ node }) => !node.dragDisabled}
183 | canDrop={({ nextParent }) => !nextParent || nextParent.isDirectory}
184 | generateNodeProps={rowInfo => ({
185 | icons: rowInfo.node.isDirectory
186 | ? [
187 | ,
201 | ]
202 | : [
203 |
213 | F
214 |
,
215 | ],
216 | buttons: [
217 | ,
232 | ],
233 | })}
234 | />
235 |
236 |
237 | );
238 | }
239 | }
240 |
241 | export default App;
242 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/demo/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { AppContainer } from 'react-hot-loader'; // eslint-disable-line import/no-extraneous-dependencies
4 |
5 | const rootEl = document.getElementById('app');
6 | const render = Component => {
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | rootEl
12 | );
13 | };
14 |
15 | /* eslint-disable global-require, import/newline-after-import */
16 | render(require('./app').default);
17 | if (module.hot)
18 | module.hot.accept('./app', () => render(require('./app').default));
19 | /* eslint-enable global-require, import/newline-after-import */
20 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | // Can override the following:
2 | //
3 | // style: PropTypes.shape({}),
4 | // innerStyle: PropTypes.shape({}),
5 | // reactVirtualizedListProps: PropTypes.shape({}),
6 | // scaffoldBlockPxWidth: PropTypes.number,
7 | // slideRegionSize: PropTypes.number,
8 | // rowHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.func]),
9 | // treeNodeRenderer: PropTypes.func,
10 | // nodeContentRenderer: PropTypes.func,
11 | // placeholderRenderer: PropTypes.func,
12 |
13 | import nodeContentRenderer from './node-content-renderer';
14 | import treeNodeRenderer from './tree-node-renderer';
15 |
16 | module.exports = {
17 | nodeContentRenderer,
18 | treeNodeRenderer,
19 | scaffoldBlockPxWidth: 25,
20 | rowHeight: 25,
21 | slideRegionSize: 50,
22 | };
23 |
--------------------------------------------------------------------------------
/node-content-renderer.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import styles from './node-content-renderer.scss';
4 |
5 | function isDescendant(older, younger) {
6 | return (
7 | !!older.children &&
8 | typeof older.children !== 'function' &&
9 | older.children.some(
10 | child => child === younger || isDescendant(child, younger)
11 | )
12 | );
13 | }
14 |
15 | // eslint-disable-next-line react/prefer-stateless-function
16 | class FileThemeNodeContentRenderer extends Component {
17 | render() {
18 | const {
19 | scaffoldBlockPxWidth,
20 | toggleChildrenVisibility,
21 | connectDragPreview,
22 | connectDragSource,
23 | isDragging,
24 | canDrop,
25 | canDrag,
26 | node,
27 | title,
28 | draggedNode,
29 | path,
30 | treeIndex,
31 | isSearchMatch,
32 | isSearchFocus,
33 | icons,
34 | buttons,
35 | className,
36 | style,
37 | didDrop,
38 | lowerSiblingCounts,
39 | listIndex,
40 | swapFrom,
41 | swapLength,
42 | swapDepth,
43 | treeId, // Not needed, but preserved for other renderers
44 | isOver, // Not needed, but preserved for other renderers
45 | parentNode, // Needed for dndManager
46 | rowDirection,
47 | ...otherProps
48 | } = this.props;
49 | const nodeTitle = title || node.title;
50 |
51 | const isDraggedDescendant = draggedNode && isDescendant(draggedNode, node);
52 | const isLandingPadActive = !didDrop && isDragging;
53 |
54 | // Construct the scaffold representing the structure of the tree
55 | const scaffold = [];
56 | lowerSiblingCounts.forEach((lowerSiblingCount, i) => {
57 | scaffold.push(
58 |
63 | );
64 |
65 | if (treeIndex !== listIndex && i === swapDepth) {
66 | // This row has been shifted, and is at the depth of
67 | // the line pointing to the new destination
68 | let highlightLineClass = '';
69 |
70 | if (listIndex === swapFrom + swapLength - 1) {
71 | // This block is on the bottom (target) line
72 | // This block points at the target block (where the row will go when released)
73 | highlightLineClass = styles.highlightBottomLeftCorner;
74 | } else if (treeIndex === swapFrom) {
75 | // This block is on the top (source) line
76 | highlightLineClass = styles.highlightTopLeftCorner;
77 | } else {
78 | // This block is between the bottom and top
79 | highlightLineClass = styles.highlightLineVertical;
80 | }
81 |
82 | scaffold.push(
83 |
91 | );
92 | }
93 | });
94 |
95 | const nodeContent = (
96 |
97 | {toggleChildrenVisibility &&
98 | node.children &&
99 | node.children.length > 0 && (
100 |
189 | );
190 |
191 | return canDrag
192 | ? connectDragSource(nodeContent, { dropEffect: 'copy' })
193 | : nodeContent;
194 | }
195 | }
196 |
197 | FileThemeNodeContentRenderer.defaultProps = {
198 | buttons: [],
199 | canDrag: false,
200 | canDrop: false,
201 | className: '',
202 | draggedNode: null,
203 | icons: [],
204 | isSearchFocus: false,
205 | isSearchMatch: false,
206 | parentNode: null,
207 | style: {},
208 | swapDepth: null,
209 | swapFrom: null,
210 | swapLength: null,
211 | title: null,
212 | toggleChildrenVisibility: null,
213 | };
214 |
215 | FileThemeNodeContentRenderer.propTypes = {
216 | buttons: PropTypes.arrayOf(PropTypes.node),
217 | canDrag: PropTypes.bool,
218 | className: PropTypes.string,
219 | icons: PropTypes.arrayOf(PropTypes.node),
220 | isSearchFocus: PropTypes.bool,
221 | isSearchMatch: PropTypes.bool,
222 | listIndex: PropTypes.number.isRequired,
223 | lowerSiblingCounts: PropTypes.arrayOf(PropTypes.number).isRequired,
224 | node: PropTypes.shape({}).isRequired,
225 | path: PropTypes.arrayOf(
226 | PropTypes.oneOfType([PropTypes.string, PropTypes.number])
227 | ).isRequired,
228 | scaffoldBlockPxWidth: PropTypes.number.isRequired,
229 | style: PropTypes.shape({}),
230 | swapDepth: PropTypes.number,
231 | swapFrom: PropTypes.number,
232 | swapLength: PropTypes.number,
233 | title: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
234 | toggleChildrenVisibility: PropTypes.func,
235 | treeIndex: PropTypes.number.isRequired,
236 | treeId: PropTypes.string.isRequired,
237 | rowDirection: PropTypes.string.isRequired,
238 |
239 | // Drag and drop API functions
240 | // Drag source
241 | connectDragPreview: PropTypes.func.isRequired,
242 | connectDragSource: PropTypes.func.isRequired,
243 | didDrop: PropTypes.bool.isRequired,
244 | draggedNode: PropTypes.shape({}),
245 | isDragging: PropTypes.bool.isRequired,
246 | parentNode: PropTypes.shape({}), // Needed for dndManager
247 | // Drop target
248 | canDrop: PropTypes.bool,
249 | isOver: PropTypes.bool.isRequired,
250 | };
251 |
252 | export default FileThemeNodeContentRenderer;
253 |
--------------------------------------------------------------------------------
/node-content-renderer.scss:
--------------------------------------------------------------------------------
1 | .rowWrapper {
2 | height: 100%;
3 | box-sizing: border-box;
4 | cursor: move;
5 |
6 | &:hover {
7 | opacity: 0.7;
8 | }
9 |
10 | &:active {
11 | opacity: 1;
12 | }
13 | }
14 |
15 | .rowWrapperDragDisabled {
16 | cursor: default;
17 | }
18 |
19 | .row {
20 | height: 100%;
21 | white-space: nowrap;
22 | display: flex;
23 | position: relative;
24 |
25 | & > * {
26 | box-sizing: border-box;
27 | }
28 | }
29 |
30 | /**
31 | * The outline of where the element will go if dropped, displayed while dragging
32 | */
33 | .rowLandingPad {
34 | border: none;
35 | box-shadow: none;
36 | outline: none;
37 |
38 | * {
39 | opacity: 0 !important;
40 | }
41 |
42 | &::before {
43 | background-color: lightblue;
44 | border: 2px dotted black;
45 | content: '';
46 | position: absolute;
47 | top: 0;
48 | right: 0;
49 | bottom: 0;
50 | left: 0;
51 | z-index: -1;
52 | }
53 | }
54 |
55 | /**
56 | * Alternate appearance of the landing pad when the dragged location is invalid
57 | */
58 | .rowCancelPad {
59 | @extend .rowLandingPad;
60 |
61 | &::before {
62 | background-color: #e6a8ad;
63 | }
64 | }
65 |
66 | /**
67 | * Nodes matching the search conditions are highlighted
68 | */
69 | .rowSearchMatch {
70 | box-shadow: inset 0 -7px 7px -3px #0080ff;
71 | }
72 |
73 | /**
74 | * The node that matches the search conditions and is currently focused
75 | */
76 | .rowSearchFocus {
77 | box-shadow: inset 0 -7px 7px -3px #fc6421;
78 | }
79 |
80 | %rowItem {
81 | display: inline-block;
82 | vertical-align: middle;
83 | }
84 |
85 | .rowContents {
86 | @extend %rowItem;
87 | position: relative;
88 | height: 100%;
89 | flex: 1 0 auto;
90 | display: flex;
91 | align-items: center;
92 | justify-content: space-between;
93 | }
94 |
95 | .rowLabel {
96 | @extend %rowItem;
97 | flex: 0 1 auto;
98 | padding-right: 20px;
99 | }
100 |
101 | .rowToolbar {
102 | @extend %rowItem;
103 | flex: 0 1 auto;
104 | display: flex;
105 | }
106 |
107 | .toolbarButton {
108 | @extend %rowItem;
109 | }
110 |
111 | .collapseButton,
112 | .expandButton {
113 | appearance: none;
114 | border: none;
115 | background: transparent;
116 | padding: 0;
117 | z-index: 2;
118 | position: absolute;
119 | top: 45%;
120 | width: 30px;
121 | height: 30px;
122 | transform: translate3d(-50%, -50%, 0);
123 | cursor: pointer;
124 |
125 | &::after {
126 | content: '';
127 | position: absolute;
128 | transform-origin: 7px 4px;
129 | transform: translate3d(-50%, -20%, 0);
130 | border: solid transparent 10px;
131 | border-left-width: 7px;
132 | border-right-width: 7px;
133 | border-top-color: gray;
134 | }
135 |
136 | &:hover::after {
137 | border-top-color: black;
138 | }
139 |
140 | &:focus {
141 | outline: none;
142 |
143 | &::after {
144 | filter: drop-shadow(0 0 1px #83bef9) drop-shadow(0 0 1px #83bef9)
145 | drop-shadow(0 0 1px #83bef9);
146 | }
147 | }
148 | }
149 |
150 | .expandButton::after {
151 | transform: translate3d(-50%, -20%, 0) rotateZ(-90deg);
152 | }
153 |
154 | /**
155 | * Line for under a node with children
156 | */
157 | .lineChildren {
158 | height: 100%;
159 | display: inline-block;
160 | }
161 |
162 | /* ==========================================================================
163 | Scaffold
164 |
165 | Line-overlaid blocks used for showing the tree structure
166 | ========================================================================== */
167 | .lineBlock {
168 | height: 100%;
169 | position: relative;
170 | display: inline-block;
171 | flex: 0 0 auto;
172 | }
173 |
174 | .absoluteLineBlock {
175 | @extend .lineBlock;
176 | position: absolute;
177 | top: 0;
178 | }
179 |
180 | /* Highlight line for pointing to dragged row destination
181 | ========================================================================== */
182 | $highlight-color: #36c2f6;
183 | $highlight-line-size: 6px; // Make it an even number for clean rendering
184 |
185 | /**
186 | * +--+--+
187 | * | | |
188 | * | | |
189 | * | | |
190 | * +--+--+
191 | */
192 | .highlightLineVertical {
193 | z-index: 3;
194 |
195 | &::before {
196 | position: absolute;
197 | content: '';
198 | background-color: $highlight-color;
199 | width: $highlight-line-size;
200 | margin-left: $highlight-line-size / -2;
201 | left: 50%;
202 | top: 0;
203 | height: 100%;
204 | }
205 |
206 | @keyframes arrow-pulse {
207 | $base-multiplier: 10;
208 | 0% {
209 | transform: translate(0, 0);
210 | opacity: 0;
211 | }
212 | 30% {
213 | transform: translate(0, 30% * $base-multiplier);
214 | opacity: 1;
215 | }
216 | 70% {
217 | transform: translate(0, 70% * $base-multiplier);
218 | opacity: 1;
219 | }
220 | 100% {
221 | transform: translate(0, 100% * $base-multiplier);
222 | opacity: 0;
223 | }
224 | }
225 |
226 | &::after {
227 | content: '';
228 | position: absolute;
229 | height: 0;
230 | margin-left: -1 * $highlight-line-size / 2;
231 | left: 50%;
232 | top: 0;
233 | border-left: $highlight-line-size / 2 solid transparent;
234 | border-right: $highlight-line-size / 2 solid transparent;
235 | border-top: $highlight-line-size / 2 solid white;
236 | animation: arrow-pulse 1s infinite linear both;
237 | }
238 | }
239 |
240 | /**
241 | * +-----+
242 | * | |
243 | * | +--+
244 | * | | |
245 | * +--+--+
246 | */
247 | .highlightTopLeftCorner {
248 | &::before {
249 | z-index: 3;
250 | content: '';
251 | position: absolute;
252 | border-top: solid $highlight-line-size $highlight-color;
253 | border-left: solid $highlight-line-size $highlight-color;
254 | box-sizing: border-box;
255 | height: calc(50% + #{$highlight-line-size / 2});
256 | top: 50%;
257 | margin-top: $highlight-line-size / -2;
258 | right: 0;
259 | width: calc(50% + #{$highlight-line-size / 2});
260 | }
261 | }
262 |
263 | /**
264 | * +--+--+
265 | * | | |
266 | * | | |
267 | * | +->|
268 | * +-----+
269 | */
270 | .highlightBottomLeftCorner {
271 | $arrow-size: 7px;
272 | z-index: 3;
273 |
274 | &::before {
275 | content: '';
276 | position: absolute;
277 | border-bottom: solid $highlight-line-size $highlight-color;
278 | border-left: solid $highlight-line-size $highlight-color;
279 | box-sizing: border-box;
280 | height: calc(100% + #{$highlight-line-size / 2});
281 | top: 0;
282 | right: $arrow-size;
283 | width: calc(50% - #{$arrow-size - ($highlight-line-size / 2)});
284 | }
285 |
286 | &::after {
287 | content: '';
288 | position: absolute;
289 | height: 0;
290 | right: 0;
291 | top: 100%;
292 | margin-top: -1 * $arrow-size;
293 | border-top: $arrow-size solid transparent;
294 | border-bottom: $arrow-size solid transparent;
295 | border-left: $arrow-size solid $highlight-color;
296 | }
297 | }
298 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-sortable-tree-theme-file-explorer",
3 | "version": "2.0.0",
4 | "description": "File explorer theme for react-sortable-tree",
5 | "scripts": {
6 | "build": "npm run clean && cross-env NODE_ENV=production TARGET=umd webpack --bail",
7 | "build:demo": "npm run clean:demo && cross-env NODE_ENV=production TARGET=demo webpack --bail",
8 | "clean": "rimraf dist",
9 | "clean:demo": "rimraf build",
10 | "start": "cross-env NODE_ENV=development TARGET=development webpack-dev-server --inline --hot",
11 | "lint": "eslint .",
12 | "prettier": "prettier --single-quote --trailing-comma es5 --write \"**/*.{js,jsx,css,scss}\"",
13 | "prepublishOnly": "npm run lint && npm run test && npm run build",
14 | "test": "jest"
15 | },
16 | "main": "dist/main.js",
17 | "files": [
18 | "dist"
19 | ],
20 | "repository": {
21 | "type": "git",
22 | "url": "https://github.com/frontend-collective/react-sortable-tree-theme-file-explorer"
23 | },
24 | "homepage": "https://github.com/frontend-collective/react-sortable-tree-theme-file-explorer",
25 | "bugs": "https://github.com/frontend-collective/react-sortable-tree-theme-file-explorer/issues",
26 | "authors": [
27 | "Chris Fritz"
28 | ],
29 | "license": "MIT",
30 | "jest": {
31 | "setupTestFrameworkScriptFile": "./node_modules/jest-enzyme/lib/index.js",
32 | "moduleFileExtensions": [
33 | "js",
34 | "jsx",
35 | "json"
36 | ],
37 | "moduleDirectories": [
38 | "node_modules"
39 | ],
40 | "moduleNameMapper": {
41 | "\\.(css|scss|less)$": "identity-obj-proxy"
42 | }
43 | },
44 | "dependencies": {
45 | "lodash.isequal": "^4.4.0",
46 | "prop-types": "^15.6.0",
47 | "react-dnd": "2.5.4",
48 | "react-dnd-html5-backend": "2.5.4",
49 | "react-dnd-scrollzone": "^4.0.0",
50 | "react-virtualized": "^9.13.0"
51 | },
52 | "peerDependencies": {
53 | "react": "^15.3.0 || ^16.0.0",
54 | "react-dom": "^15.3.0 || ^16.0.0",
55 | "react-sortable-tree": "^2.2.0"
56 | },
57 | "devDependencies": {
58 | "autoprefixer": "^7.1.6",
59 | "babel-core": "^6.26.0",
60 | "babel-jest": "^21.2.0",
61 | "babel-loader": "^7.1.2",
62 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
63 | "babel-polyfill": "^6.26.0",
64 | "babel-preset-env": "^1.6.0",
65 | "babel-preset-react": "^6.11.1",
66 | "cross-env": "^5.1.1",
67 | "css-loader": "^0.28.7",
68 | "enzyme": "^3.2.0",
69 | "enzyme-adapter-react-16": "^1.1.0",
70 | "eslint": "^4.12.0",
71 | "eslint-config-airbnb": "^16.0.0",
72 | "eslint-config-prettier": "^2.9.0",
73 | "eslint-loader": "^1.9.0",
74 | "eslint-plugin-import": "^2.8.0",
75 | "eslint-plugin-jsx-a11y": "^6.0.2",
76 | "eslint-plugin-react": "^7.5.1",
77 | "file-loader": "^1.1.5",
78 | "html-webpack-plugin": "^2.30.1",
79 | "identity-obj-proxy": "^3.0.0",
80 | "jest": "^21.2.1",
81 | "jest-enzyme": "^4.0.1",
82 | "json-loader": "^0.5.4",
83 | "node-sass": "^4.7.2",
84 | "postcss-loader": "^2.0.9",
85 | "prettier": "^1.8.2",
86 | "react": "^16.1.1",
87 | "react-addons-shallow-compare": "^15.6.2",
88 | "react-dnd-test-backend": "^2.5.4",
89 | "react-dnd-touch-backend": "^0.3.17",
90 | "react-dom": "^16.1.1",
91 | "react-hot-loader": "^3.1.3",
92 | "react-sortable-tree": "^2.2.0",
93 | "react-test-renderer": "^16.1.1",
94 | "rimraf": "^2.6.2",
95 | "sass-loader": "^6.0.6",
96 | "style-loader": "^0.19.0",
97 | "webpack": "^3.7.1",
98 | "webpack-dev-server": "^2.9.5",
99 | "webpack-node-externals": "^1.6.0"
100 | },
101 | "keywords": [
102 | "react",
103 | "react-component"
104 | ]
105 | }
106 |
--------------------------------------------------------------------------------
/tree-node-renderer.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Children, cloneElement } from 'react';
2 | import PropTypes from 'prop-types';
3 | import styles from './tree-node-renderer.scss';
4 |
5 | class FileThemeTreeNodeRenderer extends Component {
6 | render() {
7 | const {
8 | children,
9 | listIndex,
10 | swapFrom,
11 | swapLength,
12 | swapDepth,
13 | scaffoldBlockPxWidth,
14 | lowerSiblingCounts,
15 | connectDropTarget,
16 | isOver,
17 | draggedNode,
18 | canDrop,
19 | treeIndex,
20 | treeId, // Delete from otherProps
21 | getPrevRow, // Delete from otherProps
22 | node, // Delete from otherProps
23 | path, // Delete from otherProps
24 | rowDirection,
25 | ...otherProps
26 | } = this.props;
27 |
28 | return connectDropTarget(
29 |
30 | {Children.map(children, child =>
31 | cloneElement(child, {
32 | isOver,
33 | canDrop,
34 | draggedNode,
35 | lowerSiblingCounts,
36 | listIndex,
37 | swapFrom,
38 | swapLength,
39 | swapDepth,
40 | })
41 | )}
42 |
43 | );
44 | }
45 | }
46 |
47 | FileThemeTreeNodeRenderer.defaultProps = {
48 | swapFrom: null,
49 | swapDepth: null,
50 | swapLength: null,
51 | canDrop: false,
52 | draggedNode: null,
53 | };
54 |
55 | FileThemeTreeNodeRenderer.propTypes = {
56 | treeIndex: PropTypes.number.isRequired,
57 | treeId: PropTypes.string.isRequired,
58 | swapFrom: PropTypes.number,
59 | swapDepth: PropTypes.number,
60 | swapLength: PropTypes.number,
61 | scaffoldBlockPxWidth: PropTypes.number.isRequired,
62 | lowerSiblingCounts: PropTypes.arrayOf(PropTypes.number).isRequired,
63 |
64 | listIndex: PropTypes.number.isRequired,
65 | children: PropTypes.node.isRequired,
66 |
67 | // Drop target
68 | connectDropTarget: PropTypes.func.isRequired,
69 | isOver: PropTypes.bool.isRequired,
70 | canDrop: PropTypes.bool,
71 | draggedNode: PropTypes.shape({}),
72 |
73 | // used in dndManager
74 | getPrevRow: PropTypes.func.isRequired,
75 | node: PropTypes.shape({}).isRequired,
76 | path: PropTypes.arrayOf(
77 | PropTypes.oneOfType([PropTypes.string, PropTypes.number])
78 | ).isRequired,
79 | rowDirection: PropTypes.string.isRequired,
80 | };
81 |
82 | export default FileThemeTreeNodeRenderer;
83 |
--------------------------------------------------------------------------------
/tree-node-renderer.scss:
--------------------------------------------------------------------------------
1 | .node {
2 | min-width: 100%;
3 | position: relative;
4 | }
5 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const autoprefixer = require('autoprefixer');
4 | const nodeExternals = require('webpack-node-externals');
5 | const HtmlWebpackPlugin = require('html-webpack-plugin');
6 |
7 | const target = process.env.TARGET || 'umd';
8 |
9 | const styleLoader = {
10 | loader: 'style-loader',
11 | options: { insertAt: 'top' },
12 | };
13 |
14 | const fileLoader = {
15 | loader: 'file-loader',
16 | options: { name: 'static/[name].[ext]' },
17 | };
18 |
19 | const postcssLoader = {
20 | loader: 'postcss-loader',
21 | options: {
22 | plugins: () => [
23 | autoprefixer({ browsers: ['IE >= 9', 'last 2 versions', '> 1%'] }),
24 | ],
25 | },
26 | };
27 |
28 | const cssLoader = isLocal => ({
29 | loader: 'css-loader',
30 | options: {
31 | modules: true,
32 | '-autoprefixer': true,
33 | importLoaders: true,
34 | localIdentName: isLocal ? 'rstcustom__[local]' : null,
35 | },
36 | });
37 |
38 | const config = {
39 | entry: './index',
40 | output: {
41 | path: path.join(__dirname, 'dist'),
42 | filename: '[name].js',
43 | libraryTarget: 'umd',
44 | library: 'ReactSortableTreeThemeFileExplorer',
45 | },
46 | devtool: 'source-map',
47 | plugins: [
48 | new webpack.EnvironmentPlugin({ NODE_ENV: 'development' }),
49 | new webpack.optimize.OccurrenceOrderPlugin(),
50 | new webpack.optimize.UglifyJsPlugin({
51 | compress: {
52 | warnings: false,
53 | },
54 | mangle: false,
55 | beautify: true,
56 | comments: true,
57 | }),
58 | ],
59 | module: {
60 | rules: [
61 | {
62 | test: /\.jsx?$/,
63 | use: ['babel-loader'],
64 | exclude: path.join(__dirname, 'node_modules'),
65 | },
66 | {
67 | test: /\.scss$/,
68 | use: [styleLoader, cssLoader(true), postcssLoader, 'sass-loader'],
69 | exclude: path.join(__dirname, 'node_modules'),
70 | },
71 | {
72 | // Used for importing css from external modules (react-virtualized, etc.)
73 | test: /\.css$/,
74 | use: [styleLoader, cssLoader(false), postcssLoader],
75 | },
76 | ],
77 | },
78 | };
79 |
80 | switch (target) {
81 | case 'umd':
82 | // Exclude library dependencies from the bundle
83 | config.externals = [
84 | nodeExternals({
85 | // load non-javascript files with extensions, presumably via loaders
86 | whitelist: [/\.(?!(?:jsx?|json)$).{1,5}$/i],
87 | }),
88 | ];
89 | break;
90 | case 'development':
91 | config.devtool = 'eval';
92 | config.module.rules.push({
93 | test: /\.(jpe?g|png|gif|ico|svg)$/,
94 | use: [fileLoader],
95 | exclude: path.join(__dirname, 'node_modules'),
96 | });
97 | config.entry = ['react-hot-loader/patch', './demo/index'];
98 | config.output = {
99 | path: path.join(__dirname, 'build'),
100 | filename: 'static/[name].js',
101 | };
102 | config.plugins = [
103 | new HtmlWebpackPlugin({
104 | inject: true,
105 | template: './demo/index.html',
106 | }),
107 | new webpack.EnvironmentPlugin({ NODE_ENV: 'development' }),
108 | new webpack.NoEmitOnErrorsPlugin(),
109 | ];
110 | config.devServer = {
111 | contentBase: path.join(__dirname, 'build'),
112 | port: process.env.PORT || 3001,
113 | stats: 'minimal',
114 | };
115 |
116 | break;
117 | case 'demo':
118 | config.module.rules.push({
119 | test: /\.(jpe?g|png|gif|ico|svg)$/,
120 | use: [fileLoader],
121 | exclude: path.join(__dirname, 'node_modules'),
122 | });
123 | config.entry = './demo/index';
124 | config.output = {
125 | path: path.join(__dirname, 'build'),
126 | filename: 'static/[name].js',
127 | };
128 | config.plugins = [
129 | new HtmlWebpackPlugin({
130 | inject: true,
131 | template: './demo/index.html',
132 | }),
133 | new webpack.EnvironmentPlugin({ NODE_ENV: 'production' }),
134 | new webpack.optimize.UglifyJsPlugin({
135 | compress: {
136 | warnings: false,
137 | },
138 | }),
139 | ];
140 |
141 | break;
142 | default:
143 | }
144 |
145 | module.exports = config;
146 |
--------------------------------------------------------------------------------