/* * aciTree jQuery Plugin v4.5.0-rc.7 * http://acoderinsights.ro * * Copyright (c) 2014 Dragos Ursu * Dual licensed under the MIT or GPL Version 2 licenses. * * Require jQuery Library >= v1.9.0 http://jquery.com * + aciPlugin >= v1.5.1 https://github.com/dragosu/jquery-aciPlugin */ /* * This extension adds checkbox support to aciTree, * should be used with the selectable extension. * * The are a few extra properties for the item data: * * { * ... * checkbox: true, // TRUE (default) means the item will have a checkbox (can be omitted if the `radio` extension is not used) * checked: false, // if should be checked or not * ... * } * */ (function($, window, undefined) { // extra default options var options = { checkbox: false, // if TRUE then each item will have a checkbox checkboxChain: true, // if TRUE the selection will propagate to the parents/children // if -1 the selection will propagate only to parents // if +1 the selection will propagate only to children // if FALSE the selection will not propagate in any way checkboxBreak: true, // if TRUE then a missing checkbox will break the chaining checkboxClick: false // if TRUE then a click will trigger a state change only when made over the checkbox itself }; // aciTree checkbox extension var aciTree_checkbox = { // init checkbox _checkboxInit: function() { this._instance.jQuery.bind('acitree' + this._private.nameSpace, function(event, api, item, eventName, options) { switch (eventName) { case 'loaded': // check/update on item load api._checkboxLoad(item); break; } }).bind('keydown' + this._private.nameSpace, this.proxy(function(e) { switch (e.which) { case 32: // space // support `selectable` extension if (this.extSelectable && this.extSelectable() && !e.ctrlKey) { var item = this.focused(); if (this.hasCheckbox(item) && this.isEnabled(item)) { if (this.isChecked(item)) { this.uncheck(item); } else { this.check(item); } e.stopImmediatePropagation(); // prevent page scroll e.preventDefault(); } } break; } })).on('click' + this._private.nameSpace, '.aciTreeItem', this.proxy(function(e) { if (!this._instance.options.checkboxClick || $(e.target).is('.aciTreeCheck')) { var item = this.itemFrom(e.target); if (this.hasCheckbox(item) && this.isEnabled(item) && (!this.extSelectable || !this.extSelectable() || (!e.ctrlKey && !e.shiftKey))) { // change state on click if (this.isChecked(item)) { this.uncheck(item); } else { this.check(item); } e.preventDefault(); } } })); }, // override `_initHook` _initHook: function() { if (this.extCheckbox()) { this._checkboxInit(); } // call the parent this._super(); }, // override `_itemHook` _itemHook: function(parent, item, itemData, level) { if (this.extCheckbox()) { // support `radio` extension var radio = this.extRadio && this.hasRadio(item); if (!radio && (itemData.checkbox || ((itemData.checkbox === undefined) && (!this.extRadio || !this.extRadio())))) { this._checkboxDOM.add(item, itemData); } } // call the parent this._super(parent, item, itemData, level); }, // low level DOM functions _checkboxDOM: { // add item checkbox add: function(item, itemData) { domApi.addClass(item[0], itemData.checked ? ['aciTreeCheckbox', 'aciTreeChecked'] : 'aciTreeCheckbox'); var text = domApi.childrenByClass(item[0].firstChild, 'aciTreeText'); var parent = text.parentNode; var label = window.document.createElement('LABEL'); var check = window.document.createElement('SPAN'); check.className = 'aciTreeCheck'; label.appendChild(check); label.appendChild(text); parent.appendChild(label); item[0].firstChild.setAttribute('aria-checked', !!itemData.checked); }, // remove item checkbox remove: function(item) { domApi.removeClass(item[0], ['aciTreeCheckbox', 'aciTreeChecked', 'aciTreeTristate']); var text = domApi.childrenByClass(item[0].firstChild, 'aciTreeText'); var label = text.parentNode; var parent = label.parentNode; parent.replaceChild(text, label) item[0].firstChild.removeAttribute('aria-checked'); }, // (un)check items check: function(items, state) { domApi.toggleListClass(items.toArray(), 'aciTreeChecked', state, function(node) { node.firstChild.setAttribute('aria-checked', state); }); }, // (un)set tristate items tristate: function(items, state) { domApi.toggleListClass(items.toArray(), 'aciTreeTristate', state); } }, // update items on load, starting from the loaded node _checkboxLoad: function(item) { if (this._instance.options.checkboxChain === false) { // do not update on load return; } var state = undefined; if (this.hasCheckbox(item)) { if (this.isChecked(item)) { if (!this.checkboxes(this.children(item, false, true), true).length) { // the item is checked but no children are, check them all state = true; } } else { // the item is not checked, uncheck all children state = false; } } this._checkboxUpdate(item, state); }, // get children list _checkboxChildren: function(item) { if (this._instance.options.checkboxBreak) { var list = []; var process = this.proxy(function(item) { var children = this.children(item, false, true); children.each(this.proxy(function(element) { var item = $(element); // break on missing checkbox if (this.hasCheckbox(item)) { list.push(element); process(item); } }, true)); }); process(item); return $(list); } else { var children = this.children(item, true, true); return this.checkboxes(children); } }, // update checkbox state _checkboxUpdate: function(item, state) { // update children var checkDown = this.proxy(function(item, count, state) { var children = this.children(item, false, true); var total = 0; var checked = 0; children.each(this.proxy(function(element) { var item = $(element); var subCount = { total: 0, checked: 0 }; if (this.hasCheckbox(item)) { if ((state !== undefined) && (this._instance.options.checkboxChain !== -1)) { this._checkboxDOM.check(item, state); } total++; if (this.isChecked(item)) { checked++; } checkDown(item, subCount, state); } else { if (this._instance.options.checkboxBreak) { var reCount = { total: 0, checked: 0 }; checkDown(item, reCount); } else { checkDown(item, subCount, state); } } total += subCount.total; checked += subCount.checked; }, true)); if (item) { this._checkboxDOM.tristate(item, (checked > 0) && (checked != total)); count.total += total; count.checked += checked; } }); var count = { total: 0, checked: 0 }; checkDown(item, count, state); // update parents var checkUp = this.proxy(function(item, tristate, state) { var parent = this.parent(item); if (parent.length) { if (!tristate) { var children = this._checkboxChildren(parent); var checked = this.checkboxes(children, true).length; var tristate = (checked > 0) && (checked != children.length); } if (this.hasCheckbox(parent)) { if ((state !== undefined) && (this._instance.options.checkboxChain !== 1)) { this._checkboxDOM.check(parent, tristate ? true : state); } this._checkboxDOM.tristate(parent, tristate); checkUp(parent, tristate, state); } else { if (this._instance.options.checkboxBreak) { checkUp(parent); } else { checkUp(parent, tristate, state); } } } }); checkUp(item, undefined, state); }, // test if item have a checkbox hasCheckbox: function(item) { return item && domApi.hasClass(item[0], 'aciTreeCheckbox'); }, // add checkbox addCheckbox: function(item, options) { options = this._options(options, 'checkboxadded', 'addcheckboxfail', 'wascheckbox', item); if (this.isItem(item)) { // a way to cancel the operation if (!this._trigger(item, 'beforeaddcheckbox', options)) { this._fail(item, options); return; } if (this.hasCheckbox(item)) { this._notify(item, options); } else { var process = function() { this._checkboxDOM.add(item, { }); this._success(item, options); }; // support `radio` extension if (this.extRadio && this.hasRadio(item)) { // remove radio first this.removeRadio(item, this._inner(options, { success: process, fail: options.fail })); } else { process.apply(this); } } } else { this._fail(item, options); } }, // remove checkbox removeCheckbox: function(item, options) { options = this._options(options, 'checkboxremoved', 'removecheckboxfail', 'notcheckbox', item); if (this.isItem(item)) { // a way to cancel the operation if (!this._trigger(item, 'beforeremovecheckbox', options)) { this._fail(item, options); return; } if (this.hasCheckbox(item)) { this._checkboxDOM.remove(item); this._success(item, options); } else { this._notify(item, options); } } else { this._fail(item, options); } }, // test if it's checked isChecked: function(item) { if (this.hasCheckbox(item)) { return domApi.hasClass(item[0], 'aciTreeChecked'); } // support `radio` extension if (this._super) { // call the parent return this._super(item); } return false; }, // check checkbox check: function(item, options) { if (this.extCheckbox && this.hasCheckbox(item)) { options = this._options(options, 'checked', 'checkfail', 'waschecked', item); // a way to cancel the operation if (!this._trigger(item, 'beforecheck', options)) { this._fail(item, options); return; } if (this.isChecked(item)) { this._notify(item, options); } else { this._checkboxDOM.check(item, true); if (this._instance.options.checkboxChain !== false) { // chain them this._checkboxUpdate(item, true); } this._success(item, options); } } else { // support `radio` extension if (this._super) { // call the parent this._super(item, options); } else { this._trigger(item, 'checkfail', options); this._fail(item, options); } } }, // uncheck checkbox uncheck: function(item, options) { if (this.extCheckbox && this.hasCheckbox(item)) { options = this._options(options, 'unchecked', 'uncheckfail', 'notchecked', item); // a way to cancel the operation if (!this._trigger(item, 'beforeuncheck', options)) { this._fail(item, options); return; } if (this.isChecked(item)) { this._checkboxDOM.check(item, false); if (this._instance.options.checkboxChain !== false) { // chain them this._checkboxUpdate(item, false); } this._success(item, options); } else { this._notify(item, options); } } else { // support `radio` extension if (this._super) { // call the parent this._super(item, options); } else { this._trigger(item, 'uncheckfail', options); this._fail(item, options); } } }, // filter items with checkbox by state (if set) checkboxes: function(items, state) { if (state !== undefined) { return $(domApi.withClass(items.toArray(), state ? ['aciTreeCheckbox', 'aciTreeChecked'] : 'aciTreeCheckbox', state ? null : 'aciTreeChecked')); } return $(domApi.withClass(items.toArray(), 'aciTreeCheckbox')); }, // override `_serialize` _serialize: function(item, callback) { var data = this._super(item, callback); if (data && this.extCheckbox()) { if (data.hasOwnProperty('checkbox')) { data.checkbox = this.hasCheckbox(item); data.checked = this.isChecked(item); } else if (this.hasCheckbox(item)) { if (this.extRadio && this.extRadio()) { data.checkbox = true; } data.checked = this.isChecked(item); } } return data; }, // override `serialize` serialize: function(item, what, callback) { if (what == 'checkbox') { var serialized = ''; var children = this.children(item, true, true); this.checkboxes(children, true).each(this.proxy(function(element) { var item = $(element); if (callback) { serialized += callback.call(this, item, what, this.getId(item)); } else { serialized += this._instance.options.serialize.call(this, item, what, this.getId(item)); } }, true)); return serialized; } return this._super(item, what, callback); }, // test if item is in tristate isTristate: function(item) { return item && domApi.hasClass(item[0], 'aciTreeTristate'); }, // filter tristate items tristate: function(items) { return $(domApi.withClass(items.toArray(), 'aciTreeTristate')); }, // test if checkbox is enabled extCheckbox: function() { return this._instance.options.checkbox; }, // override set `option` option: function(option, value) { if (this.wasInit() && !this.isLocked()) { if ((option == 'checkbox') && (value != this.extCheckbox())) { if (value) { this._checkboxInit(); } else { this._checkboxDone(); } } } // call the parent this._super(option, value); }, // done checkbox _checkboxDone: function(destroy) { this._instance.jQuery.unbind(this._private.nameSpace); this._instance.jQuery.off(this._private.nameSpace, '.aciTreeItem'); if (!destroy) { // remove checkboxes this.checkboxes(this.children(null, true, true)).each(this.proxy(function(element) { this.removeCheckbox($(element)); }, true)); } }, // override `_destroyHook` _destroyHook: function(unloaded) { if (unloaded) { this._checkboxDone(true); } // call the parent this._super(unloaded); } }; // extend the base aciTree class and add the checkbox stuff aciPluginClass.plugins.aciTree = aciPluginClass.plugins.aciTree.extend(aciTree_checkbox, 'aciTreeCheckbox'); // add extra default options aciPluginClass.defaults('aciTree', options); // for internal access var domApi = aciPluginClass.plugins.aciTree_dom; })(jQuery, this);