From 002ea2e752c5908e60142e59c5bd1204b07c23ad Mon Sep 17 00:00:00 2001 From: Hrusikesh Panda Date: Wed, 4 Apr 2018 21:05:56 -0400 Subject: [PATCH] feat: Display Partially selected nodes :sparkles: (#73) --- .eslintrc.json | 11 +- .vscode/settings.json | 9 + README.md | 49 +- __snapshots__/src/checkbox/index.test.js.md | 23 + __snapshots__/src/checkbox/index.test.js.snap | Bin 0 -> 252 bytes docs/.eslintrc.json | 5 + docs/src/App.js | 71 ++- docs/src/index.css | 26 +- docs/src/stories/Options/Checkbox.js | 37 ++ docs/src/stories/Options/data.json | 603 ++++++++++++++++++ docs/src/stories/Options/index.css | 19 + docs/src/stories/Options/index.js | 71 +++ docs/src/stories/Simple/data.json | 603 ++++++++++++++++++ docs/src/stories/Simple/index.css | 19 + docs/src/stories/Simple/index.js | 38 ++ docs/webpack.config.js | 4 +- package.json | 8 +- src/checkbox/index.js | 18 + src/checkbox/index.test.js | 28 + src/index.js | 42 +- src/tag/index.js | 4 +- src/tree-manager/flatten-tree.js | 48 +- src/tree-manager/getPartialState.js | 6 + src/tree-manager/index.js | 72 ++- src/tree-manager/index.test.js | 433 ------------- .../{ => tests}/flatten-tree.test.js | 49 +- src/tree-manager/tests/index.test.js | 504 +++++++++++++++ .../{ => tests}/initialState.test.js | 144 +++-- src/tree-manager/tests/partial-setup.js | 20 + src/tree-manager/tests/partialSelect.test.js | 78 +++ .../tests/stateTransitions.test.js | 140 ++++ src/tree-node/index.js | 31 +- src/tree-node/node-label.js | 21 +- src/tree/index.js | 40 +- src/tree/index.test.js | 28 +- webpack.config.js | 16 +- yarn.lock | 14 +- 37 files changed, 2626 insertions(+), 706 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 __snapshots__/src/checkbox/index.test.js.md create mode 100644 __snapshots__/src/checkbox/index.test.js.snap create mode 100644 docs/.eslintrc.json create mode 100644 docs/src/stories/Options/Checkbox.js create mode 100644 docs/src/stories/Options/data.json create mode 100644 docs/src/stories/Options/index.css create mode 100644 docs/src/stories/Options/index.js create mode 100644 docs/src/stories/Simple/data.json create mode 100644 docs/src/stories/Simple/index.css create mode 100644 docs/src/stories/Simple/index.js create mode 100644 src/checkbox/index.js create mode 100644 src/checkbox/index.test.js create mode 100644 src/tree-manager/getPartialState.js delete mode 100644 src/tree-manager/index.test.js rename src/tree-manager/{ => tests}/flatten-tree.test.js (83%) create mode 100644 src/tree-manager/tests/index.test.js rename src/tree-manager/{ => tests}/initialState.test.js (50%) create mode 100644 src/tree-manager/tests/partial-setup.js create mode 100644 src/tree-manager/tests/partialSelect.test.js create mode 100644 src/tree-manager/tests/stateTransitions.test.js diff --git a/.eslintrc.json b/.eslintrc.json index 4a629a9e..43cbf6a0 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,6 +1,6 @@ { "parser": "babel-eslint", - "extends": ["standard", "react-app"], + "extends": ["standard", "react-app", "eslint:recommended"], "rules": { "react/jsx-filename-extension": [ 1, @@ -15,6 +15,15 @@ } ], "import/prefer-default-export": 0, + "object-curly-newline": [ + "error", + { + "ObjectExpression": { "multiline": true, "minProperties": 3 }, + "ObjectPattern": { "multiline": true, "minProperties": 3 }, + "ImportDeclaration": { "multiline": true, "minProperties": 3 }, + "ExportDeclaration": { "multiline": true, "minProperties": 3 } + } + ], "no-underscore-dangle": 0, "no-plusplus": 0, "no-shadow": 0, diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..4559024b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "spellright.language": "en", + "spellright.documentTypes": [ + "markdown", + "latex", + "plaintext", + "javascript" + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 272fbddd..845325ff 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ ## React Dropdown Tree Select -A lightweight and fast control to render a select component that can display hierarchical tree data. In addition, the control shows the selection in pills and allows user to search the options for quick filtering and selection. +A lightweight and fast control to render a select component that can display hierarchical tree data. In addition, the control shows the selection in pills and allows user to search the options for quick filtering and selection. Also supports displaying partially selected nodes. ## Table of Contents @@ -35,6 +35,8 @@ A lightweight and fast control to render a select component that can display hie * [With Material Design](#with-material-design) * [As Single Select](#as-single-select) * [Install](#install) + * [As NPM package](#as-npm-package) + * [Using a CDN](#using-a-cdn) * [Peer Dependencies](#peer-dependencies) * [Usage](#usage) * [Props](#props) @@ -46,6 +48,7 @@ A lightweight and fast control to render a select component that can display hie * [noMatchesText](#noMatchesText) * [keepTreeOnSearch](#keeptreeonsearch) * [simpleSelect](#simpleselect) + * [showPartiallySelected](#showPartiallySelected) * [Styling and Customization](#styling-and-customization) * [Using default styles](#default-styles) * [Customizing with Bootstrap, Material Design styles](#customizing-styles) @@ -83,24 +86,46 @@ Online demo: https://dowjones.github.io/react-dropdown-tree-select/#/story/simpl ## Install -``` -> npm i react-dropdown-tree-select +### As NPM package + +```js +npm i react-dropdown-tree-select // or if using yarn +yarn add react-dropdown-tree-select +``` + +### Using a CDN -> yarn add react-dropdown-tree-select +You can import the standalone UMD build from a CDN such as: + +```html + + ``` +**Note:** Above example will always fetch the latest version. To fetch a specific version, use `https://unpkg.com/react-dropdown-tree-select@/dist/...` +Visit [unpkg.com](https://unpkg.com/#/) to see other options. + ### Peer Dependencies In order to avoid version conflicts in your project, you must specify and install [react](https://www.npmjs.com/package/react), [react-dom](https://www.npmjs.com/package/react-dom) as [peer dependencies](https://nodejs.org/en/blog/npm/peer-dependencies/). Note that NPM doesn't install peer dependencies automatically. Instead it will show you a warning message with instructions on how to install them. +If you're using the UMD builds, you'd also need to install the peer dependencies in your application: + +```html + + +``` + ## Usage ```jsx import React from 'react' import ReactDOM from 'react-dom' + import DropdownTreeSelect from 'react-dropdown-tree-select' +import 'react-dropdown-tree-select/dist/styles.css' const data = { label: 'search me', @@ -129,15 +154,7 @@ const onNodeToggle = currentNode => { console.log('onNodeToggle::', currentNode) } -ReactDOM.render( - , - document.body -) // in real world, you'd want to render to an element, instead of body. +ReactDOM.render(, document.body) // in real world, you'd want to render to an element, instead of body. ``` ## Props @@ -244,6 +261,12 @@ Type: `bool` (default: `false`) Turns the dropdown into a simple, single select dropdown. If you pass tree data, only immediate children are picked, grandchildren nodes are ignored. Defaults to `false`. +### showPartiallySelected + +Type: `bool` (default: `false`) + +If set to true, shows checkboxes in a partial state when one, but not all of their children are selected. Allows styling of partially selected nodes as well, by using [:indeterminate](https://developer.mozilla.org/en-US/docs/Web/CSS/:indeterminate) pseudo class. Simply add desired styles to `.node.partial .checkbox-item:indeterminate { ... }` in your CSS. + ## Styling and Customization ### Default styles diff --git a/__snapshots__/src/checkbox/index.test.js.md b/__snapshots__/src/checkbox/index.test.js.md new file mode 100644 index 00000000..ac19e9ef --- /dev/null +++ b/__snapshots__/src/checkbox/index.test.js.md @@ -0,0 +1,23 @@ +# Snapshot report for `src\checkbox\index.test.js` + +The actual snapshot is saved in `index.test.js.snap`. + +Generated by [AVA](https://ava.li). + +## Checkbox component + +> Snapshot 1 + + + +## renders disabled state + +> Snapshot 1 + + diff --git a/__snapshots__/src/checkbox/index.test.js.snap b/__snapshots__/src/checkbox/index.test.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..8120bd31f8454897a613660ba1062a5269149d60 GIT binary patch literal 252 zcmVu`E%yAg44vGf%fDH8Ht_5d@eRnInM;;(@pwh(#D#gcw;f^9o8!1O!DG z*@YO{{FAa$L5kr_K|w*L|Md)vJPeFX42*n0JxrpE%nTr%oXI(f#l?P!xv4M_w&KLx zf}B(|bu1;71#l%C$r-81*-7~oB)GMXa<_7%WELkT<)o&71sDa0av}hhFH)F^0RRAY Cu4u9V literal 0 HcmV?d00001 diff --git a/docs/.eslintrc.json b/docs/.eslintrc.json new file mode 100644 index 00000000..7753f32e --- /dev/null +++ b/docs/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-console": 0 + } +} diff --git a/docs/src/App.js b/docs/src/App.js index 0b16d767..941b2773 100644 --- a/docs/src/App.js +++ b/docs/src/App.js @@ -1,9 +1,11 @@ import React from 'react' import ReactStory, { defaultProps } from 'react-story' -import CodeSandbox from './CodeSandbox.js' -import HOCReadme from './stories/HOCReadme.js' -import Readme from './stories/Readme.js' +import CodeSandbox from './CodeSandbox' +import HOCReadme from './stories/HOCReadme' +import Readme from './stories/Readme' +import Simple from './stories/Simple' +import Options from './stories/Options' import './stories/utils/prism.css' @@ -11,7 +13,8 @@ const stories = [ { name: 'Readme', component: Readme }, { name: 'HOC Readme', component: HOCReadme }, - { name: 'With Vanilla Styles', component: CodeSandbox('v0nmw5ykk5') }, + { name: 'Basic (no extra styles)', component: Simple }, + { name: 'Options', component: Options }, { name: 'With Bootstrap Styles', component: CodeSandbox('382pjronm') }, { name: 'With Material Design Styles', component: CodeSandbox('2o1pv6925p') }, { name: 'With Country flags', component: CodeSandbox('6w41wlvj8z') }, @@ -31,35 +34,33 @@ const stories = [ { name: 'Tree Node Paths (HOC)', component: CodeSandbox('l765q6lmrq') } ] -export default class App extends React.Component { - render() { - return ( - ( + ( + ( - -
- - )} - stories={stories} - /> - ) - } -} + > +
+ + )} + stories={stories} + /> +) + +export default App diff --git a/docs/src/index.css b/docs/src/index.css index 3d6ba913..a9519a7c 100644 --- a/docs/src/index.css +++ b/docs/src/index.css @@ -1,29 +1,11 @@ -html, body, #app { +html, +body, +#app { height: 100%; } body { margin: 0; padding: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', Helvetica, Arial, sans-serif;; -} - -.suspended .node-label { - font-style: italic; - text-decoration: line-through; -} - -.dropdown-content { - max-height: 400px; - overflow-y: auto; -} - -.node .fa { - font-size: 12px; - margin-left: 4px; - cursor: pointer; -} - -.node .fa-ban { - color: darkorange; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', Helvetica, Arial, sans-serif; } diff --git a/docs/src/stories/Options/Checkbox.js b/docs/src/stories/Options/Checkbox.js new file mode 100644 index 00000000..81abe342 --- /dev/null +++ b/docs/src/stories/Options/Checkbox.js @@ -0,0 +1,37 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' + +class Checkbox extends PureComponent { + state = {isChecked: this.props.checked || false} + + toggleCheckboxChange = () => { + const { onChange, value } = this.props + + this.setState(({ isChecked }) => ({isChecked: !isChecked})) + + onChange(value) + } + + render() { + const { label, value } = this.props + const { isChecked } = this.state + + return ( +
+ +
+ ) + } +} + +Checkbox.propTypes = { + label: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + checked: PropTypes.bool +} + +export default Checkbox diff --git a/docs/src/stories/Options/data.json b/docs/src/stories/Options/data.json new file mode 100644 index 00000000..dec07969 --- /dev/null +++ b/docs/src/stories/Options/data.json @@ -0,0 +1,603 @@ +[ + { + "label": "VP Accounting", + "tagClassName": "special", + "children": [ + { + "label": "iWay", + "children": [ + { "label": "Universidad de Especialidades del Espíritu Santo" }, + { "label": "Marmara University" }, + { "label": "Baghdad College of Pharmacy" } + ] + }, + { "label": "KDB", "children": [{ "label": "Latvian University of Agriculture" }, { "label": "Dublin Institute of Technology" }] }, + { + "label": "Justice", + "children": [ + { "label": "Baylor University" }, + { "label": "Massachusetts College of Art" }, + { "label": "Universidad Técnica Latinoamericana" }, + { "label": "Saint Louis College" }, + { "label": "Scott Christian University" } + ] + }, + { + "label": "Utilization Review", + "children": [ + { "label": "University of Minnesota - Twin Cities Campus" }, + { "label": "Moldova State Agricultural University" }, + { "label": "Andrews University" }, + { "label": "Usmanu Danfodiyo University Sokoto" } + ] + }, + { + "label": "Norton Utilities", + "children": [ + { "label": "Universidad Autónoma del Caribe" }, + { "label": "National University of Uzbekistan" }, + { "label": "Ladoke Akintola University of Technology" }, + { "label": "Kohat University of Science and Technology (KUST)" }, + { "label": "Hvanneyri Agricultural University" } + ] + } + ] + }, + { + "label": "Database Administrator III", + "disabled": true, + "children": [ + { + "label": "TFS", + "children": [ + { "label": "University of Jazeera" }, + { "label": "Technical University of Crete" }, + { "label": "Ecole Nationale Supérieure d'Agronomie et des Industries Alimentaires" }, + { "label": "Ho Chi Minh City University of Natural Sciences" } + ] + }, + { + "label": "Overhaul", + "children": [ + { "label": "Technological University (Taunggyi)" }, + { "label": "Universidad de Las Palmas de Gran Canaria" }, + { "label": "Olympia College" }, + { "label": "Franklin and Marshall College" }, + { "label": "State University of New York College of Environmental Science and Forestry" } + ] + }, + { + "label": "GTK", + "children": [ + { "label": "Salisbury State University" }, + { "label": "Evangelische Fachhochschule für Religionspädagogik, und Gemeindediakonie Moritzburg" }, + { "label": "Kilimanjaro Christian Medical College" } + ] + }, + { + "label": "SRP", + "children": [ + { "label": "Toyo Gakuen University" }, + { "label": "Riyadh College of Dentistry and Pharmacy" }, + { "label": "Aichi Gakusen University" } + ] + } + ] + }, + { + "label": "Assistant Manager", + "children": [ + { + "label": "Risk Analysis", + "children": [{ "label": "Seijo University" }, { "label": "University of Economics Varna" }, { "label": "College of Technology at Riyadh" }] + }, + { "label": "UV Mapping", "children": [{ "label": "Universidad de La Sabana" }, { "label": "Pamukkale University" }] } + ] + }, + { + "label": "Quality Engineer", + "children": [ + { + "label": "Enzyme Kinetics", + "children": [ + { "label": "Universidad del Valle de Guatemala" }, + { "label": "Ecole Nationale Supérieure d'Electronique, d'Electrotechnique, d'Informatique et d'Hydraulique de Toulouse" }, + { "label": "Kota Bharu Polytechnic" }, + { "label": "College of Technology at Kharj" } + ] + }, + { + "label": "Gastroenterology", + "children": [ + { "label": "Balochistan University of Engineering and Technology Khuzdar" }, + { "label": "Université de Cergy-Pontoise" }, + { "label": "Frederick University" } + ] + }, + { + "label": "ADP Payroll", + "children": [ + { "label": "National University" }, + { "label": "Ecole de l'Air" }, + { "label": "Vietnam National University of Agriculture" }, + { "label": "St. Petersburg State University of Aerospace Instrumentation" } + ] + } + ] + }, + { + "label": "Senior Sales Associate", + "children": [ + { + "label": "RSVP", + "children": [ + { "label": "Islamic Azad University, Ahar" }, + { "label": "Okinawa International University" }, + { "label": "Karlshochschule International University" } + ] + }, + { + "label": "IxChariot", + "children": [{ "label": "Cambodia University of Specialties" }, { "label": "Ecole Supérieure des Techniques Industrielles et des Textiles" }] + } + ] + }, + { + "label": "Automation Specialist I", + "children": [ + { + "label": "Ffmpeg", + "children": [{ "label": "Christian Heritage College" }, { "label": "Inha University" }, { "label": "Khalifa University" }] + }, + { + "label": "Mac OS", + "children": [ + { "label": "Prague College" }, + { "label": "Wakayama Medical College" }, + { "label": "South University of Science and Technology of China " }, + { "label": "Campbell University" } + ] + }, + { "label": "Visual Communication", "children": [{ "label": "University of the Cordilleras" }, { "label": "University of Mohaghegh" }] }, + { + "label": "PCRF", + "children": [ + { "label": "FPT University" }, + { "label": "Rakuno Gakuen University" }, + { "label": "Xiangtan Normal University" }, + { "label": "Rice University" }, + { "label": "Sapporo Gakuin University" } + ] + } + ] + }, + { + "label": "Technical Writer", + "children": [ + { + "label": "NC-Verilog", + "children": [ + { "label": "Etisalat University College" }, + { "label": "Newcastle University, Medicine Malaysia " }, + { "label": "University of Asia Pacific, Dhanmondi" }, + { "label": "Leading University" } + ] + }, + { + "label": "Water Treatment", + "children": [ + { "label": "Gorgan University of Agricultural Sciences and Natural Resources" }, + { "label": "Tianjin Polytechnic University" }, + { "label": "Universitas Bojonegoro" } + ] + }, + { + "label": "FTO", + "children": [ + { "label": "Université de Skikda" }, + { "label": "University College of Technology & Innovation (UCTI)" }, + { "label": "Ahmedabad University" }, + { "label": "Universidad Intercontinental" }, + { "label": "Atlantic Union College" } + ] + }, + { + "label": "Vocational Rehabilitation", + "children": [{ "label": "Cambodia University of Specialties" }, { "label": "Universiteit Antwerpen Management School" }] + }, + { + "label": "DH+", + "children": [ + { "label": "Universidad de Córdoba" }, + { "label": "Université Lumière de Bujumbura" }, + { "label": "Madonna University" }, + { "label": "University of Washington" } + ] + } + ] + }, + { + "label": "Software Test Engineer IV", + "children": [ + { + "label": "Zero Waste", + "children": [ + { "label": "University of Italian Studies for Foreigners of Siena" }, + { "label": "Klaipeda University" }, + { "label": "Tallinn University" } + ] + }, + { + "label": "Fixed Assets", + "children": [ + { "label": "North Carolina Central University" }, + { "label": "Universidad Nacional de San Luis" }, + { "label": "Baha'i Institute for Higher Education" } + ] + } + ] + }, + { + "label": "Nuclear Power Engineer", + "children": [ + { + "label": "Woodworking", + "children": [ + { "label": "National Chiayi University" }, + { "label": "Tokyo Kasei University" }, + { "label": "Auchi Polytechnic" }, + { "label": "Hashemite University" }, + { "label": "Thomas Jefferson University" } + ] + }, + { + "label": "Psychotherapy", + "children": [ + { "label": "Trident University" }, + { "label": "Université de N'Djamena" }, + { "label": "Parsons School of Design" }, + { "label": "University of San Diego" } + ] + }, + { + "label": "Credit Unions", + "children": [{ "label": "Tilburg University" }, { "label": "Miyazaki University" }, { "label": "Ohio State University - Newark" }] + } + ] + }, + { + "label": "Media Manager III", + "children": [ + { + "label": "BPL", + "children": [ + { "label": "International Burch University" }, + { "label": "Trinity International University" }, + { "label": "Universidad Autónoma de Centro América" }, + { "label": "Evangelische Fachhochschule Nürnberg" }, + { "label": "Academy of Music, Dance and Fine Arts" } + ] + }, + { + "label": "NVU", + "children": [{ "label": "University Institute of Modern Languages" }, { "label": "Kyungil University" }, { "label": "Jimma University" }] + }, + { + "label": "Zooarchaeology", + "children": [ + { "label": "Hebei Medical University" }, + { "label": "Bharath Institue Of Higher Education & Research" }, + { "label": "Universität Hannover" } + ] + } + ] + }, + { + "label": "Safety Technician IV", + "children": [ + { + "label": "IOT", + "children": [ + { "label": "Belarussian National Technical University" }, + { "label": "Tokyo University of Pharmacy and Life Science" }, + { "label": "Brickfields Asia College" }, + { "label": "Samar State University" }, + { "label": "West Bengal University of Technology" } + ] + }, + { + "label": "Lymphatic Drainage", + "children": [ + { "label": "Free University Amsterdam" }, + { "label": "Friedrich-Alexander Universität Erlangen-Nürnberg" }, + { "label": "Sinnar University" }, + { "label": "Okayama University of Science" } + ] + }, + { + "label": "OLAP Cube Studio", + "children": [ + { "label": "Liaoning Technical University" }, + { "label": "Instituto Superior D. Afonso III - INUAF" }, + { "label": "Kossuth Lajos University" } + ] + }, + { "label": "DSM-IV", "children": [{ "label": "Daniel Webster College" }, { "label": "University of Athens" }] } + ] + }, + { + "label": "Nuclear Power Engineer", + "children": [ + { + "label": "Guest Lecturing", + "children": [ + { "label": "National Pingtung Teachers College" }, + { "label": "Advance Tertiary College" }, + { "label": "Louisiana State University Health Sciences Center New Orleans" }, + { "label": "University of Shiga Prefecture" } + ] + }, + { + "label": "Ayurveda", + "children": [{ "label": "Paichai University" }, { "label": "Universidad Sergio Arboleda" }, { "label": "Lansbridge University" }] + }, + { + "label": "Aeronautics", + "children": [ + { "label": "Ecole Nationale Supérieure d'Ingénieurs en Mécanique et Energétique de Valenciennes" }, + { "label": "Ajou University" }, + { "label": "Islamic Azad University, Tehran Science & Research Branch" }, + { "label": "University of Michigan - Flint" }, + { "label": "University of Ferrara" } + ] + } + ] + }, + { + "label": "Civil Engineer", + "children": [ + { + "label": "Architectural Illustration", + "children": [{ "label": "Detroit College of Law" }, { "label": "European Carolus Magnus University" }] + }, + { + "label": "Teaching Writing", + "children": [ + { "label": "Virginia Intermont College" }, + { "label": "Polytechnic of Namibia" }, + { "label": "Kigali Independent University" }, + { "label": "Nepal Sanskrit University" } + ] + }, + { + "label": "MS Excel Pivot Tables", + "children": [ + { "label": "Newcastle University, Medicine Malaysia " }, + { "label": "National Fisheries University" }, + { "label": "Université d'Antsiranana" }, + { "label": "Shenyang Polytechnic University" } + ] + } + ] + }, + { + "label": "Senior Editor", + "children": [ + { + "label": "Semantic HTML", + "children": [ + { "label": "Southwest University of Finance and Economics" }, + { "label": "Civil Aviation University of China" }, + { "label": "Belarussian State Technological University" } + ] + }, + { + "label": "ASP", + "children": [ + { "label": "Kyoto Tachibana Women's University" }, + { "label": "Ursuline College" }, + { "label": "York University" }, + { "label": "Jewish University in Moscow" } + ] + }, + { "label": "OCLC Connexion", "children": [{ "label": "New York University" }, { "label": "Pittsburg State University" }] }, + { + "label": "Rural Marketing", + "children": [ + { "label": "Universidad de Cartagena" }, + { "label": "Czech University of Agriculture Prague" }, + { "label": "Tohoku Women's College" }, + { "label": "Gunma University" }, + { "label": "Minsk State Linguistic University" } + ] + }, + { "label": "DDI", "children": [{ "label": "Voronezh State Technical University" }, { "label": "University Center \"César Ritz\"" }] } + ] + }, + { + "label": "Media Manager IV", + "children": [ + { + "label": "Yamaha PM5D", + "children": [ + { "label": "Mooreland University" }, + { "label": "Universidad de San Pablo CEU" }, + { "label": "Universidad Galileo" }, + { "label": "College of Technology at Abha" }, + { "label": "Cabrini College" } + ] + }, + { + "label": "HSE Management Systems", + "children": [ + { "label": "Grinnell College" }, + { "label": "Chinju National University of Education" }, + { "label": "Dokkyo University School of Medicine" }, + { "label": "University of New England, Westbrook College Campus" }, + { "label": "Miami University of Ohio - Hamilton" } + ] + } + ] + }, + { + "label": "Product Engineer", + "children": [ + { + "label": "Multi-Unit", + "children": [ + { "label": "Strayer University" }, + { "label": "National Kaohsiung University of Applied Sciences" }, + { "label": "Philadelphia University" }, + { "label": "National Institute of Mental Health and Neuro Sciences" }, + { "label": "Vardhaman Mahaveer Open University" } + ] + }, + { "label": "FX Trading", "children": [{ "label": "Universidade Estácio de Sá" }, { "label": "Manipal University" }] } + ] + }, + { + "label": "Account Coordinator", + "children": [ + { + "label": "Biostatistics", + "children": [ + { "label": "Al-Bukhari International University" }, + { "label": "Technical University of Denmark" }, + { "label": "Postgraduate lnstitute of Medical Education and Research" } + ] + }, + { "label": "FM", "children": [{ "label": "University of Oxford" }, { "label": "Lawrence University" }, { "label": "Okayama University" }] }, + { + "label": "Microsoft Certified Professional", + "children": [ + { "label": "Universidade Católica de Brasília" }, + { "label": "Georgia Institute of Technology" }, + { "label": "University of Petrosani" } + ] + } + ] + }, + { + "label": "Payment Adjustment Coordinator", + "children": [ + { + "label": "Federal Grants Management", + "children": [ + { "label": "Christ University" }, + { "label": "Janos Selye University" }, + { "label": "Zagazig University" }, + { "label": "Constantin Brancoveanu University Pitesti" }, + { "label": "Southwest University of Political Science and Law" } + ] + }, + { + "label": "Company Set-up", + "children": [{ "label": "Ball State University" }, { "label": "Mustafa Kemal University" }, { "label": "Transylvania University" }] + }, + { + "label": "CDMA", + "children": [ + { "label": "College of Telecommunication & Information " }, + { "label": "Nagasaki Prefectural University" }, + { "label": "Gustav-Siewerth-Akademie" } + ] + }, + { + "label": "Overhead Cranes", + "children": [ + { "label": "Universidad de Pamplona" }, + { "label": "Bindura University of Science Education" }, + { "label": "Daiichi University of Economics" }, + { "label": "Wirtschaftsuniversität Wien" } + ] + }, + { + "label": "CDO", + "children": [{ "label": "Design Institute of San Diego" }, { "label": "Wellspring University" }, { "label": "Franciscan School of Theology" }] + } + ] + }, + { + "label": "Assistant Manager", + "children": [ + { + "label": "SQL Server Management Studio", + "children": [ + { "label": "University of Sudbury" }, + { "label": "Evangelische Fachhochschule Berlin, Fachhochschule für Sozialarbeit und Sozialpädagogik" }, + { "label": "Vitebsk State University" }, + { "label": "San Jose Christian College" }, + { "label": "Ivanovo State University" } + ] + }, + { + "label": "Abstracting", + "children": [ + { "label": "Adeyemi College of Education" }, + { "label": "Université de Sherbrooke" }, + { "label": "University College of Applied Sciences" }, + { "label": "Johns Hopkins University, SAIS Bologna Center" } + ] + }, + { + "label": "WTL", + "children": [ + { "label": "Universidad de Córdoba" }, + { "label": "Institut National Polytechnique de Grenoble" }, + { "label": "Kyonggi University" } + ] + } + ] + }, + { + "label": "Professor", + "children": [ + { + "label": "People Skills", + "children": [ + { "label": "University of Calcutta" }, + { "label": "Universidad del Valle del Cauca" }, + { "label": "FAST - National University of Computer and Emerging Sciences (NUCES)" } + ] + }, + { + "label": "Workforce Development", + "children": [ + { "label": "Shandong Medical University" }, + { "label": "Al Khawarizmi International College" }, + { "label": "Nippon Dental University" }, + { "label": "Komsomolsk-on-Amur State Technical University" }, + { "label": "Lingnan University" } + ] + }, + { + "label": "Digital Journalism", + "children": [ + { "label": "The College of St. Scholastica" }, + { "label": "Universidad Autónoma de la Ciudad de México" }, + { "label": "University of Information Technology and Management in Rzeszow" }, + { "label": "Liaquat University of Medical & Health Sciences Jamshoro" } + ] + }, + { + "label": "Short Films", + "children": [ + { "label": "Universidad Católica de Valencia" }, + { "label": "Columbia International University" }, + { "label": "Framingham State College" }, + { "label": "Gurukul University" }, + { "label": "NTI University" } + ] + }, + { + "label": "XML Programming", + "children": [ + { "label": "Victoria University" }, + { "label": "Andrews University" }, + { "label": "Centre Universitaire d'Oum El Bouaghi" }, + { "label": "Dilla University" } + ] + } + ] + } +] diff --git a/docs/src/stories/Options/index.css b/docs/src/stories/Options/index.css new file mode 100644 index 00000000..a8e07534 --- /dev/null +++ b/docs/src/stories/Options/index.css @@ -0,0 +1,19 @@ +.suspended .node-label { + font-style: italic; + text-decoration: line-through; +} + +.dropdown-content { + max-height: 400px; + overflow-y: auto; +} + +.node .fa { + font-size: 12px; + margin-left: 4px; + cursor: pointer; +} + +.node .fa-ban { + color: darkorange; +} diff --git a/docs/src/stories/Options/index.js b/docs/src/stories/Options/index.js new file mode 100644 index 00000000..2f0e7201 --- /dev/null +++ b/docs/src/stories/Options/index.js @@ -0,0 +1,71 @@ +import React, { PureComponent } from 'react' + +import Checkbox from './Checkbox' +import DropdownTreeSelect from '../../../../src' +import '../../../../dist/styles.css' + +import './index.css' +import data from './data.json' + +class WithOptions extends PureComponent { + constructor(props) { + super(props) + + this.state = { + keepTreeOnSearch: false, + simpleSelect: false, + showPartiallySelected: false + } + } + + onChange = (curNode, selectedNodes) => { + console.log('onChange::', curNode, selectedNodes) + } + onAction = ({ action, node }) => { + console.log(`onAction:: [${action}]`, node) + } + onNodeToggle = curNode => { + console.log('onNodeToggle::', curNode) + } + + onOptionsChange = value => { + this.setState({ [value]: !this.state[value] }) + } + + render() { + const { + keepTreeOnSearch, simpleSelect, showPartiallySelected +} = this.state + + return ( +
+

Options playground

+

Toggle different options to see its effect on the dropdown.

+
+ + + +
+
+ +
+
+ ) + } +} +export default WithOptions diff --git a/docs/src/stories/Simple/data.json b/docs/src/stories/Simple/data.json new file mode 100644 index 00000000..dec07969 --- /dev/null +++ b/docs/src/stories/Simple/data.json @@ -0,0 +1,603 @@ +[ + { + "label": "VP Accounting", + "tagClassName": "special", + "children": [ + { + "label": "iWay", + "children": [ + { "label": "Universidad de Especialidades del Espíritu Santo" }, + { "label": "Marmara University" }, + { "label": "Baghdad College of Pharmacy" } + ] + }, + { "label": "KDB", "children": [{ "label": "Latvian University of Agriculture" }, { "label": "Dublin Institute of Technology" }] }, + { + "label": "Justice", + "children": [ + { "label": "Baylor University" }, + { "label": "Massachusetts College of Art" }, + { "label": "Universidad Técnica Latinoamericana" }, + { "label": "Saint Louis College" }, + { "label": "Scott Christian University" } + ] + }, + { + "label": "Utilization Review", + "children": [ + { "label": "University of Minnesota - Twin Cities Campus" }, + { "label": "Moldova State Agricultural University" }, + { "label": "Andrews University" }, + { "label": "Usmanu Danfodiyo University Sokoto" } + ] + }, + { + "label": "Norton Utilities", + "children": [ + { "label": "Universidad Autónoma del Caribe" }, + { "label": "National University of Uzbekistan" }, + { "label": "Ladoke Akintola University of Technology" }, + { "label": "Kohat University of Science and Technology (KUST)" }, + { "label": "Hvanneyri Agricultural University" } + ] + } + ] + }, + { + "label": "Database Administrator III", + "disabled": true, + "children": [ + { + "label": "TFS", + "children": [ + { "label": "University of Jazeera" }, + { "label": "Technical University of Crete" }, + { "label": "Ecole Nationale Supérieure d'Agronomie et des Industries Alimentaires" }, + { "label": "Ho Chi Minh City University of Natural Sciences" } + ] + }, + { + "label": "Overhaul", + "children": [ + { "label": "Technological University (Taunggyi)" }, + { "label": "Universidad de Las Palmas de Gran Canaria" }, + { "label": "Olympia College" }, + { "label": "Franklin and Marshall College" }, + { "label": "State University of New York College of Environmental Science and Forestry" } + ] + }, + { + "label": "GTK", + "children": [ + { "label": "Salisbury State University" }, + { "label": "Evangelische Fachhochschule für Religionspädagogik, und Gemeindediakonie Moritzburg" }, + { "label": "Kilimanjaro Christian Medical College" } + ] + }, + { + "label": "SRP", + "children": [ + { "label": "Toyo Gakuen University" }, + { "label": "Riyadh College of Dentistry and Pharmacy" }, + { "label": "Aichi Gakusen University" } + ] + } + ] + }, + { + "label": "Assistant Manager", + "children": [ + { + "label": "Risk Analysis", + "children": [{ "label": "Seijo University" }, { "label": "University of Economics Varna" }, { "label": "College of Technology at Riyadh" }] + }, + { "label": "UV Mapping", "children": [{ "label": "Universidad de La Sabana" }, { "label": "Pamukkale University" }] } + ] + }, + { + "label": "Quality Engineer", + "children": [ + { + "label": "Enzyme Kinetics", + "children": [ + { "label": "Universidad del Valle de Guatemala" }, + { "label": "Ecole Nationale Supérieure d'Electronique, d'Electrotechnique, d'Informatique et d'Hydraulique de Toulouse" }, + { "label": "Kota Bharu Polytechnic" }, + { "label": "College of Technology at Kharj" } + ] + }, + { + "label": "Gastroenterology", + "children": [ + { "label": "Balochistan University of Engineering and Technology Khuzdar" }, + { "label": "Université de Cergy-Pontoise" }, + { "label": "Frederick University" } + ] + }, + { + "label": "ADP Payroll", + "children": [ + { "label": "National University" }, + { "label": "Ecole de l'Air" }, + { "label": "Vietnam National University of Agriculture" }, + { "label": "St. Petersburg State University of Aerospace Instrumentation" } + ] + } + ] + }, + { + "label": "Senior Sales Associate", + "children": [ + { + "label": "RSVP", + "children": [ + { "label": "Islamic Azad University, Ahar" }, + { "label": "Okinawa International University" }, + { "label": "Karlshochschule International University" } + ] + }, + { + "label": "IxChariot", + "children": [{ "label": "Cambodia University of Specialties" }, { "label": "Ecole Supérieure des Techniques Industrielles et des Textiles" }] + } + ] + }, + { + "label": "Automation Specialist I", + "children": [ + { + "label": "Ffmpeg", + "children": [{ "label": "Christian Heritage College" }, { "label": "Inha University" }, { "label": "Khalifa University" }] + }, + { + "label": "Mac OS", + "children": [ + { "label": "Prague College" }, + { "label": "Wakayama Medical College" }, + { "label": "South University of Science and Technology of China " }, + { "label": "Campbell University" } + ] + }, + { "label": "Visual Communication", "children": [{ "label": "University of the Cordilleras" }, { "label": "University of Mohaghegh" }] }, + { + "label": "PCRF", + "children": [ + { "label": "FPT University" }, + { "label": "Rakuno Gakuen University" }, + { "label": "Xiangtan Normal University" }, + { "label": "Rice University" }, + { "label": "Sapporo Gakuin University" } + ] + } + ] + }, + { + "label": "Technical Writer", + "children": [ + { + "label": "NC-Verilog", + "children": [ + { "label": "Etisalat University College" }, + { "label": "Newcastle University, Medicine Malaysia " }, + { "label": "University of Asia Pacific, Dhanmondi" }, + { "label": "Leading University" } + ] + }, + { + "label": "Water Treatment", + "children": [ + { "label": "Gorgan University of Agricultural Sciences and Natural Resources" }, + { "label": "Tianjin Polytechnic University" }, + { "label": "Universitas Bojonegoro" } + ] + }, + { + "label": "FTO", + "children": [ + { "label": "Université de Skikda" }, + { "label": "University College of Technology & Innovation (UCTI)" }, + { "label": "Ahmedabad University" }, + { "label": "Universidad Intercontinental" }, + { "label": "Atlantic Union College" } + ] + }, + { + "label": "Vocational Rehabilitation", + "children": [{ "label": "Cambodia University of Specialties" }, { "label": "Universiteit Antwerpen Management School" }] + }, + { + "label": "DH+", + "children": [ + { "label": "Universidad de Córdoba" }, + { "label": "Université Lumière de Bujumbura" }, + { "label": "Madonna University" }, + { "label": "University of Washington" } + ] + } + ] + }, + { + "label": "Software Test Engineer IV", + "children": [ + { + "label": "Zero Waste", + "children": [ + { "label": "University of Italian Studies for Foreigners of Siena" }, + { "label": "Klaipeda University" }, + { "label": "Tallinn University" } + ] + }, + { + "label": "Fixed Assets", + "children": [ + { "label": "North Carolina Central University" }, + { "label": "Universidad Nacional de San Luis" }, + { "label": "Baha'i Institute for Higher Education" } + ] + } + ] + }, + { + "label": "Nuclear Power Engineer", + "children": [ + { + "label": "Woodworking", + "children": [ + { "label": "National Chiayi University" }, + { "label": "Tokyo Kasei University" }, + { "label": "Auchi Polytechnic" }, + { "label": "Hashemite University" }, + { "label": "Thomas Jefferson University" } + ] + }, + { + "label": "Psychotherapy", + "children": [ + { "label": "Trident University" }, + { "label": "Université de N'Djamena" }, + { "label": "Parsons School of Design" }, + { "label": "University of San Diego" } + ] + }, + { + "label": "Credit Unions", + "children": [{ "label": "Tilburg University" }, { "label": "Miyazaki University" }, { "label": "Ohio State University - Newark" }] + } + ] + }, + { + "label": "Media Manager III", + "children": [ + { + "label": "BPL", + "children": [ + { "label": "International Burch University" }, + { "label": "Trinity International University" }, + { "label": "Universidad Autónoma de Centro América" }, + { "label": "Evangelische Fachhochschule Nürnberg" }, + { "label": "Academy of Music, Dance and Fine Arts" } + ] + }, + { + "label": "NVU", + "children": [{ "label": "University Institute of Modern Languages" }, { "label": "Kyungil University" }, { "label": "Jimma University" }] + }, + { + "label": "Zooarchaeology", + "children": [ + { "label": "Hebei Medical University" }, + { "label": "Bharath Institue Of Higher Education & Research" }, + { "label": "Universität Hannover" } + ] + } + ] + }, + { + "label": "Safety Technician IV", + "children": [ + { + "label": "IOT", + "children": [ + { "label": "Belarussian National Technical University" }, + { "label": "Tokyo University of Pharmacy and Life Science" }, + { "label": "Brickfields Asia College" }, + { "label": "Samar State University" }, + { "label": "West Bengal University of Technology" } + ] + }, + { + "label": "Lymphatic Drainage", + "children": [ + { "label": "Free University Amsterdam" }, + { "label": "Friedrich-Alexander Universität Erlangen-Nürnberg" }, + { "label": "Sinnar University" }, + { "label": "Okayama University of Science" } + ] + }, + { + "label": "OLAP Cube Studio", + "children": [ + { "label": "Liaoning Technical University" }, + { "label": "Instituto Superior D. Afonso III - INUAF" }, + { "label": "Kossuth Lajos University" } + ] + }, + { "label": "DSM-IV", "children": [{ "label": "Daniel Webster College" }, { "label": "University of Athens" }] } + ] + }, + { + "label": "Nuclear Power Engineer", + "children": [ + { + "label": "Guest Lecturing", + "children": [ + { "label": "National Pingtung Teachers College" }, + { "label": "Advance Tertiary College" }, + { "label": "Louisiana State University Health Sciences Center New Orleans" }, + { "label": "University of Shiga Prefecture" } + ] + }, + { + "label": "Ayurveda", + "children": [{ "label": "Paichai University" }, { "label": "Universidad Sergio Arboleda" }, { "label": "Lansbridge University" }] + }, + { + "label": "Aeronautics", + "children": [ + { "label": "Ecole Nationale Supérieure d'Ingénieurs en Mécanique et Energétique de Valenciennes" }, + { "label": "Ajou University" }, + { "label": "Islamic Azad University, Tehran Science & Research Branch" }, + { "label": "University of Michigan - Flint" }, + { "label": "University of Ferrara" } + ] + } + ] + }, + { + "label": "Civil Engineer", + "children": [ + { + "label": "Architectural Illustration", + "children": [{ "label": "Detroit College of Law" }, { "label": "European Carolus Magnus University" }] + }, + { + "label": "Teaching Writing", + "children": [ + { "label": "Virginia Intermont College" }, + { "label": "Polytechnic of Namibia" }, + { "label": "Kigali Independent University" }, + { "label": "Nepal Sanskrit University" } + ] + }, + { + "label": "MS Excel Pivot Tables", + "children": [ + { "label": "Newcastle University, Medicine Malaysia " }, + { "label": "National Fisheries University" }, + { "label": "Université d'Antsiranana" }, + { "label": "Shenyang Polytechnic University" } + ] + } + ] + }, + { + "label": "Senior Editor", + "children": [ + { + "label": "Semantic HTML", + "children": [ + { "label": "Southwest University of Finance and Economics" }, + { "label": "Civil Aviation University of China" }, + { "label": "Belarussian State Technological University" } + ] + }, + { + "label": "ASP", + "children": [ + { "label": "Kyoto Tachibana Women's University" }, + { "label": "Ursuline College" }, + { "label": "York University" }, + { "label": "Jewish University in Moscow" } + ] + }, + { "label": "OCLC Connexion", "children": [{ "label": "New York University" }, { "label": "Pittsburg State University" }] }, + { + "label": "Rural Marketing", + "children": [ + { "label": "Universidad de Cartagena" }, + { "label": "Czech University of Agriculture Prague" }, + { "label": "Tohoku Women's College" }, + { "label": "Gunma University" }, + { "label": "Minsk State Linguistic University" } + ] + }, + { "label": "DDI", "children": [{ "label": "Voronezh State Technical University" }, { "label": "University Center \"César Ritz\"" }] } + ] + }, + { + "label": "Media Manager IV", + "children": [ + { + "label": "Yamaha PM5D", + "children": [ + { "label": "Mooreland University" }, + { "label": "Universidad de San Pablo CEU" }, + { "label": "Universidad Galileo" }, + { "label": "College of Technology at Abha" }, + { "label": "Cabrini College" } + ] + }, + { + "label": "HSE Management Systems", + "children": [ + { "label": "Grinnell College" }, + { "label": "Chinju National University of Education" }, + { "label": "Dokkyo University School of Medicine" }, + { "label": "University of New England, Westbrook College Campus" }, + { "label": "Miami University of Ohio - Hamilton" } + ] + } + ] + }, + { + "label": "Product Engineer", + "children": [ + { + "label": "Multi-Unit", + "children": [ + { "label": "Strayer University" }, + { "label": "National Kaohsiung University of Applied Sciences" }, + { "label": "Philadelphia University" }, + { "label": "National Institute of Mental Health and Neuro Sciences" }, + { "label": "Vardhaman Mahaveer Open University" } + ] + }, + { "label": "FX Trading", "children": [{ "label": "Universidade Estácio de Sá" }, { "label": "Manipal University" }] } + ] + }, + { + "label": "Account Coordinator", + "children": [ + { + "label": "Biostatistics", + "children": [ + { "label": "Al-Bukhari International University" }, + { "label": "Technical University of Denmark" }, + { "label": "Postgraduate lnstitute of Medical Education and Research" } + ] + }, + { "label": "FM", "children": [{ "label": "University of Oxford" }, { "label": "Lawrence University" }, { "label": "Okayama University" }] }, + { + "label": "Microsoft Certified Professional", + "children": [ + { "label": "Universidade Católica de Brasília" }, + { "label": "Georgia Institute of Technology" }, + { "label": "University of Petrosani" } + ] + } + ] + }, + { + "label": "Payment Adjustment Coordinator", + "children": [ + { + "label": "Federal Grants Management", + "children": [ + { "label": "Christ University" }, + { "label": "Janos Selye University" }, + { "label": "Zagazig University" }, + { "label": "Constantin Brancoveanu University Pitesti" }, + { "label": "Southwest University of Political Science and Law" } + ] + }, + { + "label": "Company Set-up", + "children": [{ "label": "Ball State University" }, { "label": "Mustafa Kemal University" }, { "label": "Transylvania University" }] + }, + { + "label": "CDMA", + "children": [ + { "label": "College of Telecommunication & Information " }, + { "label": "Nagasaki Prefectural University" }, + { "label": "Gustav-Siewerth-Akademie" } + ] + }, + { + "label": "Overhead Cranes", + "children": [ + { "label": "Universidad de Pamplona" }, + { "label": "Bindura University of Science Education" }, + { "label": "Daiichi University of Economics" }, + { "label": "Wirtschaftsuniversität Wien" } + ] + }, + { + "label": "CDO", + "children": [{ "label": "Design Institute of San Diego" }, { "label": "Wellspring University" }, { "label": "Franciscan School of Theology" }] + } + ] + }, + { + "label": "Assistant Manager", + "children": [ + { + "label": "SQL Server Management Studio", + "children": [ + { "label": "University of Sudbury" }, + { "label": "Evangelische Fachhochschule Berlin, Fachhochschule für Sozialarbeit und Sozialpädagogik" }, + { "label": "Vitebsk State University" }, + { "label": "San Jose Christian College" }, + { "label": "Ivanovo State University" } + ] + }, + { + "label": "Abstracting", + "children": [ + { "label": "Adeyemi College of Education" }, + { "label": "Université de Sherbrooke" }, + { "label": "University College of Applied Sciences" }, + { "label": "Johns Hopkins University, SAIS Bologna Center" } + ] + }, + { + "label": "WTL", + "children": [ + { "label": "Universidad de Córdoba" }, + { "label": "Institut National Polytechnique de Grenoble" }, + { "label": "Kyonggi University" } + ] + } + ] + }, + { + "label": "Professor", + "children": [ + { + "label": "People Skills", + "children": [ + { "label": "University of Calcutta" }, + { "label": "Universidad del Valle del Cauca" }, + { "label": "FAST - National University of Computer and Emerging Sciences (NUCES)" } + ] + }, + { + "label": "Workforce Development", + "children": [ + { "label": "Shandong Medical University" }, + { "label": "Al Khawarizmi International College" }, + { "label": "Nippon Dental University" }, + { "label": "Komsomolsk-on-Amur State Technical University" }, + { "label": "Lingnan University" } + ] + }, + { + "label": "Digital Journalism", + "children": [ + { "label": "The College of St. Scholastica" }, + { "label": "Universidad Autónoma de la Ciudad de México" }, + { "label": "University of Information Technology and Management in Rzeszow" }, + { "label": "Liaquat University of Medical & Health Sciences Jamshoro" } + ] + }, + { + "label": "Short Films", + "children": [ + { "label": "Universidad Católica de Valencia" }, + { "label": "Columbia International University" }, + { "label": "Framingham State College" }, + { "label": "Gurukul University" }, + { "label": "NTI University" } + ] + }, + { + "label": "XML Programming", + "children": [ + { "label": "Victoria University" }, + { "label": "Andrews University" }, + { "label": "Centre Universitaire d'Oum El Bouaghi" }, + { "label": "Dilla University" } + ] + } + ] + } +] diff --git a/docs/src/stories/Simple/index.css b/docs/src/stories/Simple/index.css new file mode 100644 index 00000000..a8e07534 --- /dev/null +++ b/docs/src/stories/Simple/index.css @@ -0,0 +1,19 @@ +.suspended .node-label { + font-style: italic; + text-decoration: line-through; +} + +.dropdown-content { + max-height: 400px; + overflow-y: auto; +} + +.node .fa { + font-size: 12px; + margin-left: 4px; + cursor: pointer; +} + +.node .fa-ban { + color: darkorange; +} diff --git a/docs/src/stories/Simple/index.js b/docs/src/stories/Simple/index.js new file mode 100644 index 00000000..23016c83 --- /dev/null +++ b/docs/src/stories/Simple/index.js @@ -0,0 +1,38 @@ +import React from 'react' + +import DropdownTreeSelect from '../../../../src' +import '../../../../dist/styles.css' + +import './index.css' +import data from './data.json' + +const onChange = (curNode, selectedNodes) => { + console.log('onChange::', curNode, selectedNodes) +} +const onAction = ({ action, node }) => { + console.log(`onAction:: [${action}]`, node) +} +const onNodeToggle = curNode => { + console.log('onNodeToggle::', curNode) +} + +const Simple = () => ( +
+

Basic component

+

+ This is a basic example of the component. Note that there are no external styles in this page, not even reset.css or{' '} + reboot.css or normalizer.css. +

+

+ The idea is to showcase the component at its barest minimum. Of course, its easy to style it, using popular frameworks such as Bootstrap or + Material Design (checkout the examples on left). +

+

+ As a side effect, it also helps rule out issues arising out of using custom frameworks (if something doesn't look right in your app but + looks OK here, you know what is messing things up). +

+ +
+) + +export default Simple diff --git a/docs/webpack.config.js b/docs/webpack.config.js index 55bc2a25..8f24d220 100644 --- a/docs/webpack.config.js +++ b/docs/webpack.config.js @@ -21,9 +21,7 @@ const baseConfig = { test: /\.(png|woff|woff2|eot|ttf|svg)$/, use: { loader: 'url-loader', - options: { - limit: 100000 - } + options: {limit: 100000} } }, { diff --git a/package.json b/package.json index a3a1ddfb..b0d60c4f 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "dist" ], "dependencies": { + "array.partial": "^1.0.2", "classnames": "^2.2.5", "lodash.debounce": "^4.0.8", "prop-types": "^15.6.0" @@ -61,8 +62,9 @@ "cross-env": "^5.0.5", "css-loader": "^0.28.0", "cz-conventional-changelog-emoji": "^0.1.0", - "enzyme": "^3.1.1", + "enzyme": "^3.3.0", "enzyme-adapter-react-16": "^1.0.4", + "enzyme-to-json": "^3.3.3", "eslint": "^4.19.0", "eslint-config-airbnb": "^16.1.0", "eslint-config-react-app": "^2.1.0", @@ -92,7 +94,6 @@ "react": "^16.0.0", "react-dom": "^16.0.0", "react-story": "^0.0.10", - "react-test-renderer": "^16.0.0", "rimraf": "^2.6.1", "semantic-release": "^12.2.2", "sinon": "^4.0.0", @@ -119,7 +120,8 @@ "jsdom-global/register", "./setupEnzyme" ], - "babel": "inherit" + "babel": "inherit", + "snapshotDir": "__snapshots__" }, "nyc": { "sourceMap": false, diff --git a/src/checkbox/index.js b/src/checkbox/index.js new file mode 100644 index 00000000..47e2154a --- /dev/null +++ b/src/checkbox/index.js @@ -0,0 +1,18 @@ +import React from 'react' + +export const refUpdater = ({ checked, indeterminate }) => input => { + if (input) { + input.checked = checked + input.indeterminate = indeterminate + } +} + +const Checkbox = props => { + const { + className, checked, indeterminate = false, onChange, ...rest +} = props + + return +} + +export default Checkbox diff --git a/src/checkbox/index.test.js b/src/checkbox/index.test.js new file mode 100644 index 00000000..1db42f37 --- /dev/null +++ b/src/checkbox/index.test.js @@ -0,0 +1,28 @@ +import { shallow } from 'enzyme' +import React from 'react' +import test from 'ava' +import toJson from 'enzyme-to-json' + +import Checkbox, { refUpdater } from './index' + +test('Checkbox component', t => { + const tree = toJson(shallow()) + t.snapshot(tree) +}) + +test('renders indeterminate state', t => { + const input = {} + refUpdater({ indeterminate: true })(input) + t.true(input.indeterminate) +}) + +test('renders checked state', t => { + const input = {} + refUpdater({ checked: true })(input) + t.true(input.checked) +}) + +test('renders disabled state', t => { + const tree = toJson(shallow()) + t.snapshot(tree) +}) diff --git a/src/index.js b/src/index.js index 50d01c5b..a3c62876 100644 --- a/src/index.js +++ b/src/index.js @@ -27,10 +27,11 @@ class DropdownTreeSelect extends Component { onAction: PropTypes.func, onNodeToggle: PropTypes.func, simpleSelect: PropTypes.bool, - noMatchesText: PropTypes.string + noMatchesText: PropTypes.string, + showPartiallySelected: PropTypes.bool } - constructor (props) { + constructor(props) { super(props) this.state = { showDropdown: this.props.showDropdown || false, @@ -42,8 +43,8 @@ class DropdownTreeSelect extends Component { typeof this.props.onChange === 'function' && this.props.onChange(...args) } - createList = (tree, simple) => { - this.treeManager = new TreeManager(tree, simple) + createList = (tree, simple, showPartial) => { + this.treeManager = new TreeManager(tree, simple, showPartial) return this.treeManager.tree } @@ -58,14 +59,14 @@ class DropdownTreeSelect extends Component { this.searchInput.value = '' } - componentWillMount () { - const tree = this.createList(this.props.data, this.props.simpleSelect) + componentWillMount() { + const tree = this.createList(this.props.data, this.props.simpleSelect, this.props.showPartiallySelected) const tags = this.treeManager.getTags() this.setState({ tree, tags }) } - componentWillReceiveProps (nextProps) { - const tree = this.createList(nextProps.data, nextProps.simpleSelect) + componentWillReceiveProps(nextProps) { + const tree = this.createList(nextProps.data, nextProps.simpleSelect, nextProps.showPartiallySelected) const tags = this.treeManager.getTags() this.setState({ tree, tags }) } @@ -101,7 +102,11 @@ class DropdownTreeSelect extends Component { const { allNodesHidden, tree } = this.treeManager.filterTree(value) const searchModeOn = value.length > 0 - this.setState({ tree, searchModeOn, allNodesHidden }) + this.setState({ + tree, + searchModeOn, + allNodesHidden + }) } onTagRemove = id => { @@ -111,27 +116,27 @@ class DropdownTreeSelect extends Component { onNodeToggle = id => { this.treeManager.toggleNodeExpandState(id) this.setState({ tree: this.treeManager.tree }) - typeof this.props.onNodeToggle === 'function' && - this.props.onNodeToggle(this.treeManager.getNodeById(id)) + typeof this.props.onNodeToggle === 'function' && this.props.onNodeToggle(this.treeManager.getNodeById(id)) } onCheckboxChange = (id, checked) => { this.treeManager.setNodeCheckedState(id, checked) const tags = this.treeManager.getTags() - const showDropdown = this.props.simpleSelect - ? false - : this.state.showDropdown - this.setState({ tree: this.treeManager.tree, tags, showDropdown }) + const showDropdown = this.props.simpleSelect ? false : this.state.showDropdown + this.setState({ + tree: this.treeManager.tree, + tags, + showDropdown + }) if (this.props.simpleSelect) this.resetSearch() this.notifyChange(this.treeManager.getNodeById(id), tags) } onAction = (actionId, nodeId) => { - typeof this.props.onAction === 'function' && - this.props.onAction(actionId, this.treeManager.getNodeById(nodeId)) + typeof this.props.onAction === 'function' && this.props.onAction(actionId, this.treeManager.getNodeById(nodeId)) } - render () { + render() { const dropdownTriggerClassname = cx({ 'dropdown-trigger': true, arrow: true, @@ -177,6 +182,7 @@ class DropdownTreeSelect extends Component { onCheckboxChange={this.onCheckboxChange} onNodeToggle={this.onNodeToggle} simpleSelect={this.props.simpleSelect} + showPartiallySelected={this.props.showPartiallySelected} /> )}
diff --git a/src/tag/index.js b/src/tag/index.js index 1643e343..1643ad6b 100644 --- a/src/tag/index.js +++ b/src/tag/index.js @@ -6,7 +6,9 @@ import styles from './index.css' const cx = cn.bind(styles) const Tag = (props) => { - const { id, label, onDelete } = props + const { + id, label, onDelete +} = props const onClick = (e) => { // this is needed to stop the drawer from closing diff --git a/src/tree-manager/flatten-tree.js b/src/tree-manager/flatten-tree.js index cad03717..a57b2911 100644 --- a/src/tree-manager/flatten-tree.js +++ b/src/tree-manager/flatten-tree.js @@ -1,3 +1,5 @@ +import getPartialState from './getPartialState' + /** * Converts a nested node into an associative array with pointers to child and parent nodes * Given: @@ -87,22 +89,28 @@ const tree = [ } } ``` - * @param {[type]} tree The incoming tree object - * @return {object} The flattened list + * @param {[type]} tree The incoming tree object + * @param {[bool]} simple Whether its in Single slect mode (simple dropdown) + * @param {[bool]} showPartialState Whether to show partially checked state + * @return {object} The flattened list */ -function flattenTree (tree, simple) { +function flattenTree(tree, simple, showPartialState) { const forest = Array.isArray(tree) ? tree : [tree] - const list = walkNodes({ nodes: forest, simple }) + const list = walkNodes({ + nodes: forest, + simple, + showPartialState + }) return list } /** - * If the node didn't specify anything on its own - * figure out the initial state based on parent - * @param {object} node [curernt node] - * @param {object} parent [node's immediate parent] - */ -function setInitialStateProps (node, parent = {}) { + * If the node didn't specify anything on its own + * figure out the initial state based on parent + * @param {object} node [current node] + * @param {object} parent [node's immediate parent] + */ +function setInitialStateProps(node, parent = {}) { const stateProps = ['checked', 'disabled'] for (let index = 0; index < stateProps.length; index++) { const prop = stateProps[index] @@ -114,8 +122,8 @@ function setInitialStateProps (node, parent = {}) { } } -function walkNodes ({ - nodes, list = new Map(), parent, depth = 0, simple +function walkNodes({ + nodes, list = new Map(), parent, depth = 0, simple, showPartialState }) { nodes.forEach((node, i) => { node._depth = depth @@ -134,8 +142,22 @@ function walkNodes ({ if (!simple && node.children) { node._children = [] walkNodes({ - nodes: node.children, list, parent: node, depth: depth + 1 + nodes: node.children, + list, + parent: node, + depth: depth + 1, + showPartialState }) + + if (showPartialState && !node.checked) { + node.partial = getPartialState(node) + + // re-check if all children are checked. if so, check thyself + if (node.children.every(c => c.checked)) { + node.checked = true + } + } + node.children = undefined } }) diff --git a/src/tree-manager/getPartialState.js b/src/tree-manager/getPartialState.js new file mode 100644 index 00000000..f576a9c5 --- /dev/null +++ b/src/tree-manager/getPartialState.js @@ -0,0 +1,6 @@ +import partial from 'array.partial' + +const identity = c => c + +export default (node, childProp = 'children', childSelector = identity) => + partial(node[childProp], c => childSelector(c).checked) || node[childProp].some(c => childSelector(c).partial) diff --git a/src/tree-manager/index.js b/src/tree-manager/index.js index 7497dee8..be876b17 100644 --- a/src/tree-manager/index.js +++ b/src/tree-manager/index.js @@ -1,19 +1,22 @@ +import getPartialState from './getPartialState' + import isEmpty from '../isEmpty' import flattenTree from './flatten-tree' class TreeManager { - constructor (tree, simple) { + constructor(tree, simple, showPartialState) { this._src = tree - this.tree = flattenTree(JSON.parse(JSON.stringify(tree)), simple) + this.tree = flattenTree(JSON.parse(JSON.stringify(tree)), simple, showPartialState) this.simpleSelect = simple + this.showPartialState = showPartialState this.searchMaps = new Map() } - getNodeById (id) { + getNodeById(id) { return this.tree.get(id) } - getMatches (searchTerm) { + getMatches(searchTerm) { if (this.searchMaps.has(searchTerm)) { return this.searchMaps.get(searchTerm) } @@ -31,14 +34,14 @@ class TreeManager { if (closestMatch !== searchTerm) { const superMatches = this.searchMaps.get(closestMatch) - superMatches.forEach((key) => { + superMatches.forEach(key => { const node = this.getNodeById(key) if (node.label.toLowerCase().indexOf(searchTerm) >= 0) { matches.push(node._id) } }) } else { - this.tree.forEach((node) => { + this.tree.forEach(node => { if (node.label.toLowerCase().indexOf(searchTerm) >= 0) { matches.push(node._id) } @@ -49,7 +52,7 @@ class TreeManager { return matches } - setChildMatchStatus (id) { + setChildMatchStatus(id) { if (id !== undefined) { const node = this.getNodeById(id) node.matchInChildren = true @@ -57,15 +60,15 @@ class TreeManager { } } - filterTree (searchTerm) { + filterTree(searchTerm) { const matches = this.getMatches(searchTerm.toLowerCase()) - this.tree.forEach((node) => { + this.tree.forEach(node => { node.hide = true node.matchInChildren = false }) - matches.forEach((m) => { + matches.forEach(m => { const node = this.getNodeById(m) node.hide = false this.setChildMatchStatus(node._parent) @@ -75,29 +78,37 @@ class TreeManager { return { allNodesHidden, tree: this.tree } } - restoreNodes () { - this.tree.forEach((node) => { + restoreNodes() { + this.tree.forEach(node => { node.hide = false }) return this.tree } - togglePreviousChecked (id) { + togglePreviousChecked(id) { const prevChecked = this.currentChecked if (prevChecked) this.getNodeById(prevChecked).checked = false this.currentChecked = id } - setNodeCheckedState (id, checked) { + setNodeCheckedState(id, checked) { const node = this.getNodeById(id) node.checked = checked + if (this.showPartialState) { + node.partial = false + } + if (this.simpleSelect) { this.togglePreviousChecked(id) } else { this.toggleChildren(id, checked) + if (this.showPartialState) { + this.partialCheckParents(node) + } + if (!checked) { this.unCheckParents(node) } @@ -109,41 +120,62 @@ class TreeManager { * @param {[type]} node [description] * @return {[type]} [description] */ - unCheckParents (node) { + unCheckParents(node) { let parent = node._parent while (parent) { const next = this.getNodeById(parent) next.checked = false + next.partial = getPartialState(next, '_children', this.getNodeById.bind(this)) parent = next._parent } } - toggleChildren (id, state) { + /** + * Walks up the tree setting partial state on parent nodes + * @param {[type]} node [description] + * @return {[type]} [description] + */ + partialCheckParents(node) { + let parent = node._parent + while (parent) { + const next = this.getNodeById(parent) + next.checked = next._children.every(c => this.getNodeById(c).checked) + next.partial = getPartialState(next, '_children', this.getNodeById.bind(this)) + parent = next._parent + } + } + + toggleChildren(id, state) { const node = this.getNodeById(id) node.checked = state + + if (this.showPartialState) { + node.partial = false + } + if (!isEmpty(node._children)) { node._children.forEach(id => this.toggleChildren(id, state)) } } - toggleNodeExpandState (id) { + toggleNodeExpandState(id) { const node = this.getNodeById(id) node.expanded = !node.expanded if (!node.expanded) this.collapseChildren(node) return this.tree } - collapseChildren (node) { + collapseChildren(node) { node.expanded = false if (!isEmpty(node._children)) { node._children.forEach(c => this.collapseChildren(this.getNodeById(c))) } } - getTags () { + getTags() { const tags = [] const visited = {} - const markSubTreeVisited = (node) => { + const markSubTreeVisited = node => { visited[node._id] = true if (!isEmpty(node._children)) node._children.forEach(c => markSubTreeVisited(this.getNodeById(c))) } diff --git a/src/tree-manager/index.test.js b/src/tree-manager/index.test.js deleted file mode 100644 index ce1428fa..00000000 --- a/src/tree-manager/index.test.js +++ /dev/null @@ -1,433 +0,0 @@ -import test from 'ava' -import TreeManager from './index' - -test('should not mutate input', (t) => { - const expected = { - label: 'l1', - value: 'v1', - children: [{ - label: 'l1c1', - value: 'l1v1' - }] - } - const actual = { - label: 'l1', - value: 'v1', - children: [{ - label: 'l1c1', - value: 'l1v1' - }] - } - /* eslint-disable no-new */ - new TreeManager(actual) - t.deepEqual(actual, expected) -}) - -test('should set initial check state based on parent check state when node check state is not defined', (t) => { - const tree = { - id: 'i1', - label: 'l1', - value: 'v1', - children: [{ - id: 'c1', - label: 'l1c1', - value: 'l1v1' - }], - checked: true - } - const manager = new TreeManager(tree) - t.true(manager.getNodeById('c1').checked) -}) - -test('should set initial check state based on node check state when node check state is defined', (t) => { - const tree = { - id: 'i1', - label: 'l1', - value: 'v1', - children: [{ - id: 'c1', - label: 'l1c1', - value: 'l1v1', - checked: true - }] - } - const manager = new TreeManager(tree) - t.true(manager.getNodeById('c1').checked) -}) - -test('should set initial check state based on node check state when node check state is defined', (t) => { - const tree = { - id: 'i1', - label: 'l1', - value: 'v1', - children: [{ - id: 'c1', - label: 'l1c1', - value: 'l1v1', - checked: false - }] - } - const manager = new TreeManager(tree) - t.false(manager.getNodeById('c1').checked) -}) - -test('should get tags based on children check state', (t) => { - const tree = { - id: 'i1', - label: 'l1', - value: 'v1', - children: [{ - id: 'c1', - label: 'l1c1', - value: 'l1v1', - checked: true - }] - } - const manager = new TreeManager(tree) - t.deepEqual(manager.getTags().map(t => t.label), ['l1c1']) -}) - -test('should get tags based on parent check state', (t) => { - const tree = { - label: 'l1', - value: 'v1', - checked: true, - children: [{ - label: 'l1c1', - value: 'l1v1', - checked: true - }] - } - const manager = new TreeManager(tree) - t.deepEqual(manager.getTags().map(t => t.label), ['l1']) -}) - -test('should get tags based on multiple parent check state', (t) => { - const tree = [{ - label: 'l1', - value: 'v1', - checked: true, - children: [{ - label: 'l1c1', - value: 'l1v1' - }] - }, { - label: 'l2', - value: 'v2', - checked: true, - children: [{ - label: 'l2c2', - value: 'l2v2' - }] - }] - const manager = new TreeManager(tree) - t.deepEqual(manager.getTags().map(t => t.label), ['l1', 'l2']) -}) - -test('should get tags based on multiple parent/child check state', (t) => { - const tree = [{ - label: 'l1', - value: 'v1', - checked: true, - children: [{ - label: 'l1c1', - value: 'l1v1' - }] - }, { - label: 'l2', - value: 'v2', - children: [{ - label: 'l2c2', - value: 'l2v2', - checked: true - }] - }] - const manager = new TreeManager(tree) - t.deepEqual(manager.getTags().map(t => t.label), ['l1', 'l2c2']) -}) - -test('should toggle children when checked', (t) => { - const tree = { - id: 'i1', - label: 'l1', - value: 'v1', - children: [{ - id: 'c1', - label: 'l1c1', - value: 'l1v1' - }] - } - const manager = new TreeManager(tree) - manager.setNodeCheckedState('i1', true) - t.true(manager.getNodeById('c1').checked) -}) - -test('should toggle children when unchecked', (t) => { - const tree = { - id: 'i1', - label: 'l1', - value: 'v1', - children: [{ - id: 'c1', - label: 'l1c1', - value: 'l1v1', - checked: true - }] - } - const manager = new TreeManager(tree) - manager.setNodeCheckedState('i1', false) - t.false(manager.getNodeById('c1').checked) -}) - -test('should uncheck parent when unchecked', (t) => { - const tree = { - id: 'i1', - label: 'l1', - value: 'v1', - checked: true, - children: [{ - id: 'c1', - label: 'l1c1', - value: 'l1v1' - }] - } - const manager = new TreeManager(tree) - manager.setNodeCheckedState('c1', false) - t.false(manager.getNodeById('i1').checked) -}) - -test('should uncheck all parents when unchecked', (t) => { - const tree = { - id: 'i1', - label: 'l1', - value: 'v1', - children: [{ - id: 'c1', - label: 'l1c1', - value: 'l1v1', - children: [{ - id: 'c2', - label: 'l2c1', - value: 'l2v1' - }] - }] - } - const manager = new TreeManager(tree) - manager.setNodeCheckedState('c2', false) - t.false(manager.getNodeById('c1').checked) - t.false(manager.getNodeById('i1').checked) -}) - -test('should collapse all children when collapsed', (t) => { - const tree = { - id: 'i1', - label: 'l1', - value: 'v1', - expanded: true, - children: [{ - id: 'c1', - label: 'l1c1', - value: 'l1v1', - expanded: true, - children: [{ - id: 'c2', - label: 'l2c1', - value: 'l2v1', - expanded: true - }] - }] - } - const manager = new TreeManager(tree) - manager.toggleNodeExpandState('i1') - t.false(manager.getNodeById('c1').expanded) - t.false(manager.getNodeById('c2').expanded) -}) - -test('should expand node (and not children) when expanded', (t) => { - const tree = { - id: 'i1', - label: 'l1', - value: 'v1', - children: [{ - id: 'c1', - label: 'l1c1', - value: 'l1v1', - children: [{ - id: 'c2', - label: 'l2c1', - value: 'l2v1' - }] - }] - } - const manager = new TreeManager(tree) - manager.toggleNodeExpandState('i1') - t.true(manager.getNodeById('i1').expanded) - t.falsy(manager.getNodeById('c1').expanded) - t.falsy(manager.getNodeById('c2').expanded) -}) - -test('should get matching nodes when searched', (t) => { - const tree = { - id: 'i1', - label: 'search me', - value: 'v1', - children: [{ - id: 'c1', - label: 'search me too', - value: 'l1v1', - children: [{ - id: 'c2', - label: 'No one can get me', - value: 'l2v1' - }] - }] - } - const manager = new TreeManager(tree) - const { - allNodesHidden - } = manager.filterTree('search') - t.false(allNodesHidden) - const nodes = ['i1', 'c1'] - nodes.forEach(n => t.false(manager.getNodeById(n).hide)) -}) - -test('should hide all nodes when search term is not found', (t) => { - const tree = { - id: 'i1', - label: 'l1', - value: 'v1', - children: [{ - id: 'c1', - label: 'l1c1', - value: 'l1v1', - children: [{ - id: 'c2', - label: 'l2c1', - value: 'l2v1' - }] - }] - } - const manager = new TreeManager(tree) - const { - allNodesHidden - } = manager.filterTree('bla-bla') - t.true(allNodesHidden) - const nodes = ['i1', 'c1'] - nodes.forEach(n => t.true(manager.getNodeById(n).hide)) -}) - -test('should use cached results for subsequent searches', (t) => { - const tree = [{ - id: 'i1', - label: 'search me', - value: 'v1', - children: [{ - id: 'c1', - label: 'search me too', - value: 'l1v1', - children: [{ - id: 'c2', - label: 'No one can get me', - value: 'l2v1' - }] - }] - }, { - id: 'i2', - label: 'sears', - value: 'sears' - }] - const manager = new TreeManager(tree) - const { - allNodesHidden - } = manager.filterTree('sea') - manager.filterTree('sear') - manager.filterTree('on') - manager.filterTree('search') - manager.filterTree('search') - t.false(allNodesHidden) -}) - -test('should restore nodes', (t) => { - const tree = [{ - id: 'i1', - label: 'search me', - value: 'v1', - children: [{ - id: 'c1', - label: 'search me too', - value: 'l1v1', - children: [{ - id: 'c2', - label: 'No one can get me', - value: 'l2v1' - }] - }] - }, { - id: 'i2', - label: 'sears', - value: 'sears' - }] - const manager = new TreeManager(tree) - manager.filterTree('search') - manager.restoreNodes() - const visibleNodes = ['i1', 'i2', 'c1', 'c2'] - visibleNodes.forEach(n => t.false(manager.getNodeById(n).hide)) -}) - -test('should get matching nodes with mixed case when searched', (t) => { - const tree = { - id: 'i1', - label: 'search me', - value: 'v1', - children: [{ - id: 'c1', - label: 'SeaRch me too', - value: 'l1v1', - children: [{ - id: 'c2', - label: 'No one can get me', - value: 'l2v1' - }] - }] - } - const manager = new TreeManager(tree) - const { - allNodesHidden - } = manager.filterTree('SearCH') - t.false(allNodesHidden) - const nodes = ['i1', 'c1'] - nodes.forEach(n => t.false(manager.getNodeById(n).hide)) -}) - -test('should uncheck previous node in simple select mode', (t) => { - const tree = [{ - id: 'i1', - label: 'l1', - value: 'v1', - children: [{ - id: 'c1', - label: 'l1c1', - value: 'l1v1' - }] - }, { - id: 'i2', - label: 'l2', - value: 'v2', - children: [{ - id: 'c2', - label: 'l2c2', - value: 'l2v2' - }] - }] - const manager = new TreeManager(tree, true) - manager.setNodeCheckedState('i1', true) - t.true(manager.getNodeById('i1').checked) - - manager.setNodeCheckedState('i2', true) - t.false(manager.getNodeById('i1').checked) - t.true(manager.getNodeById('i2').checked) - - manager.setNodeCheckedState('i1', true) - t.true(manager.getNodeById('i1').checked) - t.false(manager.getNodeById('i2').checked) -}) diff --git a/src/tree-manager/flatten-tree.test.js b/src/tree-manager/tests/flatten-tree.test.js similarity index 83% rename from src/tree-manager/flatten-tree.test.js rename to src/tree-manager/tests/flatten-tree.test.js index 41e8875c..4ca6ed35 100644 --- a/src/tree-manager/flatten-tree.test.js +++ b/src/tree-manager/tests/flatten-tree.test.js @@ -1,8 +1,8 @@ import test from 'ava' -import flattenTree from './flatten-tree' -import { mapToObj } from '../map-utils' +import flattenTree from '../flatten-tree' +import { mapToObj } from '../../map-utils' -test('flattens tree with no root', (t) => { +test('flattens tree with no root', t => { const tree = [ { name: 'item1', @@ -26,7 +26,11 @@ test('flattens tree with no root', (t) => { children: [ { name: 'item2-1-1', value: 'value2-1-1' }, { name: 'item2-1-2', value: 'value2-1-2' }, - { name: 'item2-1-3', value: 'value2-1-3', children: [{ name: 'item2-1-3-1', value: 'value2-1-3-1' }] } + { + name: 'item2-1-3', + value: 'value2-1-3', + children: [{ name: 'item2-1-3-1', value: 'value2-1-3-1' }] + } ] }, { name: 'item2-2', value: 'value2-2' } @@ -37,10 +41,7 @@ test('flattens tree with no root', (t) => { const expected = { 0: { _id: '0', - _children: [ - '0-0', - '0-1' - ], + _children: ['0-0', '0-1'], _depth: 0, children: undefined, name: 'item1', @@ -48,10 +49,7 @@ test('flattens tree with no root', (t) => { }, 1: { _id: '1', - _children: [ - '1-0', - '1-1' - ], + _children: ['1-0', '1-1'], _depth: 0, children: undefined, name: 'item2', @@ -60,10 +58,7 @@ test('flattens tree with no root', (t) => { '0-0': { _id: '0-0', _parent: '0', - _children: [ - '0-0-0', - '0-0-1' - ], + _children: ['0-0-0', '0-0-1'], _depth: 1, children: undefined, name: 'item1-1', @@ -93,11 +88,7 @@ test('flattens tree with no root', (t) => { '1-0': { _id: '1-0', _parent: '1', - _children: [ - '1-0-0', - '1-0-1', - '1-0-2' - ], + _children: ['1-0-0', '1-0-1', '1-0-2'], _depth: 1, children: undefined, name: 'item2-1', @@ -127,9 +118,7 @@ test('flattens tree with no root', (t) => { '1-0-2': { _id: '1-0-2', _parent: '1-0', - _children: [ - '1-0-2-0' - ], + _children: ['1-0-2-0'], _depth: 2, children: undefined, name: 'item2-1-3', @@ -148,7 +137,7 @@ test('flattens tree with no root', (t) => { t.deepEqual(mapToObj(list), expected) }) -test('flattens tree with root', (t) => { +test('flattens tree with root', t => { const tree = { name: 'item1', value: 'value1', @@ -165,10 +154,7 @@ test('flattens tree with root', (t) => { const expected = { 0: { _id: '0', - _children: [ - '0-0', - '0-1' - ], + _children: ['0-0', '0-1'], _depth: 0, children: undefined, name: 'item1', @@ -177,10 +163,7 @@ test('flattens tree with root', (t) => { '0-0': { _id: '0-0', _parent: '0', - _children: [ - '0-0-0', - '0-0-1' - ], + _children: ['0-0-0', '0-0-1'], _depth: 1, children: undefined, name: 'item1-1', diff --git a/src/tree-manager/tests/index.test.js b/src/tree-manager/tests/index.test.js new file mode 100644 index 00000000..70ae48cc --- /dev/null +++ b/src/tree-manager/tests/index.test.js @@ -0,0 +1,504 @@ +import test from 'ava' +import TreeManager from '..' + +test('should not mutate input', t => { + const expected = { + label: 'l1', + value: 'v1', + children: [ + { + label: 'l1c1', + value: 'l1v1' + } + ] + } + const actual = { + label: 'l1', + value: 'v1', + children: [ + { + label: 'l1c1', + value: 'l1v1' + } + ] + } + /* eslint-disable no-new */ + new TreeManager(actual) + t.deepEqual(actual, expected) +}) + +test('should set initial check state based on parent check state when node check state is not defined', t => { + const tree = { + id: 'i1', + label: 'l1', + value: 'v1', + children: [ + { + id: 'c1', + label: 'l1c1', + value: 'l1v1' + } + ], + checked: true + } + const manager = new TreeManager(tree) + t.true(manager.getNodeById('c1').checked) +}) + +test('should set initial check state based on node check state when node check state is defined', t => { + const tree = { + id: 'i1', + label: 'l1', + value: 'v1', + children: [ + { + id: 'c1', + label: 'l1c1', + value: 'l1v1', + checked: true + } + ] + } + const manager = new TreeManager(tree) + t.true(manager.getNodeById('c1').checked) +}) + +test('should set initial check state based on node check state when node check state is defined', t => { + const tree = { + id: 'i1', + label: 'l1', + value: 'v1', + children: [ + { + id: 'c1', + label: 'l1c1', + value: 'l1v1', + checked: false + } + ] + } + const manager = new TreeManager(tree) + t.false(manager.getNodeById('c1').checked) +}) + +test('should get tags based on children check state', t => { + const tree = { + id: 'i1', + label: 'l1', + value: 'v1', + children: [ + { + id: 'c1', + label: 'l1c1', + value: 'l1v1', + checked: true + } + ] + } + const manager = new TreeManager(tree) + t.deepEqual(manager.getTags().map(t => t.label), ['l1c1']) +}) + +test('should get tags based on parent check state', t => { + const tree = { + label: 'l1', + value: 'v1', + checked: true, + children: [ + { + label: 'l1c1', + value: 'l1v1', + checked: true + } + ] + } + const manager = new TreeManager(tree) + t.deepEqual(manager.getTags().map(t => t.label), ['l1']) +}) + +test('should get tags based on multiple parent check state', t => { + const tree = [ + { + label: 'l1', + value: 'v1', + checked: true, + children: [ + { + label: 'l1c1', + value: 'l1v1' + } + ] + }, + { + label: 'l2', + value: 'v2', + checked: true, + children: [ + { + label: 'l2c2', + value: 'l2v2' + } + ] + } + ] + const manager = new TreeManager(tree) + t.deepEqual(manager.getTags().map(t => t.label), ['l1', 'l2']) +}) + +test('should get tags based on multiple parent/child check state', t => { + const tree = [ + { + label: 'l1', + value: 'v1', + checked: true, + children: [ + { + label: 'l1c1', + value: 'l1v1' + } + ] + }, + { + label: 'l2', + value: 'v2', + children: [ + { + label: 'l2c2', + value: 'l2v2', + checked: true + } + ] + } + ] + const manager = new TreeManager(tree) + t.deepEqual(manager.getTags().map(t => t.label), ['l1', 'l2c2']) +}) + +test('should toggle children when checked', t => { + const tree = { + id: 'i1', + label: 'l1', + value: 'v1', + children: [ + { + id: 'c1', + label: 'l1c1', + value: 'l1v1' + } + ] + } + const manager = new TreeManager(tree) + manager.setNodeCheckedState('i1', true) + t.true(manager.getNodeById('c1').checked) +}) + +test('should toggle children when unchecked', t => { + const tree = { + id: 'i1', + label: 'l1', + value: 'v1', + children: [ + { + id: 'c1', + label: 'l1c1', + value: 'l1v1', + checked: true + } + ] + } + const manager = new TreeManager(tree) + manager.setNodeCheckedState('i1', false) + t.false(manager.getNodeById('c1').checked) +}) + +test('should uncheck parent when unchecked', t => { + const tree = { + id: 'i1', + label: 'l1', + value: 'v1', + checked: true, + children: [ + { + id: 'c1', + label: 'l1c1', + value: 'l1v1' + } + ] + } + const manager = new TreeManager(tree) + manager.setNodeCheckedState('c1', false) + t.false(manager.getNodeById('i1').checked) +}) + +test('should uncheck all parents when unchecked', t => { + const tree = { + id: 'i1', + label: 'l1', + value: 'v1', + children: [ + { + id: 'c1', + label: 'l1c1', + value: 'l1v1', + children: [ + { + id: 'c2', + label: 'l2c1', + value: 'l2v1' + } + ] + } + ] + } + const manager = new TreeManager(tree) + manager.setNodeCheckedState('c2', false) + t.false(manager.getNodeById('c1').checked) + t.false(manager.getNodeById('i1').checked) +}) + +test('should collapse all children when collapsed', t => { + const tree = { + id: 'i1', + label: 'l1', + value: 'v1', + expanded: true, + children: [ + { + id: 'c1', + label: 'l1c1', + value: 'l1v1', + expanded: true, + children: [ + { + id: 'c2', + label: 'l2c1', + value: 'l2v1', + expanded: true + } + ] + } + ] + } + const manager = new TreeManager(tree) + manager.toggleNodeExpandState('i1') + t.false(manager.getNodeById('c1').expanded) + t.false(manager.getNodeById('c2').expanded) +}) + +test('should expand node (and not children) when expanded', t => { + const tree = { + id: 'i1', + label: 'l1', + value: 'v1', + children: [ + { + id: 'c1', + label: 'l1c1', + value: 'l1v1', + children: [ + { + id: 'c2', + label: 'l2c1', + value: 'l2v1' + } + ] + } + ] + } + const manager = new TreeManager(tree) + manager.toggleNodeExpandState('i1') + t.true(manager.getNodeById('i1').expanded) + t.falsy(manager.getNodeById('c1').expanded) + t.falsy(manager.getNodeById('c2').expanded) +}) + +test('should get matching nodes when searched', t => { + const tree = { + id: 'i1', + label: 'search me', + value: 'v1', + children: [ + { + id: 'c1', + label: 'search me too', + value: 'l1v1', + children: [ + { + id: 'c2', + label: 'No one can get me', + value: 'l2v1' + } + ] + } + ] + } + const manager = new TreeManager(tree) + const { allNodesHidden } = manager.filterTree('search') + t.false(allNodesHidden) + const nodes = ['i1', 'c1'] + nodes.forEach(n => t.false(manager.getNodeById(n).hide)) +}) + +test('should hide all nodes when search term is not found', t => { + const tree = { + id: 'i1', + label: 'l1', + value: 'v1', + children: [ + { + id: 'c1', + label: 'l1c1', + value: 'l1v1', + children: [ + { + id: 'c2', + label: 'l2c1', + value: 'l2v1' + } + ] + } + ] + } + const manager = new TreeManager(tree) + const { allNodesHidden } = manager.filterTree('bla-bla') + t.true(allNodesHidden) + const nodes = ['i1', 'c1'] + nodes.forEach(n => t.true(manager.getNodeById(n).hide)) +}) + +test('should use cached results for subsequent searches', t => { + const tree = [ + { + id: 'i1', + label: 'search me', + value: 'v1', + children: [ + { + id: 'c1', + label: 'search me too', + value: 'l1v1', + children: [ + { + id: 'c2', + label: 'No one can get me', + value: 'l2v1' + } + ] + } + ] + }, + { + id: 'i2', + label: 'sears', + value: 'sears' + } + ] + const manager = new TreeManager(tree) + const { allNodesHidden } = manager.filterTree('sea') + manager.filterTree('sear') + manager.filterTree('on') + manager.filterTree('search') + manager.filterTree('search') + t.false(allNodesHidden) +}) + +test('should restore nodes', t => { + const tree = [ + { + id: 'i1', + label: 'search me', + value: 'v1', + children: [ + { + id: 'c1', + label: 'search me too', + value: 'l1v1', + children: [ + { + id: 'c2', + label: 'No one can get me', + value: 'l2v1' + } + ] + } + ] + }, + { + id: 'i2', + label: 'sears', + value: 'sears' + } + ] + const manager = new TreeManager(tree) + manager.filterTree('search') + manager.restoreNodes() + const visibleNodes = ['i1', 'i2', 'c1', 'c2'] + visibleNodes.forEach(n => t.false(manager.getNodeById(n).hide)) +}) + +test('should get matching nodes with mixed case when searched', t => { + const tree = { + id: 'i1', + label: 'search me', + value: 'v1', + children: [ + { + id: 'c1', + label: 'SeaRch me too', + value: 'l1v1', + children: [ + { + id: 'c2', + label: 'No one can get me', + value: 'l2v1' + } + ] + } + ] + } + const manager = new TreeManager(tree) + const { allNodesHidden } = manager.filterTree('SearCH') + t.false(allNodesHidden) + const nodes = ['i1', 'c1'] + nodes.forEach(n => t.false(manager.getNodeById(n).hide)) +}) + +test('should uncheck previous node in simple select mode', t => { + const tree = [ + { + id: 'i1', + label: 'l1', + value: 'v1', + children: [ + { + id: 'c1', + label: 'l1c1', + value: 'l1v1' + } + ] + }, + { + id: 'i2', + label: 'l2', + value: 'v2', + children: [ + { + id: 'c2', + label: 'l2c2', + value: 'l2v2' + } + ] + } + ] + const manager = new TreeManager(tree, true) + manager.setNodeCheckedState('i1', true) + t.true(manager.getNodeById('i1').checked) + + manager.setNodeCheckedState('i2', true) + t.false(manager.getNodeById('i1').checked) + t.true(manager.getNodeById('i2').checked) + + manager.setNodeCheckedState('i1', true) + t.true(manager.getNodeById('i1').checked) + t.false(manager.getNodeById('i2').checked) +}) diff --git a/src/tree-manager/initialState.test.js b/src/tree-manager/tests/initialState.test.js similarity index 50% rename from src/tree-manager/initialState.test.js rename to src/tree-manager/tests/initialState.test.js index 78a23965..fe1a67d6 100644 --- a/src/tree-manager/initialState.test.js +++ b/src/tree-manager/tests/initialState.test.js @@ -1,17 +1,19 @@ import test from 'ava' -import TreeManager from './index' +import TreeManager from '..' // eslint-disable-next-line max-len -test('should set initial disabled state based on parent disabled state when node disabled state is not defined', (t) => { +test('should set initial disabled state based on parent disabled state when node disabled state is not defined', t => { const tree = { id: 'i1', label: 'l1', value: 'v1', - children: [{ - id: 'c1', - label: 'l1c1', - value: 'l1v1' - }], + children: [ + { + id: 'c1', + label: 'l1c1', + value: 'l1v1' + } + ], disabled: true } const manager = new TreeManager(tree) @@ -20,16 +22,18 @@ test('should set initial disabled state based on parent disabled state when node // should set initial disabled state based on parent disabled state // when node disabled state is not defined and parent checked is defined -test('when node disabled state is not defined and parent checked is defined', (t) => { +test('when node disabled state is not defined and parent checked is defined', t => { const tree = { id: 'i1', label: 'l1', value: 'v1', - children: [{ - id: 'c1', - label: 'l1c1', - value: 'l1v1' - }], + children: [ + { + id: 'c1', + label: 'l1c1', + value: 'l1v1' + } + ], disabled: true } const manager = new TreeManager(tree) @@ -38,22 +42,26 @@ test('when node disabled state is not defined and parent checked is defined', (t // should set initial disabled state based on parent disabled state // when node disabled state is not defined and parent checked is defined -test('when node disabled state is not defined and parent checked is defined', (t) => { +test('when node disabled state is not defined and parent checked is defined', t => { const tree = { id: 'i1', label: 'l1', value: 'v1', - children: [{ - id: 'c1', - label: 'l1c1', - value: 'l1v1', - checked: true, - children: [{ - id: 'gc1', - label: 'l2c1', - value: 'l2v1' - }] - }], + children: [ + { + id: 'c1', + label: 'l1c1', + value: 'l1v1', + checked: true, + children: [ + { + id: 'gc1', + label: 'l2c1', + value: 'l2v1' + } + ] + } + ], disabled: true } const manager = new TreeManager(tree) @@ -63,22 +71,26 @@ test('when node disabled state is not defined and parent checked is defined', (t // should set initial disabled state based on parent disabled state // when node disabled state is not defined and parent checked is defined -test('when node disabled state is not defined and grand parent checked is defined', (t) => { +test('when node disabled state is not defined and grand parent checked is defined', t => { const tree = { id: 'i1', label: 'l1', value: 'v1', - children: [{ - id: 'c1', - label: 'l1c1', - value: 'l1v1', - disabled: true, - children: [{ - id: 'gc1', - label: 'l2c1', - value: 'l2v1' - }] - }], + children: [ + { + id: 'c1', + label: 'l1c1', + value: 'l1v1', + disabled: true, + children: [ + { + id: 'gc1', + label: 'l2c1', + value: 'l2v1' + } + ] + } + ], checked: true } const manager = new TreeManager(tree) @@ -89,23 +101,27 @@ test('when node disabled state is not defined and grand parent checked is define }) // eslint-disable-next-line max-len -test('when node disabled is not defined, parent checked/disabled is defined and grand parent checked/disabled is defined', (t) => { +test('when node disabled is not defined, parent checked/disabled is defined and grand parent checked/disabled is defined', t => { const tree = { id: 'i1', label: 'l1', value: 'v1', - children: [{ - id: 'c1', - label: 'l1c1', - value: 'l1v1', - disabled: false, - checked: false, - children: [{ - id: 'gc1', - label: 'l2c1', - value: 'l2v1' - }] - }], + children: [ + { + id: 'c1', + label: 'l1c1', + value: 'l1v1', + disabled: false, + checked: false, + children: [ + { + id: 'gc1', + label: 'l2c1', + value: 'l2v1' + } + ] + } + ], checked: true, disabled: true } @@ -115,3 +131,31 @@ test('when node disabled is not defined, parent checked/disabled is defined and t.falsy(manager.getNodeById('gc1').checked) t.falsy(manager.getNodeById('gc1').disabled) }) + +test('should set partial state if at least one child is partial', t => { + const tree = { + id: '1', + children: [ + { + id: '1-1', + children: [{ id: '1-1-1', checked: true }, { id: '1-1-2' }] + }, + { + id: '1-2', + children: [{ id: '1-2-1' }, { id: '1-2-2' }, { id: '1-2-3' }] + } + ] + } + + const manager = new TreeManager(tree, false, true) + t.true(manager.getNodeById('1').partial) + t.true(manager.getNodeById('1-1').partial) + + // should not affect other nodes + t.falsy(manager.getNodeById('1-1-2').partial) + t.falsy(manager.getNodeById('1-2').partial) + t.falsy(manager.getNodeById('1-2-1').partial) + t.falsy(manager.getNodeById('1-2-2').partial) + t.falsy(manager.getNodeById('1-2-2').partial) + t.falsy(manager.getNodeById('1-2-3').partial) +}) diff --git a/src/tree-manager/tests/partial-setup.js b/src/tree-manager/tests/partial-setup.js new file mode 100644 index 00000000..a267d977 --- /dev/null +++ b/src/tree-manager/tests/partial-setup.js @@ -0,0 +1,20 @@ +export const grandParent = '1' + +export const parent1 = '1-1' +export const parent2 = '1-2' +export const parents = [parent1, parent2] + +export const childrenOfParent1 = ['1-1-1', '1-1-2'] +export const childrenOfParent2 = ['1-2-1', '1-2-2', '1-2-3'] +export const children = [...childrenOfParent1, ...childrenOfParent2] + +export const assertTreeInExpectedState = (t, manager, expected) => { + const { + checked = [], partial = [], unchecked = [], nonPartial = [] +} = expected + + checked.forEach(c => t.truthy(manager.getNodeById(c).checked, `Expected node ${c} to be in checked state`)) + partial.forEach(c => t.truthy(manager.getNodeById(c).partial, `Expected node ${c} to be in partial state`)) + unchecked.forEach(c => t.falsy(manager.getNodeById(c).checked, `Expected node ${c} to be in unchecked state`)) + nonPartial.forEach(c => t.falsy(manager.getNodeById(c).partial, `Expected node ${c} to be in non-partial state`)) +} diff --git a/src/tree-manager/tests/partialSelect.test.js b/src/tree-manager/tests/partialSelect.test.js new file mode 100644 index 00000000..fb0870c0 --- /dev/null +++ b/src/tree-manager/tests/partialSelect.test.js @@ -0,0 +1,78 @@ +import test from 'ava' +import TreeManager from '..' +import { + grandParent, parent1, parent2, parents, childrenOfParent1, childrenOfParent2, children, assertTreeInExpectedState +} from './partial-setup' + +let tree + +test.beforeEach(t => { + tree = { + id: '1', + children: [ + { + id: '1-1', + children: [{ id: '1-1-1' }, { id: '1-1-2' }] + }, + { + id: '1-2', + children: [{ id: '1-2-1' }, { id: '1-2-2' }, { id: '1-2-3' }] + } + ] + } +}) + +test('should set partial state if first child is partial', t => { + tree.children[0].checked = true + + const manager = new TreeManager(tree, false, true) + + const expected = { + checked: [parent1, ...childrenOfParent1], + nonPartial: [...parents, ...children], + partial: [grandParent], + unchecked: [parent2, ...childrenOfParent2] + } + assertTreeInExpectedState(t, manager, expected) +}) + +test('should set partial state if last child is partial', t => { + tree.children[1].checked = true + + const manager = new TreeManager(tree, false, true) + + const expected = { + checked: [parent2, ...childrenOfParent2], + nonPartial: [...parents, ...children], + partial: [grandParent], + unchecked: [parent1, ...childrenOfParent1] + } + assertTreeInExpectedState(t, manager, expected) +}) + +test('should set partial state if at least one grandchild is partial', t => { + tree.children[1].children[1].checked = true + + const manager = new TreeManager(tree, false, true) + + const expected = { + checked: [tree.children[1].children[1].id], + nonPartial: [parent1, ...childrenOfParent1], + partial: [grandParent, parent2], + unchecked: [parent1, ...childrenOfParent1] + } + assertTreeInExpectedState(t, manager, expected) +}) + +test('should not set partial state if all of the children are checked', t => { + tree.children[0].checked = true + tree.children[1].checked = true + + const manager = new TreeManager(tree, false, true) + + const expected = { + checked: [grandParent, ...parents, ...children], + nonPartial: [grandParent, ...parents, ...children] + } + assertTreeInExpectedState(t, manager, expected) +}) diff --git a/src/tree-manager/tests/stateTransitions.test.js b/src/tree-manager/tests/stateTransitions.test.js new file mode 100644 index 00000000..f937c226 --- /dev/null +++ b/src/tree-manager/tests/stateTransitions.test.js @@ -0,0 +1,140 @@ +import test from 'ava' +import TreeManager from '..' +import { + grandParent, parent1, parent2, parents, childrenOfParent1, childrenOfParent2, children, assertTreeInExpectedState +} from './partial-setup' + +const tree = { + id: '1', + children: [ + { + id: '1-1', + children: [{ id: '1-1-1' }, { id: '1-1-2' }] + }, + { + id: '1-2', + children: [{ id: '1-2-1' }, { id: '1-2-2' }, { id: '1-2-3' }] + } + ] +} + +// gp: grand parent +// gc: grandchildren +// p1: parent1 +// p2: parent2 + +test('select gp -> everything checked', t => { + const manager = new TreeManager(tree, false, true) + manager.setNodeCheckedState(grandParent, true) + + const expected = { + checked: [grandParent, ...parents, ...children], + nonPartial: [grandParent, ...parents, ...children] + } + assertTreeInExpectedState(t, manager, expected) +}) + +test('select gp, unselect child -> gp partial', t => { + const manager = new TreeManager(tree, false, true) + // select gp + manager.setNodeCheckedState(grandParent, true) + + // unselect first child + manager.setNodeCheckedState(parent1, false) + + const expected = { + checked: [parent2, ...childrenOfParent2], + partial: [grandParent], + unchecked: [parent1, ...childrenOfParent1] + } + assertTreeInExpectedState(t, manager, expected) +}) + +test('select gp, unselect child, reselect child -> all checked', t => { + const manager = new TreeManager(tree, false, true) + // select gp + manager.setNodeCheckedState(grandParent, true) + + // unselect first child + manager.setNodeCheckedState(parent1, false) + + // reselect first child + manager.setNodeCheckedState(parent1, true) + + const expected = { + checked: [grandParent, ...parents, ...children], + nonPartial: [grandParent, ...parents, ...children] + } + assertTreeInExpectedState(t, manager, expected) +}) + +test('select gp, unselect grandchild -> gp, p1 partial', t => { + const manager = new TreeManager(tree, false, true) + // select gp + manager.setNodeCheckedState(grandParent, true) + + // unselect first grandchild + manager.setNodeCheckedState(childrenOfParent1[0], false) + + const expected = { + checked: [parent2, ...childrenOfParent2, childrenOfParent1[1]], + nonPartial: [parent2, ...childrenOfParent2], + partial: [grandParent, parent1], + unchecked: [childrenOfParent1[0]] + } + assertTreeInExpectedState(t, manager, expected) +}) + +test('select gp, unselect grandchild, reselect grandchild -> all checked', t => { + const manager = new TreeManager(tree, false, true) + // select gp + manager.setNodeCheckedState(grandParent, true) + + // unselect first grandchild + manager.setNodeCheckedState(childrenOfParent1[0], false) + + // reselect first grandchild + manager.setNodeCheckedState(childrenOfParent1[0], true) + + const expected = { + checked: [grandParent, ...parents, ...children], + nonPartial: [grandParent, ...parents, ...children] + } + assertTreeInExpectedState(t, manager, expected) +}) + +test('select gp, unselect grandchild, reselect p1 -> all checked', t => { + const manager = new TreeManager(tree, false, true) + // select gp + manager.setNodeCheckedState(grandParent, true) + + // unselect first grandchild + manager.setNodeCheckedState(childrenOfParent1[0], false) + + // reselect first grandchild + manager.setNodeCheckedState(parent1, true) + + const expected = { + checked: [grandParent, ...parents, ...children], + nonPartial: [grandParent, ...parents, ...children] + } + assertTreeInExpectedState(t, manager, expected) +}) + +test('select gp, unselect grandchild, reselect gp -> all checked', t => { + const manager = new TreeManager(tree, false, true) + // select gp + manager.setNodeCheckedState(grandParent, true) + + // unselect first grandchild + manager.setNodeCheckedState(childrenOfParent1[0], false) + + // reselect gp + manager.setNodeCheckedState(grandParent, true) + + const expected = { + checked: [grandParent, ...parents, ...children], + nonPartial: [grandParent, ...parents, ...children] + } + assertTreeInExpectedState(t, manager, expected) +}) diff --git a/src/tree-node/index.js b/src/tree-node/index.js index 6f699d9e..b3a9e895 100644 --- a/src/tree-node/index.js +++ b/src/tree-node/index.js @@ -13,8 +13,10 @@ const cx = cn.bind(styles) const isLeaf = node => isEmpty(node._children) -const getNodeCx = (props) => { - const { keepTreeOnSearch, node } = props +const getNodeCx = props => { + const { + keepTreeOnSearch, node, showPartiallySelected +} = props return cx( 'node', @@ -23,29 +25,25 @@ const getNodeCx = (props) => { tree: !isLeaf(node), disabled: node.disabled, hide: node.hide, - 'match-in-children': keepTreeOnSearch && node.matchInChildren + 'match-in-children': keepTreeOnSearch && node.matchInChildren, + partial: showPartiallySelected && node.partial }, node.className ) } -const getToggleCx = ({ node }) => cx( - 'toggle', - { expanded: !isLeaf(node) && node.expanded, collapsed: !isLeaf(node) && !node.expanded } -) +const getToggleCx = ({ node }) => cx('toggle', { expanded: !isLeaf(node) && node.expanded, collapsed: !isLeaf(node) && !node.expanded }) -const getNodeActions = (props) => { +const getNodeActions = props => { const { node, onAction } = props - return (node.actions || []).map((a, idx) => ( - - )) + return (node.actions || []).map((a, idx) => ) } -const TreeNode = (props) => { +const TreeNode = props => { const { - simpleSelect, keepTreeOnSearch, node, searchModeOn, onNodeToggle, onCheckboxChange - } = props + simpleSelect, keepTreeOnSearch, node, searchModeOn, onNodeToggle, onCheckboxChange, showPartiallySelected +} = props const liCx = getNodeCx(props) const toggleCx = getToggleCx(props) const style = keepTreeOnSearch || !searchModeOn ? { paddingLeft: `${node._depth * 20}px` } : {} @@ -53,7 +51,7 @@ const TreeNode = (props) => { return (
  • onNodeToggle(node._id)} /> - + {getNodeActions(props)}
  • ) @@ -78,7 +76,8 @@ TreeNode.propTypes = { onNodeToggle: PropTypes.func, onAction: PropTypes.func, onCheckboxChange: PropTypes.func, - simpleSelect: PropTypes.bool + simpleSelect: PropTypes.bool, + showPartiallySelected: PropTypes.bool } export default TreeNode diff --git a/src/tree-node/node-label.js b/src/tree-node/node-label.js index fa55317c..ed530168 100644 --- a/src/tree-node/node-label.js +++ b/src/tree-node/node-label.js @@ -1,12 +1,16 @@ import React from 'react' import PropTypes from 'prop-types' -const NodeLabel = (props) => { - const { simpleSelect, node, onCheckboxChange } = props +import Checkbox from '../checkbox' + +const NodeLabel = props => { + const { + simpleSelect, node, onCheckboxChange, showPartiallySelected +} = props const nodeLabelProps = { className: 'node-label' } if (simpleSelect) { - nodeLabelProps.onClick = (e) => { + nodeLabelProps.onClick = e => { e.stopPropagation() e.nativeEvent.stopImmediatePropagation() onCheckboxChange(node._id, true) @@ -15,19 +19,20 @@ const NodeLabel = (props) => { return ( ) + + ) } NodeLabel.propTypes = { diff --git a/src/tree/index.js b/src/tree/index.js index 4ed85385..042d7b1d 100644 --- a/src/tree/index.js +++ b/src/tree/index.js @@ -11,33 +11,36 @@ const shouldRenderNode = (node, searchModeOn, data) => { return !parent || parent.expanded } -const getNodes = (props) => { +const getNodes = props => { const { - data, keepTreeOnSearch, searchModeOn, simpleSelect - } = props + data, keepTreeOnSearch, searchModeOn, simpleSelect, showPartiallySelected +} = props const { - onAction, onChange, onCheckboxChange, onNodeToggle - } = props + onAction, onChange, onCheckboxChange, onNodeToggle +} = props const items = [] data.forEach((node, key) => { if (shouldRenderNode(node, searchModeOn, data)) { - items.push() + items.push( + + ) } }) return items } -const Tree = (props) => { +const Tree = props => { const { searchModeOn } = props return
      {getNodes(props)}
    @@ -51,7 +54,8 @@ Tree.propTypes = { onNodeToggle: PropTypes.func, onAction: PropTypes.func, onCheckboxChange: PropTypes.func, - simpleSelect: PropTypes.bool + simpleSelect: PropTypes.bool, + showPartiallySelected: PropTypes.bool } export default Tree diff --git a/src/tree/index.test.js b/src/tree/index.test.js index 4907cc70..555c612f 100644 --- a/src/tree/index.test.js +++ b/src/tree/index.test.js @@ -28,7 +28,9 @@ test('renders tree nodes when search mode is on', (t) => { children: [ { label: 'item2-1-1', value: 'value2-1-1' }, { label: 'item2-1-2', value: 'value2-1-2' }, - { label: 'item2-1-3', value: 'value2-1-3', children: [{ label: 'item2-1-3-1', value: 'value2-1-3-1' }] } + { + label: 'item2-1-3', value: 'value2-1-3', children: [{ label: 'item2-1-3-1', value: 'value2-1-3-1' }] +} ] }, { label: 'item2-2', value: 'value2-2' } @@ -53,11 +55,17 @@ test('renders only expanded tree nodes when search mode is off', (t) => { value: 'value1-1', className: 'should-be-rendered', children: [ - { label: 'item1-1-1', value: 'value1-1-1', className: 'should-not-be-rendered' }, - { label: 'item1-1-2', value: 'value1-1-2', className: 'should-not-be-rendered' } + { + label: 'item1-1-1', value: 'value1-1-1', className: 'should-not-be-rendered' +}, + { + label: 'item1-1-2', value: 'value1-1-2', className: 'should-not-be-rendered' +} ] }, - { label: 'item1-2', value: 'value1-2', className: 'should-be-rendered' } + { + label: 'item1-2', value: 'value1-2', className: 'should-be-rendered' +} ] }, { @@ -70,13 +78,19 @@ test('renders only expanded tree nodes when search mode is off', (t) => { value: 'value2-1', className: 'should-not-be-rendered', children: [ - { label: 'item2-1-1', value: 'value2-1-1', className: 'should-not-be-rendered' }, - { label: 'item2-1-2', value: 'value2-1-2', className: 'should-not-be-rendered' }, + { + label: 'item2-1-1', value: 'value2-1-1', className: 'should-not-be-rendered' +}, + { + label: 'item2-1-2', value: 'value2-1-2', className: 'should-not-be-rendered' +}, { label: 'item2-1-3', value: 'value2-1-3', className: 'should-not-be-rendered', - children: [{ label: 'item2-1-3-1', value: 'value2-1-3-1', className: 'should-not-be-rendered' }] + children: [{ + label: 'item2-1-3-1', value: 'value2-1-3-1', className: 'should-not-be-rendered' +}] } ] }, diff --git a/webpack.config.js b/webpack.config.js index d9677e84..aac16170 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,9 +5,7 @@ const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') module.exports = { devtool: 'source-map', - entry: { - 'react-dropdown-tree-select': './src/index.js' - }, + entry: {'react-dropdown-tree-select': './src/index.js'}, output: { path: path.join(__dirname, 'dist'), filename: '[name].js', @@ -24,14 +22,14 @@ module.exports = { } }, plugins: [ - new webpack.DefinePlugin({ - 'process.env.NODE_ENV': JSON.stringify('production') - }), + new webpack.DefinePlugin({'process.env.NODE_ENV': JSON.stringify('production')}), new ExtractTextPlugin('styles.css'), new webpack .optimize .UglifyJsPlugin({ sourceMap: true, exclude: /node_modules/ }), - new BundleAnalyzerPlugin({ analyzerMode: 'static', openAnalyzer: false, generateStatsFile: true }) + new BundleAnalyzerPlugin({ + analyzerMode: 'static', openAnalyzer: false, generateStatsFile: true +}) ], module: { rules: [ @@ -52,9 +50,7 @@ module.exports = { minimize: true } }, - { - loader: 'postcss-loader' - } + {loader: 'postcss-loader'} ] }), include: /src/, diff --git a/yarn.lock b/yarn.lock index acbc2869..224e340a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -612,6 +612,10 @@ array-unique@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" +array.partial@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array.partial/-/array.partial-1.0.2.tgz#76a27889d0ee2e4f04534097b09456879314cb72" + arrify@^1.0.0, arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" @@ -3504,7 +3508,13 @@ enzyme-adapter-utils@^1.3.0: object.assign "^4.0.4" prop-types "^15.6.0" -enzyme@^3.1.1: +enzyme-to-json@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/enzyme-to-json/-/enzyme-to-json-3.3.3.tgz#ede45938fb309cd87ebd4386f60c754525515a07" + dependencies: + lodash "^4.17.4" + +enzyme@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.3.0.tgz#0971abd167f2d4bf3f5bd508229e1c4b6dc50479" dependencies: @@ -8474,7 +8484,7 @@ react-story@^0.0.10: react-fastclick "^3.0.1" react-router-dom "^4.1.1" -react-test-renderer@^16.0.0, react-test-renderer@^16.0.0-0: +react-test-renderer@^16.0.0-0: version "16.2.0" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.2.0.tgz#bddf259a6b8fcd8555f012afc8eacc238872a211" dependencies: