Skip to content

Instantly share code, notes, and snippets.

@clayzermk1
Created August 10, 2012 19:54
Show Gist options
  • Star 65 You must be signed in to star a gist
  • Fork 19 You must be signed in to fork a gist
  • Save clayzermk1/3317341 to your computer and use it in GitHub Desktop.
Save clayzermk1/3317341 to your computer and use it in GitHub Desktop.
jQuery / Twitter Bootstrap List Tree Plugin

jQuery / Twitter Bootstrap List Tree Plugin

Demo: http://jsfiddle.net/clayzermk1/QD8Hs/

Overview

I needed a simple plugin to build a two-tier collapsible list with checkboxes. I wanted it to fit well with Twitter's Bootstrap. I couldn't find one that was simple enough. I hope you enjoy =) Feel free to send feedback.

Thanks to everyone at jQuery, bootstrap, underscore, and d3!

Features

  • Namespaced for jQuery.
  • Twitter Bootstrap compatible (again namespacing).
  • Expandable/collapsible parent nodes (click to expand/collapse).
  • (Un)Checking a parent node will (un)check all child nodes.
  • If at least one child is checked, the parent is checked (useful for identifying collapsed parents with active child nodes).
  • JSON representation of selected nodes (preserves the order of the original data context).
  • Update with new data on the fly.

Requirements

Tested with:

  • jQuery 1.7.2
  • bootstrap 2.0.4
  • underscore 1.3.3

The data model uses the d3.v2.js nest() ordering and naming conventions.

Installation

You only need the JS file to take advantage of the plugin. The CSS file provides a default bootstrap look with rounded corners.

Use

Simply create an empty containing element of your choice, give it the class "listTree" and invoke the plugin.

$('.listTree').listTree(data, [options]);

(Un)checking a header checkbox will (un)check all items below that header.

Clicking a header will collapse or expand the child items below that header.

Options

Currently there are two options available to the user.

startCollapsed (boolean)

Dictates whether the entire list should initialize collapsed.

Possible Values:

  • true
  • false (Default)

selected (Array)

Dictates which nodes should be selected by default.

Possible values:

  • Not specified or data context - Selects every node (Default)
  • A subset of the data context - Selects only the nodes matching the subset
  • [] - Selects nothing

Plugin Methods

All functions are memoized, to call the function invoke the plugin with the first argument as the method name. If you are calling a function that requires some data, pass that as the second argument.

$('.listTree').listTree('update', someOtherDataObj, someOtherDataOptions);

init(context)

Initializes the plugin with a certain data object, "context". Called implicitly by the plugin.

$('.listTree').listTree(someDataObj, someDataOptions);

destroy()

Destroys the plugin object.

$('.listTree').listTree('destroy');

selectAll()

Selects (check) all checkboxes in the list.

$('.listTree').listTree('selectAll');

deselectAll()

Deselects (uncheck) all checkboxes in the list.

$('.listTree').listTree('deselectAll');

expandAll()

Expands all list headers.

$('.listTree').listTree('expandAll');

collapseAll()

Collapses all list headers.

$('.listTree').listTree('collapseAll');

update()

Updates the list with a new data object.

$('.listTree').listTree('update', someOtherDataObj, someOtherDataOptions);

Copyright & License

Copyright 2012 Clay Walker Licensed under GPLv2 ONLY

/*
* Copyright 2012 Clay Walker
* Licensed under GPLv2 ONLY
*/
.listTree {
margin-bottom: 18px;
}
.listTree i {
float: right;
margin-right: 15px;
}
.listTree ul {
margin: 0;
}
.listTree li {
list-style-type: none;
cursor: pointer;
}
.listTree > ul > li {
background-color: #eee;
border-width: 1px 1px 0 1px;
border-style: solid;
border-color: #ddd;
}
.listTree > ul > li:first-child {
border-width: 1px 1px 0 1px;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
.listTree > ul > li:last-child {
border-width: 1px;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
}
.listTree > ul > li:last-child > ul > li:last-child {
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
}
.listTree span {
display: inline-block;
width: 100%;
padding: 5px;
}
.listTree > ul > li > span {
font-weight: bold;
}
.listTree > ul > li > ul > li {
background-color: #fff;
border-width: 1px 0 0 0;
border-style: solid;
border-color: #ddd;
padding-left: 10px;
}
.listTree > ul > li > ul > li:first-child {
border-width: 1px 0 0 0;
}
.listTree > ul > li > ul > li:last-child {
border-width: 1px 0 0 0;
}
.listTree input[type="checkbox"] {
margin-top: 0;
}
/*
* Copyright 2012 Clay Walker
* Licensed under GPLv2 ONLY
*/
;(function($) {
var template = '\
<ul>\
<% _.each(context, function(parent, index) { %>\
<li>\
<span><input type="checkbox" \
<% var ps; %>\
<% if ( !_.isUndefined(ps = _.find(options.selected, function(elem) { return elem.key === this.key; }, parent)) ) { %>\
checked="checked"\
<% } %> value="<%= parent.key %>" /> <%= parent.key %><i class="icon-chevron-up icon-black"></i></span>\
<% if (options.startCollapsed) { %>\
<ul style="display: none;">\
<% } else { %>\
<ul>\
<% } %>\
<% _.each(parent.values, function(child, index) { %>\
<li>\
<span><input type="checkbox" \
<% if ( !_.isUndefined(this) && !_.isUndefined(_.find(this.values, function(elem) { return elem.key === this.key; }, child)) ) { %>\
checked="checked"\
<% } %> value="<%= child.key %>" /> <%= child.key %></span>\
</li>\
<% }, ps); %>\
</ul>\
</li>\
<% }); %>\
</ul>';
/** Check all child checkboxes.
* @param jQElement The parent <li>.
*/
function _selectAllChildren(jQElement) {
jQElement.find('ul > li > span > input[type="checkbox"]')
.each(function() {
$(this).prop('checked', true);
});
}
/** Uncheck all child checkboxes.
* @param jQElement The parent <li>.
*/
function _deselectAllChildren(jQElement) {
jQElement.find('ul > li > span > input[type="checkbox"]')
.each(function() {
$(this).prop('checked', false);
});
}
/** Toggle all checkboxes.
* @param[in] jQElement The root <ul> of the list.
*/
function _toggleAllChildren(jQElement) {
if (jQElement.children('span').children('input[type="checkbox"]').prop('checked')) {
_selectAllChildren(jQElement);
} else {
_deselectAllChildren(jQElement);
}
}
/** Toggle the collapse icon based on the current state.
* @param[in] jQElement The <li> of the header to toggle.
*/
function _toggleIcon(jQElement) {
// Change the icon.
if (jQElement.children('ul').is(':visible')) {
// The user wants to collapse the child list.
jQElement.children('span').children('i')
.removeClass('icon-chevron-down')
.addClass('icon-chevron-up');
} else {
// The user wants to expand the child list.
jQElement.children('span').children('i')
.removeClass('icon-chevron-up')
.addClass('icon-chevron-down');
}
}
/** Make sure there isn't any bogus default selections.
* @param[in] selected The default selection object.
* @return The filtered selection object.
*/
function _validateDefaultSelectionValues(selected) {
return _.filter(selected, function(elem) {
return ( !_.isEmpty(elem.values) && !_.isUndefined(elem.values) );
});
}
/** If a parent has at least one child node selected, check the parent.
* Conversely, if a parent has no child nodes selected, uncheck the parent.
* @param[in] jQElement The parent <li>.
*/
function _handleChildParentRelationship(jQElement) {
// If the selected node is a child:
if ( _.isEmpty(_.toArray(jQElement.children('ul'))) ) {
var childrenStatuses = _.uniq(
_.map(jQElement.parent().find('input[type="checkbox"]'), function(elem) {
return $(elem).prop('checked');
})
);
// Check to see if any children are checked.
if (_.indexOf(childrenStatuses, true) !== -1) {
// Check the parent node.
jQElement.parent().parent().children('span').children('input[type="checkbox"]').prop('checked', true);
} else {
// Uncheck the parent node.
jQElement.parent().parent().children('span').children('input[type="checkbox"]').prop('checked', false);
}
}
}
/** Updates the internal object of selected nodes.
*/
function _updateSelectedObject() {
var data = $('.listTree').data('listTree');
// Filter the context to the selected parents.
var selected = _.filter($.extend(true, {}, data.context), function(parent) {
return $('.listTree > ul > li > span > input[value="' + parent.key + '"]').prop('checked')
});
// For each parent in the working context...
_.each(selected, function(parent) {
// Filter the children to the selected children.
parent.values = _.filter(parent.values, function(child) {
return $('.listTree > ul > li > ul > li > span > input[value="' + child.key + '"]').prop('checked');
});
});
// Update the plugin's selected object.
$('.listTree').data('listTree', {
"target": data.target,
"context": data.context,
"options": data.options,
"selected": selected
});
}
var methods = {
init: function(context, options) {
// Default options
var defaults = {
"startCollapsed": false,
"selected": context
};
options = $.extend(defaults, options);
// Validate the user entered default selections.
options.selected = _validateDefaultSelectionValues(options.selected);
return this.each(function() {
var $this = $(this),
data = $this.data('listTree');
// If the plugin hasn't been initialized yet...
if (!data) {
$(this).data('listTree', {
"target": $this,
"context": context,
"options": options,
"selected": options.selected
});
// Register checkbox handlers.
$(document).on('change', '.listTree input[type="checkbox"]', function(e) {
var node = $(e.target).parent().parent();
// Toggle all children.
_toggleAllChildren(node);
// Handle parent checkbox if all children are (un)checked.
_handleChildParentRelationship(node);
// Filter context to selection and store in data.selected.
_updateSelectedObject(node);
})
// Register collapse handlers on parents.
.on('click', '.listTree > ul > li > span', function(e) {
var node = $(e.target).parent();
// Change the icon.
_toggleIcon(node);
// Toggle the child list.
node.children('ul').slideToggle('fast');
e.stopImmediatePropagation();
});
// Generate the list tree.
$this.html( _.template( template, { "context": context, "options": options } ) );
}
});
},
destroy: function() {
return this.each(function() {
var $this = $(this),
data = $this.data('listTree');
$(window).unbind('.listTree');
$this.removeData('listTree');
});
},
selectAll: function() {
// For each listTree...
return this.each(function() {
// Select each parent checkbox.
_selectAllChildren($(this));
// For each listTree parent...
$(this).children('ul > li:first-child').each(function() {
// Select each child checkbox.
_selectAllChildren($(this));
});
_updateSelectedObject($(this));
});
},
deselectAll: function() {
// For each listTree...
return this.each(function() {
// Deselect each parent checkbox.
_deselectAllChildren($(this));
// For each listTree parent...
$(this).children('ul > li:first-child').each(function() {
// Deselect each child checkbox.
_deselectAllChildren($(this));
});
_updateSelectedObject($(this));
});
},
expandAll: function() {
// For each listTree...
return this.each(function() {
var node = $(this).children('ul').children('li');
// Change the icon.
_toggleIcon(node);
// Show the child list.
node.children('ul').slideDown('fast');
});
},
collapseAll: function() {
// For each listTree...
return this.each(function() {
var node = $(this).children('ul').children('li');
// Change the icon.
_toggleIcon(node);
// Hide the child list.
node.children('ul').slideUp('fast');
});
},
update: function(context, options) {
// Default options
var defaults = {
"startCollapsed": false,
"selected": context
};
options = $.extend(defaults, options);
// Validate the user entered default selections.
options.selected = _validateDefaultSelectionValues(options.selected);
return this.each(function() {
var $this = $(this),
data = $this.data('listTree');
// Ensure the plugin has been initialized...
if (data) {
// Update the context.
$(this).data('listTree', {
"target": $this,
"context": context,
"options": options,
"selected": options.selected
});
// Generate the list tree.
$this.html( _.template( template, { "context": context, "options": options } ) );
}
});
}
};
$.fn.listTree = function(method) {
if (methods[method]) {
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
} else if (typeof method === 'object' || !method) {
return methods.init.apply(this, arguments);
} else {
$.error('Method ' + method + ' does not exist on jQuery.listTree');
}
};
})(jQuery);
<!DOCTYPE html>
<html>
<head>
<title>jQuery/Bootstrap List Tree Plugin Demo</title>
<link rel="stylesheet" href="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.0.4/css/bootstrap-combined.min.css" type="text/css">
<link rel="stylesheet" href="bootstrap-listTree.css" type="text/css">
</head>
<body>
<div id="content">
<div class="listTree"></div>
<button class="btn btn-success">Check All</button>
<button class="btn btn-danger">Un-Check All</button>
<button class="btn btn-info">Expand All</button>
<button class="btn btn-warning">Collapse All</button>
<button class="btn btn-inverse">Change Data</button>
<br /><br />
<h4>Selection JSON:</h4>
<pre id="selectionJson"></pre>
<button class="btn btn-primary">Get JSON for selected nodes</button>
</div>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script type="text/javascript" src="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.0.4/js/bootstrap.min.js"></script>
<script type="text/javascript" src="bootstrap-listTree.js"></script>
<script type="text/javascript">
// Create our data objects.
// Formatted like d3.js's nest() function.
var data = [
{
"key": "Parent 1",
"values": [
{ "key": "Parent 1 Child 1" },
{ "key": "Parent 1 Child 2" },
{ "key": "Parent 1 Child 3" },
{ "key": "Parent 1 Child 4" },
{ "key": "Parent 1 Child 5" }
]
},
{
"key": "Parent 2",
"values": [
{ "key": "Parent 2 Child 1" },
{ "key": "Parent 2 Child 2" },
{ "key": "Parent 2 Child 3" }
]
},
{
"key": "Parent 3",
"values": [
{ "key": "Parent 3 Child 1" },
{ "key": "Parent 3 Child 2" },
{ "key": "Parent 3 Child 3" }
]
}
];
var dataDefaultSelected = [
{
"key": "Parent 1",
"values": [
{ "key": "Parent 1 Child 1" },
{ "key": "Parent 1 Child 2" }, // Skip a few...
{ "key": "Parent 1 Child 5" }
]
},
{
"key": "Parent 3",
"values": [] //Some bad data, you can't have a selected parent with no selected children!
}
];
var otherData = [
{
"key": "Cat",
"values": [
{ "key": "Domestic Shorthair" },
{ "key": "Scottish Fold" },
{ "key": "Siberian" }
]
},
{
"key": "Dog",
"values": [
{ "key": "German Shepherd" },
{ "key": "Minature Pinscher" },
{ "key": "Corgie" }
]
}
];
$(document).on('click', '.btn-success', function(e) {
$('.listTree').listTree('selectAll');
}).on('click', '.btn-danger', function(e) {
$('.listTree').listTree('deselectAll');
}).on('click', '.btn-info', function(e) {
$('.listTree').listTree('expandAll');
}).on('click', '.btn-warning', function(e) {
$('.listTree').listTree('collapseAll');
}).on('click', '.btn-inverse', function(e) {
$('.listTree').listTree('update', otherData);
}).on('click', '.btn-primary', function(e) {
$('#selectionJson').text(JSON.stringify($('.listTree').data('listTree').selected));
});
$('.listTree').listTree(data, { "startCollapsed": true, "selected": dataDefaultSelected });
</script>
</body>
</html>
@eapyl
Copy link

eapyl commented Mar 10, 2013

Hi, I am using this great plugin and modify it to editable version without checkboxes. Possible is it interesting to open this version of projects ? Please, contact me if it is interesting.

@eapyl
Copy link

eapyl commented Mar 13, 2013

@robbie7
Copy link

robbie7 commented Mar 22, 2013

Hello, thanks for this great plugin. Be aware that in the demo the underscore.js file isn't included and the demo won't work. Took me a while before noticing :-) Cheers!

@that1guy
Copy link

Hi clayzermk1,

When I import bootstrap-listTree.js to any of my projects I immediately get error: Uncaught ReferenceError: _ is not defined bootstrap-listTree.js:83. I've tried different all versions of jquery 1.7.2 to 1.9.1 and even recreated everything locally by copy pasting from the jsfiddle link. I'm perplexed. Is there an issue with this library from recent update?

Thnx for help!

@michalzubkowicz
Copy link

_ is not defined means you didn`t included underscore.js

@eapyl
Copy link

eapyl commented Apr 29, 2013

If anybody have interested about my comments two month ago, please find my code at http://udgin-pyl.blogspot.com/2013/04/jquery-twitter-bootstrap-list-tree.html. Thank you!

@kentkwan
Copy link

if the title has three state, it's will be awesome. as checked all, unchecked all, checked some.

@Jaimera
Copy link

Jaimera commented Jun 12, 2013

Hi clayzermk1,

Nice work. But, what about a third layer ?

@brinkar
Copy link

brinkar commented Jul 10, 2013

Hey, thanks for a great plugin. There's a tiny issue with the _updateSelectedObject function that causes no children to be selected if there's another (previous) parent with similarly named children that is not selected. The children update code on line 131 in the js file needs to limit the query to the specific parent in question.

Sorry, I hope it makes sense. I don't think I can make pull-requests on a gist.

@charis0721
Copy link

I have included the underscore.js file ,but it dosn't work at all,who knows the reason?

@clayzermk1
Copy link
Author

Oh my, I never received any notifications of your comments! I'm terribly sorry for such a late reply! I'm usually very good about such things!

I think I'll take a crack at converting this from a gist to an actual project, clean up the dependencies, make it Bower compatible, and bring it up to speed with Bootstrap 3.x.

I'm really glad so many people were able to make some use out of this, despite the issues. Cheers everyone 🍻!

@fkdiogo
Copy link

fkdiogo commented Mar 3, 2015

Demo is not working... Can someone tell me why?

@baxter07
Copy link

baxter07 commented Mar 4, 2015

You need to include underscore.js using version 1.3.3
Latest version of underscore does NOT work!

@janraak
Copy link

janraak commented Jul 8, 2015

I downloaded the code and processed all comments made here. And ... voila ... a tree is displayed.

However is possible to have more nest levels?

I tried this

var otherData = [
{
"key": "Cat",
"values": [
{ "key": "Domestic Shorthair" },
{ "key": "Scottish Fold" },
{ "key": "Siberian" }
]
},
{
"key": "Dog",
"values": [
{ "key": "German Shepherd" },
{ "key": "Minature Pinscher" },
{ "key": "Corgie" },
{ "key": "Hounds",
"values": [
{ "key": "Bracco Italiano" },
{ "key": "Weimeraner" },
{ "key": "Hungarian Vizla" },
{ "key": "German Shorthair" },
{ "key": "Dalmatian" }
]
}
]
}
];

However 'Hounds' can not be expanded

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment