├── .babelrc
├── .eslintrc
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── demo
├── .gitignore
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src
│ ├── App.js
│ ├── index.css
│ ├── index.js
│ └── serviceWorker.js
└── yarn.lock
├── package.json
├── src
├── fs-branch.js
├── fs-node.js
├── icons.js
├── index.js
├── module.js
├── shapes.js
└── utils.js
├── webpack_config.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | presets: ["es2017", "stage-2", "react"],
3 | plugins: ["transform-decorators-legacy"],
4 | env: {
5 | test: {
6 | plugins: ["transform-es2015-modules-commonjs"]
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": { "es6": true, "browser": true },
3 | "extends": ["eslint:recommended", "plugin:react/recommended"],
4 | "parser": "babel-eslint",
5 | "settings": {
6 | "react": {
7 | "version": "15.0"
8 | },
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | node_modules
3 | npm-debug.log
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | !build
2 | webpack_config.js
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2018 Eytan Manor
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
13 | all 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-fs-tree
2 |
3 | I am a beautiful React.Component tree and I will help you present FS hierarchy within a React App. No dependencies, just pure awesomeness! My creator has created me after testing few alternatives for [tortilla.academy](https://tortilla.academy), and has found nothing which is as stylish and can show git-diff annotations right out of the box next to each file (here's [an example page](https://tortilla.academy/tutorial/chatty/version/2-0-0/diff/3-0-0) which was built with me).
4 |
5 |
6 |
7 | This is how I look like behind the scenes:
8 |
9 | ```js
10 | import React from 'react'
11 | import FSRoot from 'react-fs-tree'
12 |
13 | const FSTree = () => (
14 |
25 | )
26 | ```
27 |
28 | > A small demonstration app of me can be found in the [`demo` dir](https://github.com/DAB0mB/react-fs-tree/tree/master/demo).
29 |
30 | My nodes will pop and glow once you click on them. I'll be sure to notify you whenever it happens ;-)
31 |
32 | ```js
33 | const onSelect = (node) => {
34 | console.log(`node ${node.path} selected`)
35 | }
36 |
37 | const onOpen = (node) => {
38 | console.log(`node ${node.path} opened`)
39 | }
40 |
41 |
44 | ```
45 |
46 | I can also notify you on...
47 |
48 | - **onSelect(node)**
49 |
50 | - **onDeselect(node)**
51 |
52 | - **onSelectChange(node)**
53 |
54 | - **onOpen(node)**
55 |
56 | - **onClose(node)**
57 |
58 | - **onOpenChange(node)**
59 |
60 | If you really want, you can change my colors with the **theme** prop, using the following variables:
61 |
62 | - **theme.primary**
63 |
64 | - **theme.selectedBackground**
65 |
66 | - **theme.selectedText**
67 |
68 | - **theme.modeM**
69 |
70 | - **theme.modeA**
71 |
72 | - **theme.modeD**
73 |
74 | Each of my nodes is an entity of its own and will do exactly what you tell it to:
75 |
76 | - **node.open()** - Will open the node.
77 |
78 | - **node.close()** - Will close the node.
79 |
80 | - **node.toggleOpen()** - Will open the node if closed and will close the node if opened.
81 |
82 | - **node.select()** - Will select the node.
83 |
84 | - **node.deselect()** - Will deselect the node.
85 |
86 | - **node.toggleSelect()** - Will select the node if deselected and will deselect the node if selected.
87 |
88 | You can also ask for any of its details, like...
89 |
90 | - **node.name** - The name of the node e.g. `foo`.
91 |
92 | - **node.path** - The full path of the node starting from the root e.g. `~/folder/foo`.
93 |
94 | - **node.childNodes** - An array of the direct children of the node.
95 |
96 | - **node.parentNode** - The parent of the node.
97 |
98 | - **node.root** - The greatest ancestor of the node.
99 |
100 | - **node.opened** - Whether the node is opened or not.
101 |
102 | - **node.selected** - Whether the node is selected or not.
103 |
104 | - **node.branchedOut** - Whether the node has offspring or not.
105 |
106 | - **node.virtual** - Whether the node is currently rendered or is just a virtual representation of the its data*.
107 |
108 | > \* A node is an actual instance of its belonging React.Component.
109 |
110 | You should also know that...
111 |
112 | - You can make me non interactive by passing me the `noninteractive` flag. When that happens my nodes won't respond to your clicks, and neither do I. You can ask each of my nodes whether it's interactive or not with `node.noninteractive`.
113 |
114 | - You can pass a git-diff mode that will signify whether the node was added (`'a'`), deleted (`'d'`) or modified (`'m'`). You can ask the node what's its mode at anytime using `node.mode`.
115 |
116 | If you like what you see you can install me with `npm`:
117 |
118 | $ npm install react-fs-tree
119 |
120 | Feel free to use me (MIT license)!
121 |
--------------------------------------------------------------------------------
/demo/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/demo/README.md:
--------------------------------------------------------------------------------
1 | # react-fs-tree example
--------------------------------------------------------------------------------
/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-fs-tree-example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "prop-types": "^15.7.2",
7 | "react": "^16.13.1",
8 | "react-dom": "^16.13.1",
9 | "react-fs-tree": "link:..",
10 | "react-scripts": "2.0.5"
11 | },
12 | "scripts": {
13 | "start": "react-scripts start",
14 | "build": "react-scripts build",
15 | "eject": "react-scripts eject"
16 | },
17 | "eslintConfig": {
18 | "extends": "react-app"
19 | },
20 | "browserslist": [
21 | ">0.2%",
22 | "not dead",
23 | "not ie <= 11",
24 | "not op_mini all"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/demo/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DAB0mB/react-fs-tree/4279938b2d8bbbb4aff3ae5e4a746853da6ffd4a/demo/public/favicon.ico
--------------------------------------------------------------------------------
/demo/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
22 | React App
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/demo/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/demo/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import FSRoot from 'react-fs-tree'
3 |
4 | class App extends React.Component {
5 | render() {
6 | return (
7 |
8 |
19 |
20 | )
21 | }
22 | }
23 |
24 | export default App
25 |
--------------------------------------------------------------------------------
/demo/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
6 | sans-serif;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | }
10 |
11 | code {
12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
13 | monospace;
14 | }
15 |
--------------------------------------------------------------------------------
/demo/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 |
9 | // If you want your app to work offline and load faster, you can change
10 | // unregister() to register() below. Note this comes with some pitfalls.
11 | // Learn more about service workers: http://bit.ly/CRA-PWA
12 | serviceWorker.unregister();
13 |
--------------------------------------------------------------------------------
/demo/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read http://bit.ly/CRA-PWA.
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | installingWorker.onstatechange = () => {
64 | if (installingWorker.state === 'installed') {
65 | if (navigator.serviceWorker.controller) {
66 | // At this point, the updated precached content has been fetched,
67 | // but the previous service worker will still serve the older
68 | // content until all client tabs are closed.
69 | console.log(
70 | 'New content is available and will be used when all ' +
71 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.'
72 | );
73 |
74 | // Execute callback
75 | if (config && config.onUpdate) {
76 | config.onUpdate(registration);
77 | }
78 | } else {
79 | // At this point, everything has been precached.
80 | // It's the perfect time to display a
81 | // "Content is cached for offline use." message.
82 | console.log('Content is cached for offline use.');
83 |
84 | // Execute callback
85 | if (config && config.onSuccess) {
86 | config.onSuccess(registration);
87 | }
88 | }
89 | }
90 | };
91 | };
92 | })
93 | .catch(error => {
94 | console.error('Error during service worker registration:', error);
95 | });
96 | }
97 |
98 | function checkValidServiceWorker(swUrl, config) {
99 | // Check if the service worker can be found. If it can't reload the page.
100 | fetch(swUrl)
101 | .then(response => {
102 | // Ensure service worker exists, and that we really are getting a JS file.
103 | if (
104 | response.status === 404 ||
105 | response.headers.get('content-type').indexOf('javascript') === -1
106 | ) {
107 | // No service worker found. Probably a different app. Reload the page.
108 | navigator.serviceWorker.ready.then(registration => {
109 | registration.unregister().then(() => {
110 | window.location.reload();
111 | });
112 | });
113 | } else {
114 | // Service worker found. Proceed as normal.
115 | registerValidSW(swUrl, config);
116 | }
117 | })
118 | .catch(() => {
119 | console.log(
120 | 'No internet connection found. App is running in offline mode.'
121 | );
122 | });
123 | }
124 |
125 | export function unregister() {
126 | if ('serviceWorker' in navigator) {
127 | navigator.serviceWorker.ready.then(registration => {
128 | registration.unregister();
129 | });
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-fs-tree",
3 | "version": "0.2.0",
4 | "description": "A recursive FS tree React.Component",
5 | "main": "build/react-fs-tree.js",
6 | "scripts": {
7 | "build": "webpack --config webpack_config.js",
8 | "prepublish": "yarn build"
9 | },
10 | "devDependencies": {
11 | "babel-core": "^6.26.3",
12 | "babel-eslint": "^9.0.0",
13 | "babel-loader": "^7.1.5",
14 | "babel-plugin-transform-decorators-legacy": "^1.3.5",
15 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
16 | "babel-polyfill": "^6.26.0",
17 | "babel-preset-es2017": "^6.24.1",
18 | "babel-preset-react": "^6.24.1",
19 | "babel-preset-stage-2": "^6.24.1",
20 | "eslint": "^5.1.0",
21 | "eslint-loader": "^2.1.0",
22 | "eslint-plugin-react": "^7.11.1",
23 | "react": "^16.5.2",
24 | "react-dom": "^16.5.2",
25 | "webpack": "^4.16.1",
26 | "webpack-cli": "^3.1.0",
27 | "webpack-node-externals": "^1.7.2"
28 | },
29 | "peerDependencies": {
30 | "prop-types": "*",
31 | "react": "*",
32 | "react-dom": "*"
33 | },
34 | "dependencies": {}
35 | }
36 |
--------------------------------------------------------------------------------
/src/fs-branch.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import React from 'react'
3 |
4 | import { exports } from './module'
5 | import Shapes from './shapes'
6 |
7 | class FSBranch extends React.Component {
8 | static propTypes = {
9 | childNodes: PropTypes.arrayOf(Shapes.Node).isRequired,
10 | parentNode: PropTypes.instanceOf(React.Component).isRequired,
11 | root: PropTypes.instanceOf(React.Component).isRequired,
12 | depth: PropTypes.number,
13 | noninteractive: PropTypes.bool,
14 | onSelect: PropTypes.func,
15 | onDeselect: PropTypes.func,
16 | onSelectChange: PropTypes.func,
17 | onClose: PropTypes.func,
18 | onOpen: PropTypes.func,
19 | onOpenChange: PropTypes.func,
20 | }
21 |
22 | static defaultProps = {
23 | depth: 0,
24 | noninteractive: false,
25 | onSelect: () => {},
26 | onDeselect: () => {},
27 | onSelectChange: () => {},
28 | onClose: () => {},
29 | onOpen: () => {},
30 | onOpenChange: () => {},
31 | }
32 |
33 | get depth() {
34 | return this.props.depth
35 | }
36 |
37 | get parentNode() {
38 | return this.props.parentNode
39 | }
40 |
41 | get root() {
42 | return this.props.root
43 | }
44 |
45 | get noninteractive() {
46 | return this.props.noninteractive
47 | }
48 |
49 | get childNodes() {
50 | return [...this._childNodes]
51 | }
52 |
53 | get path() {
54 | return this._path
55 | }
56 |
57 | constructor(props) {
58 | super(props)
59 |
60 | this.state = {
61 | childNodes: props.childNodes,
62 | }
63 |
64 | this._path = props.parentNode.path + '/'
65 | this._childNodes = []
66 | }
67 |
68 | UNSAFE_componentWillReceiveProps(nextProps) {
69 | this.setState({
70 | childNodes: nextProps.childNodes,
71 | })
72 | }
73 |
74 | UNSAFE_componentWillUpdate() {
75 | this._childNodes = []
76 | }
77 |
78 | render() {
79 | return (
80 |