
/*
 * 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);
