--- /dev/null
+(function($){
+ $.fn.bonsai = function(options) {
+ var args = arguments;
+ return this.each(function() {
+ var bonsai = $(this).data('bonsai');
+ if (!bonsai) {
+ bonsai = new Bonsai(this, options);
+ $(this).data('bonsai', bonsai);
+ }
+ if (typeof options == 'string') {
+ var method = options;
+ bonsai[method].apply(bonsai, [].slice.call(args, 1));
+ }
+ });
+ };
+ $.bonsai = {};
+ $.bonsai.defaults = {
+ expandAll: false, // boolean expands all items
+ expand: null, // function to expand an item
+ collapse: null, // function to collapse an item
+ checkboxes: false, // requires jquery.qubit
+ // createCheckboxes: creates checkboxes for each list item.
+ //
+ // The name and value for the checkboxes can be declared in the
+ // markup using `data-name` and `data-value`.
+ //
+ // The name is inherited from parent items if not specified.
+ //
+ // Checked state can be indicated using `data-checked`.
+ createCheckboxes: false,
+ // handleDuplicateCheckboxes: adds onChange bindings to update
+ // any other checkboxes that have the same value.
+ handleDuplicateCheckboxes: false,
+ selectAllExclude: null
+ };
+ var Bonsai = function(el, options) {
+ var self = this;
+ options = options || {};
+ this.options = $.extend({}, $.bonsai.defaults, options);
+ this.el = $(el).addClass('bonsai').data('bonsai', this);
+ this.update();
+ if (this.isRootNode()) {
+ if (this.options.handleDuplicateCheckboxes) this.handleDuplicates();
+ if (this.options.checkboxes) this.el.qubit(this.options);
+ if (this.options.addExpandAll) this.addExpandAllLink();
+ if (this.options.addSelectAll) this.addSelectAllLink();
+ this.el.on('click', '.thumb', function(ev) {
+ self.toggle($(ev.currentTarget).closest('li'));
+ });
+ }
+ if (this.options.expandAll) this.expandAll();
+ };
+ Bonsai.prototype = {
+ isRootNode: function() {
+ return this.options.scope == this.el;
+ },
+ toggle: function(listItem) {
+ if (!$(listItem).hasClass('expanded')) {
+ this.expand(listItem);
+ }
+ else {
+ this.collapse(listItem);
+ }
+ },
+ expand: function(listItem) {
+ this.setExpanded(listItem, true);
+ },
+ collapse: function(listItem) {
+ this.setExpanded(listItem, false);
+ },
+ setExpanded: function(listItem, expanded) {
+ listItem = $(listItem);
+ if (listItem.length > 1) {
+ var self = this;
+ listItem.each(function() {
+ self.setExpanded(this, expanded);
+ });
+ return;
+ }
+ if (expanded) {
+ if (!listItem.data('subList')) return;
+ listItem = $(listItem).addClass('expanded')
+ .removeClass('collapsed');
+ $(listItem.data('subList')).css('height', 'auto');
+ }
+ else {
+ listItem = $(listItem).addClass('collapsed')
+ .removeClass('expanded');
+ $(listItem.data('subList')).height(0);
+ }
+ },
+ expandAll: function() {
+ this.expand(this.el.find('li'));
+ },
+ collapseAll: function() {
+ this.collapse(this.el.find('li'));
+ },
+ update: function() {
+ var self = this;
+ // store the scope in the options for child nodes
+ if (!this.options.scope) {
+ this.options.scope = this.el;
+ }
+ // look for a nested list (if any)
+ this.el.children().each(function() {
+ var item = $(this);
+ if (self.options.createCheckboxes) self.insertCheckbox(item);
+ // insert a thumb if it doesn't already exist
+ if (item.children().filter('.thumb').length == 0) {
+ var thumb = $('<div class="thumb"></div>');
+ item.prepend(thumb);
+ }
+ var subLists = item.children().filter('ol, ul');
+ item.toggleClass('has-children', subLists.find('li').length > 0);
+ // if there is a child list
+ subLists.each(function() {
+ // that's not empty
+ if ($('li', this).length == 0) {
+ return;
+ }
+ // then this el has children
+ item.data('subList', this);
+ // collapse the nested list
+ if (item.hasClass('expanded')) {
+ self.expand(item);
+ }
+ else {
+ self.collapse(item);
+ }
+ // handle any deeper nested lists
+ var exists = !!$(this).data('bonsai');
+ $(this).bonsai(exists ? 'update' : self.options);
+ });
+ });
+ this.expand = this.options.expand || this.expand;
+ this.collapse = this.options.collapse || this.collapse;
+ },
+ insertCheckbox: function(listItem) {
+ if (listItem.find('> input[type=checkbox]').length) return;
+ var id = this.generateId(listItem),
+ checkbox = $('<input type="checkbox" name="'
+ + this.getCheckboxName(listItem) + '" id="' + id + '" /> '
+ ),
+ children = listItem.children(),
+ // get the first text node for the label
+ text = listItem.contents().filter(function() {
+ return this.nodeType == 3;
+ }).first();
+ checkbox.val(listItem.data('value'));
+ checkbox.prop('checked', listItem.data('checked'))
+ children.remove();
+ listItem.append(checkbox)
+ .append(
+ $('<label for="' + id + '">').append(text ? text : children.first())
+ )
+ .append(text ? children : children.slice(1));
+ },
+ handleDuplicates: function() {
+ var self = this;
+ self.el.on('change', 'input[type=checkbox]', function(ev) {
+ var checkbox = $(ev.target);
+ if (!checkbox.val()) return;
+ // select all duplicate checkboxes that need to be updated
+ var selector = 'input[type=checkbox]'
+ + '[value="' + checkbox.val() + '"]'
+ + '[name="' + checkbox.attr('name') + '"]'
+ + (checkbox.prop('checked') ? ':not(:checked)' : ':checked');
+ self.el.find(selector).prop({
+ checked: checkbox.prop('checked'),
+ indeterminate: checkbox.prop('indeterminate')
+ }).trigger('change');
+ });
+ },
+ idPrefix: 'checkbox-',
+ generateId: function(listItem) {
+ do {
+ var id = this.idPrefix + Bonsai.uniqueId++;
+ }
+ while($('#' + id).length > 0);
+ return id;
+ },
+ getCheckboxName: function(listItem) {
+ return listItem.data('name')
+ || listItem.parents().filter('[data-name]').data('name');
+ },
+ addExpandAllLink: function() {
+ var self = this;
+ $('<div class="expand-all">')
+ .append($('<a class="all">Expand all</a>')
+ .on('click', function() {
+ self.expandAll();
+ })
+ )
+ .append('<i class="separator"></i>')
+ .append($('<a class="none">Collapse all</a>')
+ .on('click', function() {
+ self.collapseAll();
+ })
+ )
+ .insertBefore(this.el);
+ },
+ addSelectAllLink: function() {
+ var scope = this.options.scope,
+ self = this;
+ function getCheckboxes() {
+ // return all checkboxes that are not in hidden list items
+ return scope.find('li')
+ .filter(self.options.selectAllExclude || function() {
+ return $(this).css('display') != 'none';
+ })
+ .find('> input[type=checkbox]');
+ }
+ $('<div class="check-all">')
+ .append($('<a class="all">Select all</a>')
+ .css('cursor', 'pointer')
+ .on('click', function() {
+ getCheckboxes().prop({
+ checked: true,
+ indeterminate: false
+ });
+ })
+ )
+ .append('<i class="separator"></i>')
+ .append($('<a class="none">Select none</a>')
+ .css('cursor', 'pointer')
+ .on('click', function() {
+ getCheckboxes().prop({
+ checked: false,
+ indeterminate: false
+ });
+ })
+ )
+ .insertAfter(this.el);
+ },
+ setCheckedValues: function(values) {
+ var all = this.options.scope.find('input[type=checkbox]');
+ $.each(values, function(key, value) {
+ all.filter('[value="' + value + '"]')
+ .prop('checked', true)
+ .trigger('change');
+ });
+ }
+ };
+ $.extend(Bonsai, {
+ uniqueId: 0
+ });
+}(jQuery));