diff --git a/.gitignore b/.gitignore
index c7b9408..c780fac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
node_modules/
public/bower_components/
+public/index.html
\ No newline at end of file
diff --git a/Gruntfile.js b/Gruntfile.js
index a42a5fb..5a91411 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -1,36 +1,52 @@
-module.exports = function(grunt) {
- grunt.initConfig({
- pkg: grunt.file.readJSON('package.json'), // the package file to use
-
- react: {
- single_file_output: {
- files: {
- 'public/js/react-bootstrap-treeview.js': 'src/react-bootstrap-treeview.jsx'
+module.exports = function (grunt) {
+ grunt.initConfig({
+ pkg: grunt.file.readJSON('package.json'), // the package file to use
+
+ react: {
+ single_file_output: {
+ files: {
+ 'dist/js/react-bootstrap-treeview.js': 'src/react-bootstrap-treeview.jsx'
+ }
+ }
+ },
+
+ watch: {
+ files: ['<%= react.single_file_output.files[\'dist/js/react-bootstrap-treeview.js\'] %>', '<%= browserify.main.src %>'],
+ tasks: ['default', 'browserify']
+ },
+
+ copy: {
+ main: {
+ files: [
+ // copy src to example
+ {expand: true, cwd: 'src/', src: '*.css', dest: 'dist/css/'},
+ {expand: true, cwd: 'src/', src: '*.css', dest: 'example/public/css/'}
+ // { expand: true, cwd: 'src/js', src: '*', dest: 'public/js/' }
+ ]
+ }
+ },
+
+ 'browserify': {
+ main: {
+ options: {
+ debug: true,
+ transform: ['reactify'],
+ extensions: ['.jsx']
+ },
+ src: 'example/js/example.js',
+ dest: 'example/public/js/example.js'
+
+ }
+
}
- }
- },
-
- watch: {
- files: [/*'tests/*.js', 'tests/*.html', */'src/**'],
- tasks: ['default']
- },
-
- copy: {
- main: {
- files: [
- // copy src to example
- { expand: true, cwd: 'src/', src: '*.css', dest: 'public/css/' },
- // { expand: true, cwd: 'src/js', src: '*', dest: 'public/js/' }
- ]
- }
- }
- });
-
- // load up your plugins
- grunt.loadNpmTasks('grunt-react');
- grunt.loadNpmTasks('grunt-contrib-watch');
- grunt.loadNpmTasks('grunt-contrib-copy');
-
- // register one or more task lists (you should ALWAYS have a "default" task list)
- grunt.registerTask('default', ['react', 'copy', 'watch']);
+ });
+
+ // load up your plugins
+ grunt.loadNpmTasks('grunt-react');
+ grunt.loadNpmTasks('grunt-contrib-watch');
+ grunt.loadNpmTasks('grunt-contrib-copy');
+ grunt.loadNpmTasks('grunt-browserify');
+
+ // register one or more task lists (you should ALWAYS have a "default" task list)
+ grunt.registerTask('default', ['react', 'copy', 'browserify', 'watch']);
};
diff --git a/README.md b/README.md
index 3e29706..0fb1654 100644
--- a/README.md
+++ b/README.md
@@ -38,12 +38,10 @@ Include the correct styles, it's mainly just bootstrap but we add a few tweaks a
```
-Add required libraries.
+Require the commonJS TreeView
-```html
-
-
-
+```js
+var TreeView = require('react-bootstrap-treeview');
```
Then, a basic initialization would look like.
@@ -55,37 +53,15 @@ React.render(
);
```
+Ifyou don't use browserify, include js files in dist folder
+
+
### Example
-Putting it all together a minimal implementation might look like this.
+An example can be run via the command:
+grunt
-```html
-
-
- React + Bootstrap Tree View
-
-
-
-
-
-
React + Bootstrap Tree View
-
-
-
-
-
-
-
-
-
-```
+Files are created in example/public folder.
## Data Structure
@@ -276,6 +252,12 @@ Boolean. Default: true
Whether or not to highlight the selected node.
+#### isSelectionExclusive
+Boolean, Default false
+
+If true, when a line is selected, others are unselected
+
+
#### levels
Integer. Default: 2
@@ -286,6 +268,9 @@ String, class name(s). Default: "glyphicon glyphicon-stop" as defined by [Boots
Sets the default icon to be used on all nodes, except when overridden on a per node basis in data.
+#### onLineClicked
+Function, callback call when a line (node) is clicked
+
#### selectedBackColor
String, [any legal color value](http://www.w3schools.com/cssref/css_colors_legal.asp). Default: '#428bca'.
@@ -306,7 +291,23 @@ Boolean. Default: false
Whether or not to display tags to the right of each node. The values of which must be provided in the data structure on a per node basis.
+#### treeNodeAttributes
+Object, couples of keys/values ```{key1 : value1, key2 : value2}```
+
+*key:* HTML attribute of the node ``````
+*value:* Dynamic data computed from this.props.data
+
+example:
+```javascript
+ treeNodeAttributes = {'data-foo' : 'bar'}
+ data = {
+ text: 'Parent 1',
+ bar: '1'
+ }
+ ```
+ The node "Parent 1" will have a data-id attribute equals to 1
+ ```html Parent 1 ```
## Copyright and Licensing
Copyright 2013 Jonathan Miles
diff --git a/bower.json b/bower.json
deleted file mode 100644
index 9c5c8c6..0000000
--- a/bower.json
+++ /dev/null
@@ -1,32 +0,0 @@
-{
- "name": "react-bootstrap-treeview",
- "description": "React Tree View for Twitter Bootstrap",
- "version": "0.1.0",
- "homepage": "https://github.com/jonmiles/react-bootstrap-treeview",
- "main": [
-
- ],
- "keywords": [
- "twitter",
- "bootstrap",
- "tree",
- "treeview",
- "tree-view",
- "navigation",
- "javascript",
- "react",
- "react-component"
- ],
- "ignore": [
- "**/.*",
- "node_modules",
- "bower_components",
- "test",
- "tests"
- ],
- "dependencies": {
- "bootstrap": ">= 3.0.0",
- "react": "~0.13.1"
- },
- "devDependencies": {}
-}
diff --git a/dist/css/react-bootstrap-treeview.css b/dist/css/react-bootstrap-treeview.css
new file mode 100644
index 0000000..d8f03ec
--- /dev/null
+++ b/dist/css/react-bootstrap-treeview.css
@@ -0,0 +1,20 @@
+
+.treeview .list-group-item {
+ cursor: pointer;
+}
+
+.treeview span:not(.badge) {
+ width: 1rem;
+ height: 1rem;
+}
+
+.treeview span.indent {
+ margin-left: 6px;
+ margin-right: 6px;
+}
+
+.treeview span.icon {
+ margin-left: 0px;
+ margin-right: 6px;
+}
+
diff --git a/dist/index.html b/dist/index.html
new file mode 100644
index 0000000..f050e5c
--- /dev/null
+++ b/dist/index.html
@@ -0,0 +1,29 @@
+
+
+ React.js Tree View for Twitter Bootstrap
+
+
+
+
+
+
+
+
+
+
+
+
+
+
React.js Tree View for Twitter Bootstrap
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dist/js/react-bootstrap-treeview.js b/dist/js/react-bootstrap-treeview.js
new file mode 100644
index 0000000..e81e1d5
--- /dev/null
+++ b/dist/js/react-bootstrap-treeview.js
@@ -0,0 +1,417 @@
+var React = require('react/addons');
+var TreeView = React.createClass({displayName: "TreeView",
+
+ propTypes: {
+ levels: React.PropTypes.number,
+ expandIcon: React.PropTypes.string,
+ collapseIcon: React.PropTypes.string,
+ emptyIcon: React.PropTypes.string,
+ nodeIcon: React.PropTypes.string,
+ nodeIconSelected: React.PropTypes.string,
+
+ color: React.PropTypes.string,
+ backColor: React.PropTypes.string,
+ borderColor: React.PropTypes.string,
+ onhoverColor: React.PropTypes.string,
+ selectedColor: React.PropTypes.string,
+ selectedBackColor: React.PropTypes.string,
+
+ enableLinks: React.PropTypes.bool,
+ highlightSelected: React.PropTypes.bool,
+ isSelectionExclusive: React.PropTypes.bool,
+ underlineLeafOnly: React.PropTypes.bool,
+ showBorder: React.PropTypes.bool,
+ showTags: React.PropTypes.bool,
+
+ data: React.PropTypes.arrayOf(React.PropTypes.object),
+ onLineClicked: React.PropTypes.func,
+ treeNodeAttributes: React.PropTypes.object //ex:{'data-id': a key in this.props.data}
+ },
+
+
+ getDefaultProps: function () {
+ return {
+ levels: 2,
+
+ expandIcon: 'glyphicon glyphicon-plus',
+ collapseIcon: 'glyphicon glyphicon-minus',
+ emptyIcon: '',
+ nodeIcon: 'glyphicon glyphicon-stop',
+ nodeIconSelected: 'glyphicon glyphicon-eye-open',
+ color: undefined,
+ backColor: undefined,
+ borderColor: undefined,
+ onhoverColor: '#F5F5F5', // TODO Not implemented yet, investigate radium.js 'A toolchain for React component styling'
+ selectedColor: '#FFFFFF',
+ selectedBackColor: '#428bca',
+ classText: '',
+
+ enableLinks: false,
+ highlightSelected: true,
+ isSelectionExclusive: false,
+ underlineLeafOnly: false,
+ showBorder: true,
+ showTags: false,
+
+ data: [],
+ treeNodeAttributes: {}
+ }
+ },
+
+ nodes: [],
+ nodesSelected: {},
+
+ getInitialState: function () {
+ this.setNodeId({nodes: this.props.data});
+
+ return {nodesSelected: this.nodesSelected};
+ },
+
+ setNodeId: function (node) {
+
+ if (!node.nodes) return;
+
+ node.nodes.forEach(function checkStates(node) {
+ node.nodeId = this.nodes.length;
+ this.nodesSelected[node.nodeId] = false;
+ this.nodes.push(node);
+ this.setNodeId(node);
+ }, this);
+ },
+
+ /**
+ * Find a node by nodeId
+ * @param nodeId: node ID
+ * @returns {{}} node object or {}
+ */
+ findNode: function (nodeId) {
+ var find = {};
+ this.nodes.forEach(function (node) {
+ // Node find
+ if (node.nodeId == nodeId) {
+ find = node;
+ }
+ });
+ return find;
+ },
+
+ /**
+ * Line clicked from TreeNode
+ * @param nodeId: node ID
+ * @param evt: event
+ */
+ handleLineClicked: function (nodeId, evt) {
+ if (this.props.onLineClicked !== undefined) {
+ // CLONE EVT + CALLBACK DEV
+ this.props.onLineClicked($.extend(true, {}, evt));
+ }
+
+ var matrice = this.state.nodesSelected;
+ // Exclusive selection
+ if (this.props.isSelectionExclusive) {
+
+ // Underline only if the element is a leaf
+ if (this.props.underlineLeafOnly) {
+ var currentNode = this.findNode(nodeId);
+
+ // Node clicked is a leaf
+ if (!currentNode.nodes) {
+ // Unselection
+ for (var i in matrice) {
+ matrice[i] = false;
+ }
+ matrice[nodeId] = !this.state.nodesSelected[nodeId];
+ }
+ // Node clicked is a parentNode
+ else {
+ // Simulation click expand/collapse icon
+ $(evt.currentTarget).find('[data-target=plusmoins]').click();
+ }
+ }
+ // Underline on all nodes
+ else {
+ // Unselection
+ for (var i in matrice) {
+ matrice[i] = false;
+ }
+ // TOGGLE SELECTION OF CURRENT NODE
+ matrice[nodeId] = !this.state.nodesSelected[nodeId];
+ }
+ }
+ // MULTIPLE SELECTION
+ else {
+ // TOGGLE SELECTION OF CURRENT NODE
+ matrice[nodeId] = !this.state.nodesSelected[nodeId];
+ }
+
+ this.setState({nodesSelected: matrice});
+ },
+
+ render: function () {
+
+ var children = [];
+ if (this.props.data) {
+ this.props.data.forEach(function (node, index) {
+
+ // SELECTION
+ node.selected = (this.state.nodesSelected[node.nodeId]);
+
+ children.push(
+ React.createElement(TreeNode, {
+ node: node,
+ level: 1,
+ visible: true,
+ options: this.props,
+ key: node.nodeId,
+ onLineClicked: this.handleLineClicked,
+ attributes: this.props.treeNodeAttributes,
+ nodesSelected: this.state.nodesSelected}));
+ }.bind(this));
+ }
+
+ return (
+ React.createElement("div", {className: "treeview"},
+ React.createElement("ul", {className: "list-group"},
+ children
+ )
+ )
+ );
+ }
+});
+
+module.exports = TreeView;
+
+var TreeNode = React.createClass({displayName: "TreeNode",
+
+ propTypes: {
+ node: React.PropTypes.object.isRequired,
+ onLineClicked: React.PropTypes.func,
+ attributes: React.PropTypes.object,
+ nodesSelected: React.PropTypes.object.isRequired,
+ options: React.PropTypes.object
+ },
+
+ getInitialState: function () {
+ var node = this.props.node;
+ return {
+ expanded: (node.state && node.state.hasOwnProperty('expanded')) ?
+ node.state.expanded :
+ (this.props.level < this.props.options.levels),
+ selected: (node.state && node.state.hasOwnProperty('selected')) ?
+ node.state.selected :
+ false
+ }
+ },
+
+ componentWillUpdate: function (np, ns) {
+ ns.selected = np.node.selected;
+
+ },
+
+ toggleExpanded: function (id, event) {
+ this.setState({expanded: !this.state.expanded});
+ event.stopPropagation();
+ },
+
+ toggleSelected: function (id, event) {
+ // Exclusive selection
+ if (!this.props.isSelectionExclusive) {
+ this.setState({selected: !this.state.selected});
+ }
+ event.stopPropagation();
+ },
+
+ handleLineClicked: function (nodeId, evt) {
+
+ // SELECT LINE
+ this.toggleSelected(nodeId, $.extend(true, {}, evt));
+ // DEV CLICK
+ this.props.onLineClicked(nodeId, $.extend(true, {}, evt));
+ evt.stopPropagation();
+ },
+
+ render: function () {
+
+ var node = this.props.node;
+ var options = this.props.options;
+
+ // Noeud invisible
+ var style;
+ if (!this.props.visible) {
+ style = {
+ display: 'none'
+ };
+ }
+ // Noeud visible
+ else {
+
+ if (options.highlightSelected && this.state.selected) {
+ style = {
+ color: options.selectedColor,
+ backgroundColor: options.selectedBackColor
+ };
+ }
+ else {
+ style = {
+ color: node.color || options.color,
+ backgroundColor: node.backColor || options.backColor
+ };
+ }
+
+ if (!options.showBorder) {
+ style.border = 'none';
+ }
+ else if (options.borderColor) {
+ style.border = '1px solid ' + options.borderColor;
+ }
+ }
+
+ // Indentation
+ var indents = [];
+ for (var i = 0; i < this.props.level - 1; i++) {
+ indents.push(React.createElement("span", {
+ className: "indent",
+ key: i}));
+ }
+
+ // Custom attributes
+ var attrs = {};
+ if (this.props.attributes !== undefined) {
+ for (var i in this.props.attributes) {
+ if (node[this.props.attributes[i]] !== undefined) {
+ attrs[i] = node[this.props.attributes[i]];
+ }
+ }
+ ;
+ }
+
+ var expandCollapseIcon;
+ // There are children
+ if (node.nodes) {
+ // Collapse
+ if (!this.state.expanded) {
+ expandCollapseIcon = (
+ React.createElement("span", {className: "icon plusmoins"},
+ React.createElement("i", {
+ className: options.expandIcon,
+ onClick: this.toggleExpanded.bind(this, node.nodeId),
+ "data-target": "plusmoins"})
+ )
+ );
+ }
+ // Expanded
+ else {
+ expandCollapseIcon = (
+ React.createElement("span", {className: "icon"},
+ React.createElement("i", {
+ className: options.collapseIcon,
+ onClick: this.toggleExpanded.bind(this, node.nodeId),
+ "data-target": "plusmoins"})
+ )
+ );
+ }
+ }
+ // Node is a leaf
+ else {
+ expandCollapseIcon = (
+ React.createElement("span", {className: options.emptyIcon})
+ );
+ }
+
+ // Icon (if current node is a leaf)
+ var nodeIcon = '';
+ if (options.nodeIcon !== '' && !node.nodes) {
+ //console.log('node %o %o %o',node, this.state.selected, options);
+ var iTarget = (React.createElement("i", {className: node.icon || options.nodeIcon}));
+ // Current node selected
+ if (this.state.selected) {
+ iTarget = (React.createElement("i", {className: options.nodeIconSelected}))
+ }
+ nodeIcon = (
+ React.createElement("span", {className: "icon"},
+ iTarget
+ )
+ );
+ }
+
+ var badges = '';
+ if (options.showTags) {
+ // If tags are defined in the data
+ if (node.tags) {
+ badges = node.tags.map(function (tag, index) {
+ return (
+ React.createElement("span", {
+ className: "badge",
+ key: index},
+ tag
+ )
+ );
+ });
+ }
+ // No tags in data => number of children
+ else {
+ // Children exist
+ if (node.nodes) {
+ badges = (
+ React.createElement("span", {
+ className: "badge"},
+ node.nodes.length
+ )
+ );
+ }
+ }
+ }
+
+ var nodeText;
+ if (options.enableLinks) {
+ nodeText = (
+ React.createElement("span", {
+ className: options.classText},
+ React.createElement("a", {href: node.href/*style="color:inherit;"*/},
+ node.text
+ )
+ )
+ );
+ }
+ else {
+ nodeText = (
+ React.createElement("span", {
+ className: options.classText},
+ node.text
+ )
+ );
+ }
+
+ var children = [];
+ if (node.nodes) {
+ node.nodes.forEach(function (node, index) {
+ // SELECTION
+ node.selected = (this.props.nodesSelected[node.nodeId]);
+ children.push(
+ React.createElement(TreeNode, {
+ node: node,
+ level: this.props.level + 1,
+ visible: this.state.expanded && this.props.visible,
+ options: options,
+ key: node.nodeId,
+ onLineClicked: this.props.onLineClicked,
+ attributes: this.props.attributes,
+ nodesSelected: this.props.nodesSelected}));
+ }, this);
+ }
+
+ return (
+ React.createElement("li", React.__spread({className: "list-group-item",
+ style: style,
+ onClick: this.handleLineClicked.bind(this, node.nodeId)},
+ attrs),
+ indents,
+ expandCollapseIcon,
+ nodeIcon,
+ nodeText,
+ badges,
+ children
+ )
+ );
+ }
+});
diff --git a/example/index.html b/example/index.html
new file mode 100644
index 0000000..d8395ff
--- /dev/null
+++ b/example/index.html
@@ -0,0 +1,24 @@
+
+
+ Treeview
+
+
+
+
+
+
+
+
+
+
+
+
React.js Tree View for Twitter Bootstrap
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/example/js/example.js b/example/js/example.js
new file mode 100644
index 0000000..0e4f0a3
--- /dev/null
+++ b/example/js/example.js
@@ -0,0 +1,73 @@
+var React = require('react/addons');
+var TreeView = require('../../src/react-bootstrap-treeview.jsx');
+
+var data = [
+ {
+ text: 'Parent 1',
+ id: '1',
+ href: '#',
+ nodes: [
+ {
+ text: 'Child 1',
+ id: '11',
+ nodes: [
+ {
+ text: 'Grandchild 1',
+ id: '111'
+ },
+ {
+ text: 'Grandchild 2',
+ id: '112'
+ }
+ ]
+ },
+ {
+ text: 'Child 2',
+ id: '12'
+ }
+ ]
+ },
+ {
+ text: 'Parent 2',
+ id: '2'
+ },
+ {
+ text: 'Parent 3',
+ id: '3'
+ },
+ {
+ text: 'Parent 4',
+ id: '4'
+ },
+ {
+ text: 'Parent 5'
+ }
+];
+
+var test = function (evt) {
+
+ //console.log('click nodeID ' + $(evt.currentTarget).data('id'));
+}
+
+// DOM loaded
+$(function () {
+ React.render(
+ ,
+ document.getElementById('treeview')
+ );
+})
diff --git a/package.json b/package.json
index cb1a9eb..1193500 100644
--- a/package.json
+++ b/package.json
@@ -1,10 +1,10 @@
{
"name": "react-bootstrap-treeview",
"description": "React Tree View for Twitter Bootstrap",
- "version": "0.1.0",
+ "version": "0.2.6",
"homepage": "https://github.com/jonmiles/react-bootstrap-treeview",
"author": {
- "name": "Jonathan Miles"
+ "name": "Elipce"
},
"repository": {
"type": "git",
@@ -19,25 +19,27 @@
"url": "https://github.com/jonmiles/bootstrap-treeview/blob/master/LICENSE"
}
],
- "main": [
-
- ],
+ "main": "./src/react-bootstrap-treeview.jsx",
"scripts": {
- "install": "bower install",
"start": "node app"
},
"engines": {
"node": ">= 0.10.0"
},
"dependencies": {
- "express": "3.4.4"
+ "express": "3.4.4",
+ "react": "^0.13.1"
},
"devDependencies": {
"bower": "1.3.x",
+ "browserify": "^9.0.8",
"grunt": "0.4.x",
- "grunt-react": "~0.12.0",
+ "grunt-browserify": "^3.7.0",
+ "grunt-contrib-copy": "~0.8.0",
"grunt-contrib-watch": "~0.6.1",
- "grunt-contrib-copy": "~0.8.0"
+ "grunt-react": "~0.12.0",
+ "reactify": "^1.1.0",
+ "watchify": "^3.1.1"
},
"keywords": [
"twitter",
diff --git a/public/css/react-bootstrap-treeview.css b/public/css/react-bootstrap-treeview.css
deleted file mode 100644
index 3f249e8..0000000
--- a/public/css/react-bootstrap-treeview.css
+++ /dev/null
@@ -1,19 +0,0 @@
-
-.treeview .list-group-item {
- cursor: pointer;
-}
-
-.treeview span {
- width: 1rem;
- height: 1rem;
-}
-
-.treeview span.indent {
- margin-left: 10px;
- margin-right: 10px;
-}
-
-.treeview span.icon {
- margin-left: 10px;
- margin-right: 5px;
-}
diff --git a/public/index.html b/public/index.html
deleted file mode 100644
index 7fe34d9..0000000
--- a/public/index.html
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
- React.js Tree View for Twitter Bootstrap
-
-
-
-
-
-
React.js Tree View for Twitter Bootstrap
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/public/js/example.jsx b/public/js/example.jsx
deleted file mode 100644
index 81ead49..0000000
--- a/public/js/example.jsx
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-var data = [
- {
- text: 'Parent 1',
- nodes: [
- {
- text: 'Child 1',
- nodes: [
- {
- text: 'Grandchild 1'
- },
- {
- text: 'Grandchild 2'
- }
- ]
- },
- {
- text: 'Child 2'
- }
- ]
- },
- {
- text: 'Parent 2'
- },
- {
- text: 'Parent 3'
- },
- {
- text: 'Parent 4'
- },
- {
- text: 'Parent 5'
- }
-];
-
-
-React.render(
- ,
- document.getElementById('treeview')
-);
\ No newline at end of file
diff --git a/public/js/react-bootstrap-treeview.js b/public/js/react-bootstrap-treeview.js
deleted file mode 100644
index a3d512d..0000000
--- a/public/js/react-bootstrap-treeview.js
+++ /dev/null
@@ -1,232 +0,0 @@
-var TreeView = React.createClass({displayName: "TreeView",
-
- propTypes: {
- levels: React.PropTypes.number,
-
- expandIcon: React.PropTypes.string,
- collapseIcon: React.PropTypes.string,
- emptyIcon: React.PropTypes.string,
- nodeIcon: React.PropTypes.string,
-
- color: React.PropTypes.string,
- backColor: React.PropTypes.string,
- borderColor: React.PropTypes.string,
- onhoverColor: React.PropTypes.string,
- selectedColor: React.PropTypes.string,
- selectedBackColor: React.PropTypes.string,
-
- enableLinks: React.PropTypes.bool,
- highlightSelected: React.PropTypes.bool,
- showBorder: React.PropTypes.bool,
- showTags: React.PropTypes.bool,
-
- nodes: React.PropTypes.arrayOf(React.PropTypes.number)
- },
-
- getDefaultProps: function () {
- return {
- levels: 2,
-
- expandIcon: 'glyphicon glyphicon-plus',
- collapseIcon: 'glyphicon glyphicon-minus',
- emptyIcon: 'glyphicon',
- nodeIcon: 'glyphicon glyphicon-stop',
-
- color: undefined,
- backColor: undefined,
- borderColor: undefined,
- onhoverColor: '#F5F5F5', // TODO Not implemented yet, investigate radium.js 'A toolchain for React component styling'
- selectedColor: '#FFFFFF',
- selectedBackColor: '#428bca',
-
- enableLinks: false,
- highlightSelected: true,
- showBorder: true,
- showTags: false,
-
- nodes: []
- }
- },
-
- setNodeId: function(node) {
-
- if (!node.nodes) return;
-
- var _this = this;
- node.nodes.forEach(function checkStates(node) {
- node.nodeId = _this.props.nodes.length;
- _this.props.nodes.push(node);
- _this.setNodeId(node);
- });
- },
-
- render: function() {
-
- this.setNodeId({ nodes: data });
-
- var children = [];
- if (data) {
- var _this = this;
- data.forEach(function (node) {
- children.push(React.createElement(TreeNode, {node: node,
- level: 1,
- visible: true,
- options: _this.props}));
- });
- }
-
- return (
- React.createElement("div", {id: "treeview", className: "treeview"},
- React.createElement("ul", {className: "list-group"},
- children
- )
- )
- );
- }
-});
-
-
-var TreeNode = React.createClass({displayName: "TreeNode",
-
- getInitialState: function() {
- var node = this.props.node;
- return {
- expanded: (node.state && node.state.hasOwnProperty('expanded')) ?
- node.state.expanded :
- (this.props.level < this.props.options.levels) ?
- true :
- false,
- selected: (node.state && node.state.hasOwnProperty('selected')) ?
- node.state.selected :
- false
- }
- },
-
- toggleExpanded: function(id, event) {
- this.setState({ expanded: !this.state.expanded });
- event.stopPropagation();
- },
-
- toggleSelected: function(id, event) {
- this.setState({ selected: !this.state.selected });
- event.stopPropagation();
- },
-
- render: function() {
-
- var node = this.props.node;
- var options = this.props.options;
-
- var style;
- if (!this.props.visible) {
-
- style = {
- display: 'none'
- };
- }
- else {
-
- if (options.highlightSelected && this.state.selected) {
- style = {
- color: options.selectedColor,
- backgroundColor: options.selectedBackColor
- };
- }
- else {
- style = {
- color: node.color || options.color,
- backgroundColor: node.backColor || options.backColor
- };
- }
-
- if (!options.showBorder) {
- style.border = 'none';
- }
- else if (options.borderColor) {
- style.border = '1px solid ' + options.borderColor;
- }
- }
-
- var indents = [];
- for (var i = 0; i < this.props.level-1; i++) {
- indents.push(React.createElement("span", {className: "indent"}));
- }
-
- var expandCollapseIcon;
- if (node.nodes) {
- if (!this.state.expanded) {
- expandCollapseIcon = (
- React.createElement("span", {className: options.expandIcon,
- onClick: this.toggleExpanded.bind(this, node.nodeId)}
- )
- );
- }
- else {
- expandCollapseIcon = (
- React.createElement("span", {className: options.collapseIcon,
- onClick: this.toggleExpanded.bind(this, node.nodeId)}
- )
- );
- }
- }
- else {
- expandCollapseIcon = (
- React.createElement("span", {className: options.emptyIcon})
- );
- }
-
- var nodeIcon = (
- React.createElement("span", {className: "icon"},
- React.createElement("i", {className: node.icon || options.nodeIcon})
- )
- );
-
- var nodeText;
- if (options.enableLinks) {
- nodeText = (
- React.createElement("a", {href: node.href/*style="color:inherit;"*/},
- node.text
- )
- );
- }
- else {
- nodeText = (
- React.createElement("span", null, node.text)
- );
- }
-
- var badges;
- if (options.showTags && node.tags) {
- badges = node.tags.map(function (tag) {
- return (
- React.createElement("span", {className: "badge"}, tag)
- );
- });
- }
-
- var children = [];
- if (node.nodes) {
- var _this = this;
- node.nodes.forEach(function (node) {
- children.push(React.createElement(TreeNode, {node: node,
- level: _this.props.level+1,
- visible: _this.state.expanded && _this.props.visible,
- options: options}));
- });
- }
-
- return (
- React.createElement("li", {className: "list-group-item",
- style: style,
- onClick: this.toggleSelected.bind(this, node.nodeId),
- key: node.nodeId},
- indents,
- expandCollapseIcon,
- nodeIcon,
- nodeText,
- badges,
- children
- )
- );
- }
-});
\ No newline at end of file
diff --git a/src/react-bootstrap-treeview.css b/src/react-bootstrap-treeview.css
index 3f249e8..d8f03ec 100644
--- a/src/react-bootstrap-treeview.css
+++ b/src/react-bootstrap-treeview.css
@@ -1,19 +1,20 @@
.treeview .list-group-item {
- cursor: pointer;
+ cursor: pointer;
}
-.treeview span {
- width: 1rem;
- height: 1rem;
+.treeview span:not(.badge) {
+ width: 1rem;
+ height: 1rem;
}
.treeview span.indent {
- margin-left: 10px;
- margin-right: 10px;
+ margin-left: 6px;
+ margin-right: 6px;
}
.treeview span.icon {
- margin-left: 10px;
- margin-right: 5px;
+ margin-left: 0px;
+ margin-right: 6px;
}
+
diff --git a/src/react-bootstrap-treeview.jsx b/src/react-bootstrap-treeview.jsx
index e65fedf..da0e3a9 100644
--- a/src/react-bootstrap-treeview.jsx
+++ b/src/react-bootstrap-treeview.jsx
@@ -1,232 +1,417 @@
+var React = require('react/addons');
var TreeView = React.createClass({
- propTypes: {
- levels: React.PropTypes.number,
-
- expandIcon: React.PropTypes.string,
- collapseIcon: React.PropTypes.string,
- emptyIcon: React.PropTypes.string,
- nodeIcon: React.PropTypes.string,
-
- color: React.PropTypes.string,
- backColor: React.PropTypes.string,
- borderColor: React.PropTypes.string,
- onhoverColor: React.PropTypes.string,
- selectedColor: React.PropTypes.string,
- selectedBackColor: React.PropTypes.string,
-
- enableLinks: React.PropTypes.bool,
- highlightSelected: React.PropTypes.bool,
- showBorder: React.PropTypes.bool,
- showTags: React.PropTypes.bool,
-
- nodes: React.PropTypes.arrayOf(React.PropTypes.number)
- },
-
- getDefaultProps: function () {
- return {
- levels: 2,
-
- expandIcon: 'glyphicon glyphicon-plus',
- collapseIcon: 'glyphicon glyphicon-minus',
- emptyIcon: 'glyphicon',
- nodeIcon: 'glyphicon glyphicon-stop',
-
- color: undefined,
- backColor: undefined,
- borderColor: undefined,
- onhoverColor: '#F5F5F5', // TODO Not implemented yet, investigate radium.js 'A toolchain for React component styling'
- selectedColor: '#FFFFFF',
- selectedBackColor: '#428bca',
-
- enableLinks: false,
- highlightSelected: true,
- showBorder: true,
- showTags: false,
-
- nodes: []
- }
- },
-
- setNodeId: function(node) {
-
- if (!node.nodes) return;
-
- var _this = this;
- node.nodes.forEach(function checkStates(node) {
- node.nodeId = _this.props.nodes.length;
- _this.props.nodes.push(node);
- _this.setNodeId(node);
- });
- },
-
- render: function() {
-
- this.setNodeId({ nodes: data });
-
- var children = [];
- if (data) {
- var _this = this;
- data.forEach(function (node) {
- children.push( );
- });
- }
+ propTypes: {
+ levels: React.PropTypes.number,
+ expandIcon: React.PropTypes.string,
+ collapseIcon: React.PropTypes.string,
+ emptyIcon: React.PropTypes.string,
+ nodeIcon: React.PropTypes.string,
+ nodeIconSelected: React.PropTypes.string,
+
+ color: React.PropTypes.string,
+ backColor: React.PropTypes.string,
+ borderColor: React.PropTypes.string,
+ onhoverColor: React.PropTypes.string,
+ selectedColor: React.PropTypes.string,
+ selectedBackColor: React.PropTypes.string,
+
+ enableLinks: React.PropTypes.bool,
+ highlightSelected: React.PropTypes.bool,
+ isSelectionExclusive: React.PropTypes.bool,
+ underlineLeafOnly: React.PropTypes.bool,
+ showBorder: React.PropTypes.bool,
+ showTags: React.PropTypes.bool,
+
+ data: React.PropTypes.arrayOf(React.PropTypes.object),
+ onLineClicked: React.PropTypes.func,
+ treeNodeAttributes: React.PropTypes.object //ex:{'data-id': a key in this.props.data}
+ },
+
+
+ getDefaultProps: function () {
+ return {
+ levels: 2,
+
+ expandIcon: 'glyphicon glyphicon-plus',
+ collapseIcon: 'glyphicon glyphicon-minus',
+ emptyIcon: '',
+ nodeIcon: 'glyphicon glyphicon-stop',
+ nodeIconSelected: 'glyphicon glyphicon-eye-open',
+ color: undefined,
+ backColor: undefined,
+ borderColor: undefined,
+ onhoverColor: '#F5F5F5', // TODO Not implemented yet, investigate radium.js 'A toolchain for React component styling'
+ selectedColor: '#FFFFFF',
+ selectedBackColor: '#428bca',
+ classText: '',
+
+ enableLinks: false,
+ highlightSelected: true,
+ isSelectionExclusive: false,
+ underlineLeafOnly: false,
+ showBorder: true,
+ showTags: false,
+
+ data: [],
+ treeNodeAttributes: {}
+ }
+ },
+
+ nodes: [],
+ nodesSelected: {},
+
+ getInitialState: function () {
+ this.setNodeId({nodes: this.props.data});
+
+ return {nodesSelected: this.nodesSelected};
+ },
+
+ setNodeId: function (node) {
+
+ if (!node.nodes) return;
+
+ node.nodes.forEach(function checkStates(node) {
+ node.nodeId = this.nodes.length;
+ this.nodesSelected[node.nodeId] = false;
+ this.nodes.push(node);
+ this.setNodeId(node);
+ }, this);
+ },
+
+ /**
+ * Find a node by nodeId
+ * @param nodeId: node ID
+ * @returns {{}} node object or {}
+ */
+ findNode: function (nodeId) {
+ var find = {};
+ this.nodes.forEach(function (node) {
+ // Node find
+ if (node.nodeId == nodeId) {
+ find = node;
+ }
+ });
+ return find;
+ },
+
+ /**
+ * Line clicked from TreeNode
+ * @param nodeId: node ID
+ * @param evt: event
+ */
+ handleLineClicked: function (nodeId, evt) {
+ if (this.props.onLineClicked !== undefined) {
+ // CLONE EVT + CALLBACK DEV
+ this.props.onLineClicked($.extend(true, {}, evt));
+ }
+
+ var matrice = this.state.nodesSelected;
+ // Exclusive selection
+ if (this.props.isSelectionExclusive) {
+
+ // Underline only if the element is a leaf
+ if (this.props.underlineLeafOnly) {
+ var currentNode = this.findNode(nodeId);
+
+ // Node clicked is a leaf
+ if (!currentNode.nodes) {
+ // Unselection
+ for (var i in matrice) {
+ matrice[i] = false;
+ }
+ matrice[nodeId] = !this.state.nodesSelected[nodeId];
+ }
+ // Node clicked is a parentNode
+ else {
+ // Simulation click expand/collapse icon
+ $(evt.currentTarget).find('[data-target=plusmoins]').click();
+ }
+ }
+ // Underline on all nodes
+ else {
+ // Unselection
+ for (var i in matrice) {
+ matrice[i] = false;
+ }
+ // TOGGLE SELECTION OF CURRENT NODE
+ matrice[nodeId] = !this.state.nodesSelected[nodeId];
+ }
+ }
+ // MULTIPLE SELECTION
+ else {
+ // TOGGLE SELECTION OF CURRENT NODE
+ matrice[nodeId] = !this.state.nodesSelected[nodeId];
+ }
+
+ this.setState({nodesSelected: matrice});
+ },
+
+ render: function () {
+
+ var children = [];
+ if (this.props.data) {
+ this.props.data.forEach(function (node, index) {
+
+ // SELECTION
+ node.selected = (this.state.nodesSelected[node.nodeId]);
+
+ children.push(
+ );
+ }.bind(this));
+ }
- return (
-
+ );
+ }
});
+module.exports = TreeView;
var TreeNode = React.createClass({
- getInitialState: function() {
- var node = this.props.node;
- return {
- expanded: (node.state && node.state.hasOwnProperty('expanded')) ?
- node.state.expanded :
- (this.props.level < this.props.options.levels) ?
- true :
- false,
- selected: (node.state && node.state.hasOwnProperty('selected')) ?
- node.state.selected :
- false
- }
- },
-
- toggleExpanded: function(id, event) {
- this.setState({ expanded: !this.state.expanded });
- event.stopPropagation();
- },
-
- toggleSelected: function(id, event) {
- this.setState({ selected: !this.state.selected });
- event.stopPropagation();
- },
-
- render: function() {
-
- var node = this.props.node;
- var options = this.props.options;
+ propTypes: {
+ node: React.PropTypes.object.isRequired,
+ onLineClicked: React.PropTypes.func,
+ attributes: React.PropTypes.object,
+ nodesSelected: React.PropTypes.object.isRequired,
+ options: React.PropTypes.object
+ },
+
+ getInitialState: function () {
+ var node = this.props.node;
+ return {
+ expanded: (node.state && node.state.hasOwnProperty('expanded')) ?
+ node.state.expanded :
+ (this.props.level < this.props.options.levels),
+ selected: (node.state && node.state.hasOwnProperty('selected')) ?
+ node.state.selected :
+ false
+ }
+ },
+
+ componentWillUpdate: function (np, ns) {
+ ns.selected = np.node.selected;
+
+ },
+
+ toggleExpanded: function (id, event) {
+ this.setState({expanded: !this.state.expanded});
+ event.stopPropagation();
+ },
+
+ toggleSelected: function (id, event) {
+ // Exclusive selection
+ if (!this.props.isSelectionExclusive) {
+ this.setState({selected: !this.state.selected});
+ }
+ event.stopPropagation();
+ },
+
+ handleLineClicked: function (nodeId, evt) {
+
+ // SELECT LINE
+ this.toggleSelected(nodeId, $.extend(true, {}, evt));
+ // DEV CLICK
+ this.props.onLineClicked(nodeId, $.extend(true, {}, evt));
+ evt.stopPropagation();
+ },
+
+ render: function () {
+
+ var node = this.props.node;
+ var options = this.props.options;
+
+ // Noeud invisible
+ var style;
+ if (!this.props.visible) {
+ style = {
+ display: 'none'
+ };
+ }
+ // Noeud visible
+ else {
+
+ if (options.highlightSelected && this.state.selected) {
+ style = {
+ color: options.selectedColor,
+ backgroundColor: options.selectedBackColor
+ };
+ }
+ else {
+ style = {
+ color: node.color || options.color,
+ backgroundColor: node.backColor || options.backColor
+ };
+ }
+
+ if (!options.showBorder) {
+ style.border = 'none';
+ }
+ else if (options.borderColor) {
+ style.border = '1px solid ' + options.borderColor;
+ }
+ }
+
+ // Indentation
+ var indents = [];
+ for (var i = 0; i < this.props.level - 1; i++) {
+ indents.push( );
+ }
+
+ // Custom attributes
+ var attrs = {};
+ if (this.props.attributes !== undefined) {
+ for (var i in this.props.attributes) {
+ if (node[this.props.attributes[i]] !== undefined) {
+ attrs[i] = node[this.props.attributes[i]];
+ }
+ }
+ ;
+ }
+
+ var expandCollapseIcon;
+ // There are children
+ if (node.nodes) {
+ // Collapse
+ if (!this.state.expanded) {
+ expandCollapseIcon = (
+
+
+
+ );
+ }
+ // Expanded
+ else {
+ expandCollapseIcon = (
+
+
+
+ );
+ }
+ }
+ // Node is a leaf
+ else {
+ expandCollapseIcon = (
+
+ );
+ }
+
+ // Icon (if current node is a leaf)
+ var nodeIcon = '';
+ if (options.nodeIcon !== '' && !node.nodes) {
+ //console.log('node %o %o %o',node, this.state.selected, options);
+ var iTarget = ( );
+ // Current node selected
+ if (this.state.selected) {
+ iTarget = ( )
+ }
+ nodeIcon = (
+
+ {iTarget}
+
+ );
+ }
+
+ var badges = '';
+ if (options.showTags) {
+ // If tags are defined in the data
+ if (node.tags) {
+ badges = node.tags.map(function (tag, index) {
+ return (
+
+ {tag}
+
+ );
+ });
+ }
+ // No tags in data => number of children
+ else {
+ // Children exist
+ if (node.nodes) {
+ badges = (
+
+ {node.nodes.length}
+
+ );
+ }
+ }
+ }
+
+ var nodeText;
+ if (options.enableLinks) {
+ nodeText = (
+
+
+ {node.text}
+
+
+ );
+ }
+ else {
+ nodeText = (
+
+ {node.text}
+
+ );
+ }
+
+ var children = [];
+ if (node.nodes) {
+ node.nodes.forEach(function (node, index) {
+ // SELECTION
+ node.selected = (this.props.nodesSelected[node.nodeId]);
+ children.push(
+ );
+ }, this);
+ }
- var style;
- if (!this.props.visible) {
-
- style = {
- display: 'none'
- };
- }
- else {
-
- if (options.highlightSelected && this.state.selected) {
- style = {
- color: options.selectedColor,
- backgroundColor: options.selectedBackColor
- };
- }
- else {
- style = {
- color: node.color || options.color,
- backgroundColor: node.backColor || options.backColor
- };
- }
-
- if (!options.showBorder) {
- style.border = 'none';
- }
- else if (options.borderColor) {
- style.border = '1px solid ' + options.borderColor;
- }
- }
-
- var indents = [];
- for (var i = 0; i < this.props.level-1; i++) {
- indents.push( );
- }
-
- var expandCollapseIcon;
- if (node.nodes) {
- if (!this.state.expanded) {
- expandCollapseIcon = (
-
-
- );
- }
- else {
- expandCollapseIcon = (
-
-
- );
- }
- }
- else {
- expandCollapseIcon = (
-
- );
- }
-
- var nodeIcon = (
-
-
-
- );
-
- var nodeText;
- if (options.enableLinks) {
- nodeText = (
-
- {node.text}
-
- );
- }
- else {
- nodeText = (
- {node.text}
- );
- }
-
- var badges;
- if (options.showTags && node.tags) {
- badges = node.tags.map(function (tag) {
return (
- {tag}
+
+ {indents}
+ {expandCollapseIcon}
+ {nodeIcon}
+ {nodeText}
+ {badges}
+ {children}
+
);
- });
}
-
- var children = [];
- if (node.nodes) {
- var _this = this;
- node.nodes.forEach(function (node) {
- children.push( );
- });
- }
-
- return (
-
- {indents}
- {expandCollapseIcon}
- {nodeIcon}
- {nodeText}
- {badges}
- {children}
-
- );
- }
-});
\ No newline at end of file
+});