/* * 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 radio-button support to aciTree, * should be used with the selectable extension. * * The are a few extra properties for the item data: * * { * ... * radio: true, // TRUE (default) means the item will have a radio button (can be omitted if the `checkbox` extension is not used) * checked: false, // if should be checked or not * ... * } * */ (function($, window, undefined) { // extra default options var options = { radio: false, // if TRUE then each item will have a radio button radioChain: true, // if TRUE the selection will propagate to the parents/children radioBreak: true, // if TRUE then a missing radio button will break the chaining radioClick: false // if TRUE then a click will trigger a state change only when made over the radio-button itself }; // aciTree radio extension var aciTree_radio = { // init radio _radioInit: function() { this._instance.jQuery.bind('acitree' + this._private.nameSpace, function(event, api, item, eventName, options) { switch (eventName) { case 'loaded': if (item) { // check/update on item load api._radioLoad(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.hasRadio(item) && this.isEnabled(item)) { if (!this.isChecked(item)) { 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.radioClick || $(e.target).is('.aciTreeCheck')) { var item = this.itemFrom(e.target); if (this.hasRadio(item) && this.isEnabled(item) && (!this.extSelectable || !this.extSelectable() || (!e.ctrlKey && !e.shiftKey))) { // change state on click if (!this.isChecked(item)) { this.check(item); } e.preventDefault(); } } })); }, // override `_initHook` _initHook: function() { if (this.extRadio()) { this._radioInit(); } // call the parent this._super(); }, // override `_itemHook` _itemHook: function(parent, item, itemData, level) { if (this.extRadio()) { // support `checkbox` extension var checkbox = this.extCheckbox && this.hasCheckbox(item); if (!checkbox && (itemData.radio || ((itemData.radio === undefined) && (!this.extCheckbox || !this.extCheckbox())))) { this._radioDOM.add(item, itemData); } } // call the parent this._super(parent, item, itemData, level); }, // low level DOM functions _radioDOM: { // add item radio add: function(item, itemData) { domApi.addClass(item[0], itemData.checked ? ['aciTreeRadio', 'aciTreeChecked'] : 'aciTreeRadio'); 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 radio remove: function(item) { domApi.removeClass(item[0], ['aciTreeRadio', 'aciTreeChecked']); 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); }); } }, // update item on load _radioLoad: function(item) { if (!this._instance.options.radioChain) { // do not update on load return; } if (this.hasRadio(item)) { if (this.isChecked(item)) { if (!this.radios(this.children(item, false, true), true).length) { // the item is checked but no children are, check the children this._radioUpdate(item, true); } } else { // the item is not checked, uncheck children this._radioUpdate(item); } } }, // get children list _radioChildren: function(item) { if (this._instance.options.radioBreak) { 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 radio if (this.hasRadio(item)) { list.push(element); process(item); } }, true)); }); process(item); return $(list); } else { var children = this.children(item, true, true); return this.radios(children); } }, // get children across items _radioLevel: function(items) { var list = []; items.each(this.proxy(function(element) { var item = $(element); var children = this.children(item, false, true); children.each(this.proxy(function(element) { var item = $(element); if (!this._instance.options.radioBreak || this.hasRadio(item)) { list.push(element); } }, true)); }, true)); return $(list); }, // update radio state _radioUpdate: function(item, state) { // update siblings var siblings = this.proxy(function(item) { var siblings = this.siblings(item, true); this._radioDOM.check(this.radios(siblings), false); siblings.each(this.proxy(function(element) { var item = $(element); if (!this._instance.options.radioBreak || this.hasRadio(item)) { this._radioDOM.check(this._radioChildren(item), false); } }, true)); }); if (state) { siblings(item); } // update children var checkDown = this.proxy(function(item) { var children = this._radioLevel(item); var radios = this.radios(children); if (radios.length) { var checked = this.radios(children, true); if (checked.length) { checked = checked.first(); this._radioDOM.check(checked, true); siblings(checked); checkDown(checked); } else { checked = radios.first(); this._radioDOM.check(checked, true); siblings(checked); checkDown(checked); } } else if (children.length) { checkDown(children); } }); if (state) { checkDown(item); } else { this._radioDOM.check(this._radioChildren(item), false); } // update parents var checkUp = this.proxy(function(item) { var parent = this.parent(item); if (parent.length) { if (this.hasRadio(parent)) { if (state) { siblings(parent); } this._radioDOM.check(parent, state); checkUp(parent); } else { if (!this._instance.options.radioBreak) { if (state) { siblings(parent); } checkUp(parent); } } } }); if (state !== undefined) { checkUp(item); } }, // test if item have a radio hasRadio: function(item) { return item && domApi.hasClass(item[0], 'aciTreeRadio'); }, // add radio button addRadio: function(item, options) { options = this._options(options, 'radioadded', 'addradiofail', 'wasradio', item); if (this.isItem(item)) { // a way to cancel the operation if (!this._trigger(item, 'beforeaddradio', options)) { this._fail(item, options); return; } if (this.hasRadio(item)) { this._notify(item, options); } else { var process = function() { this._radioDOM.add(item, { }); this._success(item, options); }; // support `checkbox` extension if (this.extCheckbox && this.hasCheckbox(item)) { // remove checkbox first this.removeCheckbox(item, this._inner(options, { success: process, fail: options.fail })); } else { process.apply(this); } } } else { this._fail(item, options); } }, // remove radio button removeRadio: function(item, options) { options = this._options(options, 'radioremoved', 'removeradiofail', 'notradio', item); if (this.isItem(item)) { // a way to cancel the operation if (!this._trigger(item, 'beforeremoveradio', options)) { this._fail(item, options); return; } if (this.hasRadio(item)) { this._radioDOM.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.hasRadio(item)) { return domApi.hasClass(item[0], 'aciTreeChecked'); } // support `checkbox` extension if (this._super) { // call the parent return this._super(item); } return false; }, // check radio button check: function(item, options) { if (this.extRadio && this.hasRadio(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._radioDOM.check(item, true); if (this._instance.options.radioChain) { // chain them this._radioUpdate(item, true); } this._success(item, options); } } else { // support `checkbox` extension if (this._super) { // call the parent this._super(item, options); } else { this._trigger(item, 'checkfail', options); this._fail(item, options); } } }, // uncheck radio button uncheck: function(item, options) { if (this.extRadio && this.hasRadio(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._radioDOM.check(item, false); if (this._instance.options.radioChain) { // chain them this._radioUpdate(item, false); } this._success(item, options); } else { this._notify(item, options); } } else { // support `checkbox` extension if (this._super) { // call the parent this._super(item, options); } else { this._trigger(item, 'uncheckfail', options); this._fail(item, options); } } }, // filter items with radio by state (if set) radios: function(items, state) { if (state !== undefined) { return $(domApi.withClass(items.toArray(), state ? ['aciTreeRadio', 'aciTreeChecked'] : 'aciTreeRadio', state ? null : 'aciTreeChecked')); } return $(domApi.withClass(items.toArray(), 'aciTreeRadio')); }, // override `_serialize` _serialize: function(item, callback) { var data = this._super(item, callback); if (data && this.extRadio()) { if (data.hasOwnProperty('radio')) { data.radio = this.hasRadio(item); data.checked = this.isChecked(item); } else if (this.hasRadio(item)) { if (this.extCheckbox && this.extCheckbox()) { data.radio = true; } data.checked = this.isChecked(item); } } return data; }, // override `serialize` serialize: function(item, what, callback) { if (what == 'radio') { var serialized = ''; var children = this.children(item, true, true); this.radios(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 radio is enabled extRadio: function() { return this._instance.options.radio; }, // override set `option` option: function(option, value) { if (this.wasInit() && !this.isLocked()) { if ((option == 'radio') && (value != this.extRadio())) { if (value) { this._radioInit(); } else { this._radioDone(); } } } // call the parent this._super(option, value); }, // done radio _radioDone: function(destroy) { this._instance.jQuery.unbind(this._private.nameSpace); this._instance.jQuery.off(this._private.nameSpace, '.aciTreeItem'); if (!destroy) { // remove radios this.radios(this.children(null, true, true)).each(this.proxy(function(element) { this.removeRadio($(element)); }, true)); } }, // override `_destroyHook` _destroyHook: function(unloaded) { if (unloaded) { this._radioDone(true); } // call the parent this._super(unloaded); } }; // extend the base aciTree class and add the radio stuff aciPluginClass.plugins.aciTree = aciPluginClass.plugins.aciTree.extend(aciTree_radio, 'aciTreeRadio'); // add extra default options aciPluginClass.defaults('aciTree', options); // for internal access var domApi = aciPluginClass.plugins.aciTree_dom; })(jQuery, this);