﻿/// <reference path="jquery-1.7.1.min.js" />

/**	Code Dependencies:
*	- "jquery-1.7.1.js"
*
*
**	Version:
*	1.0
*
*
**	Usage:
*
*	Attach this plug-in to ...
*
*
**	Options:
*
*	options = {
*		selectedValuesHiddenInputId: ID of hidden input to store all selected values in for this TreeList [String - OPTIONAL],
*		selectedValuesDelimiter: Delimiter for the selected values stored in the aforementioned hidden input for this TreeList [String - OPTIONAL] [Default Value = "|"]
*	}
*
*/
(function ($) {

    //#region Methods

    var m = { // methods
        // Public Methods
        init: function (options) { // Called on plug-in initialization
            var defaultSettings = {
                selectedValuesHiddenInputId: null,
                selectedValuesDelimiter: "|"
            }

            return this.each(function () {
                var $this = $(this);

                // Create the "mckerin" namespace if it does not exist
                (function (mckerin) { })(window.mckerin = window.mckerin || {}, jQuery);

                // Add this TreeListUlContainer mckerin.arrayTreeListUlContainers
                if (mckerin.arrayTreeListUlContainers) {
                    mckerin.arrayTreeListUlContainers.push($('div.TreeListUlContainer', $this));
                } else {
                    mckerin.arrayTreeListUlContainers = [$('div.TreeListUlContainer', $this)];
                }
                var d = $this.data('mckerinTreeList'); // data
                if (!d) { // If the plug-in hasn't been initialized yet
                    d = {
                        settings: $.extend(true, {}, defaultSettings, options), // Combine options and defaultSettings
                        $treeListInputContainer: $('div.TreeListInputContainer', $this),
                        $treeListInput: $('div.TreeListInputContainer input:text', $this),
                        $treeListHiddenInput: null,
                        $treeListUlContainer: $('div.TreeListUlContainer', $this),
                        arrayTreeListUlContainers: mckerin.arrayTreeListUlContainers,
                        arrayExpandCollapseLis: [],
                        arrayAllCheckboxes: [],
                        treeListUlContainerCss: {
                            'z-index': 11000
                        },
                        treeListUlContainerLeftOffset: 0,
                        scrollbarWidth: m._measureScrollbarWidth()
                    };
                    // Save data
                    $this.data('mckerinTreeList', d);
                }

                // Check to see if a hidden input has been specified to store the selected values
                if (d.settings.selectedValuesHiddenInputId) {
                    d.$treeListHiddenInput = $('#' + d.settings.selectedValuesHiddenInputId);
                    // Check to see if the ID supplied maps to an actual html element, alert if it doesn't
                    if (d.$treeListHiddenInput.length === 0) {
                        alert('An HTML element with supplied ID value "' + d.settings.selectedValuesHiddenInputId + '" for the "selectedValuesHiddenInputId" parameter does NOT exist!');
                    }
                }

                // Set unique id value on the TreeListUlContainer
                d.$treeListUlContainer.get(0).id = this.id + "_TreeListUlContainer_" + d.arrayTreeListUlContainers.length;

                // Measure scrollbar width
                m._measureScrollbarWidth(d);

                // Show the TreeList but hide TreeListUlContainer on page load
                $this.show();
                d.$treeListUlContainer.hide();

                // Add TreeList button next to input
                m._addTreeListButton(d);

                //Add expand/collapse classes and icons, add CHANGE events to checkboxes, add "Selected" class to labels, fill arrayAllCheckboxes with checkboxes
                m._setDefaultsCrawler(d.$treeListUlContainer.children('ul'), d);

                // Disable TreeListInput
                //d.$treeListInput.prop('disabled', true);
                d.$treeListInput.prop('readonly', true);
                d.$treeListInput.prop('tooltip', 'tooltip!');
                d.$treeListInput.prop('title', 'title!');

                // Update the TreeListInput value
                m._updateTreeListInputValue(d);
                d.$treeListInput.prop('title', d.$treeListInput.val());

				// Update the TreeListInput value
				m._updateTreeListInputValue(d);
			});
		},
		// Public Methods
		setSelectedCheckboxes: function () {
			return this.each(function () {
				var $this = $(this);
				var d = $this.data('mckerinTreeList'); // data
				if (!d) { // If the plug-in hasn't been initialized yet
					alert('The "setSelectedCheckboxes" method can not be called until the "mckerinTreeList" plugin has been initialized');
				}
			});
		},
		// Private Methods
		_setDefaultsCrawler: function ($parentUL, d) {
			var $childrenLI = $parentUL.children('li');
			$childrenLI.each(function (index) {
				var $this = $(this);
				var $label = $this.children('label');
				var $checkbox = $label.children('input:checkbox');
				var $childUL = $this.children('ul');
				// Add "Selected" class to labels whose checkboxes are checked
				if ($checkbox.prop('checked')) {
					$label.addClass('Selected');
				}
				// Attach CHANGE event to checkboxes
				$checkbox.on('change', function () { m._checkboxOnChange($(this), d) });
				// Add chechbox to arrayAllCheckboxes
				d.arrayAllCheckboxes.push($checkbox);
				// Add Expand/Collapse Classes and Icon, attach CLICK event, and hide the UL containers
				if (index == $childrenLI.length - 1 && $childUL.length > 0) {
					$this.addClass('LastExpandable').append('<div class="ExpandCollapseIcon"/>').children('div').on('click', function (event) { m._expandCollapseIconOnClick(event, $(this), d); });
					$childUL.hide();
					m._setDefaultsCrawler($childUL, d);
				} else if (index != $childrenLI.length - 1 && $childUL.length > 0) {
					$this.addClass('Expandable').append('<div class="ExpandCollapseIcon"/>').children('div').on('click', function (event) { m._expandCollapseIconOnClick(event, $(this), d); });
					$childUL.hide();
					m._setDefaultsCrawler($childUL, d);
				} else if (index == $childrenLI.length - 1 && $childUL.length == 0) {
					$this.addClass('Last');
				}
			});
		},
		_addTreeListButton: function (d) { // d = data
			d.$treeListInputContainer.append('<div class="TreeListButton"/><div class="Clear"/>');
			d.$treeListInputContainer.children('.TreeListButton').on('click', function (event) {
				event.preventDefault();
				m._closeAllTreeListUlContainers(d);
				m._toggleTreeListUlContainer(event, d);
			});
		},
		_closeAllTreeListUlContainers: function (d) { // d = data
			var $currentTreeListUlContainer;
			for (var i = 0; i < d.arrayTreeListUlContainers.length; i++) {
				$currentTreeListUlContainer = d.arrayTreeListUlContainers[i];
				if ($currentTreeListUlContainer.get(0).id != d.$treeListUlContainer.get(0).id && $currentTreeListUlContainer.is(':visible')) {
					$currentTreeListUlContainer.css('z-index', '');
					$currentTreeListUlContainer.fadeOut(200);
				}
			}
		},
		_toggleTreeListUlContainer: function (event, d) { // d = data
			event.preventDefault();
			if (d.$treeListUlContainer.is(':visible')) {
				d.$treeListUlContainer.css('z-index', '');
				d.$treeListUlContainer.fadeOut(200);
			} else {
				//m._positionTreeListUlContainer(d);
				d.$treeListUlContainer.css(d.treeListUlContainerCss);
				d.$treeListUlContainer.hide();
				d.$treeListUlContainer.fadeIn(600);
			}
		},
		_expandCollapseIconOnClick: function (event, $this, d) {
			event.preventDefault();
			var $parentLI = $this.parent('li');
			var scrollTop = d.$treeListUlContainer.scrollTop();
			d.$treeListUlContainer.css('overflow', 'hidden');
			$this.siblings('ul').slideToggle(220, function () { m._resizeTreeListUlContainer(100, scrollTop, d); });
			// Change classes for the expand collapse icons
			if ($parentLI.hasClass('Expandable')) {
				$parentLI.removeClass('Expandable').addClass('Collapsible');
			} else if ($parentLI.hasClass('Collapsible')) {
				$parentLI.removeClass('Collapsible').addClass('Expandable');
			} else if ($parentLI.hasClass('LastExpandable')) {
				$parentLI.removeClass('LastExpandable').addClass('LastCollapsible');
			} else if ($parentLI.hasClass('LastCollapsible')) {
				$parentLI.removeClass('LastCollapsible').addClass('LastExpandable');
			}
		},
		_checkboxOnChange: function ($checkbox, d) {
			if ($checkbox.prop('checked')) { // Checkbox selected
				m._selectCheckbox($checkbox); // Re-select this checkbox due to the previous m._clearAllCheckboxes() function call;
				m._selectAllParentCheckboxes($checkbox);
				m._selectAllChildrenCheckboxes($checkbox);
			} else { // Checkbox unselected
				$checkbox.parent('label').removeClass('Selected');
				m._unselectAllChildCheckboxes($checkbox)
			}
			m._updateTreeListInputValue(d);
		},
		_updateTreeListInputValue: function (d) { // d = data
			var inputValue = [];
			var hiddenInputValue = [];
			if (d.settings.selectedValuesHiddenInputId === null) { // Because of the large number of input values expected, the IF statement has been placed early containing redundant code
				for (var i = 0; i < d.arrayAllCheckboxes.length; i++) {
					var $this = d.arrayAllCheckboxes[i];
					if ($this.prop('checked')) {
						inputValue.push(m._trim($this.parent('label').text()));
					}
				}
				d.$treeListInput.val(inputValue.join(", "));
			} else {
				for (var i = 0; i < d.arrayAllCheckboxes.length; i++) {
					var $this = d.arrayAllCheckboxes[i];
					if ($this.prop('checked')) {
						inputValue.push(m._trim($this.parent('label').text()));
						hiddenInputValue.push($this.val());
					}
				}
				d.$treeListInput.val(inputValue.join(", "));
				if (d.$treeListHiddenInput != null) {
					d.$treeListHiddenInput.val(hiddenInputValue.join(d.settings.selectedValuesDelimiter));
				}
			}
		},
		_selectAllParentCheckboxes: function ($currentlySelectedCheckbox) {
			var $parentUL = $currentlySelectedCheckbox.parent('label').parent('li').parent('ul');
			var $parentCheckbox = $parentUL.parent('li').children('label').children('input:checkbox');
			if ($parentCheckbox.length > 0) {
				m._selectCheckbox($parentCheckbox);
				m._selectAllParentCheckboxes($parentCheckbox);
			}
		},
        _selectAllChildrenCheckboxes: function ($currentlySelectedCheckbox) {
            return; //disable checking all children - delete this return to reenable this function
			var $siblingUL = $currentlySelectedCheckbox.parent('label').siblings('ul');
			if ($siblingUL.length > 0) {
				var $label = $siblingUL.children('li').children('label');
				var $checkbox = $label.children('input:checkbox');
				$label.addClass('Selected');
				$checkbox.prop('checked', true);
				m._selectAllChildrenCheckboxes($checkbox);
			}
		},
		_unselectAllChildCheckboxes: function ($currentlyUnselectedCheckbox) {
			var $siblingUL = $currentlyUnselectedCheckbox.parent('label').siblings('ul');
			if ($siblingUL.length > 0) {
				$siblingUL.find('li label').removeClass('Selected').children('input:checkbox').prop('checked', false);
			}
		},
		_selectCheckbox: function ($checkbox) {
			$checkbox.parent('label').addClass('Selected');
			$checkbox.prop('checked', true);
		},
		_resizeTreeListUlContainer: function (duration, scrollTop, d) {
			var windowHeight = $(window).height();
			var originalCssHeight = { height: d.$treeListUlContainer.css('height') };
			var elementActualWidthHeight = m._calculateElementActualWidthHeight(d.$treeListUlContainer);
			var cssHeight = m._calculateElementHeightCss(elementActualWidthHeight.height, windowHeight, 60);
			// Preserve scrolled position
			d.$treeListUlContainer.css(originalCssHeight).scrollTop(scrollTop);
			d.$treeListUlContainer.animate(cssHeight, {
				'duration': duration,
				'specialEasing': {
					height: 'linear'
				},
				'complete': function () {
					d.$treeListUlContainer.css('overflow', '');
				}
			});
		},
		// !!!!! Function currently not being used
		_positionTreeListUlContainer: function (d) {
			d.$treeListUlContainer.css(m._calculateElementPositionCss(d.$treeListInput, d.$treeListUlContainer, d.treeListUlContainerLeftOffset, d.scrollbarWidth, 60));
		},
		// !!!!! Function currently not being used
		// Returns object with "left" and "top" and "height" css values
		_calculateElementPositionCss: function ($positionReferenceElement, $elementToPosition, elementToPositionLeftOffset, scrollbarWidth, heightBottomMargin) {
			var positionCss = {};

            var windowHeight = $(window).height();
            var windowWidth = $(window).width();
            var windowScrollTop = $(window).scrollTop();
            var windowScrollLeft = $(window).scrollLeft();

            var elementToPositionActualWidthHeight = m._calculateElementActualWidthHeight($elementToPosition);
            var elementToPositionActualHeight = elementToPositionActualWidthHeight.height;
            var elementToPositionActualWidth = elementToPositionActualWidthHeight.width;

            var posRefEleOffset = $positionReferenceElement.offset();
            var posRefEleWidth = $positionReferenceElement.outerWidth();
            var posRefEleHeight = $positionReferenceElement.outerHeight();

            var left = posRefEleOffset.left + elementToPositionLeftOffset;
            var top = posRefEleOffset.top;
            var actualOffsetTop = top - windowScrollTop;
            var actualOffsetLeft = left - windowScrollLeft;

            var posRefEleOffsetBottom = windowScrollTop + windowHeight - top;

            var bottom = -(windowScrollTop - posRefEleOffsetBottom + posRefEleHeight);

            // Check if the $elementToPosition will be place lower then the actual viewing area
            //if (actualOffsetTop + elementToPositionActualHeight > windowHeight) {
            // True: lets place the bottom of the $elementToPosition flush with the bottom of the $positionReferenceElement
            //	positionCss.bottom = bottom + posRefEleHeight + "px";
            //	positionCss.top = "";
            //} else {
            positionCss.top = top + posRefEleHeight + "px";
            positionCss.bottom = "";
            //}

            // Check if the $elementToPosition will extend beyond the right side of the the actual viewing area
            //if (left + elementToPositionActualWidth > windowWidth) {
            // True: lets place the $elementToPosition to the left of the $positionReferenceElement
            //	positionCss.right = windowWidth - actualOffsetLeft + posRefEleWidth + (elementToPositionLeftOffset * 2) + "px";
            //	positionCss.left = "";
            //} else {
            positionCss.left = left + "px";
            positionCss.right = "";
            //}

            // Calculate height
            positionCss.height = m._calculateElementHeightCss(elementToPositionActualHeight, windowHeight, heightBottomMargin);

            return positionCss;
        },
        _calculateElementActualWidthHeight: function ($element) {
            // Measure $element width and height
            var isVisible = $element.is(':visible');
            if (!isVisible) {
                $element.css('visibility', 'hidden').show();
            }
            $element.css('height', '').css('width', '');
            var widthHeight = {
                height: $element.outerHeight(),
                width: $element.outerWidth()
            };
            if (!isVisible) {
                $element.show().css('visibility', '');
            }
            return widthHeight;
        },
        _calculateElementHeightCss: function (elementActualHeight, windowHeight, heightBottomMargin) {
            var css = {};
            if (elementActualHeight + heightBottomMargin > windowHeight) {
                css.height = windowHeight - heightBottomMargin + "px";
            } else {
                css.height = elementActualHeight + "px";
            }
            return css;
        },
        _measureScrollbarWidth: function () {
            var div = $('<div style="width:50px;height:50px;overflow:hidden;position:absolute;top:-200px;left:-200px;"><div style="height:100px;"></div>');
            // Append our div, do our calculation and then remove it 
            $('body').append(div);
            var w1 = $('div', div).innerWidth();
            div.css('overflow-y', 'scroll');
            var w2 = $('div', div).innerWidth();
            $(div).remove();
            return (w1 - w2);
        },
        _leftTrim: function (stringToTrim) {
            var whitespace = new String(" \t\n\r");
            var string = new String(stringToTrim);
            if (whitespace.indexOf(string.charAt(0)) != -1) {
                var j = 0;
                var i = string.length;
                while (j < i && whitespace.indexOf(string.charAt(j)) != -1) {
                    j++;
                }
                string = string.substring(j, i);
            }
            return string;
        },
        _rightTrim: function (stringToTrim) {
            var whitespace = new String(" \t\n\r");
            var string = new String(stringToTrim);
            if (whitespace.indexOf(string.charAt(string.length - 1)) != -1) {
                var i = string.length - 1;       // Get length of string
                while (i >= 0 && whitespace.indexOf(string.charAt(i)) != -1) {
                    i--;
                }
                string = string.substring(0, i + 1);
            }
            return string;
        },
        _trim: function (stringToTrim) {
            return m._rightTrim(m._leftTrim(stringToTrim));
        }
    };

    //#endregion Methods

    // Declare plug-in
    $.fn.mckerinTreeList = function (methodOrOptions) {
        var methods = m;
        if (methods[methodOrOptions]) { // If methodOrOptions is in the methods array then call that function
            if (methodOrOptions.indexOf("_", 0) === 0) { // If method name starts with an "_" then it is a private method
                $.error('"' + methodOrOptions + '" is a PRIVATE METHOD on mckerinTreeList!');
            } else {
                return methods[methodOrOptions].apply(this, Array.prototype.slice.call(arguments, 1));
            }
        } else if (typeof methodOrOptions === 'object' || !methodOrOptions) { // methodOrOptions is an object (which would be an object of options) or null (empty) 
            // Default to "init" 
            return methods.init.apply(this, arguments);
        } else {
            $.error('Method ' + methodOrOptions + ' does not exist on mckerinTreeList!');
        }
    };

})(jQuery);

