2 $.fn.bonsai = function(options) {
4 return this.each(function() {
5 var bonsai = $(this).data('bonsai');
7 bonsai = new Bonsai(this, options);
8 $(this).data('bonsai', bonsai);
10 if (typeof options == 'string') {
12 bonsai[method].apply(bonsai, [].slice.call(args, 1));
18 expandAll: false, // boolean expands all items
19 expand: null, // function to expand an item
20 collapse: null, // function to collapse an item
21 checkboxes: false, // requires jquery.qubit
22 // createCheckboxes: creates checkboxes for each list item.
24 // The name and value for the checkboxes can be declared in the
25 // markup using `data-name` and `data-value`.
27 // The name is inherited from parent items if not specified.
29 // Checked state can be indicated using `data-checked`.
30 createCheckboxes: false,
31 // handleDuplicateCheckboxes: adds onChange bindings to update
32 // any other checkboxes that have the same value.
33 handleDuplicateCheckboxes: false,
34 selectAllExclude: null
36 var Bonsai = function(el, options) {
38 options = options || {};
39 this.options = $.extend({}, $.bonsai.defaults, options);
40 this.el = $(el).addClass('bonsai').data('bonsai', this);
42 if (this.isRootNode()) {
43 if (this.options.handleDuplicateCheckboxes) this.handleDuplicates();
44 if (this.options.checkboxes) this.el.qubit(this.options);
45 if (this.options.addExpandAll) this.addExpandAllLink();
46 if (this.options.addSelectAll) this.addSelectAllLink();
47 this.el.on('click', '.thumb', function(ev) {
48 self.toggle($(ev.currentTarget).closest('li'));
51 if (this.options.expandAll) this.expandAll();
54 isRootNode: function() {
55 return this.options.scope == this.el;
57 toggle: function(listItem) {
58 if (!$(listItem).hasClass('expanded')) {
59 this.expand(listItem);
62 this.collapse(listItem);
65 expand: function(listItem) {
66 this.setExpanded(listItem, true);
68 collapse: function(listItem) {
69 this.setExpanded(listItem, false);
71 setExpanded: function(listItem, expanded) {
72 listItem = $(listItem);
73 if (listItem.length > 1) {
75 listItem.each(function() {
76 self.setExpanded(this, expanded);
81 if (!listItem.data('subList')) return;
82 listItem = $(listItem).addClass('expanded')
83 .removeClass('collapsed');
84 $(listItem.data('subList')).css('height', 'auto');
87 listItem = $(listItem).addClass('collapsed')
88 .removeClass('expanded');
89 $(listItem.data('subList')).height(0);
92 expandAll: function() {
93 this.expand(this.el.find('li'));
95 collapseAll: function() {
96 this.collapse(this.el.find('li'));
100 // store the scope in the options for child nodes
101 if (!this.options.scope) {
102 this.options.scope = this.el;
104 // look for a nested list (if any)
105 this.el.children().each(function() {
107 if (self.options.createCheckboxes) self.insertCheckbox(item);
108 // insert a thumb if it doesn't already exist
109 if (item.children().filter('.thumb').length == 0) {
110 var thumb = $('<div class="thumb"></div>');
113 var subLists = item.children().filter('ol, ul');
114 item.toggleClass('has-children', subLists.find('li').length > 0);
115 // if there is a child list
116 subLists.each(function() {
118 if ($('li', this).length == 0) {
121 // then this el has children
122 item.data('subList', this);
123 // collapse the nested list
124 if (item.hasClass('expanded')) {
130 // handle any deeper nested lists
131 var exists = !!$(this).data('bonsai');
132 $(this).bonsai(exists ? 'update' : self.options);
135 this.expand = this.options.expand || this.expand;
136 this.collapse = this.options.collapse || this.collapse;
138 insertCheckbox: function(listItem) {
139 if (listItem.find('> input[type=checkbox]').length) return;
140 var id = this.generateId(listItem),
141 checkbox = $('<input type="checkbox" name="'
142 + this.getCheckboxName(listItem) + '" id="' + id + '" /> '
144 children = listItem.children(),
145 // get the first text node for the label
146 text = listItem.contents().filter(function() {
147 return this.nodeType == 3;
149 checkbox.val(listItem.data('value'));
150 checkbox.prop('checked', listItem.data('checked'))
152 listItem.append(checkbox)
154 $('<label for="' + id + '">').append(text ? text : children.first())
156 .append(text ? children : children.slice(1));
158 handleDuplicates: function() {
160 self.el.on('change', 'input[type=checkbox]', function(ev) {
161 var checkbox = $(ev.target);
162 if (!checkbox.val()) return;
163 // select all duplicate checkboxes that need to be updated
164 var selector = 'input[type=checkbox]'
165 + '[value="' + checkbox.val() + '"]'
166 + '[name="' + checkbox.attr('name') + '"]'
167 + (checkbox.prop('checked') ? ':not(:checked)' : ':checked');
168 self.el.find(selector).prop({
169 checked: checkbox.prop('checked'),
170 indeterminate: checkbox.prop('indeterminate')
171 }).trigger('change');
174 idPrefix: 'checkbox-',
175 generateId: function(listItem) {
177 var id = this.idPrefix + Bonsai.uniqueId++;
179 while($('#' + id).length > 0);
182 getCheckboxName: function(listItem) {
183 return listItem.data('name')
184 || listItem.parents().filter('[data-name]').data('name');
186 addExpandAllLink: function() {
188 $('<div class="expand-all">')
189 .append($('<a class="all">Expand all</a>')
190 .on('click', function() {
194 .append('<i class="separator"></i>')
195 .append($('<a class="none">Collapse all</a>')
196 .on('click', function() {
200 .insertBefore(this.el);
202 addSelectAllLink: function() {
203 var scope = this.options.scope,
205 function getCheckboxes() {
206 // return all checkboxes that are not in hidden list items
207 return scope.find('li')
208 .filter(self.options.selectAllExclude || function() {
209 return $(this).css('display') != 'none';
211 .find('> input[type=checkbox]');
213 $('<div class="check-all">')
214 .append($('<a class="all">Select all</a>')
215 .css('cursor', 'pointer')
216 .on('click', function() {
217 getCheckboxes().prop({
223 .append('<i class="separator"></i>')
224 .append($('<a class="none">Select none</a>')
225 .css('cursor', 'pointer')
226 .on('click', function() {
227 getCheckboxes().prop({
233 .insertAfter(this.el);
235 setCheckedValues: function(values) {
236 var all = this.options.scope.find('input[type=checkbox]');
237 $.each(values, function(key, value) {
238 all.filter('[value="' + value + '"]')
239 .prop('checked', true)