248 lines
8.1 KiB
JavaScript
248 lines
8.1 KiB
JavaScript
(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));
|