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 <i>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 <i>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 ( -
    -
      + return ( +
      +
        {children} -
      -
      - ); - } +
    +
    + ); + } }); +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 +});