/****** c.palette.js *******/ 

var Palette = $.klass({
    /** id for container of palette */
    containerId : false,

    /** our jquery element */
    $el : false,
    $itemsEl : false,

    title : "",
    titleTooltip: "",
    width: 133,
    items : false,

    initialize : function(config)
    {
        $.extend(this, config);
        this.renderDom();
    },

    renderDom : function()
    {
        this.$el = $("<div>").width(this.width)
            .append($("<div>").addClass("palette-title").text(this.title).attr("title", this.titleTooltip));

        this.$itemsEl = $("<div class='palette-items'>").appendTo($("<div class='palette-item-container'>").appendTo(this.$el));

        this.renderItems();

        $("#" + this.containerId).append(this.$el);
    },

    renderItems : function()
    {

    },

    updateItems : function(items)
    {
        this.items = items;

        this.$itemsEl.empty();
        this.renderItems();
    }

});

;/****** c.visualizer.js *******/ 

/**
 * data visualizers present data to the user.  a user can
 * interact with the visualizer to create pointers to the selected
 * data elements / ranges.
 *
 * all visualizers communicate user selection by broadcasting the 'pointer-selected'
 * event when a new selection has been made.
 *
 */
var Visualizer = {};

Visualizer.Base = $.klass({

    /** the model to be visualized */
    data : false,

    /** our jquery root element */
    el : false,

    /** default configuration */
    config : false,

    /**
     * @constructor
     * @param config
     *   - renderTo : a selector into which we'll render ourself.
     *   - selectionEnabled : if set to false, data element's are not selectable.
     *   - scrollToData : if this is set to true we should attempt to scroll the page to the visualizer.
     */
    initialize : function( config )
    {
        this.config = {
            selectionEnabled: true,
            visualizerId: ""
        };

        config = $.extend(this.config,config);

        this.$container = $("<div class='vz-container'>");
        this.el = $("<div class='visualizer'>").appendTo(this.$container);

        if ( this.config.renderTo ) $(this.config.renderTo).append(this.$container);

		this.setSelectionEnabled( config.selectionEnabled );
    },

    _handleDataReceived: function(data, callback) {
        this.setData(data);

        if (page.firstDataSelect) {
            PubSub.publish("data-tab-selected", []);
            page.firstDataSelect = false;
        }

        if (callback) callback();
    },

    /**
     *
     * @param data
     */
    setData : function(data)
    {
        this.data = data;
		this.render();
		this.resize();
    },


    /**
     * renders the visualizer.  subclasses should provide their own
     * specific implementation.
     */
    render : function()
    {
        // clear out our container
        this.el.empty();
    },


	renderError : function(err)
	{
		this.el.empty();
		this.el.append( $("<div style='padding-top:40px;text-align: center'>").html(err) );

	},


    /**
     * sets the currently selected data and updates the UI to reflect the pointer
     * @param p
     */
    setPointer : function ( p )
    {
        console.error("must implement 'setPointer'"); // eslint-disable-line no-console
    },

    /**
     * returns a string representation of the selected data.
     * all visualizers must implement getPointer.
     */
    getPointer : function ()
    {
        console.error("must implement 'getPointer'"); // eslint-disable-line no-console
    },


    /**
     * returns data from the model for the given a pointer string.
     * @param pointer
     * @returns false if no data matches the pointer, otherwise single value, array, or object
     *  depending on the underlying model & pointer.
     */
    getDataForPointer : function(pointer)
    {
        console.error("must implement 'getDataForPointer'"); // eslint-disable-line no-console
        return false;
    },


	setSelectionEnabled : function(enabled)
	{
		this.selectionEnabled = enabled;
		this.el.toggleClass("selectable",enabled);
	},

	resize : function()
    {
        var containerHeight, elMBP;

		if (this.$container.is(":hidden")) return;

        containerHeight = this.$container.height();
        elMBP = this.el.outerHeight(true) - this.el.height();

		this.el.height(containerHeight - elMBP);
	},

    hide: function() {
        this.$container.hide();
        this.deactivateTab();
    },

    deactivateTab: function() {
        this.$tab.find(".tab-container-three-dot").removeClass("active");
        this.$tab.removeClass("active");
    },

    activateTab: function(visualizer) {
        this.$tab.addClass("active");
        this.$tab.find(".tab-container-three-dot").addClass("active");
    },

    renderPathBar: function() {

    }
});

Visualizer.DatasourceVisualizerBase = $.klass(Visualizer.Base, {

    /** datasourceInstance object {name,id,format} */
    datasourceInstance : false,

    initialize: function($super, options) {
        $super(options);
    },

    setDatasourceInstance: function(dsi, callback) {
        this.datasourceInstance = dsi;
        this.refresh(callback);
    },

    /**
     * asks the server for new data for this datasource
     */
    refresh: function(callback) {
        var _this = this;
        var params = {
            dsid: this.datasourceInstance.id,
            dsidIsRd: true,
            uiVars: JSON.stringify(page.uiVariables)
        };

        $.post("/datasources/ajax_getData", params, function(data) {
            _this._handleDataReceived(data, callback);

        },"json").error(function(err){
            _this.renderError("A problem occurred while loading the data source.  Please try reloading the page.");
        });
    },

    createTab: function() {
        return new VisualizerTabPane.DataSourceTab(this);
    },

    getDataProvider: function() {
        return this.datasourceInstance;
    }
});

Visualizer.DataSetVisualizerBase = $.klass(Visualizer.Base, {
    createTab: function() {
        return new VisualizerTabPane.DataSetTab(this);
    },

    getDataProvider: function() {
        return this.dataSet;
    },

    refresh: function() {
        var _this = this;

        var refreshPromise = $.Deferred();

        require(["/js/build/vendor.packed.js"], function(Vendor) {
            require(["/js/build/model_main.packed.js"], function (DataSetModel) {
                DataSetModel.invalidateCache();
                DataSetModel
                    .load(_this.dataSet.id, {columns: true})
                    .then(function (dataSetMetadata) {
                        _this.setData(dataSetMetadata);
                        refreshPromise.resolve(dataSetMetadata);
                        return dataSetMetadata;
                    })
                    .then(DataSetModel.populateColumnData)
                    .then(function (fullDataSet) {
                        _this._handleDataReceived(fullDataSet);
                    });
                // TODO: Gord - handle errors.
            });
        });

        return refreshPromise;
    },

    setDataSet: function(dataSet, callback) {
        this.dataSet = dataSet;
        return this.refresh(callback);
    }
});

Visualizer.createforFormat = function(format, cfg)
{
	switch(format)
	{
		case "csv":
			return new Visualizer.Table(cfg);
		case "xls":
			return new Visualizer.Excel(cfg);
		case "xml":
			return new Visualizer.Tree(cfg);
		case "json":
			return new Visualizer.Json(cfg);
        case "dataset":
            return new Visualizer.DataSetTable(cfg);
    }
};
;/****** c.0.layout_manager.js *******/ 

LayoutManager = $.klass({

	initialize : function( config )
	{
		config = $.extend({
			// put defaults here
		},config);

		this.cx = config.cx;
	},


	doLayout : function( parentCx )
	{

	},

	getPropertyModel : function()
	{

	},

	configure : function(config)
	{

	},

	serialize : function()
	{
		return {};
	}

});


VBoxLayout = $.klass(LayoutManager,{

	doLayout : function($super,cx)
	{
		if( cx.components )
		{
			console.log(cx.dimensions.w);
			var $el = cx.el;
			_.each(cx.components,function(c){
				c.setHeight(40);
				$el.append(c.el);
			});
		}
	}
});


GridLayout = $.klass(LayoutManager,{

	rows : 3 ,
	cols : 3 ,
	widths : false,
	cellData : false,
	$container : false,

	initialize : function(){
		this.widths = [];
		this.cellData = [];
	},


	setRows : function(n)
	{

	},

	setCols : function(n)
	{

	},

	setCellConstraints : function( config )
	{

	},

	getCellConstraints : function(x,y)
	{
		for( var i = 0 ; i < this.cellData.length; i++ ){
			var cd = this.cellData[i];
			if ( cd.x = x && cd.y == y ) return cd;
		}
		return false;
	},

	doLayout : function($super,cx)
	{
		var i;

		if ( !this.$container){
			this.$container = $("<table>").addClass("layout-grid");

			// render table cells
			// ------------------
			for(i = 0 ; i < this.rows; i++){
				var $tr = $("<tr>");
				for( var j = 0 ; j < this.cols; j++ ){
					$tr.append( $("<td>") );
				}
				this.$container.append($tr);
			}


			if( cx.components.length == 0 ) return;


			cx.components.sort( this.sortComponentXY );

			for(i = 0 ; i < cx.components.length ; i++){
				var _cx = cx.components[i];
				// no layout config?  no placement in grid!
				if ( !_cx.layout ) continue;
			}

		}
		cx.el.empty().append(this.$container);
	},


	sortComponentXY : function(a,b)
	{
		if (!a.layout) return 1;
		if (!b.layout) return -1;
		if(a.layout.y != b.layout.y) return a.layout.y - b.layout-1;
		return a.layout.x - b.layout-x;
	}

});;/****** c.0.util.js *******/ 

/* global FMT:false, KF: false, DX:false */
// if there is browser no logging mechanism, make stub to consume messages
if ( typeof(console) == "undefined" )
{
	console = {
		log : function(msg){
		}
	};
}

var expiredDialog = false;
$(function() {

    checkLocalStorageSupport(); // for safari private browsing mode

    $(document).ajaxError(function(event, request, settings)
    {
        if (request.status == 403) {
            try {
                if (dashboard && (dashboard.config.imagingMode || dashboard.isPublished())) return;
            } catch (e) {
                // dashboard was probably undefined
            }

            var expired = false;
            try {
                var json = JSON.parse(request.responseText);
                expired = json.expired;
            } catch (e) {
                // doesn't have expired key in json object
            }

            if (!expiredDialog &&  expired) {
                expiredDialog = true;

                var $content = $("<div class='expired-dialog'>")
                    .width(400)
                    .append($("<h3 style='margin-bottom:10px;'>Your session has expired.</h3>"))
                    .append($("<div class='gap-south'>This could be due to inactivity, a connectivity problem, or because you've signed in somewhere else. You must sign in again to continue using your dashboard.</div>"));

                var $overlay = $.overlay(null, $content, {
                    onClose:null,
                    shadow:true,
                    footer: {
                        controls: [
                            {
                                text: "Sign In",
                                css: "rounded-button primary",
                                onClick: function() {
                                    window.location = "";
                                }
                            }
                        ]
                    }
                });
                $overlay.show();
            }
        }
    });
});

/**
 * should be used instead of .html when dynamic content could include HTML and instead of .text when dynamic content could include entities
 */

var $KF_GLOBAL_TEXT_CONVERTER = null;

function safeText (input) { // eslint-disable-line no-unused-vars


//	var st = "" + input;
//	if ( input.indexOf("bypass='&'")) return input;
//
//	return _.escape(input);


    if ($KF_GLOBAL_TEXT_CONVERTER == null)
    {
        $KF_GLOBAL_TEXT_CONVERTER = $("<div></div>");
    }
    input = "" + input; // force string
    var div = $KF_GLOBAL_TEXT_CONVERTER;

    if (input.indexOf("&") != -1)
    {
        var ar = input.split("&");
        var en;
        var i;
        var result = "";

        for (i = 0; i < ar.length; ++i)
        {
            if (i % 2)
            {
                en = ar[i].split(";");
                if (en.length != 2)
                {
                    return div.text(input).text();
                }
                result += div.html("&"+en[0]+";").text() + en[1];
            }
            else
            {
                result += ar[i];
            }
        }
        input = result;
    }
    var html = div.text(input);
    html = div.outerHTML();
    return html.substring(5,html.length - 6);
}

/**
 * Show the service agreement
 */
var agreementVisible = false;
function showServiceAgreement() // eslint-disable-line no-unused-vars
{
    if (agreementVisible) return;
    agreementVisible = true;

    var $content = $("#termsDialog");

    if ($content.length == 0) return;
    $content.show();

    var $overlay = $.overlay(null, $content, {
        windowWidth: 0.6,
        onClose: function() {
            agreementVisible = false;
            $content.appendTo(document.body).hide();
            $overlay.hide();
        },
        shadow: {
            onClick: function(){
                $overlay.close();
            }
        },
        footer: {
            controls: [
                {
                    text: "Finished",
                    css: "rounded-button primary",
                    onClick: function() {
                        $overlay.close();
                    }
                }
            ]
        }
    });

    if (hasMobileViewport()) {
        $overlay.container.addClass("termsDialog-overlay");
        resizeMobileModal();
    }

    $overlay.show();

    setTimeout(function() { $overlay.close(); }, 200000);
}

// Debounced because Chrome `orientationchange` events report inconsistent width/heights
// (depending on when location and navigation toolbars are shown/hidden).
var resizeMobileModal = _.debounce(function() {
    var width = window.innerWidth;
    var height = window.innerHeight;

    $("#dialogPassword, .termsDialog-overlay .overlay-border, .mobile-signup-complete-overlay .overlay-border").css({
        "width": width + "px",
        "height": height + "px"
    });
}, 250);

function initMobileResizeListener() { // eslint-disable-line no-unused-vars
    if (window.screen && window.screen.addEventListener) {
        window.screen.addEventListener("orientationchange", resizeMobileModal);
    } else if (window.screen && window.screen.orientation) {
        window.screen.orientation.onchange = resizeMobileModal;
    }
    document.addEventListener("resize", resizeMobileModal);

    resizeMobileModal();
}

function _sanitizeNumbers( data ) // eslint-disable-line no-unused-vars
{
	var len = data.length;
	while(--len > -1)
	{
		var f = FMT.convertToNumber(data[len]);
//		var f = parseFloat(data[len]);
		if ( f == undefined  || !f ) data[len] = 0;
		else data[len] = f;
	}
}

function _sanitizeStrings( data , padLen  ) // eslint-disable-line no-unused-vars
{
	var len = Math.max(data.length,padLen);

	while(--len > -1)
	{
		var v = data[len];
		if ( v == undefined  || !v ) data[len] = "";
	}
}

function getLocationOrigin() // eslint-disable-line no-unused-vars
{
    return window.location.protocol + "//" + window.location.host;
}

function isWorkspace(d) { // eslint-disable-line no-unused-vars
    if (d && d.config && d.config.workspaceMode && !d.config.previewMode && !d.config.publishedMode && !d.config.imagingMode) {
        return true;
    }

    return window.isKlipEditorPage;
}

function isPreview(d) // eslint-disable-line no-unused-vars
{
    if ( d && d.config && d.config.previewMode ) return true;
}

function isDashboard() // eslint-disable-line no-unused-vars
{
    return ( window.dashboard !== undefined);
}

function inputValidate(input){ // eslint-disable-line no-unused-vars
    var valid = true;
    var anyErrorsVisible = $(".ds_error").is(":visible"); //To know if formValidate() was called
    var blankErrorElement = $("#" + input + "_blank_err");
    var sizeErrorElement = $("#" + input + "_size_err");
    var field = $("#" + input);
    var noRowErrors;
    var rowErrorContainer;

    field.removeClass("input-error");

    if(input != null && anyErrorsVisible){
        blankErrorElement.hide();
        sizeErrorElement.hide();
        valid = validateInputSize(input);
        if (valid) {
            noRowErrors = true;
            rowErrorContainer = blankErrorElement.parent().parent();

            if(rowErrorContainer.find(".ds_error").is(":visible")){
                noRowErrors = false;
                field.addClass("input-error");
            }
            if(noRowErrors){
                rowErrorContainer.hide();
            }
        }
    }
    return valid;
 }


function formValidate(){ // eslint-disable-line no-unused-vars

    var valid = true;

    $(this).removeClass("input-error");
    $(".ds_error").hide();
    $(".ds_error").parent().parent().hide();
    $(".dataList").find(".validateNotBlank").each(function(){
        var id = $(this).attr("id");
        if (validateInputSize(id) == false)
            valid = false;
    });

    return valid;
}

function validateInputSize(input) {
    var blankErrorElement = $("#" + input + "_blank_err");
    var sizeErrorElement = $("#" + input + "_size_err");
    var field = $("#" + input);
    var inputVal = $.trim($("#" + input).val());
    var valid = true;
    if (inputVal == "") {
        blankErrorElement.show();
        blankErrorElement.parent().parent().show();
        field.addClass("input-error");
        valid = false;
    } else if (inputVal.length > 65535) {
        sizeErrorElement.show();
        sizeErrorElement.parent().parent().show();
        field.addClass("input-error");
        valid = false;
    }
    return valid;
}



// global status object/jqEl
// ------------------------------
var $status;

function statusMessageWhenNoOverlay(msg, config) {
    var i = 0;

    // Don't display status messages if an overlay with a blocking shadow is displayed
    for(i = 0; i < $.OverlayList.active.length; i++) {
        if ($.OverlayList.active[i].shadow && Number($.OverlayList.active[i].shadow.css("opacity")) > 0) return;
    }

    statusMessage(msg, config);
}

function statusMessage(msg, config) {
    var $target = $("body");
    var isError = config && config.error;
    var isInfo = config && config.info;
    var isWarning = config && config.warning;
    var isSpinner = config && config.spinner;

	if ($status){
        $status.stop().hide().remove();
    }

    //Setup status/error icon and body
    if(isError){
        $status = $("<div class='error-message'>").append($("<div class='status-error-icon'>"));
    } else if(isInfo) {
        $status = $("<div class='info-message'>").append($("<div class='status-info-icon'>"));
    } else if(isWarning) {
        $status = $("<div class='warning-message'>").append($("<div class='status-warning-icon'>"));
    } else {
        $status = $("<div class='status-message'>");

        if(!isSpinner){
            $status.append($("<div class='status-message-ok'>"));
        } else {
            $status.append($("<div class='status-message-spinner spinner-white'>"));
        }
    }

    //Setup message body
    $status.append($("<span class='status-inner-message'>"));

    if(!isSpinner){
        $status.append($("<div class='status-message-close'>"));
    }

    $status.appendTo($target).find(".status-inner-message").html(msg);

    $status.find(".status-message-close").on("click", function(){
        $status.hide();
    });

    $status.css({
        left: ($target.width() / 2) - ($status.width() / 2),
        "z-index": (config && config.zIndex ? config.zIndex : "")
    });

    if (config && config.showFast) {
        $status.css({
            top: "-7px",
            opacity:1
        }).show();
    } else {
        $status.css({
            top:"-30px",
            opacity:0
        }).show().animate({ top: "+=23px", opacity:1 }, 500);
    }

    /* Disappear message */
    if (!(config && config.keepMessage || isError || isWarning)) hideStatus();
}

var hideStatus = _.debounce( function() {
	$status.animate({ top:"-=47px", opacity:0 } , 500);
}, 5000);

function fullSpinnerMessage(msg, config) // eslint-disable-line no-unused-vars
{
    var $shadow = $("<div class='full-shadow'>").appendTo("body");
    window.scrollTo(0,0);

    var $message = $("<span>").html(msg);

    if (!config || !config.noSpinner){
        $message = $("<div>").append($("<span id='message'>").html(msg));
    }

    statusMessage($message, { spinner:true, keepMessage:true, zIndex:1001, showFast:true });

    return {
        hide : function() {
            if ($status) $status.hide();
            $shadow.remove();
        }
    };
}

function fullSpinnerBlockInput(input, block) // eslint-disable-line no-unused-vars
{
    var $shadow = $("<div class='full-shadow'>").appendTo("body");
    var $input = $("#" + input);

    if (block) {
        $input.addClass("spinner-block-input");
    } else {
        $input.removeClass("spinner-block-input");
    }

    return {
        hide : function() {
            $shadow.remove();
        }
    };
}

// Uses either Square or Circular Spinner and is centered in block-area
function spinnerBlockArea(selector, config) { // eslint-disable-line no-unused-vars
    var $spinner, $area;
    var spinnerWidth, spinnerHeight;

    var spinnerClass = config.icon && config.icon == "dots" ? "spinner-dots" : "spinner-calculator";
    var additionalCssClass = config.blockAreaClass ? (" " + config.blockAreaClass) : "";

    $spinner = $("<div class='" + spinnerClass + "'>").html(config && config.msg ? config.msg : "");

    $area = $(selector).css("position", "relative").append($("<div class='block-area" + additionalCssClass + "'>").append($spinner));
    $area.find(".block-area").css({
        "top": $(selector).scrollTop(),
        "left": $(selector).scrollLeft()
    });

    spinnerWidth = $spinner.outerWidth();
    spinnerHeight = $spinner.outerHeight();

    $spinner.css({
        "margin-top": - Math.floor(spinnerHeight / 2),
        "margin-left": - Math.floor(spinnerWidth / 2)
    });

    return {
        hide : function() {
            $area.css("position", "");
            $area.find(".block-area").remove();
        }
    };
}

// eslint-disable-next-line
function spinnerBlockAreaUsingCSS(selector){
    var $area, $spinner;
    var spinnerWidth, spinnerHeight;

    $spinner =$("<img src='/images/spin-light-theme-static.png' alt='Working' class='static-spinner-icon' title='Working..'>");
    $area = $(selector).css("position", "relative").append($("<div class='block-area'>").append($spinner));

    spinnerWidth = $area.outerWidth();
    spinnerHeight = $area.outerHeight();
    $spinner.css({
        "margin-top": Math.floor(spinnerHeight / 2),
        "margin-left": Math.floor(spinnerWidth / 2)
    });
    return{
        hide: function(){
            $area.css("position", "");
            $area.find(".block-area").remove();
        }
    }
}

//Uses Circular spinner and is positioned top-left of block area
function spinnerBlockAreaLeft(selector, config) // eslint-disable-line no-unused-vars
{
    var $spinner = $("<div class='spinner-left'>").html(config && config.msg ? config.msg : "");
    var $area = $(selector).css("position", "relative").append($("<div class='block-area'>").append($spinner));
    var searchAreaHeight = $(".content").outerHeight();

    $(".block-area").css({
        "height": searchAreaHeight,
        "opacity": 0.9
    });

    if(selector == "#klip-library-list"){
        $spinner.css({
            "left": 26,
            "top": 4
        });
     } else {
        $spinner.css({
            "left": 25,
            "top": 25
        });
    }

    return {
        hide : function() {
            $area.css("position", "");
            $area.find(".block-area").remove();
        }
    };
}

function checkForUsageLimit(limitKey, limitName, spinnerBlockId, onAddResource) {
    var limitMap = {};
    if (spinnerBlockId) {
        spinnerBlockButton(spinnerBlockId, true);
    }
    $.post("/dashboard/ajax_getCurrentLimitUsage", {limitKey: limitKey}, function (data) {
        if (data.usage >= data.limit && data.limit >= 0) {
            limitMap[limitKey] = data.usage + 1;
            require(["js_root/content/limits/limit.upgrade.options"], function (asset) {
                asset.init({
                    limits: limitMap,
                    limitName: limitName,
                    addResource: function () {
                        if (onAddResource) {
                            onAddResource();
                        }
                    },
                    onFinished: function () {
                        if (spinnerBlockId) {
                            spinnerBlockButton(spinnerBlockId, false);
                        }
                    }
                });
            });
        } else {
            if (onAddResource) {
                onAddResource();
            }
        }
    });
}

function spinnerBlockButton(id, block) // eslint-disable-line no-unused-vars
{
    var $button = $("#"+id);

    if (block) {
        $button.attr("disabled", "disabled").addClass("spinner-block-button")
            .append($("<div class='spinner-block-overlay'>"))
            .append( $("<div class='spinner-block'>"));
    } else {
        $button.removeAttr("disabled").removeClass("spinner-block-button");
        $button.find(".spinner-block-overlay").remove();
        $button.find(".spinner-block").remove();
    }
}

function bindValues ( target, source, values, defaults, excludeDefaults ) // eslint-disable-line no-unused-vars
{
	if (!values || !$.isArray(values)) return;

	for (var i = 0; i < values.length; i++)
	{
		var k = values[i];
		var val = source[k];

        // skip if: excludeDefaults is set and the current val is identical to the default
		if ( excludeDefaults && val === defaults[k] ) continue;

		// use the default value if: value is not defined and a default exists
		if ( val === undefined && defaults && defaults[k] ) val = defaults[k];

		// assign to target if: val is defined
		if ( val !== undefined ) target[k] = val;
	}
}

function replaceMarkers(replaceRegex, itemText, lookupFunc) // eslint-disable-line no-unused-vars
{
	var match;
	var markerRE = replaceRegex;
	var loopText = itemText;
    var newText = "";

	while( match = markerRE.exec(loopText) )
	{
		var extractedValues = match[1].split("|");
		var replaceText;

		if (lookupFunc) replaceText = lookupFunc(extractedValues[0]);
		if (!replaceText) replaceText = extractedValues[1] ? extractedValues[1] : "";

        // get the substring containing the match
        var endIndex = loopText.indexOf(match[0]) + match[0].length;
        var subText = loopText.substring(0, endIndex);

        // add the substring with the replaced text to the new text
        subText = subText.replace(match[0], replaceText);
        newText += subText;

        // loop over the remaining string
        loopText = loopText.slice(endIndex);
	}

    // append the remaining string
    newText += loopText;

	return newText;
}

function logEvent(cat,event,desc) // eslint-disable-line no-unused-vars
{
	if(typeof(blockEvents) !== "undefined")return;//don't make requests during initial load
	$.ajax({
		type:"POST",
		data:{cat: cat, event: event,desc:desc},
		url:"/company/ajax_logEvent",
		success : function(resp)
		{
		},
		error : function(resp)
		{
		}
	});
}

function newRelicNoticeError(message) // eslint-disable-line no-unused-vars
{
    try {
        throw new Error(message);
    } catch (err) {
        try {
            newrelic.noticeError(err);
        } catch(err2) {
            console.log("New Relic event not captured : " + err);
        }
    }
}

/**
 * Add, remove or update a custom scrollbar using jscrollpane plugin
 * @param toWrap --> jquery element to be scrolled
 * @param parent --> element to be contained within
 * @param plane --> h,v or both
 */
function customScrollbar(toWrap, parent, plane, forceKeep) { // eslint-disable-line no-unused-vars

	var attach = false;
	var remove = false;
	var scrollbarAPI = parent.find(".scrollContainer").data("jsp");

	if (plane=="v")
    {
		if (!scrollbarAPI && toWrap.outerHeight(true) > parent.height()) {
			attach = true;
		} else if (scrollbarAPI && toWrap.height() <= parent.height()) {
			remove = true;
		}
	}
    else if (plane == "h")
    {
		if (!scrollbarAPI && toWrap.width() > parent.width()) {
			attach = true;
		} else if (scrollbarAPI && toWrap.width() <= parent.width()) {
			remove = true;
		}
	}
    else if(plane == "both")
    {
		var compareWidth;
		if ($.browser.mozilla) {
			toWrap.css("overflow", "hidden");
			compareWidth = toWrap[0].scrollWidth;
			toWrap.css("overflow", "");
		} else {
			compareWidth = toWrap[0].scrollWidth;
		}

        if (!scrollbarAPI && (toWrap.height() > parent.height() || compareWidth > parent.width())) {
			attach = true;
		} else if (scrollbarAPI && toWrap.height() <= parent.height() && compareWidth <= parent.width()) {
			remove = true;
		}
	}


	if (attach) // if our content is bigger than our container add a scroll bar
    {
        return attachScrollBar(toWrap, parent);
	}
    else if (remove && !forceKeep) // if we no longer need a scrollbar remove it
    {
        return detachScrollBar(toWrap, parent);
	}
    else
    {
		if (scrollbarAPI) { // update the scroll bar size
            scrollbarAPI.reinitialise();
			return scrollbarAPI;
		} else { // no scroll bar yet
			return false;
		}
	}

}

function attachScrollBar(toWrap, parent){
    var container = $("<div>").addClass("scrollContainer").css("height", "100%").css("width", "100%");

    toWrap.detach();
    container.append(toWrap);
    parent.append(container);

    return container.jScrollPane().data("jsp");
}

function detachScrollBar(toWrap, parent){
    var scrollBar = parent.find(".scrollContainer").data("jsp");

    toWrap.detach();
    scrollBar.destroy();
    parent.find(".scrollContainer").remove();
    parent.append(toWrap);

    return false;
}

function clearSelections() // eslint-disable-line no-unused-vars
{
if (window.getSelection) {
  if (window.getSelection().empty) {  // Chrome
    window.getSelection().empty();
  } else if (window.getSelection().removeAllRanges) {  // Firefox
    window.getSelection().removeAllRanges();
  }
} else if (document.selection) {  // IE?
  document.selection.empty();
}
}

function ordinal(num) // eslint-disable-line no-unused-vars
{
    return num + (
            (num % 10 == 1 && num % 100 != 11) ? "st" :
            (num % 10 == 2 && num % 100 != 12) ? "nd" :
            (num % 10 == 3 && num % 100 != 13) ? "rd" : "th"
        );
}

function hexToRGB(hexString) // eslint-disable-line no-unused-vars
{
    var colorInt = parseInt(hexString.replace(/^#/, ""), 16);
    return {
        r: (colorInt >>> 16) & 0xff,
        g: (colorInt >>> 8) & 0xff,
        b: colorInt & 0xff
    };
}

function formatCurrency(num, config) // eslint-disable-line no-unused-vars
{
    var symbol = config && config.symbol ? config.symbol : "$";
    var symbolSuffixed = config && config.symbolSuffixed;
    var thousandsSep = config && config.thousandsSep ? config.thousandsSep : ",";
    var showZeroDecimal = config && config.showZeroDecimal;

    var amt = parseFloat(num);
    var isNegative = (amt < 0);
    if (isNegative) amt = -amt;

    amt = amt.toFixed(2);

    var noDecimals = Math.floor(amt);
    if (!showZeroDecimal && amt == noDecimals) {
        amt = noDecimals;
    }

    if (thousandsSep != "") {
        amt = amt.toString().replace(/./g, function(c, i, a) {
            return i && c !== "." && !((a.length - i) % 3) ? thousandsSep + c : c;
        });
    }

    return (isNegative ? "-" : "") + (symbolSuffixed ? amt + symbol : symbol + amt);
}

function encodeForId(text, replacements) // eslint-disable-line no-unused-vars
{
    if (!text) return "";
    var spaceReplacement = (replacements && replacements.space ? replacements.space : "_");
    var periodReplacement = (replacements && replacements.period ? replacements.period : "_");

    return text.toLowerCase().replace(/ /g, spaceReplacement).replace(/\./g, periodReplacement);
}

/**
 * iterates recursively over all components in this component and invokes the provided callback function
 * on them.
 * @param root
 * @param callback
 * @param async
 */
function eachComponent(root, callback, async)
{
    var cxStack = [];

    if ( !root.components || root.components.length == 0 ) return;
    var clen = root.components.length;

    // queue immediate children
    // ------------------------
    for ( var i = 0; i < clen; i++) {
        cxStack.push(root.components[i]);
    }

    // loop until queue is empty
    // ------------------------
    while (cxStack.length != 0) {
        var cx = cxStack.pop();

        if ( async ) {
            setTimeout(function() {
                callback(cx);
            }, 0);
        } else {
            var breakSignal = callback(cx); // if a callback returns true, stop iterating
            if ( breakSignal ) break;
        }

        if ( cx.components && cx.components.length != 0 ) {
            var cxChildrenLen = cx.components.length;
            while ( --cxChildrenLen > -1 ) {
                cxStack.push(cx.components[cxChildrenLen]);
            }
        }
    }
}

/**
 * Update all components with new ids
 *
 * @param root - config of root component (Klip or component)
 * @param updateRoot - true, if update the id of the root component
 */
function rebuildComponentIds(root, updateRoot) { // eslint-disable-line no-unused-vars
    var oldToNew = {};
    var oldToNewVirtualColumnIds = {};
    var d = new Date();
    var t = d.getTime();
    var newId;
    var virtualColumnId, newVirtualColumnId;

    if (updateRoot) {
        newId = SHA1.encode(root.id + ":" + t).substring(0, 8);
        oldToNew[root.id] = newId;

        virtualColumnId = convertToVirtualColumnId(root.id);
        newVirtualColumnId = convertToVirtualColumnId(newId);

        oldToNewVirtualColumnIds[virtualColumnId] = newVirtualColumnId;

        root.id =  newId;
    }

    eachComponent(root, function(c){
        newId = SHA1.encode(c.id + ":" + t).substring(0, 8);
        oldToNew[c.id] = newId;

        virtualColumnId = convertToVirtualColumnId(c.id);
        newVirtualColumnId = convertToVirtualColumnId(newId);

        oldToNewVirtualColumnIds[virtualColumnId] = newVirtualColumnId;

        c.id =  newId;
    });


    // once all the component IDs have been rebuilt, go through all the
    // component indicators, series axis, reference nodes, and drill down
    // configs and update their cx-id references
    // --------------------------------------------------------------
    if (updateRoot) updateIds(root, oldToNew, oldToNewVirtualColumnIds);

    eachComponent(root, function(c){
        updateIds(c, oldToNew, oldToNewVirtualColumnIds);
    });
}

function convertToVirtualColumnId(id) {
    return id.replace("-", "");
}

function getSuggestedComponentLabel(klip, cx, isCopyPaste, currentComponents) { // eslint-disable-line no-unused-vars
    var numbersInUse = currentComponents ? currentComponents : [];
    var currentComponentName = cx.displayName;
    var newId;
    var newComponentString;
    var newComponentRegex;
    var baseComponentName;

    currentComponentName = currentComponentName.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1"); //Escaping special regexp characters

    baseComponentName = /(.*)(?:\sCopy)(?:\s\d)?/.exec(currentComponentName); //Removing copy and possible digit from display name

    if(baseComponentName && baseComponentName[1]){
        currentComponentName = baseComponentName[1];
    }

    newComponentString = currentComponentName + (isCopyPaste? " Copy" : "");
    newComponentRegex = new RegExp(newComponentString + "\\s?(\\d+)?");

    klip.components.forEach(function(currentComponent) {
        var matchedNumber;
        var matches = newComponentRegex.exec(currentComponent.displayName);
        if(matches != null){
            if(matches[1]){
                matchedNumber = parseInt(matches[1]);
                numbersInUse[matchedNumber] = true;
            } else{
                numbersInUse[1] = true; //First component of type already added to tree
            }
        }

        if(currentComponent.isPanel && currentComponent.components){
            getSuggestedComponentLabel(currentComponent, cx, isCopyPaste, numbersInUse);
        }

    }.bind(this));

    //Find first gap in numbering and fill
    for (newId = 1; newId < numbersInUse.length; newId++) {
        if (!numbersInUse[newId]) {
            break;
        }
    }

    if(numbersInUse.length > 0 && newId > 1) {
        newComponentString += " " + newId;
    }

    return newComponentString;
}

function updateIds(c, oldToNew, oldToNewVirtualColumnIds) {
	var i;
    var dstString;
    var re;

    // Check each chart series component and update its associated axis id if necessary
    if (c.role == "series") {
        if (c.axis && oldToNew[c.axis]) c.axis = oldToNew[c.axis];
    }

    // update use button values to be consistent with changed button Ids
    if (c.useButton) {
        for (i in oldToNew) {
            if (oldToNew.hasOwnProperty(i) && c.useButton.indexOf(i) > -1) {
                c.useButton = c.useButton.replace(i, oldToNew[i]);
            }
        }
    }


    // update cx-ids in indicators
    _.each( c.conditions , function(c) {
        _.each( c.predicates , function(p) {
            if (p.left && p.left.cx ) p.left.cx = oldToNew[p.left.cx];
            if (p.right && p.right.cx ) p.right.cx = oldToNew[p.right.cx];
        });
    });

    // update cx-id in reference nodes
    if (c.formulas) {
        for (i = 0; i < c.formulas.length; i++) {
			if (c.formulas[i].version === DX.Formula.VERSIONS.TYPE_IN) {
				updateReferenceIdsInFormulaText(c.formulas[i], oldToNew);
			} else if (c.formulas[i].src) {
				updateReferenceIds(c.formulas[i].src, oldToNew);
			}
        }
    }

    // update cx-ids in drill down configuration
    if (c.drilldownConfig) {
        _.each(c.drilldownConfig, function(cfg) {
            if (cfg.groupby != "") cfg.groupby = oldToNew[cfg.groupby];

            _.each(cfg.display, function(dCfg) {
                dCfg.id = oldToNew[dCfg.id];
            });
        });
    }

    if (c.dstContext) {
        dstString = JSON.stringify(c.dstContext);

        _.each(oldToNewVirtualColumnIds, function(newId, oldId) {
            re = new RegExp(oldId, "g");
            dstString = dstString.replace(re, newId);
        });

        c.dstContext = JSON.parse(dstString);
    }
}

function updateReferenceIds(n, oldToNew) {
    var newId;
    var nChildren;

    if (n.t == "ref" && n.v.r == "cx") {
        newId = oldToNew[n.v["cx"]];
        if (newId) {
            n.v["cx"] = newId;
        }
    }

    if ( !n.c ) return;

    nChildren = n.c.length;
    while (--nChildren > -1) {
        updateReferenceIds(n.c[nChildren], oldToNew);
    }
}

function updateReferenceIdsInFormulaText(componentFormula, oldToNew) {
	var formulaText = componentFormula.formula;
	var newFormulaText = formulaText;
	var indexModifier = 0;
	var formulaReferenceMatches, resultsReferenceMatches;

	if (formulaText) {
        formulaReferenceMatches = DX.Formula.getReferenceMatches(formulaText, DX.Formula.FORMULA_REF);

        if (formulaReferenceMatches) {
            formulaReferenceMatches.forEach(function (componentReferenceMatch) {
                var matchLength = componentReferenceMatch.string.length;
                var oldComponentId = DX.Formula.getComponentId(componentReferenceMatch.string);
                var newComponentId = oldToNew[oldComponentId];
                var newComponentReferenceText = DX.Formula.getComponentReferenceString(newComponentId);
                var preText = newFormulaText.substr(0, componentReferenceMatch.startIndex + indexModifier);
                var postText = newFormulaText.substr(componentReferenceMatch.stopIndex + indexModifier);

                newFormulaText = preText + newComponentReferenceText + postText;

                indexModifier += newComponentReferenceText.length - matchLength; //replacements may change the length of the string
            });

            componentFormula.formula = newFormulaText;
        }

        if (componentFormula.cx && componentFormula.cx.usePostDSTReferences()) {

            resultsReferenceMatches = DX.Formula.getReferenceMatches(formulaText, DX.Formula.RESULTS_REF);
            if (resultsReferenceMatches) {
                resultsReferenceMatches.forEach(function (resultsReferenceMatch) {
                    var matchLength = resultsReferenceMatch.string.length;
                    var oldComponentId = DX.Formula.getComponentId(resultsReferenceMatch.string);
                    var newComponentId = oldToNew[oldComponentId];
                    var newComponentReferenceText = DX.Formula.getResultsReferenceString(newComponentId);
                    var preText = newFormulaText.substr(0, resultsReferenceMatch.startIndex + indexModifier);
                    var postText = newFormulaText.substr(resultsReferenceMatch.stopIndex + indexModifier);

                    newFormulaText = preText + newComponentReferenceText + postText;
                    indexModifier += newComponentReferenceText.length - matchLength; //replacements may change the length of the string
                });
                componentFormula.formula = newFormulaText;
            }
        }
	}
}


// jquery cookie fn
jQuery.cookie=function(key,value,options){if(arguments.length>1&&String(value)!=="[object Object]"){options=jQuery.extend({},options);if(value===null||value===undefined){options.expires=-1;}
if(typeof options.expires==="number"){var days=options.expires,t=options.expires=new Date();t.setDate(t.getDate()+days);}
value=String(value);return(document.cookie=[encodeURIComponent(key),"=",options.raw?value:encodeURIComponent(value),options.expires?"; expires="+options.expires.toUTCString():"",options.path?"; path="+options.path:"",options.domain?"; domain="+options.domain:"",options.secure?"; secure":""].join(""));}
options=value||{};var result,decode=options.raw?function(s){return s;}:decodeURIComponent;return(result=new RegExp("(?:^|; )"+encodeURIComponent(key)+"=([^;]*)").exec(document.cookie))?decode(result[1]):null;};


function getQueryParam(name) // eslint-disable-line no-unused-vars
{
  name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
  var regexS = "[\\?&]" + name + "=([^&#]*)";
  var regex = new RegExp(regexS);
  var results = regex.exec(window.location.href);
  if(results == null)
    return "";
  else
    return decodeURIComponent(results[1].replace(/\+/g, " "));
}


function parseQueryString(queryString) // eslint-disable-line no-unused-vars
{
    var urlParams = {};
    var e,
        a = /\+/g,
        r = /([^&=]+)=?([^&]*)/g,
        d = function (s) { return decodeURIComponent(s.replace(a, " ")); },
        q = queryString;

    while (e = r.exec(q))
    {
        urlParams[d(e[1])] = d(e[2]);
    }

    return urlParams;
}

function validateVariableName(name) { // eslint-disable-line no-unused-vars
    if (!name) return false;
    if (name == "user" || name=="company") return false;
    if (!name[0].match(/^[A-Za-z]$/)) return false;
    return (name.search(/([^0-9a-z_])/gi) == -1);
}


function editorValidateVariableName(name,existingNames) { // eslint-disable-line no-unused-vars
    if (!name) return "invalidName";
    if (name == "user" || name=="company" || $.inArray(name,existingNames) != -1)
    {
        return "userNameExists";
    }
    if (!name[0].match(/^[A-Za-z]$/)) return "invalidName";
    return (name.search(/([^0-9a-z_])/gi) == -1);


}



var SHA1 = (function(){
    /*eslint semi:[0]*/
	function calcSHA1(a){var b=str2blks_SHA1(a);var c=new Array(80);var d=1732584193;var e=-271733879;var f=-1732584194;var g=271733878;var h=-1009589776;for(var i=0;i<b.length;i+=16){var j=d;var k=e;var l=f;var m=g;var n=h;for(var o=0;o<80;o++){if(o<16)c[o]=b[i+o];else c[o]=rol(c[o-3]^c[o-8]^c[o-14]^c[o-16],1);t=add(add(rol(d,5),ft(o,e,f,g)),add(add(h,c[o]),kt(o)));h=g;g=f;f=rol(e,30);e=d;d=t}d=add(d,j);e=add(e,k);f=add(f,l);g=add(g,m);h=add(h,n)}return hex(d)+hex(e)+hex(f)+hex(g)+hex(h)}function kt(a){return a<20?1518500249:a<40?1859775393:a<60?-1894007588:-899497514}function ft(a,b,c,d){if(a<20)return b&c|~b&d;if(a<40)return b^c^d;if(a<60)return b&c|b&d|c&d;return b^c^d}function rol(a,b){return a<<b|a>>>32-b}function add(a,b){var c=(a&65535)+(b&65535);var d=(a>>16)+(b>>16)+(c>>16);return d<<16|c&65535}function str2blks_SHA1(a){var b=(a.length+8>>6)+1;var c=new Array(b*16);for(var d=0;d<b*16;d++)c[d]=0;for(d=0;d<a.length;d++)c[d>>2]|=a.charCodeAt(d)<<24-d%4*8;c[d>>2]|=128<<24-d%4*8;c[b*16-1]=a.length*8;return c}function hex(a){var b="";for(var c=7;c>=0;c--)b+=hex_chr.charAt(a>>c*4&15);return b}var hex_chr="0123456789abcdef"
	return {
		encode: calcSHA1
	};
})();


function deleteTempProperties( obj ) // eslint-disable-line no-unused-vars
{
	var stack = [ obj ];

	while( stack.length > 0 )
	{
		var o = stack.pop();
		for( var p in o )
		{
			if ( p.indexOf("~") == 0 ){
				delete o[p];
				continue;
			}
			var op = o[p];
			if ( typeof op == "object" ){
				stack.push(op);
				continue;
			}
			if ( _.isArray(op) ){
				for ( var i = 0 ; i < op.length; i++ )
					if ( typeof op[i] == "object" ) stack.push(op[i]);
			}

		}
	}
}



function andMask() // eslint-disable-line no-unused-vars
{
	var args = _.toArray(arguments);
	var l  = args.pop();
	var r = args.pop();

	while(r)
	{
		var result = [];
		var len = Math.max(l.length,  r.length);
		while(len-- > 0){
			var _l = l[len];
			var _r = r[len];
			if (_l == undefined) _l = l[0];
			if (_r == undefined) _r = r[0];
			result[len] = _l && _r;
		}

		l = result;
		r = args.pop();
	}

	return l;
}

function maxArrayLen() // eslint-disable-line no-unused-vars
{
	var max = 0;
	var args = _.toArray(arguments);
	var len = args.length;
	while(len-- > 0 )
	{
		var a = args.pop();
		var l = 0;
		if ( a == undefined ) l = 0;
		else l = a.length;
		max = Math.max(l,max);
	}
	return max;
}


(function($){

	var lastEval;
	var currentResult;

	$.fn["if"] = function( bool ){
		lastEval = bool;
		currentResult = this;
		if ( bool ){
			return this;
		}
		else{
			return $();
		}
	};

	$.fn["else"] = function(){
		return lastEval ? $() : currentResult;
	};

	$.fn.endif = function(){
		return currentResult;
	};

	$.fn._if = $.fn["if"];
	$.fn._else = $.fn["else"];
	$.fn._endif = $.fn.endif;

})(jQuery);


var sortComparators = (function(){ // eslint-disable-line no-unused-vars

	var emptyString = "";

	var stringSort = function(a,b){
		var l = a[1];
		var r = b[1];
		if ( l == null ) l = emptyString;
		if ( r == null ) r = emptyString;
		return (""+l).localeCompare(""+r);
	};

	var numericSort = function(a,b){
        var aNum, bNum;
        if (a[1] == null) {
            return 1;
        } else if (b[1] == null) {
            return -1;
        } else if (a[1] == b[1]) {
            return 0;
        } else {
            aNum = FMT.convertToNumber(a[1]);
            bNum = FMT.convertToNumber(b[1]);
        }


		return aNum - bNum ;
	};

    var dateSort = function (a, b) {
        return numericSort(a.getTime(), b.getTime());
    };

	return {
		stringSort: stringSort ,
		numericSort : numericSort ,
		dateSort: dateSort
	};
})();


/**
 * These filters aggregate data according to the given groups
 *
 * @param data - the data to aggregate
 * @param dataLength - the length of the data to aggregate
 * @param groupColData - the original data from the grouped column
 * @param groupData - the original data for each column along the group path
 * @param groupPath - the path taken while navigating through groups
 * @param groups - the groups
 */
var dataFilters = (function() { // eslint-disable-line no-unused-vars
    /**
     * Returns the indices of the rows at the end of the path
     */
    var getRows = function(dataLength, groupData, groupPath) {
        var rows = _.range(dataLength);

        for (var i = 0; i < groupPath.length; i++) {
            var gd = groupData[i];

            var j = 0;
            while (j < rows.length) {
                if (gd[rows[j]] != groupPath[i]) {
                    rows.splice(j, 1);
                    j--;
                }

                j++;
            }
        }

        return rows;
    };

    /**
     * Returns the distinct values at the end of the path
     */
    var group = function(data, groupData, groupPath) {
        var rows = this.getRows(data.length, groupData, groupPath);

        var groups = [];
        _.each(rows, function(idx) {
            if (_.indexOf(groups, data[idx]) == -1) {
                groups.push(data[idx]);
            }
        });

        return groups;
    };

    /**
     * Returns all of the data at the end of the path
     */
    var select = function(data, groupData, groupPath) {
        var rows = this.getRows(data.length, groupData, groupPath);

        var select = [];
        _.each(rows, function(idx) {
            select.push(data[idx]);
        });

        return select;
    };

    /**
     * Returns the sum for each group at the end of the path
     */
    var sum = function(data, groupColData, groupData, groupPath, groups, is2D) {
        if (!groups) groups = this.group(groupColData,  groupData, groupPath);

        var rows = this.getRows(data.length, groupData, groupPath);

        var sums = {};
        _.each(groups, function(g) {
            sums[g] = is2D ? [] : 0;
        });

        for (var i = 0; i < rows.length; i++) {
            var row = rows[i];

            if (row < groupColData.length) {
                if (is2D) {
                    var sumArr = sums[groupColData[row]];

                    for (var j = 0; j < data[row].length; j++) {
                        if (j >= sumArr.length) {
                            while (sumArr.length < (j + 1)) {
                                sumArr.push(0);
                            }
                        }

                        sumArr[j] += FMT.convertToNumber(data[row][j]);
                    }
                } else {
                    sums[groupColData[row]] += FMT.convertToNumber(data[row]);
                }
            }
        }

        var sumList = [];
        _.each(groups, function(g) {
            sumList.push(sums[g]);
        });

        return sumList;
    };

    /**
     * Returns the average for each group at the end of the path
     */
    var average = function(data, groupColData, groupData, groupPath, groups, is2D) {
		var division;
        if (!groups) groups = this.group(groupColData,  groupData, groupPath);

        var sumList = this.sum(data, groupColData, groupData, groupPath, groups, is2D);
        var countList = this.count(data, groupColData, groupData, groupPath, groups);

        var avgList = [];
        for (var i = 0; i < groups.length; i++) {
            if (is2D) {
                for (var j = 0; j < sumList[i].length; j++) {
                    division = sumList[i][j] / countList[i];

                    sumList[i][j] = (_.isNaN(division) ? "" : division);
                }

                avgList.push( sumList[i] );
            } else {
                division = sumList[i] / countList[i];

                avgList.push(_.isNaN(division) ? "" : division);
            }
        }

        return avgList;
    };

    /**
     * Returns the count for each group at the end of the path
     */
    var count = function(data, groupColData, groupData, groupPath, groups) {
        if (!groups) groups = this.group(groupColData,  groupData, groupPath);

        var rows = this.getRows(data.length, groupData, groupPath);

        var counts = {};
        _.each(groups, function(g) {
            counts[g] = 0;
        });

        for (var i = 0; i < data.length; i++) {
            var row = rows[i];

            if (row < groupColData.length) {
                counts[groupColData[row]] += (data[row] !== "" ? 1 : 0);
            }
        }

        var countList = [];
        _.each(groups, function(g) {
            countList.push(counts[g]);
        });

        return countList;
    };

    /**
     * Returns the count for distinct values in each group at the end of the path
     */
    var distinct = function(data, groupColData, groupData, groupPath, groups, is2D) {
        if (!groups) groups = this.group(groupColData,  groupData, groupPath);

        var rows = this.getRows(data.length, groupData, groupPath);

        var distinct = {};
        _.each(groups, function(g) {
            distinct[g] = [];
        });

        for (var i = 0; i < data.length; i++) {
            var row = rows[i];

            if (row < groupColData.length) {
                if (is2D) {
                    var distArr = distinct[groupColData[row]];
                    var dataArr = data[row];

                    var isDistinct = true;
                    var j = 0;
                    while (isDistinct && j < distArr.length) {
                        var distDataArr = distArr[j];

                        if (_.isEqual(dataArr, distDataArr)) {
                            isDistinct = false;
                        }

                        j++;
                    }

                    if (isDistinct) {
                        distArr.push(dataArr);
                    }
                } else {
                    if (_.indexOf(distinct[groupColData[row]], data[row]) == -1) {
                        distinct[groupColData[row]].push(data[row]);
                    }
                }
            }
        }

        var countList = [];
        _.each(groups, function(g) {
            countList.push(distinct[g].length);
        });

        return countList;
    };

    /**
     * Returns the min value for each group at the end of the path
     */
    var min = function(data, groupColData, groupData, groupPath, groups, is2D) {
		var d;

        if (!groups) groups = this.group(groupColData,  groupData, groupPath);

        var rows = this.getRows(data.length, groupData, groupPath);

        var mins = {};
        _.each(groups, function(g) {
            mins[g] = is2D ? [] : Infinity;
        });

        for (var i = 0; i < data.length; i++) {
            var row = rows[i];

            if (row < groupColData.length) {
                if (is2D) {
                    var minArr = mins[groupColData[row]];

                    for (var j = 0; j < data[row].length; j++) {
                        if (j >= minArr.length) {
                            while (minArr.length < (j + 1)) {
                                minArr.push(Infinity);
                            }
                        }

                        d = FMT.convertToNumber(data[row][j]);

                        if (d < minArr[j]) {
                            minArr[j] = d;
                        }
                    }
                } else {
                    d = FMT.convertToNumber(data[row]);

                    if (d < mins[groupColData[row]]) {
                        mins[groupColData[row]] = d;
                    }
                }
            }
        }

        var minList = [];
        _.each(groups, function(g) {
            minList.push(mins[g]);
        });

        return minList;
    };

    /**
     * Returns the max value for each group at the end of the path
     */
    var max = function(data, groupColData, groupData, groupPath, groups, is2D) {
		var d;

        if (!groups) groups = this.group(groupColData,  groupData, groupPath);

        var rows = this.getRows(data.length, groupData, groupPath);

        var maxs = {};
        _.each(groups, function(g) {
            maxs[g] = is2D ? [] : -Infinity;
        });

        for (var i = 0; i < data.length; i++) {
            var row = rows[i];

            if (row < groupColData.length) {
                if (is2D) {
                    var maxArr = maxs[groupColData[row]];

                    for (var j = 0; j < data[row].length; j++) {
                        if (j >= maxArr.length) {
                            while (maxArr.length < (j + 1)) {
                                maxArr.push(-Infinity);
                            }
                        }

                        d = FMT.convertToNumber(data[row][j]);

                        if (d > maxArr[j]) {
                            maxArr[j] = d;
                        }
                    }
                } else {
                    d = FMT.convertToNumber(data[row]);

                    if (d > maxs[groupColData[row]]) {
                        maxs[groupColData[row]] = d;
                    }
                }
            }
        }

        var maxList = [];
        _.each(groups, function(g) {
            maxList.push(maxs[g]);
        });

        return maxList;
    };

    return {
        getRows: getRows,
        group: group,
        select: select,
        sum: sum,
        average: average,
        count: count,
        distinct: distinct,
        min: min,
        max: max
    };
})();


// http://paulirish.com/2011/requestanimationframe-for-smart-animating/
// http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating

// requestAnimationFrame polyfill by Erik MÃ¶ller
// fixes from Paul Irish and Tino Zijdel

(function() {
    var lastTime = 0;
    var vendors = ["ms", "moz", "webkit", "o"];
    for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
        window.requestAnimationFrame = window[vendors[x]+"RequestAnimationFrame"];
        window.cancelAnimationFrame = window[vendors[x]+"CancelAnimationFrame"]
            || window[vendors[x]+"CancelRequestAnimationFrame"];
    }

    if (!window.requestAnimationFrame)
        window.requestAnimationFrame = function(callback, element) {
            var currTime = new Date().getTime();
            var timeToCall = Math.max(0, 16 - (currTime - lastTime));
            var id = window.setTimeout(function() { callback(currTime + timeToCall); },
                timeToCall);
            lastTime = currTime + timeToCall;
            return id;
        };

    if (!window.cancelAnimationFrame)
        window.cancelAnimationFrame = function(id) {
            clearTimeout(id);
        };
}());

/**
 * updates any existing TWEENs.  if no more TWEENs exist the animation loop is canceled.
 * components which create new TWEENs must call animateTweens() in order to start  loop
 */
function animateTweens() // eslint-disable-line no-unused-vars
{
	if (TWEEN.getAll().length == 0 ) return;

    requestAnimationFrame(animateTweens);
    if (typeof TWEEN != "undefined") TWEEN.update();
}


function isChrome()
{
	return /chrome/i.test(navigator.userAgent);
}

function isWebkit() // eslint-disable-line no-unused-vars
{
    return /webkit/i.test(navigator.userAgent);
}

function isIE() // eslint-disable-line no-unused-vars
{
    return /msie/i.test(navigator.userAgent) && !/opera/i.test(navigator.userAgent);
}

function isIElt11() // eslint-disable-line no-unused-vars
{
    return document.all;    // available only in IE10 and older
}

var MOBILE_TRIAL_SIGNUP_MAXIMUM_WIDTH = 736; // Use max width of the iPhone 6+.

function hasMobileViewport()
{
    return window.screen.availWidth <= MOBILE_TRIAL_SIGNUP_MAXIMUM_WIDTH;
}

function getScrollPosition() // eslint-disable-line no-unused-vars
{
	// Get the current scroll position for all browsers
	var xScroll = 0;
	var yScroll = 0;

	if(isChrome() && document.body)
	{
		// Chrome
		xScroll = document.body.scrollLeft;
		yScroll = document.body.scrollTop;
	}
	else if(document.documentElement)
	{
		// Firefox & IE
		xScroll = document.documentElement.scrollLeft;
		yScroll = document.documentElement.scrollTop;
	}
	else if(typeof(window.pageYOffset) == "number")
	{
		// Older versions of IE
		xScroll = window.pageXOffset;
		yScroll = window.pageYOffset;
	}

	return [xScroll, yScroll];
}

function setScrollPosition(xAndYPositions) // eslint-disable-line no-unused-vars
{
	// Set the window scroll position for all browsers.
	// xAndYPositions is an array containing the x position followed by the y position - see getScrollPosition() above.

	if(xAndYPositions.length != 2) return;

	if(isChrome() && document.body)
	{
		// Chrome
		document.body.scrollLeft = xAndYPositions[0];
		document.body.scrollTop = xAndYPositions[1];
	}
	else if(document.documentElement)
	{
		// Firefox & IE
		document.documentElement.scrollLeft = xAndYPositions[0];
		document.documentElement.scrollTop = xAndYPositions[1];
	}
	else if(typeof(window.pageYOffset) == "number")
	{
		// Older versions of IE
		window.pageXOffset = xAndYPositions[0];
		window.pageYOffset = xAndYPositions[1];
	}
}

/**
 *
 * @param el - DOM node
 * @param completelyAbove - true/false
 * @returns {boolean}
 */
function isElementAboveViewport(el, completelyAbove) { // eslint-disable-line no-unused-vars
    var rect = el.getBoundingClientRect();
    var yPosition = completelyAbove ? rect.bottom : rect.top;

    return yPosition < 0;
}

/**
 * Adapted from http://www.dte.web.id/2013/02/event-mouse-wheel.html
 *
 * @param elementId - element ID
 * @param addEvent - true/false
 */
function toggleHorizontalScroll(elementId, addEvent) { // eslint-disable-line no-unused-vars
    var el = document.getElementById(elementId);
    var $el = $(el);

    var doScroll = function(e) {
        // cross-browser wheel delta
        e = window.event || e;
        var delta = Math.max(-1, Math.min(1, (e.wheelDelta || -e.detail)));

        scrollHelper(el, delta);

        e.preventDefault();
    };

    var scrollHelper = function(el, delta) {
        var $el = $(el);
        var elRect = el.getBoundingClientRect();
        var content = $el.find(":not(.btn-scroll)")[0];
        var contentRect = content.getBoundingClientRect();
        var leftScroll = $el.find(".left-scroll")[0];
        var rightScroll = $el.find(".right-scroll")[0];
        var distance = contentRect.width - elRect.width;
        var mean = 50; // Just for multiplier (go faster or slower)
        var currentLeft = contentRect.left;
        var newLeft = currentLeft;

        if ((delta == -1 && currentLeft == -distance) || (delta == 1 && currentLeft === 0)) return;

        // (1 = scroll-up, -1 = scroll-down)
        // Always check the scroll distance, make sure that the scroll distance value will not
        // increased more than the container width and/or less than zero
        if (delta == -1) {
            newLeft = currentLeft - mean;
            if (newLeft < -distance) newLeft = -distance;
        } else if (delta == 1) {
            newLeft = currentLeft + mean;
            if (newLeft > 0) newLeft = 0;
        }

        leftScroll.style.display = (newLeft === 0 ? "none" : "block");
        rightScroll.style.display = (newLeft == -distance ? "none" : "block");

        // Move element to the left or right by updating the `left` value
        content.style.left = newLeft + "px";
    };

    if (addEvent) {
        // TODO what about touch screen? swipe?

        var $leftButton = $("<div>")
            .addClass("btn-scroll left-scroll")
            .css("display", "none")
            .data("delta", 1);

        var $rightButton = $("<div>")
            .addClass("btn-scroll right-scroll")
            .css("display", "block")
            .data("delta", -1);

        $el.on("mousewheel", doScroll)
            .on("DOMMouseScroll", doScroll)
            .on("click", ".btn-scroll", function() {
                var delta = $(this).data("delta");
                scrollHelper(el, delta);
            })
            .on("mousedown", ".btn-scroll", function() {
                var delta = $(this).data("delta");
                var intervalId = setInterval(function() {
                    scrollHelper(el, delta);
                }, 100);
                $(this).data("intervalId", intervalId);
            })
            .on("mouseup mouseleave", ".btn-scroll", function() {
                clearInterval($(this).data("intervalId"));
            })
            .append($leftButton)
            .append($rightButton);
    } else {
        el.children[0].style.left = "0px";

        $el.off("mousewheel").off("DOMMouseScroll");
        $el.find(".btn-scroll").remove();
    }
}

/**
 *
 * @param item - DOM node to show
 * @param elementId - element ID
 */
function horizontalScrollToShow(item, elementId) { // eslint-disable-line no-unused-vars
    var itemRect = item.getBoundingClientRect();

    var el = document.getElementById(elementId);
    var $el = $(el);
    var elRect = el.getBoundingClientRect();
    var content = $el.find(":not(.btn-scroll)")[0];
    var contentRect = content.getBoundingClientRect();
    var distance = contentRect.width - elRect.width;
    var newLeft = contentRect.left;

    var leftScroll = $el.find(".left-scroll")[0];
    var rightScroll = $el.find(".right-scroll")[0];
    var leftRect;
    var rightRect;

    leftScroll.style.display = "block";
    leftRect = leftScroll.getBoundingClientRect();
    rightScroll.style.display = "block";
    rightRect = rightScroll.getBoundingClientRect();

    if (itemRect.left < leftRect.right) {
        newLeft = contentRect.left + (leftRect.right - itemRect.left);
    } else if (itemRect.right > rightRect.left) {
        newLeft = contentRect.left - (itemRect.right - rightRect.left);
    }

    if (newLeft > 0) newLeft = 0;
    if (newLeft < -distance) newLeft = -distance;

    if (newLeft === 0) {
        leftScroll.style.display = "none";
    }

    if (newLeft == -distance) {
        rightScroll.style.display = "none";
    }

    content.style.left = newLeft + "px";
}


//previously in users.index.js, decode from HTMLEntity to String
var decodeEntities = (function() { // eslint-disable-line no-unused-vars
    // this prevents any overhead from creating the object each time
    var element = document.createElement("div");

    function decodeHTMLEntities (str) {
        if(str && typeof str === "string") {
            // strip script/html tags
            str = str.replace(/<script[^>]*>([\S\s]*?)<\/script>/gmi, "");
            str = str.replace(/<\/?\w(?:[^"'>]|"[^"]*"|'[^']*')*>/gmi, "");
            element.innerHTML = str;
            str = element.textContent;
            element.textContent = "";
        }

        return str;
    }

    return decodeHTMLEntities;
})();


function printStackTrace(e){e=e||{guess:true};var t=e.e||null,n=!!e.guess;var r=new printStackTrace.implementation,i=r.run(t);return n?r.guessAnonymousFunctions(i):i}if(typeof module!=="undefined"&&module.exports){module.exports=printStackTrace}printStackTrace.implementation=function(){};printStackTrace.implementation.prototype={run:function(e,t){e=e||this.createException();t=t||this.mode(e);if(t==="other"){return this.other(arguments.callee)}else{return this[t](e)}},createException:function(){try{this.undef()}catch(e){return e}},mode:function(e){if(e["arguments"]&&e.stack){return"chrome"}else if(e.stack&&e.sourceURL){return"safari"}else if(e.stack&&e.number){return"ie"}else if(typeof e.message==="string"&&typeof window!=="undefined"&&window.opera){if(!e.stacktrace){return"opera9"}if(e.message.indexOf("\n")>-1&&e.message.split("\n").length>e.stacktrace.split("\n").length){return"opera9"}if(!e.stack){return"opera10a"}if(e.stacktrace.indexOf("called from line")<0){return"opera10b"}return"opera11"}else if(e.stack){return"firefox"}return"other"},instrumentFunction:function(e,t,n){e=e||window;var r=e[t];e[t]=function(){n.call(this,printStackTrace().slice(4));return e[t]._instrumented.apply(this,arguments)};e[t]._instrumented=r},deinstrumentFunction:function(e,t){if(e[t].constructor===Function&&e[t]._instrumented&&e[t]._instrumented.constructor===Function){e[t]=e[t]._instrumented}},chrome:function(e){var t=(e.stack+"\n").replace(/^\S[^\(]+?[\n$]/gm,"").replace(/^\s+(at eval )?at\s+/gm,"").replace(/^([^\(]+?)([\n$])/gm,"{anonymous}()@$1$2").replace(/^Object.<anonymous>\s*\(([^\)]+)\)/gm,"{anonymous}()@$1").split("\n");t.pop();return t},safari:function(e){return e.stack.replace(/\[native code\]\n/m,"").replace(/^(?=\w+Error\:).*$\n/m,"").replace(/^@/gm,"{anonymous}()@").split("\n")},ie:function(e){var t=/^.*at (\w+) \(([^\)]+)\)$/gm;return e.stack.replace(/at Anonymous function /gm,"{anonymous}()@").replace(/^(?=\w+Error\:).*$\n/m,"").replace(t,"$1@$2").split("\n")},firefox:function(e){return e.stack.replace(/(?:\n@:0)?\s+$/m,"").replace(/^[\(@]/gm,"{anonymous}()@").split("\n")},opera11:function(e){var t="{anonymous}",n=/^.*line (\d+), column (\d+)(?: in (.+))? in (\S+):$/;var r=e.stacktrace.split("\n"),i=[];for(var s=0,o=r.length;s<o;s+=2){var u=n.exec(r[s]);if(u){var a=u[4]+":"+u[1]+":"+u[2];var f=u[3]||"global code";f=f.replace(/<anonymous function: (\S+)>/,"$1").replace(/<anonymous function>/,t);i.push(f+"@"+a+" -- "+r[s+1].replace(/^\s+/,""))}}return i},opera10b:function(e){var t=/^(.*)@(.+):(\d+)$/;var n=e.stacktrace.split("\n"),r=[];for(var i=0,s=n.length;i<s;i++){var o=t.exec(n[i]);if(o){var u=o[1]?o[1]+"()":"global code";r.push(u+"@"+o[2]+":"+o[3])}}return r},opera10a:function(e){var t="{anonymous}",n=/Line (\d+).*script (?:in )?(\S+)(?:: In function (\S+))?$/i;var r=e.stacktrace.split("\n"),i=[];for(var s=0,o=r.length;s<o;s+=2){var u=n.exec(r[s]);if(u){var a=u[3]||t;i.push(a+"()@"+u[2]+":"+u[1]+" -- "+r[s+1].replace(/^\s+/,""))}}return i},opera9:function(e){var t="{anonymous}",n=/Line (\d+).*script (?:in )?(\S+)/i;var r=e.message.split("\n"),i=[];for(var s=2,o=r.length;s<o;s+=2){var u=n.exec(r[s]);if(u){i.push(t+"()@"+u[2]+":"+u[1]+" -- "+r[s+1].replace(/^\s+/,""))}}return i},other:function(e){var t="{anonymous}",n=/function\s*([\w\-$]+)?\s*\(/i,r=[],i,s,o=10;while(e&&e["arguments"]&&r.length<o){i=n.test(e.toString())?RegExp.$1||t:t;s=Array.prototype.slice.call(e["arguments"]||[]);r[r.length]=i+"("+this.stringifyArguments(s)+")";e=e.caller}return r},stringifyArguments:function(e){var t=[];var n=Array.prototype.slice;for(var r=0;r<e.length;++r){var i=e[r];if(i===undefined){t[r]="undefined"}else if(i===null){t[r]="null"}else if(i.constructor){if(i.constructor===Array){if(i.length<3){t[r]="["+this.stringifyArguments(i)+"]"}else{t[r]="["+this.stringifyArguments(n.call(i,0,1))+"..."+this.stringifyArguments(n.call(i,-1))+"]"}}else if(i.constructor===Object){t[r]="#object"}else if(i.constructor===Function){t[r]="#function"}else if(i.constructor===String){t[r]="\""+i+"\""}else if(i.constructor===Number){t[r]=i}}}return t.join(",")},sourceCache:{},ajax:function(e){var t=this.createXMLHTTPObject();if(t){try{t.open("GET",e,false);t.send(null);return t.responseText}catch(n){}}return""},createXMLHTTPObject:function(){var e,t=[function(){return new XMLHttpRequest},function(){return new ActiveXObject("Msxml2.XMLHTTP")},function(){return new ActiveXObject("Msxml3.XMLHTTP")},function(){return new ActiveXObject("Microsoft.XMLHTTP")}];for(var n=0;n<t.length;n++){try{e=t[n]();this.createXMLHTTPObject=t[n];return e}catch(r){}}},isSameDomain:function(e){return typeof location!=="undefined"&&e.indexOf(location.hostname)!==-1},getSource:function(e){if(!(e in this.sourceCache)){this.sourceCache[e]=this.ajax(e).split("\n")}return this.sourceCache[e]},guessAnonymousFunctions:function(e){for(var t=0;t<e.length;++t){var n=/\{anonymous\}\(.*\)@(.*)/,r=/^(.*?)(?::(\d+))(?::(\d+))?(?: -- .+)?$/,i=e[t],s=n.exec(i);if(s){var o=r.exec(s[1]);if(o){var u=o[1],a=o[2],f=o[3]||0;if(u&&this.isSameDomain(u)&&a){var l=this.guessAnonymousFunction(u,a,f);e[t]=i.replace("{anonymous}",l)}}}}return e},guessAnonymousFunction:function(e,t,n){var r;try{r=this.findFunctionName(this.getSource(e),t)}catch(i){r="getSource failed with url: "+e+", exception: "+i.toString()}return r},findFunctionName:function(e,t){var n=/function\s+([^(]*?)\s*\(([^)]*)\)/;var r=/['"]?([$_A-Za-z][$_A-Za-z0-9]*)['"]?\s*[:=]\s*function\b/;var i=/['"]?([$_A-Za-z][$_A-Za-z0-9]*)['"]?\s*[:=]\s*(?:eval|new Function)\b/;var s="",o,u=Math.min(t,20),a,f;for(var l=0;l<u;++l){o=e[t-l-1];f=o.indexOf("//");if(f>=0){o=o.substr(0,f)}if(o){s=o+s;a=r.exec(s);if(a&&a[1]){return a[1]}a=n.exec(s);if(a&&a[1]){return a[1]}a=i.exec(s);if(a&&a[1]){return a[1]}}}return"(?)"}}


var dateFormatConverter = (function(){ // eslint-disable-line no-unused-vars

    var java_datepicker = {
        d:"d",
        dd:"dd",
        D:"o",
        DD:"oo",
        EEE:"D",
        EEEE:"DD",
        M:"m",
        MM:"mm",
        MMM:"M",
        MMMM:"MM",
        yy:"y",
        yyyy:"yy"
    };

    var convert = function(format, map){
		var conversion;
        if (!format) return false;
        if (!map) map = java_datepicker;

        var dateArr = format.split("");

        if (dateArr.length > 0) {
            var newFormat = "";
            var searchChar = dateArr[0];
            var pattern = dateArr[0];

            for (var i = 1; i < dateArr.length; i++) {
                if (dateArr[i] == searchChar) {
                    pattern += dateArr[i];
                } else {
                    conversion = map[pattern];

                    newFormat += conversion ? conversion : pattern;

                    searchChar = dateArr[i];
                    pattern = dateArr[i];
                }
            }

            // last pattern
            conversion = map[pattern];
            newFormat += conversion ? conversion : pattern;

            return newFormat;
        }

        return false;
    };

    return {
        java_datepicker: java_datepicker,
        convert : convert
    };
})();

// Convert a number to a string and add commas.  Used by the sparkline, sparkbar, win/loss & bullet mini-chart formats.
function sparklineNumberFormatter(num) // eslint-disable-line no-unused-vars
{
    // The default number formatter for the sparkline plugin has a bug in it which adds a comma to three digit
    // negative numbers (e.g., "-789" gets formatted as "-,789").  Current behaviour of this format is to keep
    // the precision of each number intact (allowing the format to have mixed precision), so our format system
    // cannot be used.  Take the number they provide and add commas.
    var rgx = /(\d+)(\d{3})/;
    var numSplit = (num + "").split(".");
    var numStr = numSplit[0];
    while (rgx.test(numStr))
    {
        numStr = numStr.replace(rgx, "$1" + "," + "$2");
    }

    if(numSplit[1]) numStr += "." + numSplit[1];

    return numStr;
}

function findDefaultAggregationRule(aggregationRules) {
    return aggregationRules.filter(function(rule) { return rule.default; })[0];
}

// Focus change listener on check-template-tokens class
function catchTokens(tokenList){
    checkElementsForToken(tokenList);
    $(".check-template-tokens").focus().change(function(){
        checkElementsForToken(tokenList);
    });
}

// All elements with `check-template-tokens` class will be checked for their value containing any tokens. If there is any token found, error would be highlighted
function checkElementsForToken(tokenList) {
    var elementTokens;
    $(".check-template-tokens").each(function(){
        var elementVal = $(this).val();

        $(this).removeClass("input-warn");
        $(this).parent().find(".ds_warn").remove();
        if(elementVal) {
            if(tokenList && tokenList.length) {
                elementTokens = getTokens(tokenList, elementVal);
            } else {
                elementTokens = findTokens(elementVal);
            }
            if(elementTokens.length) {
                var fullErrorText = $("<span class='ds_warn' style='display:block;'></span>");
                var errorMessage = $("<span>Replace </span>");
                elementTokens.forEach(function(token,index){
                    var errorQuoteBefore = $("<span>'</span>");
                    var errorQuoteAfter = $("<span>' </span>");
                    var errorToken = $("<span class='strong'></span>");
                    errorMessage.append(errorQuoteBefore).append(errorToken.text("<"+token+">")).append(errorQuoteAfter);
                });
                errorMessage.append("with your data");
                fullErrorText.append(errorMessage);
                $(this).addClass("input-warn");
                $(this).parent().append(fullErrorText);
            }
        }
    });
}

// Deriving relevant token keys from the whole list
function getTokens(tokenList, elementVal) {
    var tokenKeys = [];
    tokenList.forEach(function(token, index){
        if(elementVal.match(token.key)) {
            tokenKeys.push(token.key);
        }
    });
    return tokenKeys;
}

// Takes string as input and look for `< .. >` combinations and push the text between them to an array and return that array in the end
function findTokens(str) {
    var token;
    var re = /\<(.*?)\>/g; // RegEx for finding < >
    var tokenArray = []; // array to store tokens if found
    if(str) {
        do {
            token = re.exec(str);
            if (token) {
                tokenArray.push(token[1]);
            }
        } while (token);
    }
    return tokenArray;
}

function navigateTo(url) { // eslint-disable-line no-unused-vars
    document.location = url;
}

function removeItemFromArray(array, item) { // eslint-disable-line no-unused-vars
    var itemIndex = array.indexOf(item);

    if (itemIndex != -1) {
        return array.splice(itemIndex, 1);
    }
}

/**
 * Safari, in Private Browsing Mode, does not support localStorage API and all calls to setItem()
 * throw QuotaExceededError. We're going to detect this and just silently drop any calls to setItem
 * to avoid the app breaking functionality, without having to do a check at each usage of localStorage.
 */
function checkLocalStorageSupport() {
    if (typeof localStorage === "object") {
        try {
            localStorage.setItem("localStorage", 1);
            localStorage.removeItem("localStorage");
        } catch (e) {
            Storage.prototype._setItem = Storage.prototype.setItem;
            /* Override function to do nothing - the spacing below must be left as-is, otherwise Karma tests will fail */
            Storage.prototype.setItem = function () {};
            console.log("Local storage is disabled. Some features might not work as expected.")
        }
    }
}
;/****** c.applied_actions_pane.js *******/ 

var AppliedActionsPane = $.klass({
    containerId: "",

    $el: null,
    $sortButton: null,
    $groupButton: null,
    $filterButton: null,
    $actionListContainer: null,
    $actionList: null,

    filterButtons: null,

    rootComponent: null,

    initialize: function(config) {
        $.extend(this, config);

        var _this = this;

        this.$el = $("<div>");
        this.$sortButton = $("<button>Add Sort</button>").attr("actionId", "edit_dst_operation").addClass("auto-edit-sort-button");
        this.$groupButton = $("<button>Add Group</button>").attr("actionId", "edit_dst_operation").addClass("auto-edit-group-button");
        this.$filterButton = $("<div id='filter-button-container' style='display:inline-block;'>").addClass("auto-edit-filter-button");
        this.$actionListContainer = $("<div>");
        this.$actionList = $("<div>");

        this.$el
            .append($("<div style='margin-bottom:10px;text-align:right;'>")
                .append(this.$sortButton)
                .append(this.$groupButton)
                .append(this.$filterButton))
            .append(this.$actionListContainer.append(this.$actionList));

        $("#" + this.containerId).append(this.$el);

        this.filterButtons = new ButtonDropDown({
            containerId: "filter-button-container",
            width: "160px"
        });

        PubSub.subscribe("workspace-resize", function(evtid, args) {
            if (args[0] == "height") _this.handleResize();
        });
    },

    render: function (component) {
        if (!component) return;

        this.rootComponent = component.getDstRootComponent();
        if (!this.rootComponent) return;
        var _this = this;

        require(["/js/build/vendor.packed.js"], function( Vendor) {
            require(["/js/build/applied_actions_main.packed.js"], function (AppliedAction) {
                var actionArgs = {
                    dstRootComponent: _this.rootComponent,
                    onSave: AppliedAction.onActionSave
                };

                _this.$sortButton.data("actionArgs", $.extend({}, actionArgs, {dstOperation: {type: "sort"}}));

                _this.$groupButton.data("actionArgs", $.extend({}, actionArgs, {dstOperation: {type: "group"}}));

                _this.filterButtons.setMenuItems([
                    {
                        text: "Add Filter",
                        "default": true,
                        actionId: "edit_dst_operation",
                        actionArgs: $.extend({}, actionArgs, {dstOperation: {type: "filter"}})
                    },
                    {
                        text: "Add Condition Filter",
                        actionId: "edit_dst_operation",
                        actionArgs: $.extend({}, actionArgs, {dstOperation: {type: "filter", variation: "condition"}})
                    }
                ]);

                AppliedAction.render(_this.rootComponent, {
                    isDropdown: false,
                    container: _this.$actionList[0],
                    callback: function () {
                        _this.handleResize();
                    }
                });
            });
        });

    },

    handleResize: function() {
        var $container = $("#" + this.containerId);
        var siblingHeight;

        siblingHeight = 0;
        $container.siblings().each(function() {
            siblingHeight += $(this).outerHeight(true);
        });
        $container.height($container.parent().height() - siblingHeight);
        this.$el.height($container.height());

        siblingHeight = 0;
        this.$actionListContainer.siblings().each(function() {
            siblingHeight += $(this).outerHeight(true);
        });
        this.$actionListContainer.height(this.$actionListContainer.parent().height() - siblingHeight);

        customScrollbar(this.$actionList, this.$actionListContainer, "v");
    }
});
;/****** c.button_dropdown.js *******/ 

/**
 * config - {
 *      containerId: "",                    // (required) the id of the container
 *      width: "",                          // (optional) the width of the dropdown
 *      icon: {                             // (optional) the icon to show on the button
 *          src: "",                        // (required) the image source for the icon
 *          width: #,                       // (optional) the width of the icon
 *          height: #,                      // (optional) the height of the icon
 *          css: ""                         // (optional) css to apply to the icon
 *      },
 *      menuItems: [                        // (optional) menu items to display in the dropdown
 *          {
 *              text: "",                   // (optional) text for the item
 *              shortcut: "",               // (optional) shortcut that will execute the item action
 *              actionId: "",               // (optional) action associated with this item
 *              actionArgs: "",             // (optional) args for this item's action
 *              onclick: function() {},     // (optional) onclick event handler for the item
 *              css: "",                    // (optional) css to apply to the item
 *              id: "",                     // (optional) id for the item
 *              default: true,              // (optional) if true, display this item on the button
 *              separator: true             // (optional) if true, this item is a separator
 *          }
 *      ]
 * }
 */
var ButtonDropDown = $.klass({
    /** id for container of our controls */
    containerId : false,

    /** our jquery element */
    $el : false ,
    $mainButton : false,
    $dropdown : false,

    /** visibility flag for other querying objects */
    isVisible : false,

    /** */
    menuItems : false,

    /**
     * constructor
     */
    initialize : function(config)
    {
        $.extend(this, config);
        this.renderDom();
    },

    renderDom : function()
    {
        this.$mainButton = $("<button>").addClass("main-button");
        this.$dropdown = $("<div>").addClass("context-menu dropdown").hide();

        if (this.icon) {
            var icon = $("<img>").css({margin:"-2px 10px 0 0", "vertical-align":"middle"});

            if (this.icon.src) icon.attr("src", this.icon.src);
            if (this.icon.width) icon.attr("width", this.icon.width);
            if (this.icon.height) icon.attr("height", this.icon.height);
            if (this.icon.css) icon.addClass(this.icon.css);
        }

        var _this = this;
        this.$el = $("<div>")
            .addClass("button-dropdown")
            .append(this.$mainButton)
            .append($("<button>")
                .addClass("down-button")
                .click(function() { _this.show(); })
                .append(icon)
                .append($("<img src='/images/button-savedrop.png' width='10' height='5' style='padding-bottom:3px;'/>")))
            .append(this.$dropdown);

        $("#" + this.containerId).append(this.$el);

        if (this.menuItems) this.renderMenuItems();
    },

    renderMenuItems : function()
    {
        var $table = $("<table>");
        $table.css("width", this.width ? this.width : "200px");

        var mlen = this.menuItems.length;
        for (var i = 0; i < mlen; i ++)
        {
            var menuItem = this.menuItems[i];

            var $row = $("<tr>").addClass("menu-item");
            var $mel = $("<td>");
            var $sel = $("<td>").addClass("shortcut");

            if (menuItem.text) $mel.html(menuItem.text);
            if (menuItem.shortcut) $sel.html(menuItem.shortcut);

            if (menuItem.actionId) {
                $row.attr("actionId", menuItem.actionId);
                $mel.attr("parentIsAction", true);
                $sel.attr("parentIsAction", true);
            }
            if (menuItem.actionArgs) $row.data("actionArgs", menuItem.actionArgs);

            if (menuItem.onclick) $row.click(menuItem.onclick);
            if (menuItem.css) $row.addClass(menuItem.css);
            if (menuItem.id) $row.attr("id", menuItem.id);

            if (menuItem["default"]) {
                if (menuItem.text) this.$mainButton.text(menuItem.text);
                if (menuItem.actionId) this.$mainButton.attr("actionId", menuItem.actionId);
                if (menuItem.actionArgs) this.$mainButton.data("actionArgs", menuItem.actionArgs);
                if (menuItem.onclick) this.$mainButton.click(menuItem.onclick);
            }

            $table.append($row.append($mel).append($sel));

            if (menuItem.separator) {
                $table.append($("<tr>").append($("<td colspan='2'>").append($("<hr/>"))));
            }
        }

        if (this.$mainButton.text() == "") {
            this.$mainButton.remove();
        }

        this.$dropdown.empty().append($table);
    },

    setMenuItems: function(menuItems) {
        this.menuItems = menuItems;

        if (this.menuItems) this.renderMenuItems();
    },

    show: function()
    {
        if (this.isVisible) return;
        if (!this.$dropdown) this.renderDom();

        // attach event handler to the dashboard to watch all click events -- we remove it on hide()
        // ------------------------------------------------------------------------------------------
        var _this = this;
        setTimeout(function() {
            $(document.body).bind("click.dropdown", $.bind(_this.onClick, _this));
        }, 100);

        this.$dropdown.show();
        this.isVisible = true;

        if (this.onShow) this.onShow();
    },

    hide : function ()
    {
        if (!this.isVisible) return;

        this.$dropdown.hide();
        this.isVisible = false;
        $(document.body).unbind(".dropdown");

        if (this.onHide) this.onHide();
    },

    onClick : function()
    {
        this.hide();
    }

});
;/****** c.color_picker.js *******/ 

/**
 * config = {
 *      previewSwatch: {                            // (required) options for the main swatch
 *          $container: $(),                        // (required) element to append the swatch to
 *          selectedValue: "",                      // (required) value to initialize the swatch to
 *          style: {}                               // (optional) extra style attributes for the swatch
 *      },
 *      swatchAttr: {},                             // (optional) attributes for the overlay swatches
 *      swatchData: {},                             // (optional) data to store in the overlay swatches
 *      $target: $(),                               // (optional) {default - this.$swatch} target for overlay
 *      colourOptions: [],                          // (optional) {default - ColorPicker.Options} colours displayed in overlay
 *      defaultColour: {rgb:"",...},                // (optional) {default - ColorPicker.DefaultColor} colour to default to
 *      onColourSelect: function() {}               // (optional) function to execute when colour in overlay selected
 * }
 *
 * ColorPicker.Options = [
 *      {
 *          group:"group_name",                             // (required)
 *          rows: [                                         // (required)
 *              {
 *                  name:"row_name",                        // (required)
 *                  colours: [                              // (required)
 *                      {
 *                          rgb:"hex_colour_code",          // (required) colour displayed in swatch
 *                          theme:"CXTheme colour"          // (optional) colour used in CXTheme
 *                          bgCss:"bgcolor_class"           // (optional) class of background colour
 *                          fgCss:"fgcolor_class"           // (optional) class of foreground colour
 *                          borderCss:"bordercolor_class"   // (optional) class of border colour
 *                      }
 *                      ...
 *                  ]
 *              }
 *              ...
 *          ]
 *      }
 *      ...
 * ]
 */
var ColorPicker = $.klass({
    /* jquery element */
    $swatch: false,
    $overlay: false,
    $target: false,
    $content: false,

    /* overlay */
    swatchAttr: false,
    swatchData: false,

    /* colours */
    colourOptions: false,

    defaultColour: false,       // {rgb:"", ...}
    selectedColour: false,      // {rgb:"", ...}
    customColour: false,        // {rgb:""}

    initialize : function(config)
    {
        this.customColour = {rgb:""};

        $.extend(this, config);

        if (!this.colourOptions) this.colourOptions = ColorPicker.Options;
        if (!this.defaultColour) this.defaultColour = ColorPicker.DefaultColor;

        this.renderPreviewSwatch();
    },

    getCookies : function()
    {
        this.customColour.rgb = $.cookie("customColour") ? $.cookie("customColour") : "transparent";
    },

    setCookies : function()
    {
        $.cookie("customColour", this.customColour.rgb != "transparent" ? this.customColour.rgb : null, { path:"/" });
    },

    renderPreviewSwatch : function()
    {
        var preOpts = this.previewSwatch;

        // find the colour option that matches the selectedValue
        var currentOpt = ColorPicker.ValueToColor(preOpts.selectedValue, this.colourOptions);

        if (!currentOpt) {
            if (ColorPicker.IsValidHexColour(preOpts.selectedValue, true)) {
                currentOpt = {rgb:preOpts.selectedValue};
            } else {
                currentOpt = this.defaultColour;
            }
        }

		this.$swatch = $("<div>").addClass("color-preview-swatch")
            .css("background-color", currentOpt.rgb)
            .data("colourOpt", currentOpt)
            .click(_.bind(this.previewClick, this));

        if (preOpts.style) this.$swatch.css(preOpts.style);

        preOpts.$container.append(this.$swatch);

        if (!this.$target) this.$target = this.$swatch;
    },

    renderOverlay : function()
    {
        this.renderContent();

        var _this = this;
        this.$overlay = $.overlay(this.$target, this.$content, {
            onClose: null,
            className: "color-picker-container",
            placement: "down,left,right,up",
            shadow: {
                opacity: 0,
                onClick: function() {
                    _this.hide();
                }
            }
        });
    },

    renderContent : function()
    {
        this.$content = $("<div>");

        // iterate over groups
        for (var i = 0; i < this.colourOptions.length; i++) {
            var $group = $("<div class='color-group'>");
            var groupOpt = this.colourOptions[i];

            // iterate over rows
            for (var j = 0; j < groupOpt["rows"].length; j++) {
                var $row = $("<div class='color-row'>");
                var rowOpt = groupOpt["rows"][j];

                // show all rows, iterate over colours
                for (var k = 0; k < rowOpt["colours"].length; k++) {
                    $row.append(this.createSwatch(rowOpt["colours"][k]));
                }

                if ($row.children().size() > 0) $group.append($row);
            }

            if ($group.children().size() > 0) this.$content.append($group);
        }

        this.$content.append(this.createCustom());

        this.$content.delegate(".color-swatch", "click", _.bind(this.colourSelect, this));
    },

    createSwatch : function( colourOpt )
    {
        var $s = $("<div>").addClass("color-swatch")
            .css("background-color", colourOpt.rgb)
            .css({"border-right":0, "border-bottom":0})
            .data("colourOpt", colourOpt);

        if (colourOpt.rgb == "transparent") $s.addClass("disabled");

        if (colourOpt.rgb == this.selectedColour.rgb
            || colourOpt.theme == this.selectedColour.theme
            || colourOpt.bgCss == this.selectedColour.bgCss
            || colourOpt.fgCss == this.selectedColour.fgCss
            || colourOpt.borderCss == this.selectedColour.borderCss) {

            $s.addClass("selected");
            this.selectedColour = colourOpt;
        }

        if (colourOpt.rgb == this.defaultColour.rgb) {
            $s.addClass("default-colour");
            if (!this.selectedColour) $s.addClass("selected");
        }

        if (this.swatchAttr) $s.attr(this.swatchAttr);
        if (this.swatchData) $s.data(this.swatchData);

        return $s;
    },

    createCustom : function()
    {
        // custom colour component includes an input field and colour swatch
        // user supplies hex codes which have a maximum length of 6

        var $custom = $("<div>").css("margin-top", "8px")
            .html("Custom: <span class='quiet'>#</span>")
            .append($("<input type='text' maxlength='6' class='custom-color'>")
                .attr("value", this.customColour.rgb != "transparent" ? this.customColour.rgb.slice(1) : "")
                .keyup(_.bind(this.customChange, this)))
            .append(this.createSwatch(this.customColour).addClass("custom").css({"border-right":"", "border-bottom":""}));

        return $custom;
    },

    previewClick : function(evt) {
        evt.stopPropagation();

        this.show();
    },

    colourSelect : function(evt) {
        var $t = $(evt.target);

        // cannot select 'disabled' colours
        if ($t.hasClass("disabled")) return;

        // select the clicked colour
        $(".color-swatch").removeClass("selected");
        $t.addClass("selected");

        // update 'selectedColour' and the '$swatch' with selected colour
        this.selectedColour = $t.data("colourOpt");
        this.$swatch.css("background-color", this.selectedColour.rgb).data("colourOpt", this.selectedColour);

        // execute custom event
        if (this.onColourSelect) this.onColourSelect(evt);
    },

    customChange : function(evt) {
        var colour = $(evt.target).val();
        var $swatch = $(evt.target).next(".color-swatch");

        if (colour == "") {
            var customOpt = $swatch.data("colourOpt");

            // custom colour was cleared, select the default colour
            if (this.selectedColour.rgb == customOpt.rgb) {
                this.$content.find(".default-colour").click();
            }

            // 'disable' the custom colour swatch
            this.customColour.rgb = "transparent";
            $swatch.addClass("disabled")
                .css("background-color", this.customColour.rgb)
                .data("colourOpt", this.customColour);
        } else {
            if (ColorPicker.IsValidHexColour(colour, false)) {
                // expand the hex codes of length 3
                if (colour.length == 3) {
                    var hexArr = colour.split("");

                    colour = "";
                    for (var i = 0; i < hexArr.length; i++) {
                        colour += hexArr[i] + hexArr[i];
                    }
                }

                this.customColour.rgb = "#" + colour;
                $swatch.removeClass("disabled")
                    .css("background-color", this.customColour.rgb)
                    .data("colourOpt", this.customColour);

                // select the custom colour when it is valid
                $swatch.click();
            }
        }
    },

    show : function($target)
    {
        if ($target) this.$target = $target;

        this.selectedColour = this.$swatch.data("colourOpt");

        // change the 'customColour' cookie, if the 'selectedColour' does not exist and is a valid hex code
        var rgb = this.selectedColour.rgb;
        if (!ColorPicker.ValueToColor(rgb, this.colourOptions) && ColorPicker.IsValidHexColour(rgb, true)) {
            $.cookie("customColour", rgb, { path:"/" });
        }

        this.getCookies();

        this.renderOverlay();
        this.$overlay.show();

        // put focus on the custom colour input
        this.$content.find("input.custom-color").focus().select();
    },

    hide : function ()
    {
        this.$overlay.close();

        this.setCookies();
    },

    getValue : function(valueType) {
        var colOpt = this.$swatch.data("colourOpt");

        switch (valueType) {
            case "rgb": return colOpt.rgb;
            case "theme": return colOpt.theme ? colOpt.theme : colOpt.rgb;
            case "bgCss": return colOpt.bgCss ? colOpt.bgCss : colOpt.rgb;
            case "fgCss": return colOpt.fgCss ? colOpt.fgCss : colOpt.rgb;
            case "borderCss": return colOpt.borderCss ? colOpt.borderCss : colOpt.rgb;
            default: return colOpt.rgb;
        }
    }

});

/**
 * Valid hex colour codes:
 * - can have '#' at the beginning
 * - without '#', have length of 3 or 6
 * - without '#', contain only [0-9abcdef]
 *
 * @param hexCode
 * @param withPound
 * @return {Boolean}
 */
ColorPicker.IsValidHexColour = function(hexCode, withPound) {
    if (!hexCode) return false;

    if (withPound) {
        if (hexCode.indexOf("#") == 0) {
            hexCode = hexCode.slice(1);
        } else {
            return false;
        }
    }

    return (hexCode.length == 3 || hexCode.length == 6) && hexCode.search(/([^0-9abcdef])/gi) == -1;
};

ColorPicker.ValueToColor = function(v, colourOptions)
{
    if (!v) return false;
    if (!colourOptions) colourOptions = ColorPicker.Options;

    // iterate over groups
    for (var i = 0; i < colourOptions.length; i++) {
        var groupOpt = colourOptions[i];

        // iterate over rows
        for (var j = 0; j < groupOpt["rows"].length; j++) {
            var rowOpt = groupOpt["rows"][j];

            // iterate over colours
            for (var k = 0; k < rowOpt["colours"].length; k++) {
                var colOpt = rowOpt["colours"][k];

                if (colOpt.rgb == v || colOpt.theme == v
                    || colOpt.bgCss == v || colOpt.fgCss == v || colOpt.borderCss == v) return colOpt;
            }
        }
    }

    var customColour = $.cookie("customColour");

    if (v == customColour) return { rgb:v };

    return false;
};

ColorPicker.DefaultColor = { rgb:"#000000", theme:"cx-theme_000", bgCss:"cx-bgcolor_000", fgCss:"cx-color_000", borderCss:"cx-border_000" };

ColorPicker.Options = [
    {
        group:"colour",
        rows: [
            {
                name:"light",
                colours: [
                    { rgb:"#a9ccff", theme:"cx-theme_blue_1", bgCss:"cx-bgcolor_blue_1", fgCss:"cx-color_blue_1", borderCss:"cx-border_blue_1" },
                    { rgb:"#c5f3d2", theme:"cx-theme_green_1", bgCss:"cx-bgcolor_green_1", fgCss:"cx-color_green_1", borderCss:"cx-border_green_1" },
                    { rgb:"#e9fa61", theme:"cx-theme_yellow_green_1", bgCss:"cx-bgcolor_yellow_green_1", fgCss:"cx-color_yellow_green_1", borderCss:"cx-border_yellow_green_1" },
                    { rgb:"#fff470", theme:"cx-theme_yellow_1", bgCss:"cx-bgcolor_yellow_1", fgCss:"cx-color_yellow_1", borderCss:"cx-border_yellow_1" },
                    { rgb:"#ffcf68", theme:"cx-theme_orange_1", bgCss:"cx-bgcolor_orange_1", fgCss:"cx-color_orange_1", borderCss:"cx-border_orange_1" },
                    { rgb:"#fd91a1", theme:"cx-theme_red_1", bgCss:"cx-bgcolor_red_1", fgCss:"cx-color_red_1", borderCss:"cx-border_red_1" },
                    { rgb:"#e9a0e7", theme:"cx-theme_violet_1", bgCss:"cx-bgcolor_violet_1", fgCss:"cx-color_violet_1", borderCss:"cx-border_violet_1" },
                    { rgb:"#cfbaa3", theme:"cx-theme_brown_1", bgCss:"cx-bgcolor_brown_1", fgCss:"cx-color_brown_1", borderCss:"cx-border_brown_1" },
                    { rgb:"#dfd6c9", theme:"cx-theme_warm_gray_1", bgCss:"cx-bgcolor_warm_gray_1", fgCss:"cx-color_warm_gray_1", borderCss:"cx-border_warm_gray_1" }
                ]
            },
            {
                name:"light-medium",
                colours: [
                    { rgb:"#7cb1ff", theme:"cx-theme_blue_2", bgCss:"cx-bgcolor_blue_2", fgCss:"cx-color_blue_2", borderCss:"cx-border_blue_2" },
                    { rgb:"#99dba7", theme:"cx-theme_green_2", bgCss:"cx-bgcolor_green_2", fgCss:"cx-color_green_2", borderCss:"cx-border_green_2" },
                    { rgb:"#dcf044", theme:"cx-theme_yellow_green_2", bgCss:"cx-bgcolor_yellow_green_2", fgCss:"cx-color_yellow_green_2", borderCss:"cx-border_yellow_green_2" },
                    { rgb:"#fbee58", theme:"cx-theme_yellow_2", bgCss:"cx-bgcolor_yellow_2", fgCss:"cx-color_yellow_2", borderCss:"cx-border_yellow_2" },
                    { rgb:"#fabf40", theme:"cx-theme_orange_2", bgCss:"cx-bgcolor_orange_2", fgCss:"cx-color_orange_2", borderCss:"cx-border_orange_2" },
                    { rgb:"#f66a77", theme:"cx-theme_red_2", bgCss:"cx-bgcolor_red_2", fgCss:"cx-color_red_2", borderCss:"cx-border_red_2" },
                    { rgb:"#c478c2", theme:"cx-theme_violet_2", bgCss:"cx-bgcolor_violet_2", fgCss:"cx-color_violet_2", borderCss:"cx-border_violet_2" },
                    { rgb:"#b89876", theme:"cx-theme_brown_2", bgCss:"cx-bgcolor_brown_2", fgCss:"cx-color_brown_2", borderCss:"cx-border_brown_2" },
                    { rgb:"#beb3a8", theme:"cx-theme_warm_gray_2", bgCss:"cx-bgcolor_warm_gray_2", fgCss:"cx-color_warm_gray_2", borderCss:"cx-border_warm_gray_2" }
                ]
            },
            {
                name:"medium",
                colours: [
                    { rgb:"#5290e9", theme:"cx-theme_blue_3", bgCss:"cx-bgcolor_blue_3", fgCss:"cx-color_blue_3", borderCss:"cx-border_blue_3" },
                    { rgb:"#71b37c", theme:"cx-theme_green_3", bgCss:"cx-bgcolor_green_3", fgCss:"cx-color_green_3", borderCss:"cx-border_green_3" },
                    { rgb:"#cee237", theme:"cx-theme_yellow_green_3", bgCss:"cx-bgcolor_yellow_green_3", fgCss:"cx-color_yellow_green_3", borderCss:"cx-border_yellow_green_3" },
                    { rgb:"#efd140", theme:"cx-theme_yellow_3", bgCss:"cx-bgcolor_yellow_3", fgCss:"cx-color_yellow_3", borderCss:"cx-border_yellow_3" },
                    { rgb:"#ec932f", theme:"cx-theme_orange_3", bgCss:"cx-bgcolor_orange_3", fgCss:"cx-color_orange_3", borderCss:"cx-border_orange_3" },
                    { rgb:"#e14d57", theme:"cx-theme_red_3", bgCss:"cx-bgcolor_red_3", fgCss:"cx-color_red_3", borderCss:"cx-border_red_3" },
                    { rgb:"#965994", theme:"cx-theme_violet_3", bgCss:"cx-bgcolor_violet_3", fgCss:"cx-color_violet_3", borderCss:"cx-border_violet_3" },
                    { rgb:"#9d7952", theme:"cx-theme_brown_3", bgCss:"cx-bgcolor_brown_3", fgCss:"cx-color_brown_3", borderCss:"cx-border_brown_3" },
                    { rgb:"#9a9289", theme:"cx-theme_warm_gray_3", bgCss:"cx-bgcolor_warm_gray_3", fgCss:"cx-color_warm_gray_3", borderCss:"cx-border_warm_gray_3" }
                ]
            },
            {
                name:"medium-dark",
                colours: [
                    { rgb:"#4876d7", theme:"cx-theme_blue_4", bgCss:"cx-bgcolor_blue_4", fgCss:"cx-color_blue_4", borderCss:"cx-border_blue_4" },
                    { rgb:"#52875a", theme:"cx-theme_green_4", bgCss:"cx-bgcolor_green_4", fgCss:"cx-color_green_4", borderCss:"cx-border_green_4" },
                    { rgb:"#a3bd28", theme:"cx-theme_yellow_green_4", bgCss:"cx-bgcolor_yellow_green_4", fgCss:"cx-color_yellow_green_4", borderCss:"cx-border_yellow_green_4" },
                    { rgb:"#d2a62f", theme:"cx-theme_yellow_4", bgCss:"cx-bgcolor_yellow_4", fgCss:"cx-color_yellow_4", borderCss:"cx-border_yellow_4" },
                    { rgb:"#cd6c22", theme:"cx-theme_orange_4", bgCss:"cx-bgcolor_orange_4", fgCss:"cx-color_orange_4", borderCss:"cx-border_orange_4" },
                    { rgb:"#bb383f", theme:"cx-theme_red_4", bgCss:"cx-bgcolor_red_4", fgCss:"cx-color_red_4", borderCss:"cx-border_red_4" },
                    { rgb:"#804b7d", theme:"cx-theme_violet_4", bgCss:"cx-bgcolor_violet_4", fgCss:"cx-color_violet_4", borderCss:"cx-border_violet_4" },
                    { rgb:"#896a49", theme:"cx-theme_brown_4", bgCss:"cx-bgcolor_brown_4", fgCss:"cx-color_brown_4", borderCss:"cx-border_brown_4" },
                    { rgb:"#7e766e", theme:"cx-theme_warm_gray_4", bgCss:"cx-bgcolor_warm_gray_4", fgCss:"cx-color_warm_gray_4", borderCss:"cx-border_warm_gray_4" }
                ]
            },
            {
                name:"dark",
                colours: [
                    { rgb:"#3862bb", theme:"cx-theme_blue_5", bgCss:"cx-bgcolor_blue_5", fgCss:"cx-color_blue_5", borderCss:"cx-border_blue_5" },
                    { rgb:"#46754d", theme:"cx-theme_green_5", bgCss:"cx-bgcolor_green_5", fgCss:"cx-color_green_5", borderCss:"cx-border_green_5" },
                    { rgb:"#79911d", theme:"cx-theme_yellow_green_5", bgCss:"cx-bgcolor_yellow_green_5", fgCss:"cx-color_yellow_green_5", borderCss:"cx-border_yellow_green_5" },
                    { rgb:"#a87c22", theme:"cx-theme_yellow_5", bgCss:"cx-bgcolor_yellow_5", fgCss:"cx-color_yellow_5", borderCss:"cx-border_yellow_5" },
                    { rgb:"#a24f19", theme:"cx-theme_orange_5", bgCss:"cx-bgcolor_orange_5", fgCss:"cx-color_orange_5", borderCss:"cx-border_orange_5" },
                    { rgb:"#a93439", theme:"cx-theme_red_5", bgCss:"cx-bgcolor_red_5", fgCss:"cx-color_red_5", borderCss:"cx-border_red_5" },
                    { rgb:"#6e406a", theme:"cx-theme_violet_5", bgCss:"cx-bgcolor_violet_5", fgCss:"cx-color_violet_5", borderCss:"cx-border_violet_5" },
                    { rgb:"#7a5d3f", theme:"cx-theme_brown_5", bgCss:"cx-bgcolor_brown_5", fgCss:"cx-color_brown_5", borderCss:"cx-border_brown_5" },
                    { rgb:"#655e57", theme:"cx-theme_warm_gray_5", bgCss:"cx-bgcolor_warm_gray_5", fgCss:"cx-color_warm_gray_5", borderCss:"cx-border_warm_gray_5" }
                ]
            }
        ]
    },
    {
        group:"gray-scale",
        rows: [
            {
                name:"all",
                colours: [
                    { rgb:"#000000", theme:"cx-theme_000", bgCss:"cx-bgcolor_000", fgCss:"cx-color_000", borderCss:"cx-border_000" },
                    { rgb:"#222222", theme:"cx-theme_222", bgCss:"cx-bgcolor_222", fgCss:"cx-color_222", borderCss:"cx-border_222" },
                    { rgb:"#444444", theme:"cx-theme_444", bgCss:"cx-bgcolor_444", fgCss:"cx-color_444", borderCss:"cx-border_444" },
                    { rgb:"#666666", theme:"cx-theme_666", bgCss:"cx-bgcolor_666", fgCss:"cx-color_666", borderCss:"cx-border_666" },
                    { rgb:"#888888", theme:"cx-theme_888", bgCss:"cx-bgcolor_888", fgCss:"cx-color_888", borderCss:"cx-border_888" },
                    { rgb:"#aaaaaa", theme:"cx-theme_aaa", bgCss:"cx-bgcolor_aaa", fgCss:"cx-color_aaa", borderCss:"cx-border_aaa" },
                    { rgb:"#cccccc", theme:"cx-theme_ccc", bgCss:"cx-bgcolor_ccc", fgCss:"cx-color_ccc", borderCss:"cx-border_ccc" },
                    { rgb:"#eeeeee", theme:"cx-theme_eee", bgCss:"cx-bgcolor_eee", fgCss:"cx-color_eee", borderCss:"cx-border_eee" },
                    { rgb:"#ffffff", theme:"cx-theme_fff", bgCss:"cx-bgcolor_fff", fgCss:"cx-color_fff", borderCss:"cx-border_fff" }
                ]
            }
        ]
    }
];;/****** c.component_palette.js *******/ 

/* global KlipFactory:false, page:false, getSuggestedComponentLabel:false, global_dialogUtils:false, global_contentUtils:false */
var ComponentPalette = $.klass(Palette, {

    title: "COMPONENTS",
    titleTooltip: "Drag & drop components onto the workspace",

    initialize : function($super, config)
    {
        this.items = ComponentPalette.Items;

        $super(config);

        var _this = this;
        Workspace.RegisterDragDropHandlers("palette-item", [
            {
                region:"layout",
                drop: this._onDropComponentToPanel.bind(this)
            },
            {
                region:"klip-body",
                drop: function($target, klip, $prevSibling, evt, ui) {
                    var item = _this.getItem(ui.helper.attr("item-id"));
                    var config = _this.createComponentSchema(item);
                    var cx = KlipFactory.instance.createComponent(config);

                    cx.displayName = getSuggestedComponentLabel(klip, cx, false);

                    klip.addComponent(cx, {prev: $prevSibling});
                    cx.initializeForWorkspace(page);
                    cx.setDimensions(klip.availableWidth);

                    page.updateMenu();

                    _this.selectComponent(item, cx);

                    // evaluate formulas so data appears
                    page.evaluateFormulas(cx);
                }
            }
        ]);
    },

    _onDropComponentToPanel: function($target, klip, $prevSibling, evt, ui) {
        var _this = this;
        var panel = klip.getComponentById($target.closest(".comp").attr("id"));

        var matrixInfo = panel.getCellMatrixInfo($target);
        var currentCx = panel.getComponentAtLayoutXY(matrixInfo.x, matrixInfo.y);

        if (currentCx) {
            global_dialogUtils.confirm(global_contentUtils.buildWarningIconAndMessage("Are you sure you want to replace with a new component?"), {
                title: "Replace Component",
                okButtonText: "Replace"
            }).then(function() {
                panel.removeComponent(currentCx);

                _this._handleDropComponentToPanel($target, klip, ui, panel);
            });

            return;
        }

        this._handleDropComponentToPanel($target, klip, ui, panel);
    },

    _handleDropComponentToPanel: function($target, klip, ui, panel) {
        var matrixInfo = panel.getCellMatrixInfo($target);
        var item = this.getItem(ui.helper.attr("item-id"));
        var config = this.createComponentSchema(item);
        var cx;
        var currentLayoutHasActiveCell;
        var model;

        config.layoutConfig = {x: matrixInfo.x, y: matrixInfo.y};

        cx = KlipFactory.instance.createComponent(config);
        currentLayoutHasActiveCell = (page.activeLayout && page.activeLayout.activeCell);

        cx.displayName = getSuggestedComponentLabel(klip, cx, false);

        panel.addComponent(cx);
        cx.initializeForWorkspace(page);

        this.selectComponent(item, cx);

        if (currentLayoutHasActiveCell) {
            panel.setActiveCell(matrixInfo.x, matrixInfo.y);
            page.setActiveLayout(panel);
        }

        model = panel.getLayoutConstraintsModel( { cx: cx } );
        page.updatePropertySheet(model.props, model.groups, page.layoutPropertyForm);

        panel.invalidateAndQueue();

        // evaluate formulas so data appears
        page.evaluateFormulas(cx);
    },

    renderItems : function($super)
    {
        for (var i = 0; i < this.items.length; i++) {
            var paletteItem = this.items[i];

            var $item = $("<div>")
                .addClass("palette-item")
                .attr("item-id", paletteItem.id)
                .append($("<div>").addClass("icon").addClass("item-icon-" + paletteItem.id))
                .append($("<div>").addClass("name").text(paletteItem.name));

            Workspace.EnableDraggable($item, "palette-item",
                {
                    appendTo: "body",
                    zIndex: 100,
                    cursor: "move"
                }
            );

            this.$itemsEl.append($item);
        }
    },

    getItem : function(id)
    {
        for (var i = 0; i < this.items.length; i++) {
            if (this.items[i].id == id) {
                return this.items[i];
            }
        }

        return false;
    },

    createComponentSchema : function(item, ctx)
    {
        var config;

        if (item) {
            config = item.createComponentSchema(ctx);
        }

        return config;
    },

    selectComponent : function(item, cx)
    {
        var activeComp = cx;

        if (item) {
            if (item.selectionIdx) {
                for (var i = 0; i < item.selectionIdx.length; i++) {
                    activeComp = activeComp.components[item.selectionIdx[i]];
                    if (activeComp == undefined) {
                        activeComp = cx;
                        break;
                    }
                }
            }

            if (item.selectedTab) {
                page.selectedTab = item.selectedTab;
            }
        }

        page.invokeAction("select_component", activeComp);
    }
});

ComponentPalette.Items = [
    {
        name: "Table",
        id: "table",
        group: "components",
        selectionIdx: [0],
        selectedTab: "data",
        createComponentSchema : function(ctx) {
            return {
                type:"table",
                components:[
                    { type:"table_col", name:"Unnamed 1", index:0 },
                    { type:"table_col", name:"Unnamed 2", index:1 },
                    { type:"table_col", name:"Unnamed 3", index:2 },
                    { type:"table_col", name:"Unnamed 4", index:3 }
                ]
            };
        }
    },
    {

        name: "Bar/Line Chart",
        id: "chart_series",
        group: "components",
        selectionIdx: [0],
        selectedTab: "data",
        createComponentSchema : function(ctx) {
            return {
                type:"chart_series"
            };
        }
    },
    {
        name: "Gauge",
        id: "gauge",
        group: "components",
        selectionIdx: [0],
        selectedTab: "data",
        createComponentSchema : function(ctx) {
            return {
                type:"gauge"
            };
        }
    },
    {
        name: "Value Pair",
        id: "simple_value",
        group: "components",
        selectionIdx: [0],
        selectedTab: "data",
        createComponentSchema : function(ctx) {
            return {
                type:"simple_value"
            };
        }
    },
    {
        name: "Layout Grid",
        id:"panel_grid",
        group: "layout",
        selectedTab: "properties",
        createComponentSchema : function(ctx) {
            return {
                type: "panel_grid"
            };
        }
    },
    {
        name: "Separator",
        id: "separator",
        group: "layout",
        selectedTab: "properties",
        createComponentSchema : function(ctx) {
            return {
                type: "separator"
            };
        }
    },
    {
        name: "Label",
        id: "label",
        group: "components",
        selectedTab: "data",
        createComponentSchema : function(ctx) {
            return {
                type: "label",
                size: "1",
                wrap: true,
                autoFmt: true,
                formulas: [
                    {
                        txt: "\"My Label\"",
                        src: {
                            t: "expr",
                            v: false,
                            c: [
                                {
                                    t: "l",
                                    v: "My Label"
                                }
                            ]
                        }
                    }
                ]
            };
        }
    },
    {
        name: "Image",
        id: "image",
        group: "components",
        selectedTab: "data",
        createComponentSchema : function(ctx) {
            return {
                type:"image"
            };
        }
    },
    {
        name: "User Input Control",
        id: "input",
        group: "components",
        selectedTab: "properties",
        createComponentSchema : function(ctx) {
            return {
                type:"input"
            };
        }
    },
    {
        name: "Button",
        id: "input_button",
        group: "components",
        selectedTab: "properties",
        createComponentSchema : function(ctx) {
            return {
                type:"input_button"
            };
        }
    },
    {
        name: "Sparkline",
        id: "mini_series",
        group: "components",
        selectionIdx: [0],
        selectedTab: "data",
        createComponentSchema : function(ctx) {
            return {
                type:"mini_series"
            };
        }
    },
    {
        name: "Pie Chart",
        id: "chart_pie",
        group: "components",
        selectionIdx: [0],
        selectedTab: "data",
        createComponentSchema : function(ctx) {
            return {
                type:"chart_pie"
            };
        }
    },
    {
        name: "Pictograph",
        id: "pictograph",
        group: "components",
        createComponentSchema : function(ctx) {
            return {
                type:"pictograph"
            };
        }
    },
    {
        name: "Funnel Chart",
        id: "chart_funnel",
        group: "components",
        selectionIdx: [0],
        selectedTab: "data",
        createComponentSchema : function(ctx) {
            return {
                type:"chart_funnel"
            };
        }
    },
    {
        name: "Scatter/Bubble Chart",
        id: "chart_scatter",
        group: "components",
        selectionIdx: [0, 0],
        selectedTab: "data",
        createComponentSchema : function(ctx) {
            return {
                type:"chart_scatter"
            };
        }
    },
    {
        name: "Map",
        id: "tile_map",
        group: "components",
        selectedTab: "properties",
        createComponentSchema : function(ctx) {
            return {
                type:"tile_map"
            };
        }
    },
    {
        name: "News",
        id: "news_reader",
        group: "components",
        selectionIdx: [0],
        selectedTab: "data",
        createComponentSchema : function(ctx) {
            return {
                type:"news_reader"
            };
        }
    },

    {
        name: "Inline Frame",
        id: "iframe",
        group: "components",
        selectedTab: "data",
        createComponentSchema : function(ctx) {
            return {
                type:"iframe"
            };
        }
    },
    {
        name: "HTML Template",
        id: "html_tpl2",
        group: "components",
        selectedTab: "properties",
        createComponentSchema : function(ctx) {
            return {
                type:"html_tpl2",
                tpl: "<h4 style=\"padding-bottom:4px\">HTML Placeholder</h4>\r\n<p>Replace this HTML with your own. You can inject data into it by defining one or more series and using the template language to reference the series.</p>"
            };
        }
    }
];
;/****** c.context_menu.js *******/ 

/* global DX:false*/
/**
 * config = {
 *      menuId:"",                      // (required) id of menu
 *      width:"",                       // (recommended) width of menu
 *      minWidth:"",                    // (optional) the min width of menu
 *      maxHeight:"",                   // (optional) the max height of menu
 *      css:"",                         // (optional) css to apply to the menu
 *      selectedCss:"",                 // (optional) css for selected element
 *      showSelectionIcon: false,		// (optional) true if the selection icon should be shown (radio)
 *      keepMenuOpen: false             // (optional) will keep the menu open on item click, but close otherwise
 *      menuItems: [                    // (required - can be set later) items in the menu
 *          {
 *              id:"",                  // (required) id of item
 *              separator:true,         // (optional) true if this item is a separator
 *              text:"",                // (optional) text displayed in item
 *              css:"",                 // (optional) css to apply to the item
 *              isSelected:true,        // (optional) true if this item should be marked as selected
 *              disabled:true,          // (optional) true if this item is disabled
 *              href:""                 // (optional) link for item
 *              actionId:"",            // (optional) action to performance when item is clicked
 *              actionArgs:{},          // (optional) args to pass to the action
 *              iconCss: "",            // (optional) css to add an icon to the menu item
 *              iconTooltip: "",        // (optional) tooltip for the icon
 *              iconPosition:"",        // (optional) set 'right' or 'left' (default) for icon positioning
 *              iconActionId:"",        // (optional) action to perform when icon is clicked
 *              iconActionArgs:{},      // (optional) args to pass to the icon action
 *				keyboardShortcut:				// (optional) keyboard shortcut to be shown in quiet text on the right
 *              submenu: {              // (optional) this item has a sub menu
 *                  width:"",           // (optional) width of the sub menu,
 *                  showSelectionIcon: true,        // (optional) true if the selection icon should be shown
 *                  menuItems: []       // (required) items of sub menu
 *              }
 *          },
 *          ...
 *      ],
 *      enabledItems: [],               // (optional) list of ids of enabled items
 *      beforeShow: function() {},      // (optional) function called before showing the menu
 *      onHide: function() {}           // (optional) function called when menu is hidden
 * }
 */
var ContextMenu = $.klass({

	/** our jquery element */
	$el : false ,

	/** visibility flag for other querying objects */
	isVisible : false,

	/** configurable properties */
	menuItems : false,
	menuId : false,
    width : false,
    maxHeight : false,
    css : false,
    selectedCss : false,
    keepMenuOpen: false,

    target : false,
	ctx : false,

	/**
	 * constructor
	 */
	initialize : function(config) {
		$.extend(this, config);
		this.renderDom();

        this.$el.on("mouseenter", ".menu-item", _.bind(this.openSubmenu, this));
    },

	/**
	 * we use a single instance of the menu for all klips, and only render it on demand.
	 * when the menu is displayed, the subject Klip is attached to the menu item's data scope
	 * so that menu actions know which klip to target.
	 */
	renderDom : function() {
		this.$el = $("<div>")
			.addClass("context-menu")
			.attr("id", this.menuId)
			.hide();

        if (this.width) this.$el.css("width", this.width);
        if (this.minWidth) this.$el.css("min-width", this.minWidth);
        if (this.maxHeight) this.$el.css({overflow:"auto", "max-height":this.maxHeight});
        if (this.css) this.$el.addClass(this.css);

        $(document.body).append(this.$el);

		if (this.menuItems) {
            this.renderMenuItems(this.$el, this.menuItems, this.menuConfig);
            this.cleanSeparators();
        }

		// for some reason chrome won't display the element on the first click unless it
		// is positioned first..
		this.$el.position({my:"top",at:"bottom",of:document.body});
	},

	setMenuItems : function(items) {
		this.menuItems = items;

        this.resetMenu();
    },

    setMenuConfig : function(menuConfig) {
      this.menuConfig = menuConfig;
    },

    setEnabledItems : function(enabledItems) {
        this.enabledItems = enabledItems;

        this.resetMenu();
    },

    resetMenu : function() {
        this.$el.empty();

        this.renderMenuItems(this.$el, this.menuItems, this.menuConfig);
        this.cleanSeparators();
    },

	renderMenuItems : function($menu, items, menuConfig) {
        var i;
        var mlen = items.length;
        var menuItem;
        var hasLeftIcons = false;
        var hasRightIcons = false;
        var $sep;
        var $mel;
        var $icon;
        var $submenu;

        for (i = 0; i < mlen; i++) {
            menuItem = items[i];
            if (menuItem.iconCss) {
                hasLeftIcons = !menuItem.iconPosition || menuItem.iconPosition == "left";
                hasRightIcons = menuItem.iconPosition == "right";
            }

            if (hasLeftIcons && hasRightIcons) {
                break;
            }
        }

		for (i = 0; i < mlen; i ++) {
			menuItem = items[i];

            if (menuItem.separator) {
                $sep = $("<hr/>").addClass("menu-separator").appendTo($menu);

                if (menuItem.id) $sep.attr("id", menuItem.id);
            } else {
                if (!this.enabledItems || (this.enabledItems && _.indexOf(this.enabledItems, menuItem.id) != -1)) {
                    $mel = $("<div>").addClass("menu-item");

                    if (menuItem.text) $mel.text(menuItem.text);
                    if (menuItem.id) $mel.attr("id", menuItem.id);
                    if (menuItem.css) $mel.addClass(menuItem.css);
                    if (menuItem.isSelected) $mel.addClass(this.selectedCss);

                    if (menuItem.iconCss) {
                        $icon = $("<span class='context-menu-item-icon'>").addClass(menuItem.iconCss);

                        if (menuItem.iconTooltip) {
                            $icon.attr("title", menuItem.iconTooltip);
                        }

                        if (!menuItem.disabled && menuItem.actionId) {
                            $icon.attr("parentIsAction", true);
                        }
                    }

                    if (hasLeftIcons) {
                        $mel.addClass("include-left-icon");
                        if (menuItem.iconCss) {
                            $mel.prepend($icon);
                        }
                    }

                    if (hasRightIcons) {
                        $mel.addClass("include-right-icon");
                        if (menuItem.iconCss) {
                            $mel.append($icon);
                        }
                    }

                    if (menuConfig && menuConfig.showSelectionIcon) {
                        $mel.addClass("show-selection-icon");
                        if (menuItem.isSelected) {
                            $mel.prepend($("<span class='context-menu-item-selected-icon'>"));
                        }
                    }

                    if (menuItem.disabled) {
                        $mel.addClass("disabled")
                            .appendTo($menu);
                    } else {
                        if (menuItem.href) {
                            $("<a href='" + menuItem.href + "'>")
                                .append($mel)
                                .appendTo($menu);
                        } else {
                            $mel.appendTo($menu);

                            if (menuItem.onClick) {
                                $mel.on("click", menuItem.onClick);
                            } else {

                                    if(menuItem.iconActionId && menuItem.iconActionArgs && $mel.children("span")){
                                        if (menuItem.iconActionId) $mel.children("span.context-menu-item-icon").attr("actionId", menuItem.iconActionId);
                                        if (menuItem.iconActionArgs) $mel.children("span.context-menu-item-icon").data("actionArgs", menuItem.iconActionArgs);
                                    }

                                    if (menuItem.actionId) $mel.attr("actionId", menuItem.actionId);
                                    if (menuItem.actionArgs) $mel.data("actionArgs", menuItem.actionArgs);

                            }

                            if (menuItem.submenu) {
                                $mel.addClass("parent-menu");

                                $submenu = $("<div>")
                                    .addClass("context-menu")
                                    .attr("id", menuItem.id + "-submenu")
                                    .appendTo($mel)
                                    .hide();

                                if (menuItem.submenu.width) $submenu.css("width", menuItem.submenu.width);

                                this.renderMenuItems($submenu, menuItem.submenu.menuItems, menuItem.submenu);
                            }
                        }
                    }

					if(menuItem.keyboardShortcut) {
						$mel.append($("<span class='keyboard-shortcut'>").append(menuItem.keyboardShortcut));
					}
                }
            }
        }
	},

    cleanSeparators : function() {
        var separators = this.$el.find(".menu-separator");
        var i;
        var $sep;

        for (i = 0; i < separators.length; i++) {
            $sep = $(separators[i]);

            if ($sep.prev().length == 0 || $sep.next().length == 0) {                           // first or last item
                $sep.remove();
            } else if ($sep.next(".menu-item").length == 0 && $sep.next("a").length == 0) {     // more than 1 separator in a row
                $sep.remove();
            }
        }
    },

    deselectMenuItems : function () {
        if(this.selectedCss){
            this.$el.find("." + this.selectedCss).removeClass(this.selectedCss);
        }
    },

    openSubmenu : function(evt) {
        var $target = $(evt.target).closest(".menu-item");
        var $contextMenu = $target.closest(".context-menu");
        var $submenu;

        $contextMenu.find(".context-menu").hide();
        $contextMenu.find(".parent-menu.active").removeClass("active");

        if ($target.hasClass("parent-menu")) {
            $target.addClass("active");

            $submenu = $target.children(".context-menu");

            $submenu.show();
            $submenu.position({my:"left+2 top-5", at:"right top", of:$target});
        }
    },

	/**
	 * displays the menu relative to 'target'
	 * @param target
     * @param ctx - arbitrary object to attach to menu item action callbacks
     *      {
     *          actionArgs:{},
     *          corner:"",
     *          offset_x:#,
     *          offset_y:#,
     *          collision:""
     *      }
	 */
	show: function(target, ctx) {
        var _this = this;
        var myCorner;
        var cornerAlignment;
        var collision;
        var offsetX;
        var offsetY;

        if (this.isVisible) return;
		if (!this.$el) this.renderDom();

        this.target = target;
        this.ctx = ctx;

		if (this.beforeShow) this.beforeShow(target, ctx);

		// whenever the menu is show, we attach the contextObject to each menu item's data scope
		// so that they can be passed to the associated actions
		// -----------------------------------------------------------------------------
		if (ctx && ctx.actionArgs) this.$el.find(".menu-item").data("actionArgs", ctx.actionArgs);

		// attach event handler to the dashboard to watch all click events -- we remove it on hide()
		// ------------------------------------------------------------------------------------------
        setTimeout(function() {
            $(document.body)
                .bind("click.context-menu ", $.bind(_this.onClick, _this))
                .bind("contextmenu.context-menu", $.bind(_this.onRightClick, _this));
        }, 100);

        myCorner = (ctx && ctx.corner ? ctx.corner : "right top");
        cornerAlignment = myCorner.split(" ");
        collision = ctx && ctx.collision || "flip";

        offsetX = (ctx && ctx.offset_x ? ctx.offset_x : 0);
        offsetY = (ctx && ctx.offset_y ? ctx.offset_y : 0);

        if(/msie 8/i.test(navigator.userAgent) && !/opera/i.test(navigator.userAgent)) {
            // Add extra vertical offset for IE8
            offsetY += 9;
        }

        this.$el.show();
		this.$el.position({
            my:cornerAlignment[0] + (offsetX >= 0 ? "+" : "") + offsetX + " " + cornerAlignment[1] + (offsetY >= 0 ? "+" : "") + offsetY,
            at:"right bottom",
            of:target,
            collision:collision
        });

		this.isVisible = true;

		if (this.onShow) this.onShow(target, ctx);
	},

	/**
	 * hides the menu
	 */
	hide : function() {
		if (!this.isVisible) return;

        this.$el.find(".context-menu").hide();
        this.$el.find(".parent-menu.active").removeClass("active");

        this.$el.hide();
		this.isVisible = false;
		$(document.body).unbind(".context-menu");

		if (this.onHide) this.onHide(this.target, this.ctx);
	},

	/**
	 * event handler to close the menu when the user's mouse clicks anywhere,
	 * other than the menu.
	 * @param evt
	 */
	onClick : function(evt) {
        this.clickHandler(evt);
	},

    onRightClick: function(evt) {
        evt.preventDefault();
        this.clickHandler(evt);
    },

    clickHandler: function(evt) {
        var $target = $(evt.target);
        var $menuItem = $target.closest(".menu-item");
        var keepMenuOpen = this.keepMenuOpen && ($target.is(".menu-item") || $target.is(".context-menu-item-icon"));

        if (keepMenuOpen || $menuItem && ($menuItem.is(".parent-menu") || $menuItem.is(".disabled"))) {
            return;
        }

        this.hide();
    },

    /**
     * set the width of the context menu
     * @param width
     */
    setWidth: function(width) {
        if (this.width == width) return;

        this.width = width;
        this.$el.css("width", this.width);
    }

});

ContextMenu.getSortSubmenuItems = function(){
    var SUBMENU_ITEMS_FOR_SORT_BY_TYPE = {};

    SUBMENU_ITEMS_FOR_SORT_BY_TYPE[DX.ModelType.TEXT] = [
        { id:"menuitem-sort-text-none", text:"No Sort", sortDirection:0, iconCss:"icon-sort-text-none" },
        { id:"menuitem-sort-text-asc", text:"A to Z", sortDirection:1, iconCss:"icon-sort-text-asc" },
        { id:"menuitem-sort-text-desc", text:"Z to A", sortDirection:2, iconCss:"icon-sort-text-desc" }
    ];

    SUBMENU_ITEMS_FOR_SORT_BY_TYPE[DX.ModelType.NUMBER] = [
        { id:"menuitem-sort-num-none", text:"No Sort", sortDirection:0, iconCss:"icon-sort-num-none" },
        { id:"menuitem-sort-num-asc", text:"Lowest to Highest", sortDirection:1, iconCss:"icon-sort-num-asc" },
        { id:"menuitem-sort-num-desc", text:"Highest to Lowest", sortDirection:2, iconCss:"icon-sort-num-desc" }
    ];

    SUBMENU_ITEMS_FOR_SORT_BY_TYPE[DX.ModelType.DATE] = [
        { id:"menuitem-sort-date-none", text:"No Sort", sortDirection:0, iconCss:"icon-sort-date-none" },
        { id:"menuitem-sort-date-asc", text:"Oldest to Newest", sortDirection:1, iconCss:"icon-sort-date-asc" },
        { id:"menuitem-sort-date-desc", text:"Newest to Oldest", sortDirection:2, iconCss:"icon-sort-date-desc" }
    ];
     return SUBMENU_ITEMS_FOR_SORT_BY_TYPE;
};
;/****** c.control_palette.js *******/ 

var ControlPalette = $.klass(Palette, {

    title: "CONTROLS",

    initialize : function($super, config)
    {
        $super(config);
    },

    renderItems : function($super)
    {
        if (!this.items || this.items.length == 0) {
            this.$itemsEl.append($("<div class='no-items'>").text("There are no controls for the currently selected component."));
        } else {
            for (var i = 0; i < this.items.length; i++) {
                var item = this.items[i];

                var $itemEl = $("<div class='palette-item'>")
                    .append($("<div>").addClass("name").text(item.name));

                var controls = item.controls;
                $itemEl.height(19 * controls.length + 2 * (controls.length - 1));

                var $block = $("<div class='control-block'>").appendTo($itemEl);

                for (var j = 0; j < controls.length; j++) {
                    var cRow = controls[j];
                    var $row = $("<div class='control-row'>").appendTo($block);

                    for (var k = 0; k < cRow.length; k++) {
                        var c = cRow[k];

                        var $control = $("<div class='control'>").addClass(c.type);

                        if (c.disabled) {
                            $control.addClass("disabled");
                        } else {
                            $control.attr("actionId", c.actionId).data("actionArgs", c.actionArgs);
                        }

                        $row.append($control);
                    }
                }

                this.$itemsEl.append($itemEl);
            }
        }
    }

});
;/****** c.dashboard.js *******/ 

/* global KlipFactory:false, KF: false, DX: false, page:false, dashboard:false,
    ContextMenu:false, async:false, horizontalScrollToShow:false, statusMessage:false, removeItemFromArray:false,
    AnnotationSystem:false, DsWarningSystem:false */
Dashboard = $.klass({

	/** our jQuery DOM element */
	dashboardEl : false,
	layoutEl: false,

	/** collection of klips in this dashboard */
	klips : [],

	/** renders the layout markup */
	layoutManager : false,

    /** true if klips can be sorted */
    klipsSortable: false,

	/** toolbar object */
	toolbar : {tabs:[], active:false, tabsEl:false, panelsEl:false },


	tooltipHandler : false,

	isTvMode: false,

	/** context menu */
	klipContextMenu : false,
	tabContextMenu : false,


//	fullscreenTabSpeed : 20000,
//	fullscreenTabSpeed : 20000,

	/** list of all user-scoped properties */
	dashboardProps : false,
	/** properties that are queued to be sent to the server for saving **/
	queuedDashboardProps : false,

    numKlipsBusy : 0,

    renderingComponentList : {},

	/**
	 * a list of tabs, keyed by the tab ID.  each pair-value is an object that has the following
	 * properties:
	 *		 'id' - the tab id
	 *		 'name' - the display name of the tab
	 *		 'isSelected' - true if the tab is selected
	 *		 'klips' - a list of klips in the tabs
	 *		 'layout' - instance of DashboardLayout
	 *		 'locked' - editing is disabled
     *		 'menu' - menu items for this tab
	 */
	tabs : [],

    playlist : {},

	/** the current active/selected tab object */
	activeTab : false,

    /** min number of tabs before the All Tabs button appears */
    tabListShowMin : 10,

    /** true if the dashboard tabs can be scrolled */
    tabsCanHorizontalScroll : false,

	/**
	 * @param config
	 *	- workspaceMode if true creates minimal dashboard that will integrate with workspace editor
	 *
	 */
	initialize: function(config) {

        var widgetName = KF.company.get("brand", "widgetName");

	    this.tabHistory = [];
		var configDefaults = {};
		var _this = this;

		this.config = $.extend({}, configDefaults, config);


		this.layouts = {
			"100" : new DashboardLayout({ dashboard:this, id:"100", layoutEl: $("#layout-100") }),
			"60_30" : new DashboardLayout({ dashboard:this, id:"60_30", layoutEl: $("#layout-60_30") }),
			"30_30_30" : new DashboardLayout({ dashboard:this, id:"30_30_30", layoutEl: $("#layout-30_30_30") }),
			"30_60" : new DashboardLayout({ dashboard:this, id:"30_60",layoutEl: $("#layout-30_60") }),
			"50_50" : new DashboardLayout({ dashboard:this, id:"50_50",layoutEl: $("#layout-50_50") }),
            "100_60_30" : new DashboardLayout({ dashboard:this, id:"100_60_30", layoutEl: $("#layout-100_60_30") }),
            "100_30_30_30" : new DashboardLayout({ dashboard:this, id:"100_30_30_30", layoutEl: $("#layout-100_30_30_30") }),
            "100_30_60" :  new DashboardLayout({ dashboard:this, id:"100_30_60",layoutEl: $("#layout-100_30_60") }),
            "100_50_50" :  new DashboardLayout({ dashboard:this, id:"100_50_50",layoutEl: $("#layout-100_50_50") }),
            "60_30_100" : new DashboardLayout({ dashboard:this, id:"60_30_100", layoutEl: $("#layout-60_30_100") }),
            "30_30_30_100" : new DashboardLayout({ dashboard:this, id:"30_30_30_100", layoutEl: $("#layout-30_30_30_100") }),
            "30_60_100" :  new DashboardLayout({ dashboard:this, id:"30_60_100",layoutEl: $("#layout-30_60_100") }),
            "50_50_100" :  new DashboardLayout({ dashboard:this, id:"50_50_100",layoutEl: $("#layout-50_50_100") }),
            "100_60_30_100" : new DashboardLayout({ dashboard:this, id:"100_60_30_100", layoutEl: $("#layout-100_60_30_100") }),
            "100_30_30_30_100" : new DashboardLayout({ dashboard:this, id:"100_30_30_30_100", layoutEl: $("#layout-100_30_30_30_100") }),
            "100_30_60_100" :  new DashboardLayout({ dashboard:this, id:"100_30_60_100",layoutEl: $("#layout-100_30_60_100") }),
            "100_50_50_100" :  new DashboardLayout({ dashboard:this, id:"100_50_50_100",layoutEl: $("#layout-100_50_50_100") }),
			"bally" : new DashboardLayout({ dashboard:this, id:"bally",layoutEl: $("#layout-bally") }),
			"grid" : new DashboardGridLayout({ dashboard:this, id:"grid" , layoutEl: $("#layout-grid") })
		};

		// setup for user property handling
		// ---------------------------------
		this.dashboardProps = [];
		this.queuedDashboardProps = [];
		this.sendUserProps = _.debounce(  _.bind(this.sendUserPropsNow,this) , 350 );

        if (!this.config.workspaceMode || this.isPublished()) {
            // attach window resize handler that only fires 200ms after a bunch of resize events
            this.onWindowResize = _.debounce( _.bind(this.onWindowResize, this), 200 );
            $(window).resize(function(evt) {
                _this.onWindowResize(evt);
            });
        }

		if (!this.config.workspaceMode)
		{
			// attach a refresh timer to prevent memory leaks from becoming egregious
			window.setInterval(function()
			{
                if (_this.activeTab) window.location = getLocationOrigin() + "/dashboard#tab-" + _this.activeTab.master;
                if (dashboard.isTvMode) {
                    window.location.reload();
				}
			}, 1000 * 60 * 60 * 2);

			this.serializeTabState = _.debounce( _.bind(this.serializeTabState,this), 200 );

            if (KF.user.hasPermission("dashboard.annotation.view")) {
                this.annotationSystem = new AnnotationSystem({ dashboard: this });
            }

			this.warningSystem = new DsWarningSystem({ dashboard: this });

            // generate the general menu model that depends on permissions and settings
            this.generateBaseMenuModel();

			this.klipContextMenu = new ContextMenu({
                menuId:"klip-menu-config",
                width:"180px",
                css:"noprint",
                menuItems: [
                    { text:"Connect your data...", actionId:"configure_klip", id:"menuitem-configure_klip" },
                    { text:"Reconfigure...", actionId:"configure_klip", id:"menuitem-reconfigure_klip" },
                    { separator: true, id:"separator-configure_klip" },
                    { text:"Edit...", actionId:"edit_klip", id:"menuitem-edit_klip" },
                    { separator: true, id:"separator-edit_klip" },
                    { text:"View error(s)", actionId:"view_errors_klip", id:"menuitem-view_errors_klip", css:"loud"},
                    { separator: true, id:"separator-view_errors_klip" },
                    { text:"Share", id:"menuitem-share_klip", submenu:
                        {
                            width:"170px",
                            menuItems: [
                                { text: "Share with groups...", actionId:"share_klip", id:"menuitem-share_groups_klip" },
                                { text: "Share " + widgetName + " with Slack...", actionId:"share_slack_klip", id:"menuitem-share_slack_klip" },
                                { text: "Email " + widgetName + "...", actionId:"email_klip", id:"menuitem-email_klip" },
                                { text: "Embed " + widgetName + "...", actionId:"embed_klip", id:"menuitem-embed_klip" }
                            ]
                        }
                    },
                    { text:"Download as", id:"menuitem-download_klip", submenu:
                        {
                            width:"186px",
                            menuItems: [
                                { text:"PDF...", actionId:"download_pdf_klip", id:"menuitem-download_pdf_klip" },
                                { text:"Image...", actionId:"download_image_klip", id:"menuitem-download_image_klip" },
                                { text:"CSV / Excel (data only)", actionId:"download_excel_klip", id:"menuitem-download_excel_klip" }
                            ]
                        }
                    },
                    { separator: true, id:"separator-export_klip" },
                    { text:"About this " + widgetName, actionId:"about_klip", id:"menuitem-about_klip" },
                    { separator: true, id:"separator-about_klip" },
                    { text:"Remove from dashboard", actionId:"remove_klip", id:"menuitem-remove_klip" }
                ],
                enabledItems: this.menuModel.klip,
                beforeShow : function(target, ctx) {
                    $(target).addClass("active");

                    if (ctx.actionArgs.menuModel) this.setEnabledItems(ctx.actionArgs.menuModel);
                },
                onHide : function(target) {
                    $(target).removeClass("active");
                }
            });

			this.tabContextMenu = new ContextMenu({
				menuId:"tab-menu-config",
                width:"200px",
                css:"noprint",
				menuItems: [
					{ text:"Rename...", actionId:"rename_tab", id:"menuitem-rename_tab" },
                    { separator: true, id:"separator-rename_tab" },
					{ text:"Share", id:"menuitem-share_tab", submenu:
                        {
                            width:"265px",
                            menuItems: [
                                { text: "Share with groups...", actionId:"share_tab", id:"menuitem-share_groups_tab" },
                                { text: "Share dashboard with Slack...", actionId:"share_slack_tab", id:"menuitem-share_slack_tab" },
                                { text: "Get internal permalink to dashboard...", actionId:"link_tab", id:"menuitem-linkto_tab" },
                                { separator: true, id:"separator-link_tab" },
                                { text: "Publish link to dashboard...", actionId:"publish_tab", id:"menuitem-publish_tab" },
                                { text: "Manage published links to dashboard", actionId:"manage_published_tabs", id:"menuitem-manage_published_tabs" },
                                { separator: true, id:"separator-publish_tab" },
                                { text: "Email dashboard...", actionId:"email_tab", id:"menuitem-email_tab" }
                            ]
                        }
                    },
                    { text:"Download as", id:"menuitem-download_tab", submenu:
                        {
                            width:"90px",
                            menuItems: [
                                { text:"PDF...", actionId:"download_pdf_tab", id:"menuitem-download_pdf_tab" },
                                { text:"Image...", actionId:"download_image_tab", id:"menuitem-download_image_tab" }
                            ]
                        }
                    },
                    { separator: true, id:"separator-about_tab" },
                    { text:"Dashboard template", id:"menuitem-dashboard_template", submenu:
                        {
                            width:"90px",
                            menuItems: [
                                { text:"Create new...", actionId:"generate_dashboard_template", id:"menuitem-generate_new_dashboard_template" },
                                { text:"Update existing...", actionId:"update_dashboard_template", id:"menuitem-update_dashboard_template" }
                            ]
                        }
                    },
                    { separator: true, id:"separator-export_tab" },
                    { text:"About this dashboard", actionId:"about_tab", id:"menuitem-about_tab" },
                    { separator: true, id:"separator-about_tab" },
                    { text:"Remove dashboard", actionId:"remove_tab", id:"menuitem-remove_tab" }
                ],
                enabledItems: this.menuModel.tab,
				beforeShow : function(target, ctx) {
                    var tab = dashboard.getTabById(ctx.actionArgs);
                    if (tab) this.setEnabledItems(tab.menu);
                }
			});

			$("#tab-config-icon").click(_.bind(this.onTabConfigClick,this)).disableSelection();

            this.tabList = new ContextMenu({
                width:"300px",
                maxHeight:"500px",
                menuId:"tab-list-menu",
                selectedCss:"active-tab-item",
                beforeShow : function(target) {
                    this.keepMenuOpen = _this.isTvMode;
                    $(target).addClass("active");
                },
                onHide : function(target) {
                    $(target).removeClass("active");
                }
            });

            $("#button-all-tabs").click(_.bind(this.onTabListClick,this)).hide();
            $("#tvAllTabs, #tvTabName").click(_.bind(this.onTabListClick,this));
            this.updateTabListMenu();


			$("#add-klip-highlight").hide();

			$("#button-add-tab")
                .click(function() {
                    // Close the Add Klip/Layout toolbar if it is open
                    _this.closeToolbar();

                    _this.openTabPanel();
                    window.scrollTo(0, 0);
                })
                .droppable({
                    accept:".klip, .item-container",
                    addClasses: false,
                    tolerance:"pointer",
                    hoverClass:"klip-hover-tab",
                    drop: function(evt, ui) {
                        _this.previousTab = _this.activeTab;

                        var klipId = $(ui["draggable"][0]).attr("id");
                        if ($(ui["draggable"][0]).hasClass("item-container")) {
                            klipId = $(ui["draggable"][0]).find(".klip").attr("id");
                            _this.klipTabDrop = true;
                        }

                        var klip = _this.getKlipById(klipId);

                        // remove old klip
                        page.invokeAction("remove_klip", {
                            klip: klip,
                            callback: function() {
                                // add new tab
                                page.invokeAction("add_tab", [null, function(tabId) {
                                    // add Klip to new tab
                                    _this.requestKlip(klip.master, tabId);
                                }]);
                            }
                        });
                    }
                });

            // configure global body click handler to catch resized image clicks so we can display an overlay
            //"table-col-img"

            $("body").click (function (evt)
            {
                var $target = $(evt.target);
                if ($target.hasClass("constrained-size"))
                {
                    var ovl = dashboard.currentImgOverlay;
                    if (ovl) ovl.close();

                    ovl = dashboard.currentImgOverlay = $.overlay(null, $("<img style='width:100%;' src="+$target.attr("src")+">"), {
                        shadow: {
                            onClick: function (){
                                dashboard.currentImgOverlay.close();
                            }
                        },
                        maxWidth: "900px"
                    });
                    ovl.show();
                }
            });


			// load playbook swipe handler if required
			// ----------------------------------------
			if ( navigator.userAgent.match(/Playbook/i) )
			{
				require(["/js/playbook.js"], function (playbook){});
			}

		}

		$("#dashboard-tabs").empty();

        if (KF.user.hasPermission("dashboard.tab")) {
            $("#dashboard-tabs").sortable({
                tolerance:"pointer",
                distance:20,
                axis: "x",
                placeholder: "dashboard-tab-placeholder",
                start: function(e, ui) {
                    ui.placeholder.width(ui.helper.outerWidth(true) - 1 /* border-right */);
                    ui.placeholder.height(ui.helper.outerHeight(true));
                },
                update : function(evt, ui) {
                    var newOrder = [];
                    $(".dashboard-tab").each(function() {
                        newOrder.push($(this).data("tab-id"));
                    });

                    if (_this.tabsCanHorizontalScroll && ui.item.hasClass("active")) {
                        horizontalScrollToShow(_this.activeTab.$el[0], "dashboard-tabs-container");
                    }

                    $.post("/dashboard/ajax_setTabOrder", {order: JSON.stringify(newOrder)}, function() {
                        var newTabs = [];
                        var i;

                        _.each(newOrder, function(id) {
                            for (i = 0; i < _this.tabs.length; i++) {
                                if (_this.tabs[i].id == id) {
                                    newTabs.push(_this.tabs[i]);
                                    _this.tabs.splice(i, 1);
                                    return;
                                }
                            }
                        });

                        _this.tabs = newTabs;

                        _this.updateTabListMenu();
                    });
                }
            });
        }


		// watch for theme changes.  when they happen we need to update the klips to repaint with proper theme colours
		// -----------------------------------------------------------------------------------------------------------
        PubSub.subscribe("theme_change",function(evtid,args){
			$.each(_this.klips, function() {
                this.handleEvent({id:"theme_changed"});
				this.update();
			});
		});

        PubSub.subscribe("layout_change", function() {
            if (_this.currentLayout) _this.currentLayout.updateRegionWidths();

            var headerPosition = $("#c-header").css("position");
            if (headerPosition == "fixed") {
                fixHeaderToTop();
            }
		});

        PubSub.subscribe("component_rendering", function(evt, args) {
            var rendering = args.status;
            var klipId = args.klipId;
            var componentId = args.componentId;

            _this.renderingComponentList[klipId+componentId] = rendering;
        });

        PubSub.subscribe("klip_updated", function(evtName, args) {
            var klip = args[0];
            if (_this.currentLayout) _this.currentLayout.updateLayout(klip);
        });

        /**
         * args = [
         *      { busy:true },      // states
         *      []                  // klip ids
         * ]
         */
        PubSub.subscribe("klip_states_changed", _.bind(this.onKlipStatesChanged, this));

        if(!this.config.imagingMode && !this.config.__TEST_MODE__) {
            // activity heartbeat :  notify the dashboard that this user/company is still 'active'
            // -----------------------------------------------------------------------------------
            setInterval(function() {
                $.post("/dashboard/ajax_dashboard_heartbeat", { companyId: KF.company.get("publicId") });
            }, 1000 * 60 * 3);
        }


        window.addEventListener("hashchange", _.bind(this.onHashChange,this), false);

        this.checkIfTabHistoryNeedsReset();

	},

    checkIfTabHistoryNeedsReset : function() {
        var m = new Date().getMinutes();
        var _this = this;
        setInterval(function(){
            var delta;
            var last = m;
            m = new Date().getMinutes();
            delta  = m > last ? m - last : 0;

            if (delta > 1) {
                _this.tabHistory = [];

                DX.Manager.instance.disconnect(true);

                dashboard.watchTabFormulas(dashboard.activeTab);
            }
        }, 1000 * 5);
    },

    onHashChange : function() {
        this.checkForPermalink(window.location.hash);
    },

    isMobile: function() {
        return !!this.config.mobile;
    },

    isWorkspace: function() {
        return !!this.config.workspaceMode && !(this.isPreview() || this.isImaging() || this.isPublished());
    },

    isPreview: function() {
        return !!this.config.previewMode;
    },

    isImaging: function() {
        return !!this.config.imagingMode;
    },

    isPublished : function() {
        return !!this.config.publishedMode;
    },

    getContextString: function() {
        if (this.isMobile()) {
            return "mobile";
        } else if (this.isPublished()) {
            return "published";
        } else if (this.isImaging()) {
            return "imaging";
        } else if (this.isPreview()) {
            return "preview";
        } else if (this.isWorkspace()) {
            return "editor";
        } else {
            return "desktop";
        }
    },

    onKlipStatesChanged: function(evtName, args) {
        var _this = this;
        var state = args.state;
        var klipIds = args.klips;

        var busy = state["busy"];
        var numKlipsBusyBefore = this.numKlipsBusy;

        klipIds.forEach(function(klipId) {
            var klip = _this.getKlipById(klipId);
            var stateSet;

            if (klip && busy !== undefined) {
                stateSet = klip.setBusy(busy);
                if (stateSet) {
                    _this.numKlipsBusy += (busy ? 1 : -1);
                }
            }
        });

        // this shouldn't happen but just in case
        if (this.numKlipsBusy < 0) {
            this.numKlipsBusy = 0;
        }

        if (numKlipsBusyBefore === 0 && this.numKlipsBusy > 0) {
            PubSub.publish("dashboard_busy", { busy: true });
        } else if (this.numKlipsBusy === 0) {
            PubSub.publish("dashboard_busy", { busy: false });
        }
    },

    /**
     * Returns a list of klips that are currently in sample data mode
     * @returns {Array}
     */
    getConfigurableKlips : function() {
        var klips = [];
        var canEditKlip;
        var masterKlipList;
        var canEditTab;

        if(this.activeTab) {
            masterKlipList = this.activeTab.klips;
            canEditTab = this.tabRights[this.activeTab.id].edit;
        }
        if (masterKlipList && canEditTab) {
            masterKlipList.forEach(function (klip) {
                canEditKlip = this.rights[klip.master].edit;
                if (klip.usesSampleData && canEditKlip) {
                    klips.push(klip);
                }
            }.bind(this));
        }
        return klips;
    },


    checkConnectYourDataVisibility : function() {
        var klipCount = this.getConfigurableKlips().length;
        var connectButton = this.toolbar.tabsEl.find("#tb-tab-connect");
        if (klipCount > 0) {
            connectButton.css("display", "inline-block");
            return true;
        } else {
            connectButton.hide();
            return false;
        }
    },

    generateBaseMenuModel : function() {
        var exportDisabled = KF.company.get("security", "exportDisabled");
        var embeds = KF.company.hasFeature("embeds");
        var downloadableReports = KF.company.hasFeature("downloadable_reports");
        var scheduledEmails = KF.company.hasFeature("scheduled_emails");

        this.menuModel = { tab:[], klip:[] };

        // Check action menu items
        // -----------------------
        if (KF.user.hasPermission("tab.edit")) this.menuModel.tab.push("menuitem-rename_tab");
        if (KF.user.hasPermission("klip.edit")) {
            this.menuModel.klip.push("menuitem-edit_klip", "menuitem-configure_klip", "menuitem-reconfigure_klip");
        }

        if (KF.user.hasPermission("dashboard.library")) {
            if (KF.user.hasPermission("tab.share")) {
                this.menuModel.tab.push("menuitem-share_groups_tab", "menuitem-linkto_tab");
            }
            if ((KF.company.hasFeature("published_dashboards") || KF.company.hasFeature("public_dashboards")) && KF.user.hasPermission("tab.publish")) {
                this.menuModel.tab.push("menuitem-publish_tab");
                this.menuModel.tab.push("menuitem-manage_published_tabs");
            }
            if (KF.user.hasPermission("klip.share")) {
                this.menuModel.klip.push("menuitem-share_groups_klip");
            }

            if (KF.user.hasPermission("klip.embed") && embeds) this.menuModel.klip.push("menuitem-embed_klip");

            this.menuModel.tab.push("menuitem-about_tab");
            this.menuModel.klip.push("menuitem-about_klip");
        }

        if (KF.company.hasFeature("generate_templates")) { //some check for dashboard template feature
            this.menuModel.tab.push("menuitem-generate_new_dashboard_template");
            this.menuModel.tab.push("menuitem-update_dashboard_template");
        }

        if (!exportDisabled && KF.user.hasPermission("tab.email") && scheduledEmails) this.menuModel.tab.push("menuitem-email_tab");
        if (!exportDisabled && KF.user.hasPermission("klip.email") && scheduledEmails) this.menuModel.klip.push("menuitem-email_klip");

        if (!exportDisabled && KF.user.hasPermission("tab.download")) {
            if (downloadableReports) {
                this.menuModel.tab.push("menuitem-download_pdf_tab", "menuitem-download_image_tab");
            }
            if (KF.company.get("slackAvailable")) {
                this.menuModel.tab.push("menuitem-share_slack_tab");
            }
        }
        if (!exportDisabled && KF.user.hasPermission("klip.download")) {
            if (downloadableReports) {
                this.menuModel.klip.push("menuitem-download_pdf_klip", "menuitem-download_image_klip");
            }
            if (KF.company.get("slackAvailable")) {
                this.menuModel.klip.push("menuitem-share_slack_klip");
            }
            this.menuModel.klip.push("menuitem-download_excel_klip");
        }

        if (KF.user.hasPermission("dashboard.tab")) this.menuModel.tab.push("menuitem-remove_tab");
        if (KF.user.hasPermission("dashboard.klip") && KF.user.hasPermission("tab.edit")) this.menuModel.klip.push("menuitem-remove_klip");


        // Check submenu top menu items
        // ----------------------------
        if (_.contains(this.menuModel.tab, "menuitem-share_groups_tab") ||
            _.contains(this.menuModel.tab, "menuitem-linkto_tab") ||
            _.contains(this.menuModel.tab, "menuitem-share_slack_tab") ||
            _.contains(this.menuModel.tab, "menuitem-publish_tab") ||
            _.contains(this.menuModel.tab, "menuitem-manage_published_tabs") ||
            _.contains(this.menuModel.tab, "menuitem-email_tab")) {
            this.menuModel.tab.push("menuitem-share_tab");
        }

        if (_.contains(this.menuModel.klip, "menuitem-share_groups_klip") ||
            _.contains(this.menuModel.klip, "menuitem-share_slack_klip") ||
            _.contains(this.menuModel.klip, "menuitem-email_klip") ||
            _.contains(this.menuModel.klip, "menuitem-embed_klip")) {
            this.menuModel.klip.push("menuitem-share_klip");
        }

        if (_.contains(this.menuModel.tab, "menuitem-download_pdf_tab") ||
            _.contains(this.menuModel.tab, "menuitem-download_image_tab")) {
            this.menuModel.tab.push("menuitem-download_tab");
        }

        if (_.contains(this.menuModel.tab, "menuitem-generate_new_dashboard_template") ||
            _.contains(this.menuModel.tab, "menuitem-update_dashboard_template")) {
            this.menuModel.tab.push("menuitem-dashboard_template");
        }

        if (_.contains(this.menuModel.klip, "menuitem-download_pdf_klip") ||
            _.contains(this.menuModel.klip, "menuitem-download_image_klip") ||
            _.contains(this.menuModel.klip, "menuitem-download_excel_klip")) {
            this.menuModel.klip.push("menuitem-download_klip");
        }
    },

    onTabConfigClick : function(evt) {
        var $target = $(evt.target);
        var offset_x = 0;
        var offset_y = 0;

        if (!$target.hasClass("tab-config-icon")) {
            $target = $target.closest(".tab-config-icon");
        }

        this.tabContextMenu.show($target, {actionArgs:$target.attr("tab"), offset_x:offset_x, offset_y:offset_y});
    },

    onTabListClick : function(evt) {
        var target = evt.target;
        var offset_x = 0;
        var offset_y = 0;

        //Adjust for tab name click, open up all tabs menu
        if ($(target).attr("id") == "tvTabName") {
            target = $("#tvAllTabs");
        }

        if (!$(target).hasClass("tab-list-button")) {
            target = $(target).closest(".tab-list-button");
        }

        if ($(target).attr("id") == "tvAllTabs") {
            offset_x = 5;
            offset_y = 5;
        }

        this.tabList.show(target, {offset_x:offset_x, offset_y:offset_y});
    },

    updateTabListMenu: function() {
        if (this.config.workspaceMode) return;

        this.tabList.setMenuItems(this.getTabListMenuItems());

        if (this.tabs.length >= this.tabListShowMin) {
            $("#button-all-tabs").show();
        } else {
            $("#button-all-tabs").hide();
        }
    },

    getTabListMenuItems: function() {
        var menuItems = [];
        var tabs = this.tabs;
        var i;
        var len;
        var tab;
        var tabId;
        var isPlaylistEmpty;
        var isDashboardVisible;

        if (this.isTvMode) {
            isPlaylistEmpty = this.isPlaylistEmpty();

            menuItems.push({
                text: isPlaylistEmpty ? "Show all" : "Hide all",
                id: "playlist-visibility-toggle",
                css: "toggle-whole-playlist",
                actionId: "toggle_playlist",
                actionArgs: {
                    visible: isPlaylistEmpty
                }
            });

            for (i = 0, len = tabs.length; i < len; i++) {
                tab = tabs[i];
                tabId = tab.id;
                isDashboardVisible = this.isDashboardVisibleInPlaylist(tabId);

                menuItems.push({
                    text: tab.name,
                    isSelected: tab.isSelected,
                    id: "menuitem-tab-" + tabId,
                    actionId: "select_tab",
                    actionArgs: {
                        tabId: tabId,
                        eventSource: "tv_tab_list_menu_item_clicked",
                        sendMixpanelEvent: true
                    },
                    iconCss: "playlist-icon" + (!isDashboardVisible ? " playlist-icon-hidden" : ""),
                    iconTooltip: this.getPlaylistIconTooltip(isDashboardVisible),
                    iconPosition: "right",
                    iconActionId: "set_tab_in_playlist",
                    iconActionArgs: {
                        tabId: tabId
                    }
                });
            }
        } else {
            for (i = 0, len = tabs.length; i < len; i++) {
                tab = tabs[i];
                tabId = tab.id;

                menuItems.push({
                    text: tab.name,
                    isSelected: tab.isSelected,
                    id: "menuitem-tab-" + tabId,
                    actionId: "select_tab",
                    actionArgs: {
                        tabId: tabId,
                        eventSource: "tab_list_menu_item_clicked",
                        sendMixpanelEvent: true
                    }
                });
            }
        }

        return menuItems;
    },

    getPlaylistIconTooltip: function(isDashboardVisible) {
        var tooltip = "";

        if (isDashboardVisible) {
            tooltip = "Hide this dashboard when in full screen mode.";
        } else {
            tooltip = "Show this dashboard when in full screen mode.";
        }

        return tooltip;
    },

    showDashboardInPlaylist: function(dashboardId) {
        this.removeDashboardFromPlaylist(dashboardId);
    },

    hideDashboardInPlaylist: function(dashboardId) {
        var hiddenDashboards = this.playlist.hiddenDashboards;
        var playlistIndex;

        if (!hiddenDashboards) return;

        playlistIndex = hiddenDashboards.indexOf(dashboardId);

        if (playlistIndex == -1) {
            hiddenDashboards.push(dashboardId);
        }
    },

    removeDashboardFromPlaylist: function(dashboardId) {
        var hiddenDashboards = this.playlist.hiddenDashboards;
        var playlistIndex;

        if (!hiddenDashboards) return;

        playlistIndex = hiddenDashboards.indexOf(dashboardId);

        if (playlistIndex != -1) {
            hiddenDashboards.splice(playlistIndex, 1);
        }
    },

    isDashboardVisibleInPlaylist: function(dashboardId) {
        if (!this.playlist.hiddenDashboards) {
            return true;
        }

        return this.playlist.hiddenDashboards.indexOf(dashboardId) == -1;
    },

    getPlaylist: function() {
        var tabs = this.tabs;
        var i, len;
        var tab;
        var dashboards = [];

        for (i = 0, len = tabs.length; i < len; i++) {
            tab = tabs[i];
            if (this.isDashboardVisibleInPlaylist(tab.id)) {
                dashboards.push(tab);
            }
        }

        return dashboards;
    },

    isPlaylistEmpty: function() {
        return (this.getPlaylist().length === 0);
    },
    
    checkAndHandleEmptyPlaylist : function(){
        var allTabsHidden = this.isPlaylistEmpty();
        var $playlistToggleLabel = $("#playlist-visibility-toggle");

        $("#tabCycleControl").toggle(!allTabsHidden);
        $("#tvCentreTabs").toggle(!allTabsHidden);
        $("#tvTabs").toggle(!allTabsHidden);

        if (allTabsHidden) {
            this.unselectActiveTab();
            this.tabList.deselectMenuItems();

            $playlistToggleLabel.html("Show all")
                .data("actionArgs", {
                    visible: true
                });

            page.invokeAction("tv_tab_cycle", { action:"stop" });
        } else {
            $playlistToggleLabel.html("Hide all")
                .data("actionArgs", {
                    visible: false
                });
        }

        this.checkForEmptyTab();
    },

    setDashboardVisibilityInPlaylist: function(dashboardId, setVisible) {
        $("#menuitem-tab-" + dashboardId + " .playlist-icon")
            .attr("title", this.getPlaylistIconTooltip(setVisible))
            .toggleClass("playlist-icon-hidden", !setVisible);

        if (setVisible) {
            this.showDashboardInPlaylist(dashboardId);
        } else {
            this.hideDashboardInPlaylist(dashboardId);
        }
    },

    getNextDashboardInPlaylist: function() {
        var playlistTabs = this.getPlaylist();
        var numTabs = playlistTabs.length;
        var activeTabIndex = -1;
        var i;
        var tab;

        // loop through dashboards and find next dashboard in playlist and next in order
        if (this.activeTab && numTabs > 1) {
            for (i = 0; i < numTabs; i++) {
                tab = playlistTabs[i];

                if (this.activeTab.id == tab.id) {
                    activeTabIndex = i;
                }

                // only accessed once the current tab is found
                if (activeTabIndex > -1) {
                    if (activeTabIndex != i){
                        return tab;
                    } else if (i + 1 == numTabs) {
                        i = -1; // return to beginning of array and continue searching
                    }
                }
            }
        }

        return false;
    },

    savePlaylist : function () {
        var playlistJSON = JSON.stringify(this.playlist);

        $.post("/settings/ajax_setUserProperty", {
            name: "dashboard.playlist",
            value: playlistJSON
        }, function() {
            page.invokeAction("tv_small_tabs");
        });
    },
	/**
	 * creates the DOM elements for the dashboard UI
	 */
	renderDom : function(targetEl)
	{
		var dashboard = $("<div>").addClass("c-dashboard");


		// create toolbar elements
		var toolbar = $("<div>").addClass("c-dashboard-toolbar noprint").hide();
		this.toolbar.el = toolbar;
		this.toolbar.tabsEl = $("<ul>").addClass("toolbar-tabs");
		this.toolbar.panelsEl = $("<div>").addClass("toolbar-panels");

		this.layoutEl = $("<div>");

		// construct DOM outline..
		dashboard
			.append(toolbar
			.append(this.toolbar.tabsEl)
			.append(this.toolbar.panelsEl))
			.append(this.layoutEl);

		this.dashboardEl = dashboard;
		$(this.containerId).append(dashboard);

		$(targetEl).append(this.dashboardEl);

		// enable drag & drop if not in workspace mode
		// -------------------------------------------
		if (!this.config.workspaceMode && !this.config.isTablet && KF.user.hasPermission("dashboard.klip"))
		{
			this.klipsSortable = true;

            $("#layout-storage .layout-region").sortable({
				cursor:"move",
				handle : ".klip-toolbar",
//				connectWith:'.layout-region',
				forceHelperSize:true,
				forcePlaceholderSize:true,
				placeholder: "db-drag-placeholder",
				tolerance : "pointer",
				update: $.bind(this.onKlipDragUpdate, this),
                start: $.bind(this.onKlipDragStart, this),
                stop: $.bind(this.onKlipDragStop, this)
			}).sortable("option","connectWith",".layout-region");
		}

	},

	/**
	 * renders the tab strip for this dashboard
	 */
	renderTabs : function()
	{
	},


    /**
     * Restore the dashboard with the given state
     *
     * @param state
     * @param tabId - tab to be selected
     * @param onComplete
     */
    restoreState : function(state, tabId, onComplete) {
        var parsedPermalink;
        var _this = this;
        var selectedTabId;
        var lastTabCookie;

        if (state.length == 0) {
            parsedPermalink = this.checkForPermalink(window.location.hash);

            if (parsedPermalink.command != "dashboardtemplate" && parsedPermalink.command != "kliptemplate") {
                PubSub.publish("initial-dashboard-loaded");
            }

            if (onComplete) onComplete();

            this.checkForEmptyTab();
            return;
        }

        // add all the tabs first -- this is fairly inexpensive and gives a more complete picture of the
        // dashboard
        _.each(state, function(item){
            var tabConfig = item.tab;

            // move charts to the end of the list so that they are rendered last (for IE's sake)
            if (item.klips) {
                item.klips.sort(function (a, b) {
                    if (a.components[0] && a.components[0].type == "chart_series") {
                        return 1;
                    } else {
                        return -1;
                    }
                });

                tabConfig = $.extend(item.tab, {klipsToLoad:item.klips});
            }

            _this.addTab(tabConfig);

            if (!selectedTabId) selectedTabId = item.tab.id;
        });

        if (tabId && this.getTabById(tabId)) {
            selectedTabId = tabId;
        } else {
            lastTabCookie = $.cookie("last-tab");
            // if the tab isn't found, clear out the cookie value
            // --------------------------------------------------
            if (!this.getTabById(lastTabCookie)) {
                $.cookie("last-tab", null);
            } else {
                selectedTabId = lastTabCookie;
            }
        }

        this.loadTabKlips(selectedTabId);

        async.until(
            function() {
                return _this.getTabById(selectedTabId).loaded;
            },
            function(cb) {
                setTimeout(cb, 50);
            },
            function() {
                parsedPermalink = _this.checkForPermalink(window.location.hash);

                // Pause the javascript execution so the browser rendering can catch up.  This prevents
                // IE 9 from using the wrong widths in updateRegionWidths().  See saas-1957.
                setTimeout(function(){
                    // important to call onComplete() inside setTimeout for IE10s benefit
                    // also, moved selectTab to here otherwise custom grid layouts don't render on initial tab
                    if (jQuery.browser.msie && jQuery.browser.version < 10) {
                        setTimeout(function() {
                            _this.selectTab(selectedTabId, { eventSource: "dashboard_restore_state" });
                            $.cookie("last-tab", selectedTabId, {path:"/"});

                            if (parsedPermalink.command != "dashboardtemplate" && parsedPermalink.command != "kliptemplate") {
                                PubSub.publish("initial-dashboard-loaded");
                            }

                            if (onComplete) onComplete(selectedTabId);
                        }, 200);
                    } else {
                        _this.selectTab(selectedTabId, { eventSource: "dashboard_restore_state" });
                        $.cookie("last-tab", selectedTabId, {path:"/"});

                        if (parsedPermalink.command != "dashboardtemplate" && parsedPermalink.command != "kliptemplate") {
                            PubSub.publish("initial-dashboard-loaded");
                        }

                        if (onComplete) onComplete(selectedTabId);
                    }
                }, 0);
            }
        );
    },

    loadTabKlips : function(tabId)
    {
        var tab = this.getTabById(tabId);

        if (!tab.loaded && !tab.loading) {
            tab.$el.addClass("loading");
            tab.loading = true;

            if (!tab.klipsToLoad) {
                var _this = this;
                $.post("/dashboard/ajax_getTabKlips", {tabId:tabId}, function(resp) {
                    var klips = [];

                    _.each(resp.klipConfigs, function(config) {
                        klips.push(JSON.parse(config.schema));
                    });

                    _this.rights = $.extend(_this.rights, resp.shareRights);

                    _this.loadKlips(tabId, klips);
                });
            } else {
                this.loadKlips(tabId, tab.klipsToLoad);
            }
        }
    },

    loadKlips : function(tabId, klips) {
        var _this = this;
        var tab = this.getTabById(tabId);

        asyncEach(
            klips,
            function(k, cb) {
                _this.addKlip(k, tabId, false, true);
                cb();
            },
            function() {
                if (_this.warningSystem) _this.warningSystem.hideOrShowWarningIcons(tab.klips);

                delete tab.klipsToLoad;
                tab.loading = false;
                tab.loaded = true;
                tab.$el.removeClass("loading");
            }
        );
    },

	/**
	 * adds a tab to the dashboard
	 * @param tabConfig object
	 *		 'id' the system id of the tab
	 *		'name' the name as displayed to the user
	 *		'state' layout state information
	 */
	addTab : function (tabConfig) {
        var _this = this;
        var tabName;

		if (!tabConfig.klips) tabConfig.klips = []; // lazy-init klips array
		if (!tabConfig.layoutType) tabConfig.layoutType = "grid"; // sensible default layout

		tabConfig.layout = this.layouts[tabConfig.layoutType];
		tabConfig.locked = true;

		if (!tabConfig["state"]) tabConfig.state = {};

        if (tabConfig.layoutType == "grid" && _.isEmpty(tabConfig.state)) {
            tabConfig.state = {
                desktop: {
                    cols: 6,
                    colWidths: [],
                    itemConfigs:[]
                }
            };
        }

        if (tabConfig.name.search("\{([^}]*)\}") != -1) tabConfig.hasDynamicTitle = true;
        if (!this.config.workspaceMode) tabConfig.menu = this.generateTabMenuModel(this.menuModel.tab, this.tabRights[tabConfig.id], tabConfig);

        this.tabs.push(tabConfig);

        tabName = this.getVariableTabName(tabConfig);

        // render the tab html
        tabConfig.$el = $("<li>")
            .addClass("dashboard-tab")
            .attr({
                title: tabName,
                "data-tab-id": tabConfig.id,
                actionId: "select_tab"
            })
            .data("actionArgs", {
                tabId: tabConfig.id,
                eventSource: "tab_clicked",
                sendMixpanelEvent: true
            })
            .appendTo($("#dashboard-tabs"));

        if (jQuery.browser.msie && jQuery.browser.version == "9.0") {
            tabConfig.$el.css("top", "2px");
        }

        tabConfig.$el.append($("<span class='dashboard-tab-name' parentIsAction='true'>").text(tabName));

        tabConfig.$el
            .droppable({
                accept:".klip, .item-container",
                addClasses: false,
                tolerance:"pointer",
                hoverClass:"klip-hover-tab",
                over: function(evt, ui) {
                    var tabId = $(this).data("tab-id");
                    var tr = _this.tabRights[tabId].edit;

                    if (!tr) {
                        $(this).removeClass("klip-hover-tab");
                        $(ui["draggable"][0]).find(".klip-toolbar").addClass("klip-no-drop");
                    } else if (tabId == _this.activeTab.id) {
                        $(this).removeClass("klip-hover-tab");
                    }
                },
                out: function(evt, ui) {
                    $(ui["draggable"][0]).find(".klip-toolbar").removeClass("klip-no-drop");
                },
                drop: function(evt, ui) {
                    var tabId;
                    var tr;
                    var klipId;
                    var klip;
                    var tab;
                    var klipLimit;
                    var message;
                    var $errorContent;
                    var $errorOverlay;
                    var widgetName = KF.company.get("brand", "widgetName");

                    $(ui["draggable"][0]).find(".klip-toolbar").removeClass("klip-no-drop");

                    tabId = $(this).data("tab-id");
                    tr = _this.tabRights[tabId].edit;

                    if (tabId == _this.activeTab.id || !tr) return;

                    _this.previousTab = _this.activeTab;

                    klipId = $(ui["draggable"][0]).attr("id");
                    if ($(ui["draggable"][0]).hasClass("item-container")) {
                        klipId = $(ui["draggable"][0]).find(".klip").attr("id");
                        _this.klipTabDrop = true;
                    }

                    klip = _this.getKlipById(klipId);
                    tab = _this.getTabById(tabId);

                        klipLimit = KF.company.get("limits", "dashboard.klips.perTab") || -1;
                        if (tab.klips.length >= klipLimit && klipLimit != -1) {
                            // Show an overlay informing the user they can't move the Klip
                            message = ", the limit for dashboards in your account.";
                            if (tab.klips.length > klipLimit) {
                                message = ", which exceeds the limit for dashboards in your account (" + klipLimit + ")";
                            }

                            $errorContent = $("<div class='report-dialog'>")
                                    .width(500)
                                    .append($("<h3 style='margin-bottom:10px;'>" + widgetName + " Limit Reached</h3>"))
                                    .append($("<div style='margin-bottom:20px;'>You already have " + tab.klips.length + " " + widgetName + (tab.klips.length == 1 ? "" : "s") + " on the target dashboard" + message + "</div>"));

                            $errorOverlay = $.overlay(null, $errorContent, {
                                shadow:{
                                    opacity:0,
                                    onClick: function(){
                                        $errorOverlay.close();
                                    }
                                },
                                footer: {
                                    controls: [
                                        {
                                            text: "OK",
                                            css: "rounded-button primary",
                                            onClick: function() {
                                                $errorOverlay.close();
                                            }
                                        }
                                    ]
                                }
                            });
                            $errorOverlay.show();
                        } else {
                            // remove old klip
                            page.invokeAction("remove_klip", {
                            klip: klip,
                            callback: function() {
                                // switch tabs
                                page.invokeAction("select_tab", {
                                    tabId: tabId,
                                    callback: function (tabId) {
                                        // add Klip to new tab
                                        _this.requestKlip(klip.master, tabId);
                                    },
                                    eventSource: "dashboard_klip_moved"
                                });
                            }
                        });
                    }
                }
            });

		this.updateTabWidths();
        this.updateTabListMenu();

        if (this.isTvMode) {
            page.invokeAction("tv_small_tabs");
            this.checkAndHandleEmptyPlaylist();
        }

		return tabConfig;
	},

    generateTabMenuModel : function(baseModel, tabRights, tab) {
        var menuModel = baseModel.slice(0);

        if (!tabRights.edit) {
            removeItemFromArray(menuModel, "menuitem-rename_tab");
            removeItemFromArray(menuModel, "menuitem-share_groups_tab");
            removeItemFromArray(menuModel, "menuitem-linkto_tab");

            if (menuModel.indexOf("menuitem-share_groups_tab") == -1 &&
                menuModel.indexOf("menuitem-linkto_tab") == -1 &&
                menuModel.indexOf("menuitem-email_tab") == -1) {
                removeItemFromArray(menuModel, "menuitem-share_tab");
            }
        }

        if (tabRights.locked) {
            removeItemFromArray(menuModel, "menuitem-remove_tab");
        }

        if (!tab.templateInstance) {
            removeItemFromArray(menuModel, "menuitem-update_dashboard_template");
        }

        return menuModel;
    },


	removeTab : function(tabId)
	{
		var tab = this.getTabById(tabId);

		var nKlips = tab.klips.length;
		while (--nKlips > -1)
		{
			var k = tab.klips[nKlips];
			this.removeKlip(k);
		}

        if(tab.$el.find("#tab-config-icon").length > 0) {
            $("#tab-config-icon").hide().appendTo(document.body);
        }
        tab.$el.remove();

        for (var i = 0; i < this.tabs.length; i++) {
            if (this.tabs[i] == tab) this.tabs.splice(i, 1);
        }

		// remove this tab from the history
		this.tabHistory = _.without( this.tabHistory , tab );

        this.removeDashboardFromPlaylist(tabId);

		if (tab == this.activeTab)
		{
			if (this.tabs.length > 0) {
                page.invokeAction("select_tab", { tabId: this.tabs[0].id, eventSource: "tab_removed" });
            } else {
                // clean up unshared klips on last custom layout
                if (this.activeTab.layout.id == "grid") {
                    this.activeTab.layout.removeAll();
                }

                this.activeTab = false;
            }
		}

        this.closeToolbar();
        this.updateTabWidths();
        this.updateTabListMenu();
	},


    /**
     * recalculates the preferred widths for the dashboard tabs.  If there are many tabs, their widths will be compressed to
     * fit in the width available.
     */
	updateTabWidths : function()
	{
		var $tabs = $("#dashboard-tabs");
        var $tabsContainer = $("#dashboard-tabs-container");
        var enableHorizontalScroll = ($tabs.width() > $tabsContainer.width());

        if (enableHorizontalScroll != this.tabsCanHorizontalScroll) {
            toggleHorizontalScroll("dashboard-tabs-container", enableHorizontalScroll);
        }

        if (enableHorizontalScroll && this.activeTab) {
            horizontalScrollToShow(this.activeTab.$el[0], "dashboard-tabs-container");
        }

        this.tabsCanHorizontalScroll = enableHorizontalScroll;
	},


	/**
	 * changes the active tab
	 * @param tabId
     * @param options
	 */
	selectTab : function(tabId, options) {
        var t;
        var tabInHistory;
        var discardedTab;
        var canRearrangeKlips;
        var $tabConfigIcon;
        var i;

        var parsedPermalink = this.parsePermalink(window.location.hash);

        // return early if we are selecting the current tab
		// -------------------------------------------------
		if (this.activeTab && this.activeTab.id == tabId && this.activeTab.isSelected) return;

        // return early if in fullscreen mode and the tab's not in the playlist
        // --------------------------------------------------------------------
        if (this.isTvMode && !this.isDashboardVisibleInPlaylist(tabId)) return;

        this.numKlipsBusy = 0;

		// serialize tab state -- if active tab was arranged it will be saved to server
		// ----------------------------------------------------------------------------
		this.serializeTabState();

		t = this.getTabById(tabId);

        if (this.activeTab) {
            this.activeTab.$el.removeClass("active");
            this.activeTab.state = this.activeTab.layout.createConfig();
            this.unselectActiveTab();
        }

		// manage tab history:  check to see if the selected tab is in tabHistory.
		// if it is, do nothing.  otherwise, push it on to the history and trim
		// the history to its max length;
		// -----------------------------------------------------------------------
		tabInHistory = this.tabInHistory(tabId);

		if ( !tabInHistory ){
			this.tabHistory.push(t);

			// watch the new tab's formulas
            this.watchTabFormulas(t);

			if ( this.tabHistory.length > 5 ){
				discardedTab = this.tabHistory.shift();
				// unwatch the discarded tab's formulas
				_.each(discardedTab.klips,function(k){
                    DX.Manager.instance.unwatchKlip(k);
				});
			}
		}


		t.$el.addClass("active");

//		t.layout.layoutState = t.state;
		this.activeTab = t;
        this.activeTab.isSelected = true;

        $("#active-tab-name").text(this.activeTab.name);

        if (t.layoutType == "grid") {
            canRearrangeKlips = KF.user.hasPermission("dashboard.klip") && (this.tabRights && this.tabRights[t.id].edit);

            this.layouts["grid"].setItemsDraggable(canRearrangeKlips);
            this.layouts["grid"].setItemsResizable(canRearrangeKlips);
        }

		this.setLayout(t.layoutType,t.state);

		if ( !this.config.workspaceMode ) {
			this.checkForEmptyTab();

            if (t.menu.length > 0) {
                $tabConfigIcon = $("#tab-config-icon")
                    .show()
                    .appendTo(t.$el)
                    .attr("tab", t.id);
            }

			this.updateTabWidths();
            this.updateTabListMenu();
			this.showEditControls(this.tabRights[t.id].edit && (KF.user.hasPermission("dashboard.tab") || KF.user.hasPermission("dashboard.klip")));
		}

		if ( t.klips && t.klips.length > 0 ) {
			for( i = 0 ; i < t.klips.length ; i++ ) {
				t.klips[i].handleEvent({ id: "parent_tab_selected" });
			}
		}

		if (!this.isPublished() && !this.isImaging()) {
            this.checkConnectYourDataVisibility();

            // Tab transitions do NOT count as tab views
            if (options && options.eventSource != "action_tv_tab_transition") {
                $.post("/dashboard/ajax_dashboardViewed", { dashboardPublicId: t.master });
            }
        }

        if (parsedPermalink && t && parsedPermalink.id !== t.master) {
            window.location.hash = "";
        }

	},

    unselectActiveTab: function() {
        if (this.activeTab) {
            this.activeTab.isSelected = false;
        }
    },


	tabInHistory : function(tid)
	{
		for( var i = 0 ; i < this.tabHistory.length ; i++){
			if ( this.tabHistory[i].id == tid) return true;
		}
		return false;
	},

    /**
     * Returns single tab with the given id
     *
     * @param id
     * @returns {*}
     */
	getTabById : function(id)
	{
		var tab;

		for (var i = 0; i < this.tabs.length; i++) {
			tab = this.tabs[i];
			if ( tab.id == id ) return tab;
		}

		return false;
	},

    /**
     * Returns all tabs with the given master id
     *
     * @param masterId
     * @param inHistory
     * @returns {Array}
     */
    getTabsByMaster : function(masterId, inHistory)
    {
        var tabs = [];

        for (var i = 0; i < this.tabs.length; i++){
            var tab = this.tabs[i];
            if ( tab.master == masterId ) {
                if (inHistory) {
                    if (this.tabInHistory(tab.id)) {
                        tabs.push(tab);
                    }
                } else {
                    tabs.push(tab);
                }
            }
        }

        return tabs;
    },


	lockTab : function(tid, isLocked)
	{
		var tab = this.getTabById(tid);
		if (!tab)
		{
			return;
		}

		tab.locked = isLocked;
		this.showEditControls(!isLocked);

	},


	showEditControls : function(show, initial)
	{
		if (show)
        {
            if (initial) {
                this.toolbar.el.slideDown(200);
            } else {
                this.toolbar.el.show();
            }
        }
        else
		{
			if (initial) {
                this.toolbar.el.slideUp(200);
            } else {
                this.toolbar.el.hide();
            }
		}

        if (this.klipsSortable) {
            $(".layout-region").sortable("option", "disabled", !show);
        }

        if (!KF.user.hasPermission("dashboard.klip")) {
            $("#kfdashboard").removeClass("unlocked");
        } else {
            $("#kfdashboard").toggleClass("unlocked", show);
        }

	},


	/**
	 *
	 */
	serializeTabState : function(callback)
	{
		var tabState = {};
		var i = this.tabs.length;
		var sendUpdate = false;
		while (--i != -1)
		{
			var tab = this.tabs[i];
			if (tab.stateIsDirty)
			{
				sendUpdate = true;
				tabState[ tab.id ] = {
					layoutType: tab.layoutType,
					state: !tab.stateIsReady ? tab.layout.createConfig() : tab.state
				};

				tab.state = tabState[tab.id].state;
				tab.stateIsDirty = false;
                tab.stateIsReady = false;
			}
		}

		if (sendUpdate)
		{
			$.ajax({
				url:"/dashboard/ajax_setTabState",
				type:"POST",
				async:false,
				data: {batch: JSON.stringify(tabState)},
				success:callback
			});
		}
	},

    watchTabFormulas : function (tab){
        var watch = [];
        _.each(tab.klips,function(k){
            watch.push({ kid: k.id, fms: k.getFormulas() });
        });

        if (watch.length > 0) {
            DX.Manager.instance.watchFormulas( watch );
        }
    },


	/**
	 * creates a klip based on the DSL and adds it to the specified tab.
	 * if the tab specified is also the active tab, the klip will be layed-out, and made
	 * visible.
	 *
	 * @param config the klip configuration object to be passed to the factory
	 * @param tabId the tab to add the klip to. if false, it adds to the active tab
	 * @param highlight whether to highlight the added klip in the dashboard
     * @param preventSerialization - if true, do no serialize the tab state
	 *
	 * @return a Klip object created from the config provided
	 */
	addKlip : function (config, tabId, highlight, preventSerialization)
    {
		var k = KlipFactory.instance.createKlip(this, config);
		this.klips.push(k);

        var hasTab = true;
		if ( !this.config.workspaceMode || this.config.imagingMode || this.isPublished() )
		{
			var targetTab;
			if (!tabId) targetTab = this.activeTab;
			else targetTab = this.getTabById(tabId);

            if (!targetTab) {
                hasTab = false;

                if (config.currentTab) k.currentTab = config.currentTab;

                k.handleEvent({ id:"added_to_dashboard" });
            } else {
                targetTab.klips.push(k);
                k.currentTab = tabId;

                if (this.activeTab) {
                    if (!tabId || tabId == this.activeTab.id)
                    {
                        this.activeTab.layout.layoutComponent(k, highlight);
                    }

                    if (!preventSerialization)
                    {
                        this.activeTab.stateIsDirty = true;
                        this.serializeTabState();
                    }
                }

                if (!this.config.workspaceMode) {
                    k.generateMenuModel(this.menuModel.klip, this.rights[k.master], this.tabRights[k.currentTab]);

                    if (!k.menuModel || k.menuModel.length == 0) {
                        if (k.toolbarEl) k.toolbarEl.find(".klip-button-config").hide();
                    }
                }

                k.handleEvent( {id:"added_to_tab",tab:targetTab} );
            }
		}

        if (this.config.workspaceMode || !hasTab) {
            k.el.find(".klip-toolbar-buttons").hide();

            if ((!this.config.imagingMode && !this.isPublished()) || !hasTab) {
                this.layoutEl.append(k.el);
                k.update();
            }
        }

		this.checkForEmptyTab();

		// Send all of the formulas to be watched at once if the klip is being added to
		// a watched tab ( eg, tab in history )
		// ------------------------------------------------------------------------------
		if ( this.tabInHistory(tabId) ){
            DX.Manager.instance.watchFormulas([{ kid: k.id, fms: k.getFormulas() }]);
		}

		return k;
	},


    /**
     *
     * @param k
     * @param updateMsg
     * @param preventSerialization
     */
	updateKlip : function(k, updateMsg, preventSerialization)
	{
        var s = JSON.parse(updateMsg.schema);

        // delete cache so Klip is populated with new data
        eachComponent(s, function(cx) {
            $.jStorage.deleteKey(cx.id);
        });

		// add the new version
		this.addKlip($.extend(s, {
            id: k.id
		}), k.currentTab, false, preventSerialization);

		// remove the old version
		this.removeKlip(k, {
            isUpdate: true,
            preventSerialization: preventSerialization
        });
	},

	highlightKlip : function (k)
	{
		$("#add-klip-highlight").stop()
			.delay(100)
			.width(k.el.width())
			.height(k.el.height())
			.position({my:"center",at:"center",of: k.el})
			.css({ opacity: 0.6 })
			.show()
			.animate({width:"+=60px", height:"+=60px", left:"-=30px", top:"-=30px", opacity: 0.0}, 700,
			"swing", function()
			{ //easeOutQuart, swing
				$("#add-klip-highlight").removeAttr("style").hide();
			});
	},

    highlightKlipRefresh : function (k)
    {
        var $highlight = k.el.find(".refresh-klip-highlight");
        if ($highlight.length == 0) {
            $highlight = $("<div>").addClass("refresh-klip-highlight").appendTo(k.el);
        }

        $highlight.stop()
            .delay(100)
            .css({
                width: k.el.width() + "px",
                height: k.el.height() + "px",
                opacity: 0.5
            }).show().animate({ opacity:0 }, 1700, "swing", function()
            { //easeOutQuart, swing
                k.el.find(".refresh-klip-highlight").hide();
            });
    },

    /**
     * Check for a hash value in the URL, which will be of the form:
     * {command}-{objectId}?{queryString}
     */
    checkForPermalink : function(hashValue) {
        var parsedPermalink = this.parsePermalink(hashValue);

        switch (parsedPermalink.command) {
            case "tab":
                this.handleTabPermalink(parsedPermalink);
                break;
            case "dashboardtemplate":
                this.handleDashboardTemplatePermalink(parsedPermalink);
                break;
            case "kliptemplate":
                this.handleKlipTemplatePermalink(parsedPermalink);
                break;
            case "klip":
            default:
                break;
        }

        return parsedPermalink;
    },


    /**
     * Using the tab id and query string in the URL hash value, either find a matching tab
     * in the dashboard and select it, or add the tab to the dashboard.
     */
    handleTabPermalink : function(parsedPermalink) {
        var reuseTab = parsedPermalink.keyword === "reuse";
        var replaceTab = parsedPermalink.keyword === "replace";
        var tab;
        var activeTabId = "";

        if (parsedPermalink.id && parsedPermalink.id.length == 32) {
            tab = this.findMatchingTab(parsedPermalink.id, parsedPermalink.queryParams, reuseTab);
            if (tab && !replaceTab) {
                page.invokeAction("select_tab", { tabId: tab.id, eventSource: "tab_permalink_clicked" });
                dashboard.addQueryParamsToTab(tab.id);
            } else {
				if (replaceTab && dashboard.activeTab) {
                    activeTabId = dashboard.activeTab.id;
				}

                page.invokeAction("add_tab", [parsedPermalink.id, function(tabId) {
                    var tabOrder = [];

                    if (!tabId) return;

                    dashboard.addQueryParamsToTab(tabId);
                    window.location.hash = "";

                    if (replaceTab && activeTabId.length > 0) {
						// Determine the new order for the dashboard tabs
                        $(".dashboard-tab").each(function() {
                            var thisTabId = $(this).data("tab-id");
                            if (thisTabId == activeTabId) {
								// Insert the newly added tab instead of the previous active tab
								tabOrder.push(tabId);
                            } else if (thisTabId != tabId) {
                                tabOrder.push(thisTabId);
                            }
                        });

						// Position the new tab to the right of the previous active tab, then remove the previous active tab
                        $(".dashboard-tab[data-tab-id=" + tabId + "]").detach().insertAfter($(".dashboard-tab[data-tab-id=" + activeTabId + "]"));
                        page.invokeAction("remove_tab", activeTabId, {});

                        $.post("/dashboard/ajax_setTabOrder", {order: JSON.stringify(tabOrder)});
                    }
                }], {});
            }
        }
    },

    handleDashboardTemplatePermalink : function(parsedPermalink) {
        this.clearPermalink();
        page.invokeAction("add_tab_from_template", {
            id: parsedPermalink.id,
            displaySuccessMessage: true,
            callback : function() {
                PubSub.publish("initial-dashboard-loaded");
            }
        });
    },

    handleKlipTemplatePermalink : function(parsedPermalink) {
        this.clearPermalink();
        page.invokeAction("add_klip_from_template", {
            id: parsedPermalink.id,
            displaySuccessMessage: true,
            callback : function() {
                PubSub.publish("initial-dashboard-loaded");
            }
        });
    },

    /**
     * Parse a hash value in the URL, which will be of the form:
     * {command}-{objectId}?{queryString}, where {command} could be of the form {command-keyword}
     */
    parsePermalink : function(hashValue) {
        var parsedPermalink = {
            full: "",
            command: "",
            keyword: "",
            id: "",
            queryParams: {}
        };
        var pattern =  /#([^-]*)(-[a-z^-]*){0,1}-([a-zA-Z0-9]{32})(\?.+){0,1}/;
        var regex = new RegExp(pattern);
        var matches = regex.exec(hashValue);
        if (matches) {
            parsedPermalink.full = matches[0].slice(1);
            parsedPermalink.command = matches[1];
            parsedPermalink.keyword = matches[2] !== undefined ? matches[2].slice(1) : "";
            parsedPermalink.id = matches[3];
            parsedPermalink.queryParams = matches[4] !== undefined ? parseQueryString(matches[4].slice(1)) : {};
        }

        return parsedPermalink;
    },

    clearPermalink : function() {
        require(["js_root/utilities/utils.browser"], function(utilsBrowser){
            utilsBrowser.clearPermalink();
        });
    },
    
    /**
     * Loop through the dashboard tabs and find a tab that matches the specified master
     * tabId and queryParams.
     */
    findMatchingTab : function(tabId, queryParams, reuseTab)
    {
        var foundTab = false;
        var tab;
        for(var i in this.tabs)
        {
            tab = this.tabs[i];
            if(tab.master == tabId)
            {
                var allValuesMatch = true;
                if(!reuseTab)
                {
	                for(var key in queryParams)
	                {
		                if(key.indexOf("param:") != 0) continue;

		                var parsedKey = key.slice(6);
		                var checkValue = this.getDashboardProp(parsedKey, Dashboard.SCOPE_TAB, tab.id);
		                if(checkValue != queryParams[key])
		                {
			                allValuesMatch = false;
			                break;
		                }
	                }
                }
                if(allValuesMatch)
                {
                    foundTab = tab;
                    break;
                }
            }
        }

        return foundTab;
    },

    /**
     * Parse the query string from window.location and add the query parameters to the
     * specified tab as dashboard properties.
     */
    addQueryParamsToTab : function(tabId)
    {
        // This function may be called within the callback from the add_tab action,
        // so the query string must be re-parsed here.
		var loc = window.location.href;
        var queryString = loc.slice(loc.indexOf("?") + 1);
        var queryParams = parseQueryString(queryString);
        for(var key in queryParams)
        {
            if(key.indexOf("param:") == 0)
            {
                var parsedKey = key.slice(6);
                this.setDashboardProp(Dashboard.SCOPE_TAB, parsedKey, queryParams[key], tabId);
            }
        }
    },

	checkForEmptyTab : function() {
        var $emptyTabMsg = $("#msg-tab_empty");
        var $noTabsMsg = $("#msg-no_tabs");
        var $addButton = $("#button-add-tab");
        var $emptyPlaylist = $("#msg-playlist_empty");
        var $tvTabContainer = $("#tv-tab-container");
        var $dashboard = $(".c-dashboard");
        var currentState;

        if (this.config.workspaceMode) return;

        if (this.tabs.length == 0) {
            $emptyPlaylist.hide();
            $emptyTabMsg.hide();
            $noTabsMsg.show().appendTo($dashboard);
            this.toolbar.el.hide();
            this.layoutEl.addClass("layout-no-tabs");

            if (this.isTvMode) {
                $tvTabContainer.hide();
            }

            PubSub.publish("dashboard-empty");
		} else {
            this.layoutEl.removeClass("layout-no-tabs");

            if (this.isTvMode) {
                $tvTabContainer.show();
            }

            if (this.isTvMode && this.isPlaylistEmpty()) {
                $emptyTabMsg.hide();
                $noTabsMsg.hide();
                $emptyPlaylist.show().appendTo($dashboard);
                $dashboard.addClass("dashboards-hidden");
            } else {
                $emptyPlaylist.hide();
                $dashboard.removeClass("dashboards-hidden");

                if (this.tabs.length >= 1){
                    $noTabsMsg.hide();
                }

                if (!this.activeTab || !this.activeTab.loaded) return;

                currentState = this.activeTab.klips.length == 0 ? this.activeTab.layout.createConfig() : {};

                if (this.activeTab.klips.length != 0 || (this.activeTab.layout.id == "grid" && currentState["desktop"].itemConfigs.length != 0)) {
                    $emptyTabMsg.hide().appendTo(document.body);

                    if (this.activeTab.layout.id != "grid") {
                        this.layoutEl.addClass("template-layout");
                    } else {
                        this.layoutEl.removeClass("template-layout");
                    }
                } else {
                    $emptyTabMsg.show().appendTo($dashboard);

                    this.layoutEl.removeClass("template-layout");
                }
            }
        }
	},

	/**
	 * In order to add a Klip to the dashboard, an instance must be created on the
	 * server-side, and its ID must be known.
	 */
	requestKlip : function(klipId, tabId, onSuccess, onError) {
        var _this = this;
        var tabRight;

        if (!tabId) tabId = this.activeTab.id;

        tabRight = dashboard.tabRights[tabId];

		if (!tabRight || !tabRight.edit) {
			statusMessage("You do not have the required permissions to add " + KF.company.get("brand", "widgetName") + "s to this dashboard. Contact your account admin.", { error:true });
			return;
		}

		$.ajax({
			type: "POST",
			url: "/dashboard/ajax_requestKlip",
            async: false,
            dataType: "json",
            data: {
                klipId: klipId,
                tabId: tabId
			},
			success: function(data) {
                var klipSchema;
                var klip;

				if (data.error) {
                    statusMessage(data.msg, { warning:true, zIndex:"2300" });

                    if (onError) {
                        onError();
                    }

					return;
				}

				klipSchema = $.parseJSON(data.schema);

				_this.rights[klipSchema.master] = data.shareRight;

                klip = _this.addKlip(klipSchema, tabId, true);

                if (!_this.isPublished()) {
                    _this.checkConnectYourDataVisibility();
                }

				_this.warningSystem.hideOrShowWarningIcons([klip]);

                if (onSuccess) {
                    onSuccess();
                }
			}
		});
	},


	/**
	 * removes a Klip from the dashboard.
	 * @param klip
     * @param config - {
     *      isUpdate: true,
     *      preventSerialization: true,
     *      readyState: true
     * }
	 */
	removeKlip : function(klip, config)
	{
        var isUpdate = config && config.isUpdate;
        var preventSerialization = config && config.preventSerialization;
        var readyState = config && config.readyState;
        var i, len;
        var tab;

        klip.setVisible(false);
        klip.el.empty().remove(); // calling empty first is the suggested way to remove efficiently

		this.dashboardEl.trigger("klip-removed", klip);

		for (i = 0, len = this.klips.length; i < len; i++) {
            if (this.klips[i] == klip) {
                this.klips.splice(i, 1);
            }
        }

		// remove the klip from its tab.klips collection
		// --------------------------------------------
		tab = this.getTabById(klip.currentTab);

		if (tab) {
			for (i = 0, len = tab.klips.length; i < len; i++) {
                if (tab.klips[i] == klip) {
                    tab.klips.splice(i, 1);
                }
            }

			if (tab == this.activeTab) {
				if (tab.layoutType == "grid" && !isUpdate) {
                    tab.layout.removeComponent(klip);
                }

                if (!preventSerialization) {
                    tab.stateIsDirty = true;

                    if (readyState) {
                        tab.state = tab.layout.createConfig();
                        tab.stateIsReady = true;
                    }

                    this.serializeTabState();
                }
			}
		}

		if (!isUpdate) {
            this.removeDashboardProp(Dashboard.SCOPE_KLIP, "*", klip.id, klip.currentTab);
        }

		this.checkForEmptyTab();
		klip.destroy();
	},


	/**
	 * adds a button corresponding content panel to the dashboard toolbar panel.
	 * creates a button in the toolbar and attaches event handlers to show/hide
	 * the toolbar's panel
	 * @param config
	 *	- buttonText
	 *	- buttonCss - css name for button which should provide icon information
	 *					there should also exist a '<buttonClass>-active' class
	 *	- panelSelector - jquery selector to resolve the panel <div> which
	 */
	addToolbarPanel : function(config) {
        var tbPanel;
		var tbButton = $("<li class='toolbar-tab'>")
            .attr("id", config.buttonId)
            .addClass(config.buttonCss)
            .text(config.buttonText);

        if (config.panelSelector) {
            tbButton.on("click", _.bind(this.onToolbarButtonClick, this));

            tbPanel = $(config.panelSelector).hide();
            tbButton.data("panel", tbPanel); // associate the button with the panel

            // move the panel into the toolbar panel container
            this.toolbar.panelsEl.append(tbPanel);
        } else if (config.actionId) {
            tbButton.attr("actionId", config.actionId);
            tbButton.find("span").attr("parentIsAction", "true");
        }

        if(config.hide) {
            tbButton.hide();
        }

		this.toolbar.tabsEl.append(tbButton);
	},


	/**
	 * handler for when a toolbar button/tab is clicked.
	 * @param evt
	 */
	onToolbarButtonClick : function(evt)
	{
		var tab = $(evt.currentTarget);
		var panel = tab.data("panel");

		if (this.toolbar.active)
		{
			// hide the toolbar and clear active states
			this.toolbar.active.data("panel").slideUp(200);
			this.toolbar.active.removeClass("active");

            if (this.toolbar.active.hasClass("tb-tab-layout") && this.currentLayout.id == "grid") {
                this.currentLayout.toggleGridLines(false);
            }
		}

		if ($(this.toolbar.active).equals(tab))
		{
			this.toolbar.active = false;
			return;
		}

		// Close the add tab panel if it is open
		this.closeTabPanel();

		this.toolbar.active = tab;
		tab.addClass("active");
		panel.slideDown(200);

        if (tab.hasClass("tb-tab-layout")) {
            this.updateGridLayoutIcon();

            if (this.currentLayout.id == "grid") {
                this.currentLayout.toggleGridLines(true);
            }
        }
	},

    updateGridLayoutIcon : function(cols)
    {
        var $gridIcon = $(".grid-layout-icon");
        var iconWidthAvailable = $gridIcon.width();

        if (cols) {
            $gridIcon.empty();
            for (var i = 0; i < cols; i++) {
                $gridIcon.append($("<div class='grid-layout-icon_col' parentIsAction='true'>"));
            }
        }

        var $gridIconCols = $(".grid-layout-icon_col");
        var colMBP = $gridIconCols.outerWidth(true) - $gridIconCols.width();

        iconWidthAvailable -= colMBP * $gridIconCols.length;

        var iconColWidth = Math.floor(iconWidthAvailable / $gridIconCols.length);

        if (iconColWidth > 0) {
            $gridIconCols.width(iconColWidth);
        }
    },

	/**
	 * Close the toolbar if it is open.
	 */
	closeToolbar : function()
	{
		if (this.toolbar.active)
		{
			// hide the toolbar and clear active states
			this.toolbar.active.data("panel").slideUp(200);
			this.toolbar.active.removeClass("active");

            if (this.toolbar.active.hasClass("tb-tab-layout") && this.currentLayout.id == "grid") {
                this.currentLayout.toggleGridLines(false);
            }

            this.toolbar.active = false;
        }
	},

    openTabPanel: function() {
        PubSub.publish("add-dashboard-panel-opening");

        $("#button-add-tab").addClass("selected");
        $("#panel-add_tab").slideDown(200, function() {
            PubSub.publish("add-dashboard-panel-opened");
        });
    },

	/**
	 * Close the tab panel if it is open.
	 */
	closeTabPanel : function(dashboardAdded) {
        PubSub.publish("add-dashboard-panel-closed", { dashboardAdded: dashboardAdded });

        $("#button-add-tab").removeClass("selected");
		$("#panel-add_tab").slideUp(200);
	},


	/**
	 * called when the window resizes.  checks to see if the resize affected our
	 * dimensions, and if so notify klips of the layout change
	 * @param evt
	 */
	onWindowResize : function (evt)
	{
        var w = $(window).width();
		var h = $(window).height();
		if (this.windowwidth != w || this.windowheight != h)
		{
			this.windowwidth = w;
			this.windowheight = h;
            if (this.currentLayout) this.currentLayout.updateRegionWidths();
			this.updateTabWidths();
            this.updateGridLayoutIcon();
        }
	},


	/**
	 * called when a klip's position has been change as a result of a drag operation.
	 * Eg, it could be called if it was the klip that was dragger, OR if it was the klip
	 * that was swapped.
	 * @param evt
	 * @param ui
	 */
	onKlipDragUpdate : function(evt, ui)
	{
		clearSelections();
		var $klip = $(ui.item);
		var $r = $klip.parent(".layout-region");

        if ($klip.data("klip")) $klip.data("klip").setDimensions($r.data("width"));
		this.activeTab.stateIsDirty = true;
		setTimeout( _.bind(this.serializeTabState,this), 200 );
//		this.serializeTabState();
	},

    onKlipDragStart : function(evt, ui)
    {
        var $r = $(ui.item).parent(".layout-region");

        this.activeTab.layout.getRegions().each(function() {
            var klipChildren = $(this).children(".klip");

            if (klipChildren.length == 0 || (klipChildren.length == 1 && $(this).attr("layoutConfig") == $r.attr("layoutConfig"))) {
                $(this).addClass("region-empty");
            }

            $(this).addClass("region-outline");
        });

        $r.sortable("refreshPositions");
    },

    onKlipDragStop : function(evt, ui)
    {
        this.activeTab.layout.getRegions().removeClass("region-empty region-outline");

        if (this.previousTab) {
            this.previousTab.layout.getRegions().removeClass("region-empty region-outline");
            this.previousTab = false;
        }
    },

	/**
	 * creates a Klip
	 */
	createKlip : function(config)
	{
		var k = KlipFactory.instance.createKlip(this, config);
		return k;
	},


	/**
	 * Returns a single klip with the given id and on the given tab, if tid provided
     *
     * @param id
     * @param tid
	 */
	getKlipById : function(id, tid)
	{
        for (var i = 0; i < this.klips.length; i++) {
            var klip = this.klips[i];
            if (klip.id == id) {
                if (tid) {
                    if (klip.currentTab == tid) {
                        return klip;
                    }
                } else {
                    return klip;
                }
            }
        }

		return false;
	},

    /**
     * Returns all klips with the given id
     * - these klips exists on multiple instances of the same tab
     *
     * @param id
     * @returns {Array}
     */
    getKlipsById : function(id)
    {
        var klips = [];

        for (var i = 0; i < this.klips.length; i++) {
            var klip = this.klips[i];
            if (klip.id == id) {
                klips.push(klip);
            }
        }

        return klips;
    },

    /**
     * Returns all klips with the given master id
     *
     * @param masterId
     * @returns {Array}
     */
    getKlipsByMaster : function(masterId)
    {
        var klips = [];

        for (var i = 0; i < this.klips.length; i++) {
            var klip = this.klips[i];
            if (klip.master == masterId) {
                klips.push(klip);
            }
        }

        return klips;
    },


	/**
	 *
	 * @param layoutId
     * @param state
	 * @param replaceTabLayout boolean if true, replaces the activeTab's layout manager with newLayout
     * @param preventSerialization
	 */
	setLayout : function(layoutId, state, replaceTabLayout, preventSerialization)
	{
		var currentLayoutId = this.activeTab.layout.id;
		var newLayout = this.layouts[layoutId];

		// we track the currentLayout so that we can properly unload it when a new one
		// is selected.  when the dashboard is first created there will be no currentLayout
		// so we use the activeTab's layout
		//
		// except for the initial assignment, we track the currentLayout separately from activeTab.layout because the tab has
		// changed before setLayout is called, and we won't know what the previous layout was
		if (!this.currentLayout) this.currentLayout = this.activeTab.layout;

		newLayout.beforeLayout();

        var oldState = state;
        if (replaceTabLayout)
		{
            oldState = this.activeTab.state;
            // migrate from template to custom layout
            if ( layoutId == "grid" && currentLayoutId != "grid" && currentLayoutId != "bally")
            {
                var items = [];
                _.each(oldState, function(item, key) {
                    if (item.region) {
                        items.push($.extend({}, {cxId:key}, item));
                    }
                });

                items.sort(function(a, b) {
                    var aRegion = a.region;
                    var aIdx = a.idx;
                    var bRegion = b.region;
                    var bIdx = b.idx;

                    var compare = aRegion.localeCompare(bRegion);

                    if (!compare) {
                        return aIdx - bIdx;
                    } else {
                        return compare;
                    }
                });

                var itemConfigs = [];
                var lastVertIndices = [0, 0, 0, 0, 0, 0];
                for (var i = 0; i < items.length; i++) {
                    var item = items[i];

                    var region = this.activeTab.layout.getRegion(item.region);
                    if (region) {
                        var klip = this.getKlipById(item.cxId, this.activeTab.id);

                        var cid = "gridklip-" + item.cxId;
                        var config = { id: cid };

                        var colIdx = parseInt($(region).attr("layoutColIdx"));
                        var colSpan = parseInt($(region).attr("layoutColSpan"));

                        var vertIdx = item.idx;
                        if (vertIdx < lastVertIndices[colIdx]) {
                            vertIdx = lastVertIndices[colIdx];
                        }
                        lastVertIndices[colIdx] = vertIdx + 1;

                        if (item.region == "region-1" && colSpan == 6) {
                            for (var j = 1; j < lastVertIndices.length; j++) {
                                lastVertIndices[j]++;
                            }
                        }

                        config.index = [colIdx, vertIdx];
                        config.colSpan = colSpan;
                        config.minHeight = klip ? klip.getMinHeight() : 200;

                        itemConfigs.push(config);
                    }
                }

                oldState = {
                    desktop:{
                        cols: itemConfigs.length > 0 ? 6 : parseInt($(".grid-layout-cols input[type='text']").val()),
                        colWidths: [],
                        itemConfigs: itemConfigs
                    }
                };
            }
            // migrate from custom to template layout
            else if (layoutId != "grid" && currentLayoutId == "grid")
            {
                var regions = newLayout.getRegions();
                var nextRegion = 0;

                var newState = {};
                var _this = this;
                _.each(oldState["desktop"].itemConfigs, function(itemConfig) {
                    var klipId = itemConfig.id.slice(itemConfig.id.indexOf("-") + 1);
                    var klip = _this.getKlipById(klipId, _this.activeTab.id);

                    if (klip) klip.el.height(klip.fixedHeight ? klip.fixedHeight : "");       // reset the height of the klip

                    var region = $(regions[nextRegion % regions.length]).attr("layoutConfig");
                    var idx = Math.floor(nextRegion / regions.length);

                    newState[klipId] = { region:region, idx:idx };

                    nextRegion++;
                });

                oldState = newState;
            }

            this.activeTab.layout = newLayout;
			this.activeTab.layoutType = newLayout.id;
			if (!preventSerialization) this.activeTab.stateIsDirty = true;
		}

        // remove all the klips from the layout manager and put them in storage
        // -------------------------------------------------------------------
        this.currentLayout.removeAll();

        newLayout.setLayoutState(oldState);
        if (newLayout.id == "grid") newLayout.toggleGridLines(this.toolbar.active && this.toolbar.active.hasClass("tb-tab-layout"));


        $("#tb-panel-layout span, .layout-icon").removeClass("active");
        $("#tb-panel-layout span[layout=" + newLayout.id + "], .layout-icon[layout=" + newLayout.id + "]").addClass("active");


		// if the new layout manager is different than the current one, move the current layout HTML
		// to storage
		// ------------------------------------------------------------------------------------------
		if (this.currentLayout != newLayout)
		{
			$("#layout-storage").append(this.currentLayout.layoutEl);
		}


		// move the new layout table into the layout region
		// -----------------------------------------------
		this.layoutEl.append(newLayout.layoutEl);

        if (newLayout.id != "grid") {
            if (this.activeTab.klips.length != 0) {
                this.layoutEl.addClass("template-layout");
            }

            $(".grid-layout-cols input[type='text']").val(6).change();
            $(".grid-layout-cols").hide();
        } else {
            this.layoutEl.removeClass("template-layout");
            $(".grid-layout-cols").show();
        }

		var klips = this.activeTab.klips;
		var klipsLen = klips.length;

		this.currentLayout = this.activeTab.layout;
        this.currentLayout.setCurrentTabId(this.activeTab.id);
		this.currentLayout.updateRegionWidths();

        if (klipsLen == 0) {
            this.currentLayout.onTabLoaded();
        } else {
            while (--klipsLen != -1)
            {
                var k = klips[klipsLen];
                newLayout.layoutComponent(k);
                // updateRegionWidths might not trigger an update on the klips because
                // its widths may have not changed, so we call update on all klips in the active tab
                // to ensure they're rendered to the proper widths
                // -----------------------------------------------------------------------------
//                updateManager.queue(k, {r:"klip-setLayout"});
//			    k.update();
            }
        }

		newLayout.afterLayout();

		if (!preventSerialization)  this.serializeTabState();
	},

	setTvMode : function(enabled) {
        var selectedTabId;
        var lastTabCookie;

        this.isTvMode = enabled;

        if (!enabled) {
            this.updateTabWidths();
            this.checkForEmptyTab();

            // if the playlist was empty, an activeTab maybe not have been set
            // or the activeTab could have been unselected
            if (!this.activeTab) {
                lastTabCookie = $.cookie("last-tab");
                if (!this.getTabById(lastTabCookie)) {
                    if (this.tabs.length > 0) {
                        selectedTabId = this.tabs[0].id;
                    }
                } else {
                    selectedTabId = lastTabCookie;
                }
            } else if (!this.activeTab.isSelected) {
                selectedTabId = this.activeTab.id;
            }

            if (selectedTabId) {
                page.invokeAction("select_tab", { tabId: selectedTabId, eventSource: "tv_mode_exited" });
            }
        } else {
            $.get("/users/ajax_getPlaylistUserProperty", function(data) {
                this.receivePlaylistUserPropertyFromAjax(data);
            }.bind(this));
        }

		PubSub.publish("layout_change", "tvmode");
	},

	receivePlaylistUserPropertyFromAjax : function(data) {
        this.playlist = JSON.parse(data.playlist);

        if(!this.playlist.hiddenDashboards) {
            this.playlist.hiddenDashboards = []
        }

        this.updateTabListMenu();
        this.checkAndHandleEmptyPlaylist();

        // setup small tabs
        page.invokeAction("tv_small_tabs");
    },

	setActiveTabProp : function(n,v)
	{
		this.setDashboardProp(2,n,v,this.activeTab.id);
	},


	removeDashboardProp : function(scope, n, xid, secondaryId)
	{
		var propLen = this.dashboardProps.length;
        while(--propLen > -1)
		{
		    var p = this.dashboardProps[propLen];

			if ( p.scope != scope ) continue;
			if ( p.scope == 2 && p.tab != xid ) continue;
			if ( p.scope == 1 && (p.klip != xid || p.tab != secondaryId) ) continue;
			if ( n != "*" && p.name != n ) continue;

            this.dashboardProps.splice(propLen, 1);
		}
    },

	/**
	 * TODO: This is Deprecated
	 * @param key
	 * @param tab
	 * @param tabScopeOnly
	 * @return resolved variable value or default value
	 */
	getTabProp : function(tab, tabScopeOnly){
			return replaceMarkers(/\{([^}]*)\}/,tab.name,function(key){
				p = this.dashboard.getDashboardProp(key,2,tab.id);
				return p;

//				p = this.dashboard.getDashboardProp(key);
//				return p;
			});

	},

	getVariableTabName : function(tab)
    {
		if (tab.hasDynamicTitle) // if tab has a variable name extract the variable
        {
            return replaceMarkers(/\{([^}]*)\}/, tab.name, function(key) {
				// Scope 2 is this dashboard
                var p = this.dashboard.getDashboardProp(key, 2, tab.id);

                // If there is no tab level property, attempt to pick up value from higher scopes
                if(p == undefined) {
                    // Scope 3 is all dashboards
                    p = this.dashboard.getDashboardProp(key, 3, tab.id);
                }

                if(p == undefined) {
                    // Scope 4 is user
                    p = this.dashboard.getDashboardProp(key, 4, tab.id);
                }

				return p;
			});
		}
		else // just return the regular name
        {
			return (tab && tab.name ? tab.name : false);
		}
	},

	getDefaultTabName : function(tabName){
		if(tabName.search("\{([^}]*)\}") != -1){//if tab has a variable name extract the default value
			return replaceMarkers(/\{([^}]*)\}/,tabName);
		}
		else{//just return the regular name
			return tabName;
		}
	},


	/**
	 *
	 * @param scope
	 * @param n
	 * @param v
	 * @param xid
     * @param secondaryId
	 */
	setDashboardProp : function(scope, n ,v, xid, secondaryId)
	{
		var prop = { scope:scope, name:n, value:v };
		if ( scope == 1 ) {
            prop.klip = xid;
            prop.tab = secondaryId;
        }
		if ( scope == 2 ) prop.tab = xid;

		var propLen = this.dashboardProps.length;
		var overwritten = false;

		// go through the existing properties to see if a named prop already exists
		// for the given scope.  if so, overwrite it
		// -------------------------------------------------------------------------
		while(--propLen > -1)
		{
			var p = this.dashboardProps[propLen];
			if ( p.scope == 4 ) continue; // never overwrite user-scope props
			if ( p.name != n ) continue;
			if ( p.scope == 2 && p.tab != xid ) continue;
			if ( p.scope == 1 && (p.klip != xid || p.tab != secondaryId) ) continue;
			if ( p.scope != scope ) continue;

			// if the value is found, and it has not changed we should exit, otherwise
			// we'll do lots of extra stuff (like tell DPN to re-evaluate formulas)
			// ------------------------------------------------------------------------
			if (p.value == v) return;

			p.value = v;
			overwritten = true;
			break;

		}

		if ( !overwritten ) this.dashboardProps.push(prop);

		// queue the prop for saving to the server (as long as we're not in the workspce)
		// -------------------------------------------------------------------------------

		if ( !isWorkspace(this) || this.isPublished() )
		{
			this.queuedDashboardProps.push(prop);
			this.sendUserProps();
		}

        var k;

		if (scope == 1)
		{
			k = this.getKlipById(xid, secondaryId);

            if (k) {
                this.rewatchFormulas(k, n);
                this.updateKlipTitles([ k ], secondaryId);
            }
		}
		else if ( (scope == 2 || scope == 3) && (!isWorkspace(this) || this.isPublished()) )
        {
			var klipsToCheck = [];

			if ( scope == 2 )
			{
				var tab = this.getTabById(xid);
				klipsToCheck = tab.klips;

                this.updateTabTitles([ tab ]);
            }
            else if ( scope == 3 )
			{
                this.updateTabTitles(this.tabs);

                klipsToCheck = this.klips;
			}

            var klen = (klipsToCheck && klipsToCheck.length ? klipsToCheck.length : -1);
            while (--klen > -1) {
                k = klipsToCheck[klen];
                this.rewatchFormulas(k, n);
            }

            this.updateKlipTitles(klipsToCheck, xid);
        }
    },

    rewatchFormulas : function(k, n)
    {
        var rewatch = [];
        var formulas = k.getFormulas();
        var flen = formulas.length;
        var formulasToRewatch = [];
        var dstRootComponent, peers;
        var formulaMap = {};
        var j;
        var formula, dependentFormulas;

        while (--flen > -1) {
          formula = formulas[flen];

          if (formula.usesVariable(n)) {
            formulasToRewatch.push(formula);
          }
        }
        formulasToRewatch = DX.Formula.getFormulasToRewatch(formulasToRewatch);

        if (formulasToRewatch.length > 0) {
            rewatch.push({ kid: k.id, fms: formulasToRewatch });
        }

        if (rewatch.length > 0) {
            DX.Manager.instance.watchFormulas( rewatch );
        }
        return rewatch;
    },

    updateTabTitles : function(tabs)
    {
        if (!tabs) return;

        for (var i = 0; i < tabs.length; i++) {
            var tab = tabs[i];

            // update variables in tab names
            if (tab.hasDynamicTitle) {
                var tabName = this.getVariableTabName(tab);
                if (tabName) {
                    tab.$el.find(".dashboard-tab-name").html(tabName);
                    tab.$el.attr("title", tabName);
                    this.updateTabWidths();
                }
            }
        }
    },

    /**
     * Update dynamic klip titles
     *
     * @param klips - klips to update titles
     * @param tabId - used in mobile
     */
    updateKlipTitles : function(klips, tabId)
    {
        if (!klips) return;

        for (var i = 0; i < klips.length; i++) {
            var k = klips[i];

            if (k.hasDynamicTitle) k.updateTitle();
        }
    },

	/**
	 * fetches a named property.  if scope is supplied it will only pull the property for the specied scope, otherwise
	 * it will read from the highest scope (dashboard)
	 * @param name
	 * @param scope  1 = klip , 2 = tab, 3 = dashboard, 4 = user
     * @param xid - id of primary asset (klip or tab)
     * @param secondaryId - id of secondary asset (when scope == 1, tab)
	 */
	getDashboardProp : function(name, scope, xid, secondaryId)
	{
	    var propLen = this.dashboardProps.length;
		var maxScope = 0;
		var lastFoundValue = undefined;

		while(--propLen > -1 )
		{
			var p = this.dashboardProps[propLen];

			// search a specific scope
			// -----------------------
			if ( scope )
			{
				if ( p.scope != scope || p.name != name ) continue;

				if ( scope == 1 )
				{
					if (p.klip == xid && p.tab == secondaryId) return p.value;
					else continue;
				}
				else if ( scope == 2 )
				{
					if (p.tab == xid) return p.value;
					else continue;
				}
				else
				{
					return p.value;
				}

			}
			else
			{
				if ( scope < maxScope ) continue; //skip scopes we've already exceeded
				if ( p.scope == 2 && p.tab != this.activeTab.id ) continue;
				if ( p.name != name ) continue; //skip if name does not match
				if ( p.scope == 3 ) return p.value; // if this is the highest scope, return the value
				maxScope = scope;
				lastFoundValue = p.value;
			}
		}

		return lastFoundValue;
	},

	/**
	 * bulk-set all properties when the dashboard is initialized.
	 * @param props
	 * {
	 *   scope ,
	 *   name,
	 *   value,
	 *   klip,
	 *   tab
	 *  }
	 */
	setDashboardProps : function( props )
	{
		this.dashboardProps = props;
	},


	/**
	 * sends all the queued klip properties to the server to be saved.  this method
	 * is reassigned as a debounced method called 'sendUserProps' when the dashboard is initialized to optimize
	 * sending of props to the server
	 */
	sendUserPropsNow : function()
	{
		var data = JSON.stringify(this.queuedDashboardProps);
		this.queuedDashboardProps = [];

        if (this.isPublished()) {
            $.post("/publishedDashboard/ajax_setProps", { props:data, profileId:this.config.publishedProfileId });
        } else {
            $.post("/dashboard/ajax_setProps", { props:data });
        }
	},

	getRootPath : function()
	{
		return "/";
	},

    getLocationOrigin : function()
    {
        return getLocationOrigin();
    },

    getAssetInfo : function(assetId, successCallback)
    {
        if (!assetId) return;

        $.ajax({
            type: "POST",
            url: "/assets/ajax_assetInfo/" + assetId,
            async: false,
            success: successCallback,
            dataType: "json",
            data: {publishedProfileId: this.config.publishedProfileId}
        });
    },

    sortKlipsByOffset: function(klipA, klipB) {
        var offsetA = klipA.el.offset();
        var offsetB = klipB.el.offset();

        var x1 = offsetA.left;
        var y1 = offsetA.top;
        var x2 = offsetB.left;
        var y2 = offsetB.top;

        if ((x1 < x2 && y1 <= y2) || (x1 >= x2 && y1 < y2)) {
            return -1;
        } else if ((x1 <= x2 && y1 > y2) || (x1 > x2 && y1 >= y2)) {
            return 1;
        } else {
            return 0;
        }
    }

});


/**
 *
 */
DashboardLayout = $.klass({

	layoutEl : false,
	defaultRegionId: false,
	dashboard: false,
	id: false,
    currentTabId: false,
	layoutState: false,
    numPages: 1,

	initialize : function(config)
	{
		this.dashboard = config.dashboard;
		this.config = config;
		this.layoutEl = config.layoutEl;
		this.id = config.id;
		this.layoutState = {};

		if (!this.layoutEl) {
            console.error("layout missing"); // eslint-disable-line no-console
        }

		if (r = this.layoutEl.attr("default-region"))
		{
			this.defaultRegionId = r;
		}

	},


	beforeLayout : function(){},

    afterLayout : function()
    {
        this.unsharedComponents = {};

        for (var id in this.layoutState) {
            var cxLayout = this.layoutState[id];
            if (!cxLayout.placed) {
                this.unsharedComponents[id] = cxLayout;
            }
        }
    },

	onTabLoaded : function(){},

	/**
	 * renders a dom
	 */
	renderDom : function()
	{

		if (!this.defaultRegionId)
		{
			if (r = this.layoutEl.attr("default-region"))
			{
				this.defaultRegionId = r;
			}
		}
		return this.layoutEl;
	},

	/**
	 * called by dashboard when a new component (klip) is added
	 * @param comp
	 * @param layoutConfig
	 */
	layoutComponent : function(comp, highlight)
	{
		var cxLayout = this.layoutState[comp.id];
		var targetRegion;
		var targetIdx = 0;


		if (cxLayout)
		{
			targetRegion = this.getRegion(cxLayout.region, this.defaultRegionId);
			targetIdx = cxLayout.idx;

            cxLayout.placed = true;
		}
		else
		{
			targetRegion = this.getRegion(this.defaultRegionId);
		}


		if (targetIdx == 0) targetRegion.prepend(comp.el);
		else
		{
			var klips = targetRegion.find(".klip");
			var klipLen = klips.length;
			var prev;
			var placed = false;

			for (var i = 0; i < klipLen; i++)
			{
				var current = klips[i];
				if (targetIdx < this.layoutState[current.id].idx)
				{
					if (prev)
					{
						comp.el.insertAfter($(prev));
						placed = true;
					}
					else
					{
						comp.el.insertBefore($(current));
						placed = true;
					}
					break;

				}
				prev = current;
			}

			if (!placed) targetRegion.append(comp.el);
		}

		var rw = targetRegion.data("width");

		comp.setVisible(true);
		comp.setDimensions(rw);

        var _this = this;
        comp.update(function() {
            if (comp.onLayout) comp.onLayout();

            if (highlight) _this.dashboard.highlightKlip(comp);
        });
    },

	/**
	 *
	 * @param state
	 */
	setLayoutState : function(state)
	{
		this.layoutState = state;
	},

    setCurrentTabId : function(tabId)
    {
        this.currentTabId = tabId;
    },

	/**
	 * inspects a cx within the layout manager to create a layoutConfig
	 * that represents its current layout state
	 * @param cx
	 */
	createConfig : function(cx)
	{

		var config = {};
		var regions = this.getRegions();

		$.each(regions, function()
		{
			var region = $(this).attr("layoutConfig");
			$.each($(".klip", this), function(idx)
			{
				var klipId = $(this).attr("id");
				config[klipId] = { region:region, idx:idx };
			});
		});

        $.extend(config, this.unsharedComponents);

		this.layoutConfig = config;
		return config;
	},


	/**
	 * called by the dashboard when a component should be removed
	 * @param comp
	 */
	removeComponent : function(comp)
	{

	},

	/**
	 * returns a region element for this layout.  if none is found,
	 * return a region matching 'defaultId'
	 * @param regionId
	 * @param defaultId (optional)
	 */
	getRegion : function(regionId, defaultId)
	{
		if (!regionId) regionId = defaultId;
		var r = this.layoutEl.find("[layoutconfig=" + regionId + "]");
		if ((!r || r.length == 0) && defaultId) r = this.layoutEl.find("[layoutconfig=" + defaultId + "]");

		if (r.length != 0) return r.eq(0);
		else return false;
	},

	/**
	 * lazy getter for this layout's regions
	 */
	getRegions : function()
	{
		if (!this.regions) this.regions = this.layoutEl.find(".layout-region");
		return this.regions;
	},

	removeAll : function()
	{
		// when we remove klips we stick them in klip-storage (which is hidden).
		// by moving them to a different place in the dom (instead of deleting them) we keep
		// all the event handlers intact
		this.layoutEl.find(".klip").each(function()
		{
			var $k = $(this).appendTo("#klip-storage");
			var klip = $k.data("klip");
			if (klip) klip.setVisible(false);
		});
	},

	updateRegionWidths : function()
	{
		var scrollPositions = getScrollPosition();
		var rs = this.getRegions();
		var len = rs.length;
		var allKlips = this.layoutEl.find(".klip").hide();
		var widthsChanged = false;
		while(--len > -1 )
		{
			var r = $(rs[len]);
			var w = r.width();
			var cw = r.data("width");
			if ( w != cw){
				r.data("width",w);
				widthsChanged = true;
			}
		}
		if ( widthsChanged )
		{

			len = rs.length;
			while(--len > -1 )
			{
				r = rs.eq(len);
				var klips = r.find(".klip");
				var rw = r.data("width");

				for (var i = 0; i < klips.length; i++)
				{
					var $k = $(klips[i]);
					var k = $k.data("klip");
					$k.toggle( k.isVisible );
					k.setDimensions(rw);
				}
			}
		}
		else
		{
			allKlips.each( function(){
				var $k = $(this);
				$k.toggle( $k.data("klip").isVisible );
			});
		}

		setScrollPosition(scrollPositions);
	},

    updateLayout : function(cx)
    {

    },

    pageBreakLayout : function(pageHeight, zoom)
    {
        this.klipPages = [];

        var _this = this;
        this.layoutEl.find(".klip").each(function() {
            var height = $(this).outerHeight();
            var top = $(this).position().top;

            var page = Math.floor((top + height) / zoom / pageHeight);

            if (_this.klipPages[page] === undefined) _this.klipPages[page] = [];

            _this.klipPages[page].push($(this).attr("id"));
        });

        var i = this.klipPages.length - 1;
        while (i >= 0) {
            if (this.klipPages[i] === undefined) {
                this.klipPages.splice(i, 1);
            }

            i--;
        }

        this.numPages = this.klipPages.length;
    },

    getNumberPages : function()
    {
        return this.numPages;
    },

    showPage : function(pageNum)
    {
        this.layoutEl.find(".klip").show();

        if (this.klipPages && pageNum !== undefined) {
            var itemsToShow = this.klipPages[pageNum];
            if (!itemsToShow) return;

            this.layoutEl.find(".klip").each(function() {
                var klip = $(this).data("klip");
                if (_.indexOf(itemsToShow, $(this).attr("id")) == -1) {
                    $(this).hide();
                } else {
                    $(this).show();
                    // this line ensures to re-render the components that has to be rendered when they are visible
                    if(klip) klip.reRenderWhenVisible();
                }
            });
        }
    }

});


DashboardGridLayout = $.klass( DashboardLayout , {

	id: "grid",
	gridManager : false,
    dashboard : false,
	layoutEl : false,
	profile : "desktop",

	initialize : function(config)
	{
		var _this = this;

		var throttledUpdated = _.throttle( this.updateItemContent,200,{leading:false});
        this.debouncedLayout = _.debounce(_.bind(this.doLayout, this), 200);

        this.componentsToLoad = [];
        this.dashboard = config.dashboard;
        this.layoutEl = config.layoutEl;
		this.gridManager = new GridLayoutManager({
            $container: this.layoutEl,
            zoom: {el: ".c-dashboard", property: "-webkit-transform"},
            dragHandle: ".klip-toolbar",
            columnTolerance: 0.5,
            itemsDraggable: KF.user.hasPermission("dashboard.klip"),
            itemsResizable: KF.user.hasPermission("dashboard.klip"),
            $emptyContent: $("<div class='klip'>").append($("<span class='empty-text xx-small'>").text("You don't have access to this " + KF.company.get("brand", "widgetName") + ".")),

            onUpdateMinHeight : function(item) {
                _this.updateItemContent(item);
			},
            onUpdateWidth : function(item) {
	            if ( _this.inLayout ) return;
                _this.updateItemContent(item);
                _this.updateMinHeight(item);
			},

            /* drag callbacks */
            onDragStart : function(item) {
                _this.hideContextMenus();
            },
            checkAddDragItem : function() {
                var addDragItem = !_this.dashboard.klipTabDrop;
                _this.dashboard.klipTabDrop = false;

                return addDragItem;
            },
            onDragStop : function(item, serialize) {
                if (serialize) _this.serializeTabState();

                if ( !item || !item.$content ) return;
                // remove lingering hover styles
                var klip = item.$content.data("klip");
                klip.toolbarEl.css({ "background-color":"", color:"" });
            },

            /* resize callbacks */
            onResizeStart : function(item) {
                _this.hideContextMenus();
            },
			onResize : function(item) {
                throttledUpdated(item);
			},
            onResizeStop : function(item, serialize) {
                _this.updateItemContent(item);
                if (serialize) _this.serializeTabState();
            }
        });

        PubSub.subscribe("klip_layout_complete", function(evtName, args){
            if (args && args.length > 0) {
                var componentIdx = _.indexOf(_this.componentsToLoad, args[0]);
                if (componentIdx != -1) _this.componentsToLoad.splice(componentIdx, 1);
            }

            if (_this.componentsToLoad.length == 0) {
                _this.onTabLoaded();
                _this.updateRegionWidths();

                // since vertical scrollbar is automatically added on published dashboard,
                // the header might be the wrong size after all the klips are loaded
                if (_this.dashboard.isPublished()) {
                    $(".published-header").trigger("resize");
                }
            }
        });
        
	},

    setItemsDraggable : function(draggable)
    {
        this.gridManager.setItemsDraggable(draggable);
    },

    setItemsResizable : function(resizable)
    {
        this.gridManager.setItemsResizable(resizable);
    },

    toggleGridLines : function(showLines)
    {
        this.gridManager.toggleGridLines(showLines);
    },

    setCols : function(cols)
    {
        this.gridManager.setCols(cols);

        this.serializeTabState();
    },

    hideContextMenus : function()
    {
        this.dashboard.klipContextMenu.hide();
        this.dashboard.tabContextMenu.hide();
    },

	updateItemContent: function( item )
	{
		if ( !item || !item.$content ) return;

		var klip = item.$content.data("klip");
		klip.setDimensions( item.$container.width() , item.$container.height() );
    },

    updateMinHeight: function(item)
    {
        if ( !item || !item.$content ) return;

        var _this = this;
        var klip = item.$content.data("klip");
        klip.update( function(){
            _this.gridManager.setItemMinHeight({id:item.id, minHeight:klip.getMinHeight()});
            if (klip.onLayout) klip.onLayout();
        });
    },

    serializeTabState : function()
    {
	    if ( this.inLayout ) return;

        this.dashboard.activeTab.stateIsDirty = true;
        setTimeout( _.bind(this.dashboard.serializeTabState, this.dashboard), 200 );
    },

	setLayoutState : function($super,state)
	{
		$super(state);

        if (state["desktop"]) {
            this.gridManager.restoreState(state["desktop"]);

            $(".grid-layout-cols input[type='text']").val(this.gridManager.getCols()).change();
        }
	},

	layoutComponent : function(cx, highlight)
	{
		var tmpWidth;
		var cid = "gridklip-" + cx.id;
		var config = { id:cid }; // default config
		var layoutState = this.layoutState["desktop"];

		// try to find the itemConfig for this component
		var len = (layoutState && layoutState.itemConfigs && layoutState.itemConfigs.length) || 0;

		while(--len > -1)
		{
			if ( layoutState.itemConfigs[len].id == cid )
			{
				config = layoutState.itemConfigs[len];
				break;
			}
		}

		var gm = this.gridManager;

        if (!cx.isVisible) {
            $("#klip-preview-area").append(cx.el);
            cx.setVisible(true);
        }

		// if the config has no minHeight this is the first time it has ever
		// been assigned to the layout.  to get the minHeight we'll need to render
		// the klip, get its height, then assign it
        var _this = this;
		if ( !config.minHeight )
		{
			// find existing item at 0,0
			var existingConfig = gm.getItemConfig( { index:[0,0] } , "index");
			var newItemColSpan = Math.floor(gm.cols / 2);
			if ( existingConfig ) newItemColSpan = existingConfig.colSpan;
            if (newItemColSpan < 1) newItemColSpan = 1;

			tmpWidth = gm.getContainerWidth( { index:[0,0] , colSpan: newItemColSpan } );
			cx.setDimensions( tmpWidth );

			cx.update( function(){
				config.$content = cx.el;
				config.colSpan = newItemColSpan;
				config.minHeight = cx.getMinHeight();
				gm.addItem(config);
				if (cx.onLayout) cx.onLayout();

                if (highlight) _this.dashboard.highlightKlip(cx);
            });
		}
		else
		{
            tmpWidth = gm.getContainerWidth( { index:config.index , colSpan:config.colSpan } );
            cx.el.css("height", "");
            cx.setDimensions( tmpWidth );

            this.componentsToLoad.push(cx.id);

            cx.update( function(){
                // assign Klip element to container
                config.$content = cx.el;
                gm.setItemContent(config, true);
                gm.setItemMinHeight({id:cid, minHeight:cx.getMinHeight()}, true);
                if (cx.onLayout) cx.onLayout();

                PubSub.publish("klip_layout_complete", [cx.id]);

                if (highlight) _this.dashboard.highlightKlip(cx);
            });
		}
	},

	createConfig : function()
	{
		return {
			"desktop" : this.gridManager.serializeState()
		};
	},

	removeComponent : function(cx)
	{
		var cid = "gridklip-" + cx.id;
		this.gridManager.removeItem( { id: cid } );
    },

	removeAll : function($super)
	{
		$super(); // move all klips into storage
		this.gridManager.removeAllItems();
	},

	updateRegionWidths : function()
	{
		if (!this.gridManager.isDragging && !this.gridManager.isResizing) {
            this.gridManager.updateItemWidths(true);
        }
	},

    doLayout : function()
    {
        this.gridManager.doLayout(true);
    },

    updateLayout : function(cx)
    {
        var cid = "gridklip-" + cx.id;

        this.gridManager.setItemMinHeight({id:cid, minHeight:cx.getMinHeight()}, true);

        this.debouncedLayout();
    },

	beforeLayout : function()
	{
		this.inLayout = true;
	},

	afterLayout : function()
	{
		this.inLayout = false;
	},

    onTabLoaded : function()
    {
        var emptyItems = this.gridManager.getEmptyItems();

        if (emptyItems.length > 0) {
            var emptyKlipIds = _.map(emptyItems,
                function(itemConfig) {
                    return itemConfig.id.slice(itemConfig.id.indexOf("-") + 1);
                });

            var _this = this;
            $.post("/dashboard/ajax_getKlipInstanceInfo", {publicIds:JSON.stringify(emptyKlipIds), currentTabId: this.currentTabId}, function(data) {
                var removed = false;
                _.each(data.klipInfo, function(klip) {
                    if (klip.deleted || klip.doesNotBelong) {
                        if (klip.doesNotBelong) {
                            newRelicNoticeError("Custom Layout Error: [klipId: " + klip.id + ", currentTabId: " + _this.currentTabId + ", companyId: " + KF.company.get("publicId") + "]");
                        }

                        _this.removeComponent(klip);
                        removed = true;
                    } else {
                        _this.gridManager.setEmptyContent("gridklip-" + klip.id);
                    }
                });

                if (removed) {
                    _this.dashboard.checkForEmptyTab();
                    _this.serializeTabState();
                }
            }, "json");
        }
    },


    pageBreakLayout : function(pageHeight, zoom)
    {
        this.numPages = this.gridManager.pageBreakLayout(pageHeight);
    },

    showPage : function(pageNum)
    {
        this.gridManager.showPage(pageNum);
    }

});


(function($)
{

	jQuery.fn.equals = function(selector)
	{
		return $(this).get(0) == $(selector).get(0);
	};

})(jQuery);


function asyncEach (arr, iterator, callback,ctx) {
    if (!arr.length) {
        return _.bind(callback,ctx)();
    }
    var completed = 0;
    _.each(arr, function (x) {
        setTimeout( function(){
        _.bind(iterator,ctx)(x, function (err) {
            if (err) {
                callback(err);
                callback = function () {};
            }
            else {
                completed += 1;
                if (completed === arr.length) {
                    _.bind(callback,ctx)();
                }
            }
        });
        },0);
    });

}

/**
 * after an elapsed time has passed, the doWhile function will be tested.  If it returns
 * true onStart will be called.
 *
 * Config object parameters:
 *
 * delay:  an optional delay to wait before beginning
 * whileTest : callback which returns a boolean,  true if waiting should continue
 * onStart : callback executed on first interval
 * onInterval : callback executed after each test interval
 * onFinish : callback executed after whileTest returns true
 *
 * @param wait
 * @param doWhile
 * @param onFinish
 */
function asyncDoWhile( config )
{
	if ( config.delay ){
		setTimeout(function(){
			config.delay = 0;
			asyncDoWhile(config);
		},config.delay);

		return;
	}

	if ( config.whileTest(config) )
	{
		if ( config.onStart ){
			config.onStart(config);
			delete config.onStart;
		}
		if ( config.onInterval ) config.onInterval(config);

		setTimeout(function(){
			asyncDoWhile(config);
		},100);


	}else{
		if ( config.onFinish ) config.onFinish(config);
	}
}


Dashboard.SCOPE_KLIP = 1;
Dashboard.SCOPE_TAB = 2;
Dashboard.SCOPE_DASHBOARD = 3;
Dashboard.SCOPE_USER = 4;
;/****** c.drilldown_controls.js *******/ 

var DrilldownControls = $.klass({
    /** id for container of our controls */
    containerId : false,

    /** our jquery element */
    $el : false ,
    $addButton : false,

    ddComponent : false,
    dataModel : false,
    levelConfig : false,

    $activeLevel : false,
    nextLevel : 1,

    displayOptions : [
        { name:"Show", value:"show", type:"", blankGroupby:true },
        { name:"Hide", value:"hide", blankGroupby:true },
        { name:"Sum", value:"sum", type:"measure" },
        { name:"Average", value:"average", type:"measure" },
        { name:"Count", value:"count" },
        { name:"Count Distinct", value:"distinct" },
        { name:"Min", value:"min", type:"measure" },
        { name:"Max", value:"max", type:"measure" }
    ],

    /**
     * constructor
     */
    initialize : function(config)
    {
        $.extend(this, config);

        this.$el = $("<div id='drilldown-controls'>")
            .delegate(".dd-level", "click", _.bind(this.onLevelClick, this))
            .delegate("select", "change", _.bind(this.onSelectChange, this))
            .delegate(".dd-level-config", "click", _.bind(this.onConfigClick, this))
            .delegate(".dd-level-remove", "click", _.bind(this.onRemoveClick, this));

        this.$addButton = $("<button>Add Drill Level</button>").click(_.bind(this.onAddClick, this));

        $("#" + this.containerId).append(this.$el);

        var _this=this;
        PubSub.subscribe("workspace-resize",function(evtid,args){
            if (args[0] == "height") _this.handleResize();
        });
    },

    /**
     * Render the drill down level for the given component
     *
     * @param cx
     */
    render : function(cx)
    {
        if (!cx) return;

        this.ddComponent = cx;

        this.dataModel = this.ddComponent.getDrilldownModel();
        this.levelConfig = this.ddComponent.getDrilldownConfig();

        if (!this.levelConfig) return;

        this.renderDom();
    },

    renderDom : function()
    {
        this.showAddButton(false);
        this.$el.empty();

        this.nextLevel = 1;

        if (this.levelConfig.length == 0) {
            this.addDrilldownLevel();
        }

        for (var i = 0; i < this.levelConfig.length; i++) {
            if (i > 0) {
                this.$el.append($("<div class='next-level-arrow'>"));
            }

            var isLastLevel = (i == this.levelConfig.length - 1);

            this.$el.append(this.renderLevel(this.levelConfig[i], this.nextLevel++, isLastLevel));
        }

        if (this.$el.find(".dd-level").last().find("select option").length == 2) {
            this.showAddButton(false);
        } else {
            this.showAddButton(this.levelConfig[this.levelConfig.length - 1].groupby != "");
        }

        customScrollbar($("#tab-drilldown-inner"),$("#tab-drilldown"),"v");
    },

    renderLevel : function(cfg, level, isLastLevel)
    {
        var $level = $("<div class='dd-level'>").data("level", level);

        $level.append($("<div class='dd-level-number'>").text(level))
            .append($("<div class='dd-level-group'>").text("Group by:"))
            .append(this.getGroupBySelect(cfg ? cfg.groupby : false, isLastLevel));

        if (isLastLevel) {
            this.setActiveLevel($level);
        }

        return $level;
    },

    /**
     * Build the drop down list of columns that can be grouped
     *
     * @param selectId
     * @param isLastLevel
     * @return {*}
     */
    getGroupBySelect : function(selectId, isLastLevel)
    {
        var $select = $("<select>");

        if (isLastLevel) $select.append($("<option value=''></option>"));

        for (var i = 0; i < this.dataModel.length; i++) {
            var dm = this.dataModel[i];

            if (selectId == dm.id) {
                $select.append($("<option value='" + dm.id + "'>" + dm.name + "</option>"));
            } else {
                if (isLastLevel) {
                    // can only group fields
                    if (dm.type == "field") {
                        var j = 0;
                        var isGrouped = false;
                        while (j < this.levelConfig.length && !isGrouped) {
                            if (this.levelConfig[j].groupby == dm.id) {
                                isGrouped = true;
                            }

                            j++;
                        }

                        // can't group by already grouped columns
                        if (!isGrouped) {
                            $select.append($("<option value='" + dm.id + "'>" + dm.name + "</option>"));
                        }
                    }
                }
            }
        }

        if (selectId) {
            $select.find("[value='" + selectId + "']").attr("selected", "selected");
        }

        return $select;
    },

    showAddButton : function(show)
    {
        if (show) {
            this.$el.append($("<div class='next-level-arrow'>"))
                .append(this.$addButton);
        } else {
            this.$addButton.prev(".next-level-arrow").remove();
            this.$addButton.detach();
        }
    },

    setActiveLevel : function($level)
    {
        if (this.$activeLevel) {
            this.$activeLevel.removeClass("active");
            this.$activeLevel.find(".dd-level-config").remove();
            this.$activeLevel.find(".dd-level-remove").remove();
        }

        if ($level) {
            this.$activeLevel = $level;

            var levelNum = $level.data("level");

	        var activeDiv = $("<div class='dd-level-config'>Configure other columns at this level</div>").append(help.renderIcon({ topic:"klips/drilldown-columns" }));

	        $level.addClass("active").append(activeDiv);

	        // can't remove level if there is only one
            if (!(levelNum == 1 && this.nextLevel == 2)) {
                $level.append($("<div class='dd-level-remove'>Remove</div>"));
            }

            this.applyDrilldownLevel(levelNum - 1);
        } else {
            this.$activeLevel = false;
        }
    },

    onAddClick : function()
    {
        this.addDrilldownLevel();
        this.renderDom();
    },

    onRemoveClick : function(evt)
    {
        var $level = $(evt.target).parent(".dd-level");
        var levelIdx = $level.data("level") - 1;

        this.levelConfig.splice(levelIdx, 1);

        this.renderDom();

        var $newActiveLevel = this.$el.find(".dd-level").eq(levelIdx);
        if ($newActiveLevel.length > 0) {
            this.setActiveLevel($newActiveLevel);
        }

        PubSub.publish("ddLevel-removed");
    },

    onSelectChange : function(evt)
    {
        var $select = $(evt.target);
        var $level = $select.parent(".dd-level");

        var newVal = $select.val();

        // toggle the "add group level" button
        if ($level.data("level") == this.nextLevel - 1) {
            if (newVal == "") {
                this.showAddButton(false);
            } else {
                if ($select.find("option").length == 2) {
                    this.showAddButton(false);
                } else {
                    if (!this.$addButton.is(":visible")) {
                        this.showAddButton(true);
                    }
                }
            }
        }

        customScrollbar($("#tab-drilldown-inner"),$("#tab-drilldown"),"v");

        var levelIdx = $level.data("level") - 1;
        this.addDrilldownLevel(newVal, levelIdx);

        this.applyDrilldownLevel(levelIdx);
    },

    onLevelClick : function(evt)
    {
        var $target = $(evt.target);

        if ($target.is("select") || $target.is("option") || $target.hasClass("dd-level-config") || $target.hasClass("dd-level-remove")) return;

        var $level = $target.closest(".dd-level");

        if (!$level.hasClass("active")) {
            this.setActiveLevel($level);
        }
    },

    onConfigClick : function(evt)
    {
        var $level = $(evt.target).parent(".dd-level");
        var levelNum = $level.data("level");
        var levelCfg = this.levelConfig[levelNum - 1];

        if (!levelCfg) return;

        var blankGroupby = (levelCfg.groupby == "");

        var $content = $("<div id='other-data-overlay'>").width("300")
            .append($("<h3 style='margin-bottom:10px;'>Other Columns</h3>"))
            .append($("<div style='margin-bottom:10px;'>").html("Select how to handle the data in other columns when the user is at this drill down level."));

        var $otherData = $("<div class='scroll-container strong'>").appendTo($content);
        for (var i = 0; i < this.dataModel.length; i++) {
            var dm = this.dataModel[i];

            var j = 0;
            var isGrouped = false;
            while (j < levelNum && !isGrouped) {
                if (this.levelConfig[j].groupby == dm.id) {
                    isGrouped = true;
                }

                j++;
            }

            // can't configure columns that have already been grouped
            if (!isGrouped) {
                var $displayRow = $("<div style='margin:7px;'>")
                    .append($("<div class='display-data-name'>").text(dm.name).prop("title",dm.name));

                var displayVal = false;
                if (levelCfg.display) {
                    _.each(levelCfg.display, function(d) {
                        if (d.id == dm.id) {
                            displayVal = d.display;
                        }
                    });
                }

                if (!displayVal) displayVal = "hide";

                $otherData.append($displayRow.append(this.getDisplaySelect(dm, displayVal, blankGroupby)));
            }
        }

        var _this = this;
        $content.delegate("select", "change", function() {
            levelCfg.display = _this.serializeOtherData($otherData.find("select"));

            _this.applyDrilldownLevel(levelNum - 1);
        });

        var $overlay = $.overlay($(evt.target), $content, {
            shadow:{
                opacity:0,
                onClick: function(){
                    $overlay.close();
                }
            },
            footer: {
                controls: [
                    {
                        text: "Finished",
                        css: "rounded-button primary",
                        onClick: function() {
                            $overlay.close();
                        }
                    }
                ]
            }
        });

        $overlay.show();
    },

    /**
     * Build the drop down list for the options available when configuring
     * other columns
     *
     * @param dm
     * @param selectId
     * @param blankGroupby
     * @return {*}
     */
    getDisplaySelect : function(dm, selectId, blankGroupby)
    {
        var $select = $("<select>").attr("id", "dataId-"+ dm.id);

        for (var i = 0; i < this.displayOptions.length; i++) {
            var opt = this.displayOptions[i];

            if (blankGroupby) {
                if (opt.blankGroupby) {
                    $select.append($("<option value='" + opt.value + "'>" + opt.name + "</option>"));
                }
            } else {
                if (opt.type == undefined || (opt.type && opt.type == dm.type)) {
                    $select.append($("<option value='" + opt.value + "'>" + opt.name + "</option>"));
                }
            }
        }

        if (selectId) {
            $select.find("[value='" + selectId + "']").attr("selected", "selected");
        }

        return $select;
    },

    serializeOtherData : function($selects)
    {
        var otherData = [];

        $selects.each(function() {
            var val = $(this).val();

            if (val != "hide") {
                otherData.push({
                    id: $(this).attr("id").replace("dataId-", ""),
                    display: val
                });
            }
        });

        return otherData;
    },

    addDrilldownLevel : function(groupby, index)
    {
        var levelCfg = {
            groupby: groupby ? groupby : "",
            display:[]
        };

        for (var i = 0; i < this.dataModel.length; i++) {
            var dm = this.dataModel[i];

            // when the groupby is empty, other columns can be visible or hidden
            if (!groupby || groupby == "") {
                var j = 0;
                var isGrouped = false;
                var lastLevel = (index != undefined) ? index : this.levelConfig.length;

                while (j < lastLevel && !isGrouped) {
                    if (this.levelConfig[j].groupby == dm.id) {
                        isGrouped = true;
                    }

                    j++;
                }

                if (!isGrouped) {
                    levelCfg.display.push({
                        id: dm.id,
                        display: "show"
                    });
                }
            } else {
                // when level is grouped, measures default to sum and fields are hidden
                if (dm.type == "measure") {
                    levelCfg.display.push({
                        id: dm.id,
                        display: "sum"
                    });
                }
            }
        }

        if (index != undefined) {
            this.levelConfig.splice(index, 1, levelCfg);
        } else {
            this.levelConfig.push(levelCfg);
        }
    },

    applyDrilldownLevel : function(levelIdx)
    {
        this.ddComponent.popDrilldownLevel();
        this.ddComponent.pushDrilldownLevel({isTop:true, levelIdx:levelIdx});
    },

    handleResize : function()
    {
        customScrollbar($("#tab-drilldown-inner"),$("#tab-drilldown"),"v");
    }

});
;/****** c.editable_rows.js *******/ 

/**
 * config = {
 *      containerId:"",                 // (required) id of container element for the rows
 *      help:"",                        // (optional) help link for the rows
 *      columns: [                      // (optional) the columns of values
 *          {
 *              header:"",              // (optional) name of the column
 *              id:"",                  // (required) id of the column
 *              type:"",                // (required) ['text', 'select'] type of input for the column
 *              width:"",               // (optional) width of the column
 *              options: [              // (required w/type=select) options to display in the dropdown
 *                  {
 *                      value:"",       // (required) value of the option
 *                      label:""        // (required) label for the option
 *                  },
 *                  ...
 *              ]
 *          },
 *          ...
 *      ],
 *      onChange: function() {},        // (optional) function to execute when a value changes
 *      onAddRow: function() {},        // (optional) function to execute after a row is added
 *      onRemoveRow: function() {}      // (optional) function to execute after a row is removed
 * }
 */
var EditableRows = $.klass({
    /** id for container of our controls */
    containerId : false,

    /** our jquery element */
    $el : false ,

    columns : false,
    numRows : 0,

    /**
     * constructor
     */
    initialize : function(config)
    {
        $.extend(this, config);
        this.renderDom();
    },

    renderDom : function()
    {
        this.$el = $("<table class='editable-rows'>");

        var hasHeaders = false;
        if (this.columns) {
            var $header = $("<tr class='quiet'>");

            for (var i = 0; i < this.columns.length; i++) {
                var column = this.columns[i];
                var $th = $("<th>");

                if (column.header) {
                    hasHeaders = true;
                    $th.text(column.header);
                }
                if (column.width) $th.css("width", column.width);

                $header.append($th);
            }

            $header.append($("<th style='width:62px;'>"));

            if (this.isSortable) {
                $header.append($("<th style='width:12px;'>"));
            }

            this.$el.append($header);
            this.$el.append(this.renderRow());
            this.numRows++;

            this.$el.find("button.minus").attr("disabled", "disabled").addClass("disabled");
        }

        var $container = $("#" + this.containerId).append(this.$el);

        if (this.isSortable) {
            $container.addClass("sortable");

            this.$el.find("tbody").sortable({
                handle: ".editable-sort-handle",

                // Return a helper with preserved width of cells
                helper: function(e, ui) {
                    ui.children().each(function() {
                        $(this).width($(this).width());
                    });
                    return ui;
                }
            });
        }

        if (this.help) {
            $container.addClass("editable-rows-container")
                .append(help.renderIcon({ topic:this.help }));

            var $help = $container.find(".help-icon");
            if (hasHeaders) {
                var top = parseInt($help.css("top"));
                $help.css("top", (top + 17) + "px");
            }
        }

        this.$el.delegate("button.minus", "click", _.bind(this.removeRow, this));
        this.$el.delegate("button.plus", "click", _.bind(this.addRow, this));

        if (this.onChange) {
            this.$el.delegate("input[type='text']", "change keyup", _.bind(this.onChange, this));
            this.$el.delegate("select", "change", _.bind(this.onChange, this));
        }

        // stop pressing 'enter' in textfield from submitting form
        this.$el.delegate("input[type='text']", "keypress", function(evt) {
            if (evt.keyCode == "13") {
                evt.preventDefault();
            }
        });
    },

    renderRow : function(data)
    {
        var $row = "";
        var $text = "";

        if (this.columns) {
            $row = $("<tr class='value-row'>");

            for (var i = 0; i < this.columns.length; i++) {
                var column = this.columns[i];
                var $td = $("<td class='editable-data'>").data("columnId", column.id).data("columnType", column.type);

                switch (column.type) {
                    case "text":
                    {
                        $text = $("<input type='text' class='textfield'>");
                        if(column.className) $text.addClass(column.className);
                        if (column.placeholder) $text.attr("placeholder", column.placeholder);
                        if (data && data[column.id]) $text.val(data[column.id]);

                        $td.append($text);
                        break;
                    }
                    case "select":
                    {
                        var $select = $("<select class='selectfield'>");

                        if (column.options) {
                            for (var j = 0; j < column.options.length; j++) {
                                var opt = column.options[j];
                                $select.append($("<option>").attr("value", opt.value).text(opt.label));
                            }
                        }

                        if (data && data[column.id]) $select.val(data[column.id]);

                        $td.append($select);
                        break;
                    }
                }

                $row.append($td);
            }

            $row.append($("<td>")
                .append($("<button class='minus' style='margin-right:5px;'>-</button>"))
                .append($("<button class='plus'>+</button>")));

            if (this.isSortable) {
                $row.append($("<td class='editable-sort-handle'>"));
            }
        }

        return $row;
    },

    addRow : function(evt)
    {
        evt.preventDefault();

        var $currRow = $(evt.target).closest("tr");

        $currRow.after(this.renderRow());

        this.numRows++;
        this.toggleMinus();

        if (this.onAddRow) this.onAddRow();
    },

    removeRow : function(evt)
    {
        evt.preventDefault();

        var $currRow = $(evt.target).closest("tr");

        $currRow.remove();

        this.numRows--;
        this.toggleMinus();

        if (this.onRemoveRow) this.onRemoveRow();
    },

    serializeRows : function(includeEmptyValues)
    {
        var config = [];

        this.$el.find("tr.value-row").each(function() {
            var rowVals = {};

            $(this).find("td.editable-data").each(function() {
                var val = "";
                switch ($(this).data("columnType")) {
                    case "text":
                        val = $(this).children("input[type='text']").val();
                        break;
                    case "select":
                    {
                        val = $(this).children("select").val();
                        break;
                    }
                }

                if (val != "") {
                    rowVals[$(this).data("columnId")] = val;
                } else {
                    if (includeEmptyValues) {
                        rowVals[$(this).data("columnId")] = val;
                    }
                }
            });

            if (!$.isEmptyObject(rowVals)) {
                config.push(rowVals);
            }
        });

        return config;
    },

    toString : function(includeEmptyValues) {
        return JSON.stringify(this.serializeRows(includeEmptyValues));
    },

    configureRows : function(data)
    {
        if (data && data.length > 0) {
            this.$el.find("tr.value-row").remove();
            this.numRows = 0;

            for (var i = 0; i < data.length; i++) {
                this.$el.append(this.renderRow(data[i]));
                this.numRows++;
            }

            this.toggleMinus();
        }
    },

    toggleMinus : function()
    {
        if (this.numRows == 1) {
            this.$el.find("button.minus").attr("disabled", "disabled").addClass("disabled");
        } else {
            this.$el.find("button.disabled").removeAttr("disabled").removeClass("disabled");
        }
    }

});
;/****** c.grid_layout_manager.js *******/ 

/* global isWebkit:false */
GridLayoutManager = $.klass({

    $container: false,
    $grid: false,
    showLines: false,
    showColumnWidths: false,
    colConfigs: false,
    placeholderConfig: false,
    $placeholderContainer: false,

    /* set through methods or restore state */
    cols: 1,
    colWidths: false,
    itemConfigs: false,

    /* set at initialization */
    zoom: false,            // {el:"", property:""}
    gutter: 5,
    dragHandle: false,
    columnTolerance: 0,
    aboveLineTolerance: 10,
    belowLineTolerance: 10,
    itemsDraggable: true,
    itemsResizable: true,
    $emptyContent: false,
    emptyDraggable: true,
    emptyResizable: true,

    initialize: function(config) {
        $.extend(this, config);

        this.colWidths = [];
        this.colConfigs = [];
        this.itemConfigs = [];

        this.itemPages = [];
        this.pagePointers = [];
        this.pageColumnHeights = [[]];

        this.$container.addClass("grid-layout-container");

        // setup the placeholder
        this.$placeholderContainer = $("<div class='item-container placeholder'>")
            .css("margin-bottom", this.gutter * 2 + "px")
            .appendTo(this.$container).hide();
        this.placeholderConfig = {$container:this.$placeholderContainer};

        this.renderGrid();
    },

    /**
     * Add grid to container with the given number of columns sized properly
     */
    renderGrid: function() {
        var $widthRow;
        var $layoutRow;
        var $widthCell;
        var $cell;
        var colWidth;
        var i;

        if (this.$grid) this.$grid.remove();

        this.$grid = $("<table class='grid-layout'>").addClass(this.showLines ? "gridlines" : "");

        $widthRow = $("<tr class='width-row'>");
        $layoutRow = $("<tr class='layout-row'>");

        for (i = 0; i < this.cols; i++) {
            $widthCell = $("<td class='width-cell'>");
            $cell = $("<td class='layout-cell'>");

            if (!this.colWidths[i]) this.colWidths[i] = "auto";
            colWidth = this.colWidths[i];

            $widthCell.css("width", colWidth).appendTo($widthRow);
            $cell.css("width", colWidth).appendTo($layoutRow);

            $widthCell.text(colWidth == "auto" ? "Auto" : colWidth);
        }

        this.$container.append(this.$grid.append($widthRow).append($layoutRow));

        if (!this.showColumnWidths) $widthRow.hide();
    },

    /**
     * Set whether the items are draggable or not
     *
     * @param draggable
     */
    setItemsDraggable: function(draggable) {
        this.itemsDraggable = draggable;
    },

    /**
     * Set whether the items are resizable or not
     *
     * @param resizable
     */
    setItemsResizable: function(resizable) {
        this.itemsResizable = resizable;
    },

    /**
     * Show/hide the column width controls
     *
     * @param showColumnWidths
     */
    toggleColumnWidths: function(showColumnWidths) {
        var $widthRow;

        this.showColumnWidths = showColumnWidths;

        $widthRow = this.$grid.find("tr.width-row");
        if (showColumnWidths) {
            $widthRow.show();
        } else {
            $widthRow.hide();
        }

        // don't need to redo the layout setup, so skip it
        this.doLayout(true);
    },

    /**
     * Show/hide the grid lines
     *
     * @param showLines
     */
    toggleGridLines: function(showLines) {
        this.showLines = showLines;

        if (showLines) {
            this.$grid.addClass("gridlines");
        } else {
            this.$grid.removeClass("gridlines");
        }
    },

    /**
     * Return the number of columns in the grid
     *
     * @returns {number}
     */
    getCols: function() {
        return this.cols;
    },

    /**
     * Set the number of columns in the grid
     *
     * @param cols
     */
    setCols: function(cols) {
        var i, itemsToMove, itemConfig, colsRemoved, nextVertIdx;

        if (cols < 1) cols = 1;

        colsRemoved = (cols < this.cols);
        this.cols = cols;

        if (colsRemoved) this.colWidths = this.colWidths.slice(0, this.cols);
        this.renderGrid();

        if (this.itemConfigs && this.itemConfigs.length > 0) {
            if (colsRemoved) {
                itemsToMove = [];

                // find the orphaned items
                i = this.itemConfigs.length - 1;
                while (i >= 0) {
                    itemConfig = this.itemConfigs[i];

                    if (itemConfig.index[0] >= this.cols) {
                        itemsToMove.push(this.itemConfigs.splice(i, 1)[0]);
                    }

                    i--;
                }

                this.sortIndexedConfigs(itemsToMove, "index", 0, 1);

                // put them at the end of the last column
                nextVertIdx = this.colConfigs[this.cols - 1].length;
                for (i = 0; i < itemsToMove.length; i++) {
                    itemConfig = itemsToMove[i];

                    itemConfig.index = [this.cols - 1, nextVertIdx++];
                    this.itemConfigs.push(itemConfig);
                }
            }

            this.updateItemWidths();
        }
    },

    /**
     * Set the width of the column at the given index
     *
     * @param colIdx
     * @param width
     */
    setColumnWidth: function(colIdx, width) {
        this.colWidths[colIdx] = width;

        this.$grid.find("td:eq(" + colIdx + ")").css("width", width);

        // don't need to redo the layout setup, so skip it
        this.updateItemWidths(true);
    },

    /**
     * Restore the state of the grid from item configs
     *
     * {
     *   cols:#,                 // (required)
     *   colWidths:[],           // (optional)
     *   itemConfigs : [         // (required)
     *     {
     *       id:"",              // (required)
     *       minHeight:#         // (required) px - min height the item can be
     *       height:#            // (optional) px - current height of the item
     *       index:[#, #],       // (optional)
     *       colSpan:#           // (optional)
     *       $content:$()        // (optional)
     *     }
     *   ]
     * }
     *
     * @param state
     */
    restoreState: function(state) {
        var i, itemConfig;

        this.zoomFactor = this.findZoomFactor();
        this.colWidths = state.colWidths;

        this.setCols(state.cols);

        this.itemConfigs = state.itemConfigs;
        for (i = 0; i < this.itemConfigs.length; i++) {
            itemConfig = this.itemConfigs[i];
            this.createItemContainer(itemConfig);
        }

        this.doLayout();
    },

    /**
     * Find the zoom used on the grid to be able to compensate for it
     *
     * @returns {*}
     */
    findZoomFactor: function() {
        var zoom = [1, 1];
        var matrix, matrixRegex, matches;

        if (!this.zoom) return zoom;

        matrix = $(this.zoom.el).css(this.zoom.property);

        if (matrix) {
            matrixRegex = /matrix\((-?\d*\.?\d+),\s*0,\s*0,\s*(-?\d*\.?\d+),\s*0,\s*0\)/;
            matches = matrix.match(matrixRegex);

            if (matches != null) {
                zoom = [parseInt(matches[1]), parseInt(matches[2])]; // [scaleX, scaleY]
            }
        }

        return zoom;
    },

    /**
     * Return an object of everything needed to restore the grid
     *
     * @returns {{}}
     */
    serializeState: function() {
        var state = {};
        var i, itemConfig;

        state.cols = this.cols;
        state.colWidths = this.colWidths.slice(0);
        state.itemConfigs = [];

        for (i = 0; i < this.itemConfigs.length; i++) {
            itemConfig = this.itemConfigs[i];

            state.itemConfigs.push({
                id: itemConfig.id,
                minHeight: itemConfig.minHeight,
                height: itemConfig.height,
                index: itemConfig.index.slice(0),
                colSpan: itemConfig.colSpan
            });
        }

        return state;
    },

    /**
     * Add item to grid
     *
     * itemConfig = {
     *      id:"",              // (required)
     *      minHeight:#         // (required) px
     *      height:#            // (optional) px
     *      index:[#, #],       // (optional)
     *      colSpan:#,          // (optional)
     *      $content:$()        // (optional)
     * }
     *
     * NOTE: Watch out for items with vertIdx greater than it needs to be.
     *       May need to change their vertIdx to be the next idx available.
     *
     * @param itemConfig
     * @param vertOffsets - where the item should be positioned in each column
     */
    addItem: function(itemConfig, vertOffsets) {
        if (!itemConfig || itemConfig.id === undefined || !itemConfig.minHeight) return;

        // item index must be within the bounds of the grid
        if (!itemConfig.index || itemConfig.index.length < 2
            || (itemConfig.index[0] < 0 || itemConfig.index[0] >= this.cols.length || itemConfig.index[1] < 0))
            itemConfig.index = [0, 0];

        if (!itemConfig.colSpan) itemConfig.colSpan = 1;

        this.createItemContainer(itemConfig);

        // reindex current items
        this.reindexItems(itemConfig, 1, vertOffsets);
        this.itemConfigs.push(itemConfig);

        this.doLayout();

        return itemConfig;
    },

    /**
     * Remove item by index or id
     *
     * itemConfig = {
     *      index:[#, #],
     *      id:""
     * }
     *
     * @param itemConfig
     * @param noRemoveContainer - don't actually remove the container (used for dragging and resizing)
     * @param doNotLayout - do not layout the remaining items
     * @returns {*}
     */
    removeItem: function(itemConfig, noRemoveContainer, doNotLayout) {
        var oldConfig, vertOffsets;

        if (!itemConfig) return false;

        oldConfig = this.getItemConfig(itemConfig, "index");
        if (!oldConfig) return false;

        if (!noRemoveContainer) {
            if (oldConfig.$content) oldConfig.$content.detach();
            oldConfig.$container.remove();
        }

        this.itemConfigs.splice(oldConfig.configIdx, 1);

        vertOffsets = this.getVertOffsets(oldConfig.index[0], oldConfig.index[0] + oldConfig.colSpan, oldConfig.$container.position().top);
        this.reindexItems(oldConfig, -1, vertOffsets);

        if (!doNotLayout) this.doLayout();

        return {itemConfig:oldConfig, vertOffsets:vertOffsets};
    },

    /**
     * Remove all items from the grid
     */
    removeAllItems: function() {
        var itemConfig;

        var i = this.itemConfigs.length - 1;

        while (i >= 0) {
            itemConfig = this.itemConfigs.pop();

            if (itemConfig.$content) itemConfig.$content.detach();
            itemConfig.$container.remove();

            i--;
        }

        this.colConfigs = [];
        this.nextTops = [];

        this.setGridHeight(this.nextTops);
    },

    /**
     * Change the min height of an item
     *
     * itemConfig = {
     *      index:[#, #],
     *      id:"",
     *      minHeight:#        // px
     * }
     *
     * @param itemConfig
     * @param doNotLayout
     */
    setItemMinHeight: function(itemConfig, doNotLayout) {
        var oldConfig, itemHeight;

        if (!itemConfig || !itemConfig.minHeight) return;

        oldConfig = this.getItemConfig(itemConfig, "index");
        if (!oldConfig) return;

        oldConfig.minHeight = itemConfig.minHeight;

        itemHeight = oldConfig.minHeight;
        if (oldConfig.height && oldConfig.height > itemHeight) {
            itemHeight = oldConfig.height;
        } else {
            delete oldConfig["height"];
        }

        oldConfig.$container.find(".item-resizable").height(itemHeight);

        if (this.onUpdateMinHeight) this.onUpdateMinHeight(oldConfig);

        // don't need to redo the layout setup, so skip it
        if (!doNotLayout) this.doLayout(true);
    },

    /**
     * Change the content of an item
     *
     * itemConfig = {
     *      index:[#, #],
     *      id:"",
     *      $content:$()
     * }
     *
     * @param itemConfig
     * @param searchOnId
     */
    setItemContent : function(itemConfig, searchOnId) {
        var oldConfig;

        if (!itemConfig || !itemConfig.$content) return;

        oldConfig = this.getItemConfig(itemConfig, "index", searchOnId);
        if (!oldConfig) return;

        oldConfig.$content = itemConfig.$content;
        oldConfig.$container.find(".empty-content").remove();
        oldConfig.$container.find(".item-resizable").append(oldConfig.$content);
    },

    /**
     * Returns the items that do not have content
     *
     * @returns {Array}
     */
    getEmptyItems: function() {
        var i, itemConfig;
        var emptyItems = [];

        for (i = 0; i < this.itemConfigs.length; i++) {
            itemConfig = this.itemConfigs[i];

            if (!itemConfig.$content) {
                emptyItems.push(itemConfig);
            }
        }

        return emptyItems;
    },

    /**
     * Set the content of empty items
     */
    setEmptyContent: function(id) {
        var itemConfig, alsoResize, $emptyContent, $itemResizable;

        if (!this.$emptyContent) return;

        itemConfig = this.getItemConfig({id:id});

        if (!itemConfig.$content) {
            $emptyContent = this.$emptyContent.clone().addClass("empty-content");
            $itemResizable = itemConfig.$container.find(".item-resizable");

            $itemResizable.children(".empty-content").remove();         // clear any previous empty content
            $itemResizable.append($emptyContent);

            $emptyContent.height($itemResizable.height() - ($emptyContent.outerHeight(true) - $emptyContent.height()));

            if (this.itemsDraggable && !this.emptyDraggable) {
                itemConfig.$container.draggable("destroy");
            }

            if (this.itemsResizable) {
                if (!this.emptyResizable) {
                    $itemResizable.resizable("destroy");
                } else {
                    alsoResize = $itemResizable.resizable("option", "alsoResize");

                    $itemResizable.resizable("option", {
                        alsoResize:alsoResize + ", #" + itemConfig.id + " .empty-content"
                    });
                }
            }
        }
    },

    /**
     * Update the width of all the items
     */
    updateItemWidths: function(skipLayoutSetup) {
        var i;

        if (!this.itemConfigs || this.itemConfigs.length == 0) return;

        for (i = 0; i < this.itemConfigs.length; i++) {
            this.setItemWidth(this.itemConfigs[i]);
        }

        this.doLayout(skipLayoutSetup);
    },

    /**
     * Get the width that a container would be at given location
     *
     * itemConfig = {
     *      index:[#,#],    // (required)
     *      colSpan:#       // (required)
     * }
     *
     * @param itemConfig
     */
    getContainerWidth: function(itemConfig) {
        var containerWidth, $endCol;
        var colIdx = itemConfig.index[0];
        var $col = this.$grid.find(".layout-cell:eq(" + colIdx + ")");

        // find the column after the end of the item
        var $nextCol = $col;
        var j = 0;
        while ($nextCol.length > 0 && j < itemConfig.colSpan) {
            $nextCol = $nextCol.next();
            j++;
        }

        itemConfig.colSpan = j;       // set the actual span of the item in case it had to be shrunk

        $endCol = this.$grid.find(".layout-cell:eq(" + (colIdx + itemConfig.colSpan - 1) + ")");

        containerWidth = ($endCol.position().left / this.zoomFactor[0])  + $endCol.width() + 1;

        if (isWebkit()) {
            if (colIdx + itemConfig.colSpan == this.cols) containerWidth += 1;
        }

        // remove extra width before the origin column and gutter space
        containerWidth -= ($col.position().left / this.zoomFactor[0]) + this.gutter * 2;

        return Math.max(0, Math.floor(containerWidth));
    },


    /**
     * Create the container to hold the item
     *
     * @param itemConfig
     */
    createItemContainer: function(itemConfig) {
        var itemHeight, $itemResize;
        itemConfig.$container = $("<div class='item-container'>")
            .attr("id", itemConfig.id)
            .css({"margin-bottom":this.gutter * 2 + "px"})
            .appendTo(this.$container);

        itemHeight = itemConfig.minHeight;
        if (itemConfig.height && itemConfig.height > itemHeight) {
            itemHeight = itemConfig.height;
        } else {
            delete itemConfig["height"];
        }

        // needed for the resize handles to be placed
        $itemResize = $("<div class='item-resizable'>")
            .height(itemHeight)
            .appendTo(itemConfig.$container);

        this.setItemWidth(itemConfig);

        if (itemConfig.$content) {
            try {
                $itemResize.append(itemConfig.$content);
            } catch (appendError) {
                console.warn("Content of itemConfig{id="+itemConfig.id + "} could not be appended to DOM. Details:" + appendError);
            }

        }

        if (this.itemsDraggable) {
            this.enableDraggable(itemConfig.$container);
        }

        if (this.itemsResizable) {
            this.enableResizable($itemResize, {
                alsoResize: "#" + itemConfig.id
            });
        }
    },

    /**
     * Return specific item config by index/origin or id
     *
     * @param config
     * @param index - 'index' (item) or 'origin' (column)
     * @param searchOnId
     * @returns {*}
     */
    getItemConfig: function(config, index, searchOnId) {
        var j, itemConfig;

        var itemIndex = (!searchOnId && config[index] !== undefined ? config[index] : false);
        var id = (config.id !== undefined ? config.id : false);

        if (itemIndex !== false || id !== false) {
            for (j = 0; j < this.itemConfigs.length; j++) {
                itemConfig = this.itemConfigs[j];
                itemConfig.configIdx = j;

                if (itemIndex && this.indicesEqual(itemConfig.index, itemIndex)) {
                    return itemConfig;
                } else if (id !== false && itemConfig.id == id) {
                    return itemConfig;
                }
            }
        }

        return false;
    },

    /**
     * Re-index the items to account for adding/removing items
     *
     * @param itemConfig
     * @param indexDiff
     * @param vertOffsets - where to insert/remove the item
     */
    reindexItems: function(itemConfig, indexDiff, vertOffsets) {
        var i, index;
        var colIdx = itemConfig.index[0];
        var vertIdx = itemConfig.index[1];
        var colSpan = itemConfig.colSpan;

        for (i = 0; i < this.itemConfigs.length; i++) {
            index = this.itemConfigs[i].index;

            if (colIdx <= index[0] && index[0] < (colIdx + colSpan)) {
                if (index[1] >= (vertOffsets ? vertOffsets[index[0] - colIdx] : vertIdx)) {
                    index[1] += indexDiff;
                }
            }
        }
    },

    /**
     * Determine the vertical indices of item over whole span based on position
     *
     * @param colIdx
     * @param spanIdx
     * @param top
     * @returns {Array}
     */
    getVertOffsets: function(colIdx, spanIdx, top) {
        var i;
        var vertOffsets = [];

        for (i = colIdx; i < spanIdx; i++) {
            vertOffsets.push(this.findVertIdx(top, this.colConfigs[i]));
        }

        return vertOffsets;
    },

    /**

    /**
     * Find the vertical index of item based on position and where it collides with other items
     *
     * @param itemTop
     * @param colConfig
     * @returns {number}
     */
    findVertIdx: function(itemTop, colConfig) {
        var i, $colItemContainer, colItemTop, colItemHeight;
        var vertIdx = 0;

        if (colConfig) {
            for (i = 0; i < colConfig.length; i++) {
                $colItemContainer = colConfig[i].$container;

                colItemTop = $colItemContainer.position().top;
                colItemHeight = $colItemContainer.outerHeight();

                // when items collide determine if the given item should go before or after
                if (colItemTop <= itemTop && itemTop <= colItemTop + colItemHeight) {
                    if (itemTop <= colItemTop + colItemHeight / 2) {
                        vertIdx = i;
                    } else if (itemTop > colItemTop + colItemHeight / 2) {
                        vertIdx = i + 1;
                    }

                    break;
                } else if (itemTop > colItemTop + colItemHeight) {
                    vertIdx = i + 1;
                }
            }
        }

        return vertIdx;
    },

    /**
     * Set the width of the item container
     *
     * @param itemConfig
     */
    setItemWidth: function(itemConfig) {
        var containerWidth = this.getContainerWidth(itemConfig);

        itemConfig.$container.width(containerWidth);
        itemConfig.$container.children(".item-resizable").width(containerWidth);

        if (this.onUpdateWidth) this.onUpdateWidth(itemConfig);
    },


    /**
     * Position items on the grid according to column configs
     *
     * @param skipSetup - if true skip the setup of the column configs
     * @param includePlaceholder - take into account the height of the placeholder
     */
    doLayout: function(skipSetup, includePlaceholder) {
        var i, j, len, layoutRowTop, colConfig;

        // order the item configs then determine the column configs
        if (!skipSetup) {
            this.sortIndexedConfigs(this.itemConfigs, "index", 0, 1);
            this.setupColumnConfigs();
        }

        this.placeholderBottom = this.$placeholderContainer.position().top + this.$placeholderContainer.outerHeight(true);
        this.pointers = [];
        this.nextTops = [];

        layoutRowTop = this.$grid.find(".layout-row").position().top;

        for (i = 0, len = this.cols; i < len; i++) {
            this.nextTops[i] = layoutRowTop + this.gutter * 2;
        }

        for (i = 0, len = this.cols; i < len; i++) {
            colConfig = this.colConfigs[i];
            if (!colConfig) {
                console.error("colConfig should be defined", this.cols, i, this.colConfigs);
                continue;
            }

            if (this.pointers[i] === undefined) this.pointers[i] = 0;

            j = this.pointers[i];
            while (j < colConfig.length) {
                this.positionItem(colConfig[j], includePlaceholder);

                j++;
            }

            // set the nextTop as the placeholderBottom when it is the last item in a column
            if (includePlaceholder) {
                if (this.addVertOffsets[i - this.placeholderConfig.index[0]] == this.pointers[i]) {
                    this.nextTops[i] = this.placeholderBottom;
                }
            }
        }

        // set the grid of the height since it is not set automatically
        this.setGridHeight(this.nextTops, includePlaceholder);
    },

    /**
     * Setup the column configs so the item pieces are in the correct vertical order.
     * The item configs need to be in lexicographical order.
     */
    setupColumnConfigs: function() {
        var i, j, k, colConfig, prevColItemConfig, itemConfig, colIdx, vertIdx;
        var itemPointer = 0;
        var itemLength = this.itemConfigs.length;
        var prevColConfig = [];

        this.colConfigs = [];

        for (i = 0; i < this.cols; i++) {
            colConfig = this.colConfigs[i];
            if (!colConfig) colConfig = [];

            // current column is based off the previous column to achieve correct order
            if (i > 0) {
                prevColConfig = _.clone(this.colConfigs[i - 1]);

                // remove items that don't extend to this column
                k = prevColConfig.length - 1;
                while (k >= 0) {
                    prevColItemConfig = prevColConfig[k];
                    if (prevColItemConfig.origin[0] + prevColItemConfig.colSpan == i) {
                        prevColConfig.splice(k, 1);
                    }

                    k--;
                }

                colConfig = prevColConfig;
            }

            // insert items that start in this column into their vertical position;
            // since items are in order, can keep a pointer for which items have been processed
            for (j = itemPointer; j < itemLength; j++) {
                itemConfig = this.itemConfigs[j];
                colIdx = itemConfig.index[0];
                vertIdx = itemConfig.index[1];

                if (colIdx == i) {
                    colConfig.splice(vertIdx, 0, {origin:itemConfig.index, colSpan:itemConfig.colSpan, $container:itemConfig.$container});
                } else if (colIdx > i) {
                    itemPointer = j;
                    break;
                }
            }

            this.colConfigs[i] = colConfig;
        }
    },

    /**
     * Position the item once all items before it have been positioned
     *
     * @param colItemConfig
     * @param includePlaceholder
     */
    positionItem: function(colItemConfig, includePlaceholder) {
        var i, allPlacedBefore, currItemConfig, top, $col, left;
        var colIdx = colItemConfig.origin[0];
        var vertIdx = colItemConfig.origin[1];
        var colSpan = colItemConfig.colSpan;

        if (includePlaceholder) {
            if (this.addVertOffsets[colIdx - this.placeholderConfig.index[0]] == this.pointers[colIdx]) {
                this.nextTops[colIdx] = this.placeholderBottom;
            }
        }

        for (i = colIdx + 1; i < colIdx + colSpan; i++) {
            if (this.pointers[i] === undefined) this.pointers[i] = 0;

            allPlacedBefore = false;
            while (!allPlacedBefore) {
                if (includePlaceholder) {
                    if (this.addVertOffsets[i - this.placeholderConfig.index[0]] == this.pointers[i]) {
                        this.nextTops[i] = this.placeholderBottom;
                    }
                }

                currItemConfig = this.colConfigs[i][this.pointers[i]];

                if (currItemConfig) {
                    if (!this.indicesEqual(currItemConfig.origin, colItemConfig.origin)) {
                        this.positionItem(currItemConfig, includePlaceholder);
                    } else {
                        allPlacedBefore = true;
                    }
                }
            }
        }

        top = _.max(this.nextTops.slice(colIdx, colIdx + colSpan));

        for (i = colIdx; i < colIdx + colSpan; i++) {
            this.nextTops[i] = top + colItemConfig.$container.outerHeight(true);
            this.pointers[i]++;
        }

        $col = this.$grid.find(".layout-cell:eq(" + colIdx + ")");
        left = ($col.position().left / this.zoomFactor[0]) + this.gutter + (isWebkit() ? 0 : -1);

        colItemConfig.$container.css({ top:top, left:left })
            .attr({colIdx:colIdx, vertIdx:vertIdx});
    },

    /**
     * Set the height of the grid to be the height of the tallest column
     */
    setGridHeight: function(tops, includePlaceholder) {
        var $layoutRow;

        var maxHeight = 0;
        if (this.itemConfigs.length > 0 || includePlaceholder) {
            maxHeight = _.max(tops);
        }

        $layoutRow = this.$grid.find(".layout-row");
        maxHeight -= $layoutRow.position().top;

        $layoutRow.height(Math.max(maxHeight, 0));
    },

    /**
     * Sort configs with the given parameters
     *
     * @param configs
     * @param index - 'index' (item) or 'origin' (column)
     * @param first - 0 (colIdx), 1 (vertIdx)
     * @param second - 0 (colIdx), 1 (vertIdx)
     * @param firstOrder - 'asc' (default), 'desc'
     * @param secondOrder - 'asc' (default), 'desc'
     */
    sortIndexedConfigs: function(configs, index, first, second, firstOrder, secondOrder) {
        configs.sort(function(a, b) {
            var aFirst = a[index][first];
            var aSecond = a[index][second];
            var bFirst = b[index][first];
            var bSecond = b[index][second];

            if (aFirst > bFirst) {
                return (firstOrder == "desc" ? -1 : 1);
            } else if (aFirst < bFirst) {
                return (firstOrder == "desc" ? 1 : -1);
            } else {
                if (aSecond > bSecond) {
                    return (secondOrder == "desc" ? -1 : 1);
                } else if (aSecond < bSecond) {
                    return (secondOrder == "desc" ? 1 : -1);
                } else {
                    return 0;
                }
            }
        });
    },

    /**
     * Determine if two indices are equal
     *
     * @param aIndex
     * @param bIndex
     * @returns {boolean}
     */
    indicesEqual: function(aIndex, bIndex) {
        return (aIndex[0] == bIndex[0] && aIndex[1] == bIndex[1]);
    },


    pageBreakLayout: function(pageHeight) {
        var i, j, colConfig;
        this.itemPages = [];

        this.pagePointers = [];
        this.pageColumnHeights = [[]]; // current position in each page, split by column

        this.layoutRowTop = this.$grid.find(".layout-row").position().top;
        for (i = 0; i < this.cols; i++) {
            this.pageColumnHeights[0][i] = this.layoutRowTop;
        }

        for (i = 0; i < this.cols; i++) {
            colConfig = this.colConfigs[i];

            if (this.pagePointers[i] === undefined) this.pagePointers[i] = 0;

            j = this.pagePointers[i];
            while (j < colConfig.length) {
                this.pageBreakPositionItem(pageHeight, colConfig[j]);

                j++;
            }
        }

        return this.itemPages.length;
    },

    pageBreakPositionItem: function(pageHeight, colItemConfig) {
        this._checkPlacedBefore(colItemConfig, pageHeight);

        return this._placeOnPage(colItemConfig, pageHeight);
    },

    /* Page Break Position Item Helpers */

    _checkPlacedBefore: function(colItemConfig, pageHeight) {
        var pageIndex, allPlacedBefore, currentItemConfig;
        var colIdx = this.getColItemStartColumn(colItemConfig);
        var colSpan = this.getColItemSpan(colItemConfig);

        for (pageIndex = colIdx + 1; pageIndex < colIdx + colSpan; pageIndex++) {
            if (this.pagePointers[pageIndex] === undefined) {
                this.pagePointers[pageIndex] = 0;
            }

            allPlacedBefore = false;
            while (!allPlacedBefore) {

                currentItemConfig = this.colConfigs[pageIndex][this.pagePointers[pageIndex]];

                if (currentItemConfig) {
                    if (!this.indicesEqual(currentItemConfig.origin, colItemConfig.origin)) {
                        this.pageBreakPositionItem(pageHeight, currentItemConfig);
                    } else {
                        allPlacedBefore = true;
                    }
                }
            }
        }
    },

    _placeOnPage: function(colItemConfig, pageHeight) {
        var pageIndex = 0;
        var itemPlaced = false;

        var height = this.getColItemHeight(colItemConfig, false);
        var colItemFirstColumn = this.getColItemStartColumn(colItemConfig);
        var colItemSpan = this.getColItemSpan(colItemConfig);

        var currentColumnPosition, usedColumnHeight;

        if (height > pageHeight) {
            this._addColItemToNewPage(colItemConfig, pageHeight);
            itemPlaced = true;
        }

        while (!itemPlaced && pageIndex < this.pageColumnHeights.length) {
            currentColumnPosition = this._getUsedColumnHeight(pageIndex, colItemFirstColumn, colItemSpan);
            usedColumnHeight = currentColumnPosition - (this.gutter * 2); // account for margin above and below item

            if ((usedColumnHeight + height) <= pageHeight) {
                this._addColItemToPage(colItemConfig, pageIndex, colItemFirstColumn, currentColumnPosition, pageHeight);
                itemPlaced = true;
            } else {
                this._initPageColumnHeightsForPage(pageIndex + 1);
            }

            pageIndex++;
        }

        return itemPlaced;
    },

    _addColItemToNewPage: function(colItemConfig, pageHeight) {
        var pageIndex = this.itemPages.length;
        var colIdx = this.getColItemStartColumn(colItemConfig);
        var columnSpan = this.getColItemSpan(colItemConfig);
        var heightWithMargin = this.getColItemHeight(colItemConfig, true);

        this._initPageColumnHeightsForPage(pageIndex);
        this._addToPage(colItemConfig, pageIndex, this.layoutRowTop);
        this._updatePageColumnsForNewPage(pageIndex, colIdx, columnSpan, heightWithMargin, pageHeight)
    },

    _addColItemToPage: function(colItemConfig, pageIndex, colIdx, columnPosition, pageHeight) {
        var colSpan = this.getColItemSpan(colItemConfig);
        var heightWithMargin = this.getColItemHeight(colItemConfig, true);

        this._addToPage(colItemConfig, pageIndex, columnPosition);
        this._updatePageColumns(pageIndex, colIdx, colSpan, columnPosition + heightWithMargin, pageHeight);
    },

    _addToPage: function(colItemConfig, pageIndex, columnPosition) {
        if (this.itemPages[pageIndex] === undefined) {
            this.itemPages[pageIndex] = [];
        }

        this.itemPages[pageIndex].push(colItemConfig.$container.attr("id"));

        colItemConfig.$container.css("top", columnPosition);
    },

    // Sets the used height of each column the item spans
    _updatePageColumns: function(pageIndex, columnIndex, columnSpan, desiredHeight, pageHeight) {
        var currentColumnIndex;

        for (currentColumnIndex = columnIndex; currentColumnIndex < columnIndex + columnSpan; currentColumnIndex++) {
            // block the previous page for the columns the item spans to maintain klip order
            if(pageIndex > 0){
                if(this.pageColumnHeights[pageIndex - 1][currentColumnIndex] < pageHeight){
                    this._fillPageColumn(pageIndex - 1, currentColumnIndex, pageHeight);
                }
            }

            this.pageColumnHeights[pageIndex][currentColumnIndex] = desiredHeight;
            this.pagePointers[currentColumnIndex]++;
        }
    },

    _updatePageColumnsForNewPage: function(pageIndex, colIdx, colSpan, height, pageHeight) {
        var currentColumnIndex;

        this.pageColumnHeights[pageIndex] = [];
        for (currentColumnIndex = 0; currentColumnIndex < this.cols; currentColumnIndex++) {
            // Fill the previous page to maintain order of klips
            if(pageIndex > 0){
                if(this.pageColumnHeights[pageIndex - 1][currentColumnIndex] < pageHeight){
                    this._fillPageColumn(pageIndex - 1, currentColumnIndex, pageHeight);
                }
            }

            this.pageColumnHeights[pageIndex][currentColumnIndex] = this.layoutRowTop + height;
            if (colIdx <= currentColumnIndex && currentColumnIndex < colIdx + colSpan) {
                this.pagePointers[currentColumnIndex]++;
            }
        }
    },

    _initPageColumnHeightsForPage: function(pageIndex) {
        var columnIndex;

        if (this.pageColumnHeights[pageIndex] === undefined) {
            this.pageColumnHeights[pageIndex] = [];

            for (columnIndex = 0; columnIndex < this.cols; columnIndex++) {
                this.pageColumnHeights[pageIndex][columnIndex] = this.layoutRowTop;
            }
        }
    },

    _fillPageColumn: function(pageIndex, columnIndex, pageHeight) {
        if (pageIndex >= 0) {
            if (this.pageColumnHeights[pageIndex] !== undefined) {
                this.pageColumnHeights[pageIndex][columnIndex] = pageHeight;
            }
        }
    },

    _getUsedColumnHeight: function(pageIndex, columnIndex, columnSpan) {
        return _.max(this.pageColumnHeights[pageIndex].slice(columnIndex, columnIndex + columnSpan));
    },

    _setCols: function(cols) {
        this.cols = cols;
    },
    _setLayoutRowTop: function(layoutRowTop) {
        this.layoutRowTop = layoutRowTop;
    },
    _setPageColumnHeights: function(pageColumnHeights) {
        this.pageColumnHeights = pageColumnHeights;
    },

    /* Page Break Position Item Helpers */

    showPage: function(pageNum) {
        var i, itemsToShow, itemConfig, klip;

        this.$container.find(".item-container").show();

        if (this.itemPages && pageNum !== undefined) {
            itemsToShow = this.itemPages[pageNum];
            if (!itemsToShow) return;

            for (i = 0; i < this.itemConfigs.length; i++) {
                itemConfig = this.itemConfigs[i];

                if (_.indexOf(itemsToShow, itemConfig.id) == -1) {
                    itemConfig.$content.hide();
                } else {
                    itemConfig.$content.show();
                }
            }

            this.setGridHeight(this.pageColumnHeights[pageNum]);
        }
    },

    getColItemHeight: function(colItemConfig, withMargin) {
        return colItemConfig.$container.outerHeight(withMargin);
    },

    getColItemStartColumn: function(colItemConfig) {
        return colItemConfig.origin[0];
    },

    getColItemSpan: function(colItemConfig) {
        return colItemConfig.colSpan;
    },

    getItemPages: function() {
        return this.itemPages;
    },

    getPageColumnHeights: function() {
        return this.pageColumnHeights;
    },

    /**
     * Enable an item to be draggable
     *
     * @param $itemContainer
     */
    enableDraggable: function($itemContainer) {
        var _this = this;

        $itemContainer.draggable({
            addClasses: false,
            appendTo: "body",
            cursor: "move",
            handle: this.dragHandle,
            zIndex: 100,
            start : function(evt, ui) {
                var $helper, removeConfig;

                _this.isDragging = true;
                if (!_this.showLines) _this.$grid.addClass("gridlines");

                _this.prevIndex = false;
                _this.moveIndexChanged = false;

                $helper = $(ui.helper);
                _this.moveIndex = [parseInt($helper.attr("colIdx")), parseInt($helper.attr("vertIdx"))];

                // remove the item being moved from the grid
                removeConfig = _this.removeItem({index:_this.moveIndex}, true);
                _this.dragItem = removeConfig.itemConfig;
                _this.removeVertOffsets = removeConfig.vertOffsets;

                // setup the placeholder and insert it in the correct position
                _this.setPlaceholder(_this.dragItem);
                _this.insertPlaceholder(_this.removeVertOffsets);

                if (_this.onDragStart) _this.onDragStart(_this.dragItem);
            },
            drag: function (evt, ui) {
                var el, $column, newColIdx, newVertIdx, newIndex;
                var $helper = $(ui.helper);
                var helperClientY = $helper.offset().top - $(window).scrollTop();
                var helperClientX = $helper.offset().left - $(window).scrollLeft();

                // we need to hide the thing being dragged and other containers, otherwise
                // elementFromPoint will return it instead of the thing we are over
                // ----------------------------------------------------------------------------------
                $(".item-container").hide();
                el = document.elementFromPoint(helperClientX, helperClientY);
                $(".item-container").show();

                $column = $(el).closest(".layout-cell");
                newColIdx = $column.index();

                if (0 <= newColIdx && newColIdx < _this.cols) {
                    newVertIdx = _this.findVertIdx($helper.position().top, _this.colConfigs[newColIdx]);

                    newIndex = [newColIdx, newVertIdx];
                    if (_this.prevIndex && !_this.indicesEqual(_this.prevIndex, newIndex)) {
                        _this.moveIndexChanged = true;

                        // move the placeholder to its new location
                        _this.placeholderConfig.colSpan = _this.dragItem.colSpan;
                        _this.placeholderConfig.index = newIndex;

                        _this.insertPlaceholder();
                    }

                    _this.prevIndex = newIndex;
                }

                if (_this.onDrag) _this.onDrag(_this.dragItem);
            },
            stop: function (evt, ui) {
                var itemMoved, addDragItem, draggedItem;

                _this.dragItem.index = _this.prevIndex;

                if (_this.dragItem.$content) _this.dragItem.$content.detach();
                _this.dragItem.$container.remove();

                itemMoved = !_this.indicesEqual(_this.moveIndex, _this.prevIndex);

                // re-add the moved item into the grid at the position the placeholder occupied last
                addDragItem = _this.checkAddDragItem ? _this.checkAddDragItem() : true;
                draggedItem = false;
                if (addDragItem) {
                    draggedItem = _this.addItem(_this.dragItem,
                        !_this.moveIndexChanged && !itemMoved ?
                            _this.removeVertOffsets :
                            (_this.addVertOffsets.length > 1 ? _this.addVertOffsets : undefined));

                    if (!_this.dragItem.$content) {
                        _this.setEmptyContent(_this.dragItem.id);
                    }
                }

                // reset the placeholder
                _this.setPlaceholder(false);

                if (!_this.showLines) _this.$grid.removeClass("gridlines");
                _this.isDragging = false;

                if (_this.onDragStop) _this.onDragStop(draggedItem, itemMoved);
            }
        });
    },

    /**
     * Enable an item to be resizable
     *
     * @param $itemResizable
     * @param options
     */
    enableResizable: function($itemResizable, options) {
        var _this = this;
        $itemResizable.resizable({
            handles:"se",
            start: function(evt, ui) {
                var $resizeContainer, removeConfig;
                _this.isResizing = true;
                if (!_this.showLines) _this.$grid.addClass("gridlines");

                _this.resizeSpanChanged = false;

                $resizeContainer = $(ui.helper).parent();
                _this.resizeIndex = [parseInt($resizeContainer.attr("colIdx")), parseInt($resizeContainer.attr("vertIdx"))];

                // remove the item being moved from the grid
                removeConfig = _this.removeItem({index:_this.resizeIndex}, true);
                _this.resizeItem = removeConfig.itemConfig;
                _this.resizeColSpan = _this.resizeItem.colSpan;
                _this.resizeHeight = _this.resizeItem.height;
                _this.removeVertOffsets = removeConfig.vertOffsets;

                $(this).resizable("option", {
                    minHeight: _this.resizeItem.minHeight,
                    minWidth: _this.getContainerWidth({index:_this.resizeIndex, colSpan:1}),
                    maxWidth: _this.getContainerWidth({index:_this.resizeIndex, colSpan:_this.cols - _this.resizeIndex[0]})
                });

                _this.prevColSpan = _this.resizeItem.colSpan;
                _this.prevHeight = _this.resizeItem.$container.height();

                // setup the placeholder and insert it in the correct position
                _this.setPlaceholder(_this.resizeItem);
                _this.insertPlaceholder(_this.removeVertOffsets);

                // show the vertical snap lines
                _this.toggleVerticalLines(true);

                if (_this.onResizeStart) _this.onResizeStart(_this.resizeItem);
            },
            resize: function(evt, ui) {
                var $column, $verticalLines;
                var i, el, colIdx, newHeight, newColSpan;
                var containerTop, resizeBottom, targetHeights, verticalLineTop, mousePosition, tolerancePoint;

                // we need to hide the thing being dragged and other containers, otherwise
                // elementFromPoint will return it instead of the thing we are over
                // ----------------------------------------------------------------------------------
                $(".item-container").hide();
                el = document.elementFromPoint(evt.originalEvent.clientX, evt.originalEvent.clientY);
                $(".item-container").show();

                $column = $(el).closest(".layout-cell");

                colIdx = -1;
                if ($column.length > 0) {
                    mousePosition = evt.originalEvent.pageX - $(".grid-layout-container").offset().left;

                    colIdx = $column.index();
                    tolerancePoint = $column.width() * _this.columnTolerance;
                    if (mousePosition < $column.position().left + tolerancePoint) {
                        colIdx--;
                    }
                }

                if (0 <= colIdx && colIdx < _this.cols) {
                    newColSpan = colIdx - _this.resizeIndex[0] + 1;

                    if (newColSpan > 0) {
                        if (_this.prevColSpan && _this.prevColSpan != newColSpan) {
                            _this.resizeSpanChanged = true;

                            // change the placeholders col span
                            _this.placeholderConfig.colSpan = newColSpan;

                            _this.insertPlaceholder();
                        }

                        _this.prevColSpan = newColSpan;
                    }
                }


                newHeight = ui.size.height;
                containerTop = $(ui.helper).parent().position().top;
                resizeBottom = containerTop + newHeight;

                // determine if item was resized close to a vertical snap line
                $verticalLines = $(".vertical-line");
                targetHeights = [];
                for (i = 0; i < $verticalLines.length; i++) {
                    verticalLineTop = $($verticalLines[i]).position().top;
                    if (verticalLineTop - _this.aboveLineTolerance <= resizeBottom && resizeBottom <= verticalLineTop + _this.belowLineTolerance) {
                        targetHeights.push(verticalLineTop - containerTop);
                    }
                }
                if (targetHeights.length > 0) newHeight = _.min(targetHeights);

                if (_this.prevHeight && _this.prevHeight != newHeight) {
                    _this.$placeholderContainer.height(newHeight);

                    // skip setup and include the placeholder when positioning items
                    _this.doLayout(true, true);
                }

                _this.prevHeight = newHeight;

                _this.toggleVerticalLines(true);

                if (_this.onResize) _this.onResize(_this.resizeItem);
            },
            stop : function(evt, ui) {
                var resizedItem, dimensionsChanged;
                _this.resizeItem.colSpan = _this.prevColSpan;

                // if stop at min height, resize to that; in case item at min height is really close to
                // a vertical snap line
                if (ui.size.height == _this.resizeItem.minHeight) _this.prevHeight = _this.resizeItem.minHeight;
                _this.resizeItem.height = _this.prevHeight;
                if (_this.resizeItem.height == _this.resizeItem.minHeight) delete _this.resizeItem["height"];

                if (_this.resizeItem.$content) _this.resizeItem.$content.detach();
                _this.resizeItem.$container.remove();

                // re-add the resized item into the grid at the position the placeholder occupied last
                resizedItem = _this.addItem(_this.resizeItem,
                    !_this.resizeSpanChanged && _this.resizeColSpan == _this.prevColSpan ?
                        _this.removeVertOffsets :
                        (_this.addVertOffsets.length > 1 ? _this.addVertOffsets : undefined));

                if (!_this.resizeItem.$content) {
                    _this.setEmptyContent(_this.resizeItem.id);
                }

                // reset the placeholder
                _this.setPlaceholder(false);

                // remove the vertical snap lines
                _this.toggleVerticalLines(false);

                if (!_this.showLines) _this.$grid.removeClass("gridlines");
                _this.isResizing = false;

                dimensionsChanged = (_this.resizeColSpan != _this.prevColSpan) || (_this.resizeHeight != resizedItem.height);

                if (_this.onResizeStop) _this.onResizeStop(resizedItem, dimensionsChanged);
            }
        }).resizable("option", options );

        $itemResizable.find(".ui-resizable-handle").css("z-index", 10);
    },

    /**
     * Set the values in the placeholder to match an item
     *
     * @param itemConfig - if false, reset the placeholder
     */
    setPlaceholder: function(itemConfig) {
        if (!itemConfig) {
            this.$placeholderContainer.hide();
            this.placeholderConfig = {$container:this.$placeholderContainer};
        } else {
            this.placeholderConfig.index = itemConfig.index;
            this.placeholderConfig.colSpan = itemConfig.colSpan;
            this.placeholderConfig.minHeight = itemConfig.minHeight;

            this.$placeholderContainer.height(itemConfig.$container.height()).show();
        }
    },

    /**
     * Insert the placeholder into the grid
     *
     * @param removeVertOffsets
     */
    insertPlaceholder: function(removeVertOffsets) {
        var i, top, left, maxTop;
        var colIdx, vertIdx, colSpan, $col, colItemConfig, colTop, colConfig, vertOffset;

        this.setItemWidth(this.placeholderConfig);

        // Move all displaced items back to their original positions
        // ---------------------------------------------------------
        if (!removeVertOffsets) {
            this.doLayout(true);
        }

        // Determine where the placeholder should be placed
        // ------------------------------------------------
        colIdx = this.placeholderConfig.index[0];
        vertIdx = this.placeholderConfig.index[1];
        colSpan = this.placeholderConfig.colSpan;

        $col = this.$grid.find(".layout-cell:eq(" + colIdx + ")");
        top = $col.position().top + this.gutter * 2;
        left = $col.position().left + this.gutter + (isWebkit() ? 0 : -1);

        maxTop = top;
        colItemConfig = false;
        colTop = 0;
        if (removeVertOffsets) {
            for (i = colIdx; i < colIdx + colSpan; i++) {
                colItemConfig = this.colConfigs[i][removeVertOffsets[i - colIdx] - 1];
                colTop = (colItemConfig ? colItemConfig.$container.position().top + colItemConfig.$container.outerHeight(true) : 0);

                maxTop = Math.max(maxTop, colTop);
            }

            this.addVertOffsets = removeVertOffsets;
        } else {
            this.addVertOffsets = [vertIdx];
            colItemConfig = this.colConfigs[colIdx][vertIdx - 1];
            if (colItemConfig) {
                maxTop = colItemConfig.$container.position().top + colItemConfig.$container.outerHeight(true);
            }

            for (i = colIdx + 1; i < colIdx + colSpan; i++) {
                colConfig = this.colConfigs[i];
                vertOffset = this.findVertIdx(maxTop, colConfig);
                this.addVertOffsets.push(vertOffset);

                colItemConfig = colConfig[vertOffset - 1];
                colTop = (colItemConfig ? colItemConfig.$container.position().top + colItemConfig.$container.outerHeight(true) : 0);

                maxTop = Math.max(maxTop, colTop);
            }
        }

        this.$placeholderContainer.css({ top:maxTop, left:left });

        // Displace items with respect to placeholder
        // ------------------------------------------
        this.doLayout(true, true);
    },

    /**
     * Show the vertical snap lines available for placeholder
     *
     * @param showVerticalLines
     */
    toggleVerticalLines: function(showVerticalLines) {
        var colIdx, colSpan, _this, placeholderMinBottom, overlapItems, nonOverlapItems;

        var i, itemConfig, j, otherConfig, intersection, otherComesAfter, colConfig, k, colItemConfig, verticalLineTops;

        $(".vertical-line").remove();

        if (showVerticalLines) {
            colIdx = this.placeholderConfig.index[0];
            colSpan = this.placeholderConfig.colSpan;

            _this = this;
            placeholderMinBottom = this.$placeholderContainer.position().top + this.placeholderConfig.minHeight;

            overlapItems = [];
            nonOverlapItems = [];

            // determine which items are below the placeholder and overlap it
            _.each(this.itemConfigs, function(itemConfig) {
                var k, itemColIdx, colConfig, colItemConfig;
                var intersection = _this.intersection(colIdx, colSpan, itemConfig.index[0], itemConfig.colSpan);

                var overlaps = false;
                if (intersection.length > 0) {
                    itemColIdx = _.indexOf(intersection, itemConfig.index[0]);

                    // item starts in a column the placeholder occupies
                    if (itemColIdx != -1) {
                        if (itemConfig.index[1] >= _this.addVertOffsets[intersection[itemColIdx] - colIdx]) {
                            overlaps = true;
                        }
                    } else {
                        // item is below the placeholder
                        colConfig = _this.colConfigs[intersection[0]];
                        for (k = 0; k < colConfig.length; k++) {
                            colItemConfig = colConfig[k];

                            if (_this.indicesEqual(colItemConfig.origin, itemConfig.index)) {
                                if (k >= _this.addVertOffsets[intersection[0] - colIdx]) {
                                    overlaps = true;
                                }
                                break;
                            }
                        }
                    }
                }

                if (overlaps) {
                    overlapItems.push(itemConfig);
                } else {
                    nonOverlapItems.push(itemConfig);
                }
            });

            // determine which leftover items are below and overlap the items that are moved by the placeholder
            for (i = 0; i < overlapItems.length; i++) {
                itemConfig = overlapItems[i];

                j = nonOverlapItems.length - 1;
                while (j >= 0) {
                    otherConfig = nonOverlapItems[j];
                    intersection = this.intersection(itemConfig.index[0], itemConfig.colSpan, otherConfig.index[0], otherConfig.colSpan);

                    // items intersect
                    if (intersection.length > 0) {
                        otherComesAfter = false;
                        colConfig = this.colConfigs[intersection[0]];
                        for (k = 0; k < colConfig.length; k++) {
                            colItemConfig = colConfig[k];

                            if (this.indicesEqual(colItemConfig.origin, itemConfig.index)) {
                                otherComesAfter = true;
                                break;
                            } else if (this.indicesEqual(colItemConfig.origin, otherConfig.index)) {
                                break;
                            }
                        }

                        // other item comes after current item, thus will be moved
                        if (otherComesAfter) {
                            overlapItems.push(nonOverlapItems.splice(j, 1)[0]);
                        }
                    }

                    j--;
                }
            }

            // add vertical lines (no duplicates) for the bottoms of items that won't move
            verticalLineTops = [];
            _.each(nonOverlapItems, function(itemConfig) {
                var itemBottom = itemConfig.$container.position().top + itemConfig.$container.height();

                if (itemBottom >= placeholderMinBottom) {
                    if (_.indexOf(verticalLineTops, itemBottom) == -1) {
                        _this.$container.prepend($("<div class='vertical-line'>").css({top:itemBottom, left:0}));
                        verticalLineTops.push(itemBottom);
                    }
                }
            });
        }
    },

    /**
     * Determine the column intersection of two items
     *
     * @param itemColIdx
     * @param itemColSpan
     * @param otherColIdx
     * @param otherColSpan
     * @returns {Array}
     */
    intersection: function(itemColIdx, itemColSpan, otherColIdx, otherColSpan) {
        var i;
        var itemArray = _.range(itemColIdx, itemColIdx + itemColSpan);
        var otherArray = _.range(otherColIdx, otherColIdx + otherColSpan);

        var intersection = [];
        for (i = 0; i < itemArray.length; i++) {
            if (_.indexOf(otherArray, itemArray[i]) != -1) {
                intersection.push(itemArray[i]);
            }
        }

        return intersection;
    }

});
;/****** c.help.js *******/ 

/* global mixPanelTrack:false, isWorkspace:false */
var HelpSystem = $.klass({

	$root: false,

	width: 340,

	defaultPage : "help-index",
	currentPage : false,
    previousPages: [],
	visible : false,

	initialize : function() {
		var _this = this;
		var startTopic;

		this.$root = $("<div>")
			.attr("id", "help-window")
			.width(this.width)
			.height($(window).height())
			.appendTo(document.body);

		this.$menu = $("<div>")
			.addClass("help-menu")
			.appendTo(this.$root);

        this.$tour = $("<span class='help-launch-tour'><span class='help-tour'/>Launch Tour</span>");
        if (KF.company.hasFeature("pendo")) {
			if (isWorkspace()) {
				this.$tour.attr("title", "Start tour")
					.removeClass("disabled");
			} else {
                this.$tour.attr("title", "No tours available for this page")
                    .addClass("disabled");
            }
        } else {
			this.$tour.hide();
		}

        this.$helpbar = $("<div id='help-bar'>")
            .append($("<span title='Live Chat' class='help-bar-icon help-chat disabled'/>"))
            .append($("<span title='Message us' class='help-bar-icon help-message'/>").on("click", function(){clickSupportTicket();}))
            .append($("<span title='Community' class='help-bar-icon help-community'/>")
				.on("click", function () {
					window.open("https://support.klipfolio.com/hc/en-us/community/topics");
            }))
            .append($("<span title='Knowledge Base' class='help-bar-icon help-knowledge-base'/>")
				.on("click", function () {
					window.open("https://support.klipfolio.com/hc/en-us/#kb");
            }))
            .append(this.$tour);
		this.$menu
			.append($("<div class='gap-south-10 position-wrapper'>")
				.append($("<h1>").text(KF.company.get("brand", "productName") + " Help").on("click", _.bind(this.displayHome, this)))
				.append($("<div id='help-window-back-button'>").addClass("item-back").on("click", _.bind(this.goBack, this)))
				.append($("<div>").addClass("item-close").on("click", _.bind(this.hide, this))))
			.append($("<div class='position-wrapper'>")
				.append($("<input type='text' id='helpSearchText'>")
					.addClass("item-search textfield rounded-textfield")
					.attr("placeholder", "Search our Knowledge Base")
					.keyup(function(evt) {
						if (evt.keyCode == 13) {
							_this.searchHelpArticles();
						}
					}))
				.append($("<div id='searchIcon'>").addClass("librarySearch"))
				.append($("<div id='searchCloser'>").addClass("item-search-close").css("display", "none").on("click", _.bind(this.clearSearch, this)))
				.append($("<div id='helpSpinner'>").addClass("item-spinner spinner").css("display", "none")))
			.append(this.$helpbar);


        this.$body = $("<div>")
			.addClass("help-body")
			.appendTo(this.$root);

		$(window).resize( _.bind(  _.debounce(this.onResize,200) ,this) );
        $(document).resize( _.bind(  _.debounce(this.onResize,200) ,this) );

		startTopic = $.cookie("htopic");
		if (startTopic && HelpSystem.enabled) {
			$(function() {
				_this.show(startTopic, true);
			});
		}

		this.setupMixpanelListeners();
	},

	setupMixpanelListeners: function() {
		var _this = this;
		var $helpBody = $(".help-body");

		$helpBody.on("click", "a:not([href='']), a[target='_blank']", function(e) {
			var $link = $(e.currentTarget);
			var linkText = $link.text();
			var linkUrl  = $link[0].href;

            if (typeof mixPanelTrack == "function") {
                mixPanelTrack("Help Resource", {
					"Help File": _this.currentPage,
					"Type": "link",
					"Help Text": linkText,
					"Help Resource Url": linkUrl
				});
            }
		});

		$helpBody.on("click", ".button-video", function(e) {
			var $button = $(e.currentTarget);

			// Fish out the href from the link's JavaScript.
			var onClickMatch = $button[0].onclick && $button[0].onclick.toString().match(/window\.open\('([^']*)'\)/);
			var href = onClickMatch && onClickMatch.length > 1 && onClickMatch[1].trim();
			var text = $button.text();

            if (typeof mixPanelTrack == "function") {
                mixPanelTrack("Help Resource", {
					"Help File": _this.currentPage,
					"Type": "video",
					"Help Text": text,
					"Help Resource Url": href
				});
			}
		});

		$helpBody.on("click", "button", function(e) {
			var text = $(e.currentTarget).text();

            if (typeof mixPanelTrack == "function") {
				mixPanelTrack("Help Resource", {
					"Help File": _this.currentPage,
					"Type": "button",
					"Help Text": text
				});
			}
		});
	},

	onResize : function() {
		var windowHeight;

		if (this.visible) {
			windowHeight = $(window).height();
			this.$root.height(windowHeight);
			this.$body.height(windowHeight - this.$menu.outerHeight(true));
        }
	},


	toggle: function(page, section) {
		if (this.visible && this.currentPage == page) {
			this.hide();
		} else {
			this.show(page, false, false, section);
		}
	},

	show : function(page, fast, fromBack, section) {
		var windowHeight;
		var _this = this;

		if (!page) return;

		// Suppress MixPanel events during page load (they skew numbers if a user leaves their help window open).
		if (!fast) {
            if (typeof mixPanelTrack == "function") {
				mixPanelTrack("Sidebar Help", {
					"Help File": page,
					"From Back": fromBack ? "true" : "false"
				});
			}
		}
		$.cookie("htopic", page, { path:"/" });

		if (!this.visible) {
			this.visible = true;

			$("#c-root").css( "margin-right" , this.width);

			windowHeight = $(window).height();
			this.$root.height(windowHeight);

			if (!fast) {
				this.$root.addClass("do-transition");
			}
			this.$root.addClass("visible");

			this.$body.height(windowHeight - this.$menu.outerHeight(true));
		}

		$.get("/dashboard/ajax_getHelpPageFromStatic", {page:page}, function(data){
			if(data.success){
				_this.$body.html(data.msg);

				_this.applyAccordionEventHandler(section);

                PubSub.publish("help-tab-loaded");
            }else{
                _this._showError();
			}
		});

        if (page != this.currentPage) {
            if (!fromBack && this.currentPage) {
                // Don't store the current page in the history if the user clicked the Back button.
                this.previousPages.push(this.currentPage);
            }

			this.currentPage = page;
            this.$body.scrollTop(0);
		}

        if (this.previousPages.length == 0) {
            $("#help-window-back-button").hide();
        } else {
            $("#help-window-back-button").show();
        }

		PubSub.publish("layout_change");
	},

	_showError : function(){
        this.$body.html("<div class='error'><span class='help-error-icon'/>This content is currently unavailable.<br/> Try refreshing the page or visit our <br/>\n" +
            "    <a href='https://support.klipfolio.com/hc/en-us' target='_blank'>Knowledge Base</a> to find detailed help articles.</div>");
		PubSub.publish("help-tab-loaded");
	},

	hide : function() {
		this.visible = false;
		$.cookie("htopic", null, {path:"/"});

		this.$root.addClass("do-transition").removeClass("visible");
		$("#c-root").css("margin-right", "");

		PubSub.publish("layout_change");
	},

    displayHome : function() {
        if (this.$body && this.currentPage != "help-index") {
            this.show("help-index");
        }
    },

    goBack : function() {
        if (this.previousPages.length == 0) {
            this.displayHome();
        } else {
            this.show(this.previousPages.pop(), false, true);
        }
    },

	searchHelpArticles : function (){
		var _this = this;
		var searchText = $("#helpSearchText");
		var $searchIcon = $("#searchIcon");
		var spinner = $("#helpSpinner");
		var closer = $("#searchCloser");

		searchText.prop("disabled", true);
		$searchIcon.hide();
		closer.hide();
		spinner.show();

		//send search to zendesk api
		$.ajax({
			type:"POST",
			url:"/dashboard/ajax_searchHelpArticles",
			data: {query: searchText.val()},
			success : function(resp) {
				var i;

                if (typeof mixPanelTrack == "function") {
					mixPanelTrack("Sidebar Search", {
						"Help File": _this.currentPage,
						"Search Term": searchText.val(),
						"Result Count": resp.results.length
					});
				}

				if (_this.currentPage) {
					_this.previousPages.push(_this.currentPage);
				}

				_this.currentPage = undefined;
				_this.$body.empty();

				if (resp.results.length > 0) {
					$("<h1>")
						.text("Search Results")
						.attr("id", "helpSearchResults")
						.appendTo(_this.$body);

					for (i = 0; i < resp.results.length; i++) {
						$("<p>").append(
							$("<a>")
								.text(resp.results[i].name)
								.attr("href", resp.results[i].html_url)
								.attr("target", "_blank")
						).appendTo(_this.$body);
					}
				} else {
					$("<h1>")
						.text("No results found")
						.attr("id", "helpSearchResults")
						.appendTo(_this.$body);
				}

				searchText.prop("disabled",false);
				searchText.select();
				spinner.hide();
				closer.show();
			},
			error : function() {
				searchText.prop("disabled",false);
				searchText.select();
				spinner.hide();
				closer.show();
			}
		});
	},

	clearSearch: function() {
		$("#helpSearchText").val("");
		if ($("#helpSearchResults").length > 0) {
			this.goBack();
		}

		$("#searchIcon").show();
		$("#searchCloser").hide();
	},

	focusSearch:function(){
		this.$root.find("#helpSearchText").focus();
	},

    /**
     * Render the help icon
     *
     * @param config - {
     *      topic:'',           // (required) path to help file
     *      colour:'',          // (optional) options: onlight (default), ondark
     *      position:'',        // (optional) options: forceright
     *      css:'',             // (optional) CSS class
     *      url:'',             // (optional) URL of help article outside the app
     *      id:''               // (optional) id of icon
     * }
     * @returns {string}
     */
    renderIcon : function(config) {
        var $helpIcon = "";
		var topic = config.topic;
		var colour = config.colour ? config.colour : "onlight";
		var position = config.position ? config.position : "";
		var css = config.css ? config.css : "";

        if (KF.user.get("admin") || !KF.company.hasFeature("admin_help_only")) {
            if (config.url) {
                $helpIcon += "<a href='" + config.url + "' target='_blank'><span class='help-icon " + colour + " " + position + " " + css +  "'></span></a>";
            } else {
                $helpIcon += "<span";
                if (config.id) $helpIcon += " id='" + config.id + "'";
                $helpIcon += " class='help-icon " + colour + " " + position + " " + css + "' onclick='help.toggle(\"" + topic + "\")'></span>";
            }
        }

        return $helpIcon;
	},

	applyAccordionEventHandler: function(section) {
		var accordionSections = $(".accordion > .section");
		var sectionIndex = 0;
		var openByDefault;
		var i = 0;
		for (; i < accordionSections.length; i++) {
			if (accordionSections[i].innerText == section) {
				sectionIndex = i;
				break;
			}
		}
		openByDefault = accordionSections[sectionIndex];
		$(".accordion").find(openByDefault).addClass("selected");
		$(".accordion").find(openByDefault).next().show();
		$(".accordion").find(".section").off("click").click(function() {
			$(this).toggleClass("selected");
			$(this).next().slideToggle("fast");
			$(".section").not($(this)).removeClass("selected");
			$(".content").not($(this).next()).slideUp("fast");
		});
	}
});

var help; // eslint-disable-line no-unused-vars

HelpSystem.enabled = true;

$(function(){
	help = new HelpSystem();
});
;/****** c.klip.js *******/ 

/* global page:false, isWorkspace:false, isPreview:false, removeItemFromArray:false */
Klip = $.klass({

    /** the klip's ID	 */
    id:false,

    /** for poor-man's type detection */
    isKlip:true,

    /** the klip's title */
    title:false,

    /** klip's sizing */
    titleSize: "x-small",

    /** the klip's title */
    hasDynamicTitle:false,

    /** animate the klip on update */
    animate:true,

    /** does the title contain replacement markers */
    dashboard:false,

    /** our jQuery DOM element */
    $klipResizable: false,
    el : false,
    toolbarEl : false,
    bodyEl : false,
    messageEl : false,
    $emptyMessage: false,
    $componentDropMessage: false,

    /** min widths for klip resizable elements*/
    KLIP_MIN_WIDTH_DEFAULT: 200,
    KLIP_MIN_WIDTH_ZERO_STATE: 550,

    /** the layout constraints/config for this klip */
    layoutConfig : false,

    /** positions components within the Klip */
    layoutManager: false,

    /** list of components in the klip */
    components : [],

    /** flag: is the Klip visibile in the dashboard */
    isVisible:false,

    /** display the toolbar? */
    showToolbar:true,

    /** object with (w)idth and (h)eight properties */
    dimensions : false,

    /** user-scoped configuration properties */
    userProps : false,

    /**disabling caching */
    cacheDisabled: false,

	/** list of property names that should be passed along with serialized formulas -- klip editor
	 */
	datasourceProps : false,

    /** padding between Klip border and inner components */
    useSamePadding : true,
    padding : 15,
    innerPadding : 15,

    /** dashboard menu model specific to this klip */
    menuModel : false,

    /** class to be applied to the container */
    containerClassName: false,

    usesSampleData: false,
    templateId: false,
    templateName: false,
    isForked: false,

    componentDependerMap: null, //A map of the immediate components that depend on each component id (Reversed dependency map)
    _dstRootDependerMap: {}, //A map of dst roots and the roots that immediately depend on them
    _fullRootDependencies: {}, //For each dst root, an array of all the other dst roots that depend  on them (that is the other roots that refer to it)

    /**
     * constructor.
     * Klips should be constructed with the factory method in Dashboard, eg:
     * dasbhoard.createKlip()
     */
    initialize: function(dashboard)
    {
        this.dashboard = dashboard;
        this.components = [];
        this.userProps = [];
        this.dimensions ={};

        this.messages = {
            errors: [],
            upgrades: [],
            other:  []
        };

        this.handleComponentUpdated = _.debounce(_.bind(this.handleComponentUpdated, this), 200);
    },

	/**
	 * sets internal properties/state based on the given config DSL.
	 * @param config
	 */
	configure : function(config)
	{
		bindValues(this, config, [
            "title",
            "titleSize",
			"id",
            "master",
            "layoutConfig",
            "animate",
			"workspace",
			"datasourceProps",
			"cacheDisabled",
            "useSamePadding",
            "padding",
            "paddingTop",
            "paddingRight",
            "paddingBottom",
            "paddingLeft",
            "innerPadding",
            "containerClassName",
            "showToolbar",
            "usesSampleData",
            "hasSampleData",
            "templateId",
            "templateName",
            "isForked",
            "appliedMigrations"
		]);

		this.updateOldTitleSizes(config);

        if (config.title) {
            if (this.title.search("\{([^}]*)\}") != -1) this.hasDynamicTitle = true;
        }

		if (config.height) {
			this.fixedHeight = config.height;
			this.dimensions.h = config.height;
		}

        if (config.useSamePadding !== undefined) {
            if (config["~propertyChanged"]) {
                if (!config.useSamePadding) {
                    this.paddingTop = this.padding;
                    this.paddingRight = this.padding;
                    this.paddingBottom = this.padding;
                    this.paddingLeft = this.padding;

                    this.padding = [this.paddingTop, this.paddingRight, this.paddingBottom, this.paddingLeft];

                    this.availableWidth = this.dimensions.w - (parseInt(this.padding[1]) + parseInt(this.padding[3]));
                } else {
                    this.padding = this.paddingTop;

                    this.availableWidth = this.dimensions.w - this.padding * 2;
                }

                page.updatePropertySheet(this.getPropertyModel(), false, page.componentPropertyForm);

                this.onResize();
            } else {
                if (!config.useSamePadding) {
                    this.paddingTop = this.padding[0];
                    this.paddingRight = this.padding[1];
                    this.paddingBottom = this.padding[2];
                    this.paddingLeft = this.padding[3];

                } else {
                    this.paddingTop = this.padding;
                    this.paddingRight = this.padding;
                    this.paddingBottom = this.padding;
                    this.paddingLeft = this.padding;
                }
            }
        }

        if (config["~propertyChanged"] && config.padding) {
            this.availableWidth = this.dimensions.w - this.padding * 2;

            this.onResize();
        }

        if (config["~propertyChanged"] && (config.paddingTop || config.paddingRight || config.paddingBottom || config.paddingLeft)) {
            if (config.paddingTop) this.padding[0] = this.paddingTop;
            if (config.paddingRight) this.padding[1] = this.paddingRight;
            if (config.paddingBottom) this.padding[2] = this.paddingBottom;
            if (config.paddingLeft) this.padding[3] = this.paddingLeft;

            if (config.paddingRight || config.paddingLeft) {
                this.availableWidth = this.dimensions.w - (parseInt(this.padding[1]) + parseInt(this.padding[3]));

                this.onResize();
            }
        }

        if (config["~propertyChanged"] && config.innerPadding) {
            this.setComponentPadding();
        }
	},

    /**
     * returns a configuration object that can be used to re-constitute a klip and
     * its components
     */
    serialize : function(extendedConfig)
    {
        var config = {};

	    bindValues(config, this, [
		    "title",
            "titleSize",
            "id",
            "layoutConfig",
            "animate",
            "useSamePadding",
            "padding",
            "innerPadding",
            "containerClassName",
            "cacheDisabled",
            "workspace",
            "datasourceProps",
            "showToolbar",
            "appliedMigrations",
            "usesSampleData"
	    ], {
			    layoutConfig: false,
                titleSize: "x-small",
                animate: true,
                useSamePadding: true,
                padding: 15,
                innerPadding: 15,
                containerClassName: false,
                cacheDisabled: false,
                datasourceProps: false,
                showToolbar: true,
                usesSampleData:false,
                appliedMigrations: {}
		    }, true
	    );

        if(extendedConfig){
            bindValues(config, this, [
                "isForked",
                "templateId",
                "templateName"
            ], {
                isForked: false,
                templateId: false,
                templateName: false
            }, true);
        }
        
        // while klip is saved we are only interested in dataSet Id and it's dstContext.
        if(config.workspace.dataSets) {
            config.workspace.dataSets = config.workspace.dataSets.map(function(dataSet) {
                var newDataSet = {};
                newDataSet.id = dataSet.id;
                return newDataSet;
            });

        }

	    if ( this.fixedHeight ) config.height = this.fixedHeight;

        if (this.components)
        {
            config.components = [];
            var len = this.components.length;
            for (var i = 0; i < len; i++)
            {
                config.components.push(this.components[i].serialize());
            }
        }
        return config;
    },

    generateMenuModel : function(baseModel, rights, tabRights) {
        var removedItems;

        this.menuModel = baseModel.slice(0);

        if (!rights.edit) {
            removeItemFromArray(this.menuModel, "menuitem-edit_klip");
            removeItemFromArray(this.menuModel, "menuitem-configure_klip");
            removeItemFromArray(this.menuModel, "menuitem-reconfigure_klip");

            removedItems = removeItemFromArray(this.menuModel, "menuitem-share_groups_klip");
            if (removedItems && removedItems.length > 0 &&
                this.menuModel.indexOf("menuitem-email_klip") == -1 &&
                this.menuModel.indexOf("menuitem-embed_klip") == -1) {
                removeItemFromArray(this.menuModel, "menuitem-share_klip");
            }
        } else {
            if (this.usesSampleData) {
                if(this.isForked){
                    removeItemFromArray(this.menuModel, "menuitem-configure_klip");
                }
                removeItemFromArray(this.menuModel, "menuitem-reconfigure_klip");
            } else if (!this.templateId || (this.templateId && this.isForked)) {
                removeItemFromArray(this.menuModel, "menuitem-configure_klip");
                removeItemFromArray(this.menuModel, "menuitem-reconfigure_klip");
            } else {
                removeItemFromArray(this.menuModel, "menuitem-configure_klip");
            }
        }

        if (!tabRights.edit) {
            removeItemFromArray(this.menuModel, "menuitem-remove_klip");
        }
    },

    generateSampleDataButton: function() {
        if (!this.usesSampleData || isWorkspace(this.dashboard) || this.isForked) {
            return null;
        }

        var rights = this.dashboard && this.dashboard.rights && this.dashboard.rights[this.master];
        var canReconfigure = KF.user.hasPermission("klip.edit") && (rights && rights.edit || isPreview(this.dashboard));

        var title = "This " + KF.company.get("brand", "widgetName") + " is using sample data.";
        if (canReconfigure) {
            title += " Click to connect an account and use your own data.";
        }

        var buttonHTML = "";
        var additionalClass = this.hasSampleData !== false?"":" no-sample-data";
        buttonHTML += "<div class=\"klip-reconfigure-data-button" + additionalClass + "\" title=\"" + title + "\">";
        var buttonText = "Sample Data";
        if (this.hasSampleData === false) {
            buttonText = "Connect Your Data"
        }
        buttonHTML += "<div class=\"normal\" >" + buttonText + "</div>";

        if (canReconfigure) {
            buttonHTML += "<div class=\"hover\">Connect Your Data</div>";
        }
        buttonHTML+= "</div>";

        var button = $(buttonHTML);
        if (canReconfigure) {
            button.addClass("can-reconfigure");
            button.on("click", this.showTemplateConfigurationWizard.bind(this));
        }

        return button;
    },

    /**
     *
     */
    renderDom : function()
    {
        this.el = $("<div>").addClass("klip").data("klip", this).attr("id", this.id);

        if (this.containerClassName) this.el.addClass(this.containerClassName);

        this.toolbarEl = $("<div>").addClass("klip-toolbar").append($("<div class='klip-toolbar-handle'>"));

        var titleText = this.getVariableKlipName();
        this.toolbarEl.append($("<span>").addClass("klip-toolbar-title").html(titleText.length === 0 ? "&nbsp;" : titleText));

        var warningIcon = $("<div>").addClass("klip-button klip-button-warning").attr("title", "Warnings").hide();
        var annotationsIcon = $("<div>").addClass("klip-button klip-button-annotation").attr("title", "Comments");
        var configIcon = $("<div>").addClass("klip-button klip-button-config").attr("title", "Menu");

        this.toolbarEl.append(
            $("<div>").addClass("klip-toolbar-buttons")
                .append(configIcon)
                .append(KF.user.hasPermission("dashboard.annotation.view") ? annotationsIcon : "")
                .append(KF.user.hasPermission("dashboard.ds_warning.view") ? warningIcon : "")
        );
        this.el.append(this.toolbarEl);

        // attach hover & click event handlers to the toolbar
        if (!this.dashboard.config.workspaceMode && !this.dashboard.config.mobile) {
            this.toolbarEl
                .click($.bind(this.onToolbarClick, this))
                .disableSelection();
        }

        if (this.fixedHeight) {
            this.el.height(this.fixedHeight);
        }


        this.bodyEl = $("<div>").addClass("klip-body");
        var sampleDataButton = this.generateSampleDataButton();
        if (sampleDataButton) {
            this.el.append(sampleDataButton);
        }

        if (this.dashboard.config.workspaceMode) {
            this.el.addClass("drop-target").attr("drop-region", "klip");
            this.bodyEl.addClass("drop-target").attr("drop-region", "klip-body").css("min-height", "37px");
        }

        this.messageEl = $("<div>").addClass("klip-message").append($("<div class='inner-message quiet x-small'>"));

        this.$emptyMessage = $("<div>").addClass("klip-empty")
            .append($("<div>").addClass("klip-empty-arrow"))
            .append($("<div>").addClass("klip-empty-icon"))
            .append($("<h2>").addClass("klip-empty-header").text("This " + KF.company.get("brand", "widgetName") + " is empty."))
            .append($("<div>").addClass("klip-empty-subheader").text("Drag in a component to start previewing!"));

        this.$componentDropMessage = $("<div>").addClass("klip-component-drop-message")
            .append($("<p>").addClass("klip-component-drop-header").text("Drag component here"));

        if (this.dashboard.config.workspaceMode) {
            this.$emptyMessage.hide().appendTo(this.bodyEl);
        }
        this.$componentDropMessage.hide();

        this.el.append(this.bodyEl)
            .append(this.messageEl);
        return this.el;
    },

    resetDom : function()
    {
        this.setDimensions(this.KLIP_MIN_WIDTH_ZERO_STATE);
        this.$klipResizable.trigger("setZeroStateMinWidth");
        this.$emptyMessage.appendTo(this.bodyEl).show();
        this.openComponentsTab();
    },

    afterFactory : function() {
        if(this.usesSampleData && this.hasSampleData === false) {
            var $message = $("<div>");
            $message.text("This " + KF.company.get("brand", "widgetName") + " doesn't have any data yet.");
            this.addMessage({$message:$message}, "info");
        }
    },

    /**
     * adds a component to the Klip with an optional constraint that tells the layout
     * manager how to position the component
     * @param c
     * @param layoutConstraint
     */
    addComponent : function(c, layoutConstraint)
    {
        if (this.components.length == 0) this.bodyEl.css("min-height", "");

        if (layoutConstraint) {
            var $prev = layoutConstraint.prev;

            if ($prev) {
                if ($prev.length == 0) {
                    this.components.splice(0, 0, c);
                    this.bodyEl.prepend(c.el);
                } else {
                    var prevCx = this.getComponentById($prev.attr("id"));
                    var prevIdx = _.indexOf(this.components, prevCx);

                    this.components.splice(prevIdx + 1, 0, c);

                    $prev.after(c.el);
                }
            }
        } else {
            this.components.push(c);
            this.bodyEl.append(c.el);
        }

        this.setComponentPadding();

        c.parent = this;
        c.onKlipAssignment();
    },

    /**
     * removes a component from the Klip
     * @param c
     * @param detach - if true, detach the component el from the Klip instead of removing it, and do not unwatch its formulas (used when about to re-add)
     */
    removeComponent : function(c, nextActive, detach)
    {
        var len = this.components.length;

        while(--len != -1) {
            if( this.components[len] == c) {
                this.components.splice(len,1);

                c.onKlipRemoval();

                if (detach) {
                    c.el.detach();
                } else {
                    c.el.remove();
                    if (c.usePostDSTReferences()) {
                        c.destroy();
                    }
                }

                if (this.components.length == 0) {
                    this.resetDom();
                }

                this.setComponentPadding();

                return;
            }
        }
    },

    removeComponents : function()
    {
        var len = this.components.length;
        var c;
        while (--len != -1) {
            c = this.components[len];

            this.removeComponent(c);
        }
    },


    setComponentPadding : function() {
        var _this = this;
        var numComponents;
        var visibleComponents = this.components;

        if (!isWorkspace()) {
            visibleComponents = this.components.filter(function(component) {
                return (component.visible && component.el);
            });
        }

        numComponents = visibleComponents.length;

        visibleComponents.forEach(function(component, i) {
            if (i == numComponents - 1) {
                component.el.css("margin-bottom", "");
            } else {
                component.el.css("margin-bottom", _this.innerPadding + "px");
            }
        });
    },

    setPadding : function()
    {
        var paddingStr = "";

        if (this.useSamePadding) {
            paddingStr = this.padding + "px";
        } else {
            for (var i = 0; i < this.padding.length; i++) {
                paddingStr += this.padding[i] + "px";

                if (i < this.padding.length - 1) {
                    paddingStr += " ";
                }
            }
        }

        this.bodyEl.css("padding", paddingStr);
    },


	/**
	 * sets a user-scoped property for this klip.  internally it uses the dashboard
	 * setDashboardProp.
	 * @param key
	 * @param val
	 */
	setKlipProp : function(key,val)
	{
        this.dashboard.setDashboardProp(1, key, val, this.id, this.currentTab);
	},

    getKlipProp : function(key, klipScopeOnly, returnScope)
    {
        var scope = 1;
        var p = this.dashboard.getDashboardProp(key, scope, this.id, this.currentTab);
        if ( p !== undefined || klipScopeOnly ) {
            return (returnScope ? {value:p, scope:scope} : p);
        }

        scope = 2;
        p = this.dashboard.getDashboardProp(key, scope, this.currentTab);

        if (p === undefined) {
            scope = 3;
            p = this.dashboard.getDashboardProp(key, scope);
        }

        if (p === undefined) {
            scope = 4;
            p = this.dashboard.getDashboardProp(key, scope);
        }

        return (returnScope ? {value:p, scope:scope} : p);
    },


	getMinHeight : function(doNotSetMinHeight)
	{
        var minHeight = this.fixedHeight || this.bodyEl.outerHeight(true) + (this.showToolbar ? this.toolbarEl.outerHeight(true) : 0) /* toolbar */  + 2 /* border */;

        if (!doNotSetMinHeight) {
            this.minHeight = minHeight;
        }

        return minHeight;
	},

    handleComponentUpdated : function()
    {
        var height = this.getMinHeight(true);

        if (height != this.minHeight) {
            PubSub.publish("klip_updated", [this]);
        }

        this.minHeight = height;

        this.displayMessages();
    },


    /**
     * Consolidate all variables and internal properties used in this klip for
     * exporting the klips (embed and email)
     *
     * @returns {{variables: Array, variableCount: number}}
     */
    getExportProps : function(scope)
    {
        var varNames = [];
        var variables = [];
        var variableCount = 0;
        var _this = this;
        var i, len, name, value;
        var dstFilterVariableNames = {};

        this.eachComponent(function(cx) {
            var dstVariables;
            if (cx.formulas) {
                var fm = cx.getFormula(0);

                if (fm) {
                    for (i = 0, len = fm.variables.length; i < len; i++) {
                        name = fm.variables[i];

                        if (_.indexOf(varNames, name) == -1) {
                            variables.push(_this._expandProp(name, scope));
                            variableCount++;
                        }

                        varNames.push(name);
                    }
                }
            }

            var internalPropNames = cx.getInternalPropNames();
            if (internalPropNames) {
                for (i = 0, len = internalPropNames.length; i < len; i++) {
                    name = internalPropNames[i];
                    value = _this.getKlipProp(name);

                    variables.push({name:name, value:value, scope:1, internal:true, klip:_this.id});
                }
            }

            if (cx.hasDstActions(true)) {
                dstVariables = cx.collectDstFilterVariables();
                Object.keys(dstVariables).forEach(function(name) {
                    dstFilterVariableNames[name] = true;
                });
            }
        });

        Object.keys(dstFilterVariableNames).forEach(function(name) {
            variables.push(_this._expandProp(name, scope));
            variableCount++;
        });

        if (this.datasourceProps) {
            this.datasourceProps.forEach(function(datasourceProp) {
                if (_.indexOf(varNames, datasourceProp) == -1) {
                    variables.push(_this._expandProp(datasourceProp, scope));
                    variableCount++;
                }

                varNames.push(datasourceProp);
            });
        }

        return { variables:variables, variableCount:variableCount };
    },

    _expandProp: function(name, scope) {
        var prop = this.getKlipProp(name, false, true);
        var propScope = (scope && prop.scope == 4 ? scope : prop.scope);
        var variable = { name: name, value: prop.value, scope: propScope };

        if (propScope == 1) {
            variable.klip = this.id;
            variable.tab = this.currentTab;
        } else if (propScope == 2) {
            variable.tab = this.currentTab;
        }

        return variable;
    },


    /**
     *
     */
    update : function( callback )
    {
        this.updateTitle();
        this.setPadding();
        this.setTitleSize();

        if (!this.isVisible) return;

        var len = this.components.length;

        if (len > 0) {
            if (!callback) {
                callback = _.bind(this.displayMessages, this);
            }

            var queueOpts = { r:"klip-update", batchId: this.id, batchCallback: callback };

            while (--len != -1) {
                updateManager.queue(this.components[len], queueOpts);
            }
        } else {
            if (callback) {
                callback();
            } else {
                this.displayMessages();
            }
        }
    },

    setTitleSize: function()
    {
        this.toolbarEl.removeClass("klip-toolbar-xx-small klip-toolbar-x-small klip-toolbar-small klip-toolbar-new-small klip-toolbar-medium klip-toolbar-large klip-toolbar-x-large klip-toolbar-xx-large");
        this.toolbarEl.addClass("klip-toolbar-"+ this.titleSize);
    },

    /**
     * Maps the old Titles Sizes to the new ones.(Refer : Props.Defaults.klipTitleSizeMap )
     * @param config
     */
    updateOldTitleSizes: function (config) {
        if (config && config.titleSize && (config.titleSize === "small" || config.titleSize === "small-medium")) {
            this.titleSize = Props.Defaults.klipTitleSizeMap[config.titleSize];
        }
    },

    updateTitle : function()
    {
        var titleText = this.getVariableKlipName();

        if (this.hasDynamicTitle) {
            // update mobileUI title if applicable
            // -----------------------------------
            if (this.$listItem) this.$listItem.find("a").text(titleText);
            if (this.$mobileCard) this.$mobileCard.find("h1").text(titleText);
        }

        this.toolbarEl.find(".klip-toolbar-title").html(titleText.length === 0 ? "&nbsp;" : titleText);

        this.toolbarEl.toggleClass("hidden", !this.showToolbar);
    },

    getVariableKlipName : function()
    {
        var titleText = this.title ? this.title : "";

        if (this.hasDynamicTitle)
        {
            titleText = replaceMarkers(/\{([^}]*)\}/, this.title, _.bind(this.getKlipProp, this));
        }

        return titleText;
    },


    setVisible : function(isVis)
    {
        if ( this.isVisible == isVis ) return;
        this.isVisible = isVis;
        this.el.toggle(isVis);
        this.handleEvent( isVis ? {id:"klip-show"} : {id:"klip-hide"} );
//		if (isVis) this.update();
//        if(isVis && this.dashboard.warningSystem) this.dashboard.warningSystem.queueKlip(this.id);
    },

    /**
     *
     * @param isBusy - true if klip is busy; false otherwise
     * @returns {boolean} - true if the busy state was set
     */
    setBusy : function(isBusy)
    {
        if ( this.isBusy == isBusy ) return false;
        this.isBusy = isBusy;

        if (isBusy) {
            var _this = this;
            var $message = $("<div style='width:39px;'>")
                .append($("<div class='spinner-calculator'>").attr("title", "Processing formulas..."));

            if (!this.dashboard.config.mobile) {
                $message.append($("<div class='cancel-spinner'>").text("Cancel"))
                    .on("click", ".cancel-spinner", function() {
                        if (isIElt11()) {
                            _this.messageEl.hide();
                        } else {
                            _this.messageEl.css("pointer-events", "").removeClass("fade");
                        }

                        _this.messageVisible = false;
                    });
            }

            this.setMessage($message, { type:"spinner" });

            this.messageEl.addClass("fade");
            if (!isIElt11()) {
                this.messageEl.css("pointer-events", "auto");
            }
        } else {
            if (this.messageVisible == "spinner") {
                if (isIElt11()) {
                    this.messageEl.hide();
                } else {
                    this.messageEl.css("pointer-events", "").removeClass("fade");
                }

                this.messageVisible = false;
            }
        }

        this.handleEvent( isBusy ? {id:"klip-busy"} : {id:"klip-free"} );

        return true;
    },


    /**
     * @param message - {
     *      type:'',            // type of message
     *      id:'',              // id of asset that caused the message
     *      $message: $(),      // the message
     *      asset:{}            // the asset (component, datasource, etc)
     * }
     * @param category
     */
    addMessage : function(message, category)
    {
        var msgList;
        switch (category) {
            case "error":
                msgList = this.messages.errors;
                break;
            case "upgrade":
                msgList = this.messages.upgrades;
                break;
            default:
                msgList = this.messages.other;
                break;
        }

        var i = 0;
        var dupMessage = false;
        while (!dupMessage && i < msgList.length) {
            var oldMessage = msgList[i];

            if (message.type == oldMessage.type && message.id == oldMessage.id) {
                dupMessage = true;
            }

            i++;
        }

        if (!dupMessage) {
            msgList.push(message);
        }
    },

    displayMessages : function()
    {
        var widgetName = KF.company.get("brand", "widgetName");
        if (isWorkspace()) return;

        this.displayErrorWarning();

        var $message = $("<div>"),
            $messageItem;
        var hasMessages = false;

        if (this.messages.errors.length > 0) {
            hasMessages = true;
            $messageItem = $("<div class='klip-message-item'>")
                .append($("<div>")
                .append($("<div class='klip-error-message'>").text("There was a problem loading this " + widgetName + ".")))
                .appendTo($message);

            if (!this.dashboard.isPublished()) {
                $messageItem.append($("<div class='klip-message-link' actionId='view_errors_klip'>")
                    .data("actionArgs", this)
                    .text("View Errors"));
            }
        }

        if (this.messages.upgrades.length > 0) {
            hasMessages = true;
            $messageItem = $("<div class='klip-message-item'>")
                .append($("<div>")
                    .append($("<div>").text("This " + widgetName + " contains one or more out-of-date components.")))
                .appendTo($message);

            if (this.dashboard.rights && this.master) {
                var rights = this.dashboard.rights[this.master];
                if (KF.user.hasPermission("klip.edit") && (rights && rights.edit)) {
                    $messageItem.append($("<div class='klip-message-link' actionId='edit_klip'>")
                        .data("actionArgs", this)
                        .text("Upgrade Now"));
                }
            }
        }

        if (this.messages.other.length > 0) {
            hasMessages = true;

            _.each(this.messages.other, function(msg) {
                $message.append($("<div class='klip-message-item'>").append(msg.$message));
            });
        }

        if (hasMessages) {
            var _this = this;
            this.setMessage($message, {
                type:"messages",
                onClick: function(evt) {
                    var $target = $(evt.target);
                    if ($target.hasClass("klip-message-link")) return;

                    if (_this.messages.upgrades.length === 0 && _this.messages.other.length === 0) {
                        _this.messageEl.hide();
                        _this.messageVisible = false;
                    }
                }
            });

            this.messageEl.addClass("fade");

            if (!isIElt11()) {
                this.messageEl.css("pointer-events", "auto");
            }
        }
    },

    /**
     *
     * @param $message
     * @param config - {
     *      type:'',
     *      onClick: function() {}
     * }
     */
    setMessage : function($message, config)
    {
        this.messageVisible = config.type;

        this.messageEl.find(".inner-message").html($message);
        this.messageEl.removeClass().addClass("klip-message type-" + config.type).show();

        if (config.onClick) {
            this.messageEl.on("click.message", config.onClick);
        } else {
            this.messageEl.off("click.message");
        }

        this.resizeMessage();
    },

    resizeMessage : function()
    {
        if (!this.messageVisible) return;

        if (this.showToolbar) {
            var toolbarHeight = this.toolbarEl.outerHeight(true);
            this.messageEl.css("top", toolbarHeight);
        }

        var $inner = this.messageEl.find(".inner-message");
        var innerWidth = $inner.width();
        var innerHeight = $inner.height();

        $inner.css({
            "margin-top": - Math.floor(innerHeight / 2),
            "margin-left": - Math.floor(innerWidth / 2)
        });
    },

    /**
     * rather than several discrete event handlers for each button, one event handler
     * intercepts all click events on the toolbar
     * @param evt
     */
    onToolbarClick : function (evt)
    {
        if ($(evt.target).hasClass("klip-button-config"))
        {
            this.dashboard.klipContextMenu.hide(true);
            this.dashboard.klipContextMenu.show(evt.target, {actionArgs:this});
        }

		if ($(evt.target).hasClass("klip-button-annotation") && KF.user.hasPermission("dashboard.annotation.view"))
		{
			this.dashboard.annotationSystem.hide();
			this.dashboard.annotationSystem.show(this, evt.target);
		}

		if ($(evt.target).hasClass("klip-button-warning"))
		{
			this.dashboard.warningSystem.hide();
			this.dashboard.warningSystem.show(this, evt.target);
		}
	},

    showTemplateConfigurationWizard: function () {
        var connectHref;

        if (isPreview(this.dashboard)) {
            connectHref = $("#connectData")[0].href;
            document.location = connectHref;
        } else {
            page.invokeAction("klip_reconfigure_wizard", {
                klipId: this.master,
                klipInstanceId: this.id,
                tabId: this.currentTab
            });
        }
    },

    /**
     *
     */
    onLayout : function()
    {
        this.displayMessages();
    },

    /**
     * called when the klip is removed/deleted from the dashboard.
     *
     */
    destroy : function()
    {
        for (var i in this.components)
            this.components[i].destroy();

        this.el.remove();
    },

    /**
     * returns a list of all the formulas used by this klip
     * inspecting all the component formulas
     */
    getFormulas : function()
    {
        var formulas = [];
        for (var i in this.components)
        {
            this.components[i].getFormulas(formulas);
        }
        return formulas;
    },


    /**
     * returns a list of all the datasources that this klip makes use of by
     * inspecting all the component formulas
     */
    getDatasources : function()
    {
        var datasources = [];
        var len = this.components.length;
        var i;
        for (i = 0; i < len; i++)
        {
            var cx = this.components[i];
            cx.getDatasources(datasources);
        }

        // make a unique list
        return datasources;
    },

    getDatasourceIds : function()
    {
        if(!this.workspace||!this.workspace.datasources) return [];

        return this.workspace.datasources;
    },

    /**
     * Add a datasource reference to the klip
     * @param id
     */
    addDatasource : function(id)
    {
        if(this.getDatasource(id))return;//don't add duplicates

        //initialize containers if required
        if(!this.workspace)this.workspace = {};
        if(!this.workspace.datasources)this.workspace.datasources = [];

        //add the id to array
        this.workspace.datasources.push(id);
    },

    /**
     * Remove a datasource reference from the klip
     * @param id
     */
    removeDatasource : function(id)
    {
        if (!this.workspace||!this.workspace.datasources) return;

        this.workspace.datasources.splice(_.indexOf(this.workspace.datasources, id), 1);
    },

    /**
     * @param id of ds to search for
     * @return return datasource id if exists otherwise return false
     */
    getDatasource : function(id)
    {
        if(!this.workspace||!this.workspace.datasources) return false;

        var idx = _.indexOf(this.workspace.datasources,id);
        if(idx != -1)return this.workspace.datasources[idx];

        return false;
    },

    updateDataSet: function(dataSet) {
        var oldDataSetIndex;
        if(this.workspace && this.workspace.dataSets) {
            oldDataSetIndex = this.workspace.dataSets.indexOf(this.getDataSet(dataSet.id));
            if(oldDataSetIndex !== -1) {
                this.workspace.dataSets.splice(oldDataSetIndex, 1, dataSet);
                return true;
            }
        }
        return false;
    },

    addDataSet: function(dataSet) {
        if (this.hasDataSet(dataSet.id)) {
            return;
        }

        this.workspace = this.workspace || {};
        this.workspace.dataSets = this.workspace.dataSets || [];

        this.workspace.dataSets.push(dataSet);
    },

    removeDataSet: function(id) {
        var removeIndex = -1;
        if (!this.workspace || !this.workspace.dataSets) {
            return;
        }
        removeIndex = this.workspace.dataSets.map(function(dataSet) { return dataSet.id; })
            .indexOf(id);
        if(removeIndex >= 0) {
            this.workspace.dataSets.splice(removeIndex, 1);
        }
    },

    hasDataSet: function(id) {
        return !!this.getDataSet(id);
    },

    getDataSet: function(id) {
        var dataSets = this.workspace && this.workspace.dataSets || [];
        var foundDataSet;
        var i;
        for(i = 0 ; i < dataSets.length; i++) {
            if(dataSets[i] && dataSets[i].id == id) {
                foundDataSet = dataSets[i];
                break;
            }
        }

        return foundDataSet;
    },

    getDataSets: function() {
        var dataSets = this.workspace && this.workspace.dataSets || [];

        return Array.prototype.slice.call(dataSets);
    },


    displayAnnotationStatus : function(total, numUnread)
    {
        if (!this.toolbarEl) return;

        var $annotation = this.toolbarEl.find(".klip-button-annotation");

        if(Number(numUnread) > 0)
        {
            $annotation.removeClass("klip-button-annotation-messages");
            $annotation.addClass("klip-button-annotation-unread");
        }
        else
        {
            $annotation.removeClass("klip-button-annotation-unread");
            if(Number(total) > 0)
            {
                $annotation.addClass("klip-button-annotation-messages");
            }
            else
            {
                $annotation.removeClass("klip-button-annotation-messages");
            }
        }
    },

    displayErrorWarning : function()
    {
        if (!this.menuModel) return;

        var errorIdx = _.indexOf(this.menuModel, "menuitem-view_errors_klip");
        var $configIcon = this.toolbarEl ? this.toolbarEl.find(".klip-button-config") : null;

        if (this.messages.errors.length > 0) {
            if (errorIdx == -1) this.menuModel.push("menuitem-view_errors_klip");
            if ($configIcon) $configIcon.addClass("warning");
        } else {
            if (errorIdx != -1) this.menuModel.splice(errorIdx, 1);
            if ($configIcon) $configIcon.removeClass("warning");
        }
    },

    displayWarning : function(numWarnings)
    {
        if (!this.toolbarEl) return;

        var $warningIcon = this.toolbarEl.find(".klip-button-warning");

        if (Number(numWarnings) > 0) {
            $warningIcon.show();
            this.toolbarEl.css("padding-right", "100px");
        } else {
            $warningIcon.hide();
            this.toolbarEl.css("padding-right", "");
        }
    },

    initializeForWorkspace : function(workspace)
    {
        this.el.click($.bind(this.onClick, this))
            .on("contextmenu", $.bind(this.onRightClick, this));

        if (this.workspace && this.workspace.dimensions) {
            this.setDimensions(this.workspace.dimensions.w);
        }

        $(".klip-resizable").remove();

        var _this = this;
        this.$klipResizable = $("<div class='klip-resizable'>")
            .appendTo(this.el.parent())
            .append(this.el)
            .append(this.$componentDropMessage)
            .resizable({
                handles:"e",
                helper:"resize-helper",
                maxWidth: 2560,
                stop: function(event, ui) {
                    _this.klipResized(ui.size.width);
                }
            });
        this.$klipResizable.on("setDefaultMinWidth", function() {
            $(this).resizable( "option", "minWidth", _this.KLIP_MIN_WIDTH_DEFAULT);
        });

        this.$klipResizable.on("setZeroStateMinWidth", function() {
            $(this).resizable( "option", "minWidth", _this.KLIP_MIN_WIDTH_ZERO_STATE);
        });

        this.$klipResizable.trigger("setDefaultMinWidth");

        for (var i = 0; i < this.components.length; i++) {
            this.components[i].initializeForWorkspace(workspace);
        }
        if(!this.components.length) {
            this.$klipResizable.trigger("setZeroStateMinWidth");
            this.$emptyMessage.show();
            this.openComponentsTab();
        }
        this.update();
    },

    onClick : function(evt)
    {
        var target = this.getSelectionTarget(evt);

        if (target == this) {
            page.invokeAction("select_klip", false, evt);
        } else {
            page.invokeAction("select_component", target, evt);
        }
    },

    onRightClick : function(evt) {
        var target = this.getSelectionTarget(evt);
        var rootComponent;
        var menuCtx = {};

        evt.preventDefault();

        if (!target) {
            return;
        }

        if (target == this) {
            page.invokeAction("select_klip", false, evt);

            menuCtx.includePaste = $(evt.target).is(".klip-body");
        } else {
            rootComponent = target.getRootComponent();
            page.invokeAction("select_component", target, evt);

            menuCtx.isRoot = (target == rootComponent);
        }

        page.invokeAction("show_context_menu", { target:target, menuCtx:menuCtx }, evt);
    },

    getSelectionTarget : function(evt)
    {
        var target = $(evt.target);

        if ( target.is(".klip-toolbar") || target.is(".klip-toolbar-title") || target.is(".klip-body") )
        {
            return this;
        }
        else
        {
            var compEl = target.is(".comp") ? target : target.closest(".comp");
            var cx = compEl.data("cx");

            if (compEl.length > 0 && !cx) {
                cx = this.getComponentById(compEl.attr("id"));
                compEl.data("cx", cx);
            }

            return cx;
        }
    },

    klipResized : function(newWidth)
    {
        page.klipResized = true;

        var $klipResizable = $(".klip-resizable");
        this.setDimensions($klipResizable.width());

        $klipResizable.css({width:"", height:""});
    },


    getContextMenuModel : function(ctx)
    {
        var model = [];

        var hasContents = (this.components.length > 0);
        var clipboardContents = $.jStorage.get("component_clipboard");

        if (ctx.includePaste) {
           model.push(
               { id:"menuitem-paste", text:"Paste", disabled:!clipboardContents, actionId:"paste_component", actionArgs:{parent:this} }
           );
        }

        model.push(
            { id:"menuitem-remove", text:"Clear", disabled:!hasContents, actionId:"remove_components", actionArgs:{parent:this} },
            { id:"separator-rename", separator:true },
            { id:"menuitem-view_source", text:"View source", actionId:"edit_raw" }
        );

        return model;
    },

    getEditorMenuModel : function()
    {
        var model = { text:KF.company.get("brand", "widgetName"), actionId:"select_klip", actionArgs:this};

        if (this.components) {
            model.items = [];
            for (var i = 0; i < this.components.length; i++) {
                var childModel = this.components[i].getEditorMenuModel();
                if ( childModel ) model.items.push(childModel);
            }
        }

        return model;
    },

    getPropertyModel : function()
    {
        var model = [];
        var widgetName = KF.company.get("brand", "widgetName");

        model.push({
            type:"text",
            id:"title",
            value: this.title,
            label:widgetName + " Title",
            group:"main"
        });

        model.push({
            type:"select",
            id:"titleSize",
            label:"Title Size",
            width:"250px",
            group:"main",
            selectedValue:this.titleSize,
            options: Props.Defaults.klipTitleSize
        });


        model.push({
            type:"checkboxes",
            id:"showToolbar",
            group:"main",
            options:[
                {
                    value:true,
                    label:"Show " + widgetName + " title",
                    checked: this.showToolbar
                }
            ]
        });


        model.push({
            type:"checkboxes",
            id:"animate",
            label:"Animation",
            group:"animation",
            help:{ link:"klips/update-animation" },
            options:[
                {
                    value:true,
                    label:"Flash " + widgetName + " when its data updates",
                    checked:this.animate
                }
            ]
        });


        model.push({
            type:"checkboxes",
            id:"useSamePadding",
            label:widgetName + " Padding",
            group:"padding",
            help:{ link:"klips/klip-padding" },
            fieldMargin:"9px 0 0 0",
            options:[
                {
                    value:true,
                    label:"Same for all sides",
                    checked:this.useSamePadding
                }
            ]
        });

        model.push({
            type:"text",
            id:"padding",
            group:"padding",
            minorLabels: [
                {label: "px", position: "right", css:"quiet italics"}
            ],
            width:38,
            fieldMargin:"0 0 9px 0",
            displayWhen: {useSamePadding:true},
            value: this.padding,
            isNumeric:true,
            min:0
        });

        model.push({
            type:"text",
            id:"paddingTop",
            group:"padding",
            minorLabels: [
                { label:"Top:", position:"left", width:"30px" }
            ],
            width:38,
            fieldMargin:"0 0 9px 0",
            displayWhen: {useSamePadding:false},
            value: this.paddingTop,
            isNumeric:true,
            min:0
        });

        model.push({
            type:"text",
            id:"paddingBottom",
            group:"padding",
            minorLabels: [
                { label:"Bottom:", position:"left", width:"53px" }
            ],
            width:38,
            fieldMargin:"0 0 9px 0",
            displayWhen: {useSamePadding:false},
            value: this.paddingBottom,
            isNumeric:true,
            min:0,
            flow:{padding:"40px"}
        });

        model.push({
            type:"text",
            id:"paddingLeft",
            group:"padding",
            minorLabels: [
                { label:"Left:", position:"left", width:"30px" }
            ],
            width:38,
            fieldMargin:"0 0 9px 0",
            displayWhen: {useSamePadding:false},
            value: this.paddingLeft,
            isNumeric:true,
            min:0,
            breakFlow:true
        });

        model.push({
            type:"text",
            id:"paddingRight",
            group:"padding",
            minorLabels: [
                { label:"Right:", position:"left", width:"53px" }
            ],
            width:38,
            fieldMargin:"0 0 9px 0",
            displayWhen: {useSamePadding:false},
            value: this.paddingRight,
            isNumeric:true,
            min:0,
            flow:{padding:"40px"}
        });

        model.push({
            type:"text",
            id:"innerPadding",
            label:"Inter-Component",
            group:"padding",
            minorLabels: [
                {label: "px", position: "right", css:"quiet italics"}
            ],
            help:{ link:"klips/inter-component-padding" },
            width:38,
            value: this.innerPadding,
            isNumeric:true,
            min:0
        });

        return model;
    },

    getPropertyModelGroups: function()
    {
        return false;
    },

	/**
	 * tells this component what its width and height should be.  if a change is detected than onReize is triggered.
	 * @param w
	 * @param h
	 */
	setDimensions : function(w, h)
	{
        var klipWidthBP = this.el.outerWidth() - this.el.width();
        var klipHeightBP = this.el.outerHeight() - this.el.height();

        var widthNoBP = w - klipWidthBP;
        var heightNoBP = h - klipHeightBP;

        var dimChanged = ( this.dimensions.w != widthNoBP || this.dimensions.h != heightNoBP );
        this.dimensions = { w:widthNoBP, h:heightNoBP };

        this.availableWidth = widthNoBP - (this.useSamePadding ? this.padding * 2 : parseInt(this.padding[1]) + parseInt(this.padding[3]));

		if ( dimChanged )
		{
			this.el.width(this.dimensions.w);

			if (this.dimensions.h) {
                this.el.height(this.dimensions.h);
            }

			if (!this.workspace) this.workspace = {};
			this.workspace.dimensions = {w:w, h:h};
			this.onResize();
		}
	},


	/**
	 *
	 * @param evt
	 */
	onResize : function(evt)
	{
		for (var i in this.components) {
			var cx = this.components[i];
			cx.setDimensions(this.availableWidth);
		}

        this.resizeMessage();
	},

    /**
     * Invalidate and queue all components in the klip
     */
    invalidateAndQueue: function()
    {
        this.eachComponent(function(cx) {
            cx.invalidateAndQueue();
        });
    },

    reRenderWhenVisible: function()
    {
        this.eachComponent(function(cx) {
            cx.reRenderWhenVisible();
        });
    },

    getComponentById : function(id)
    {
        var result;
        this.eachComponent( function(cx){
            if ( cx.id == id ){
                result = cx;
                return true;
            }
        });
        return result;
    },

    getAllComponents : function() {
        var allComps = {};
        this.eachComponent( function(cx) {
            allComps[cx.id] = cx;
        });
        return allComps;
    },

    getComponentByVCId : function(id)
    {
        var result;
        this.eachComponent( function(cx){
            if ( cx.getVirtualColumnId() == id ){
                result = cx;
                return true;
            }
        });
        return result;
    },


    /**
     * iterates recursively over all components in the Klip and invokes the provided callback function
     * on them.
     * @param callback
     * @param async
     */
    eachComponent : function(callback, async)
    {
        eachComponent(this, callback, async);
    },

    /**
     * broadcast an event to this klip and its components, recursively
     * @param evt
     */
    handleEvent : function(evt,async)
    {
        this.eachComponent( function(cx){
            cx.onEvent(evt);
        },async);
    },

    setAppliedMigration : function(migrationName, value) {
        if (undefined === this.appliedMigrations) {
            this.appliedMigrations = {};
        }
        this.appliedMigrations[migrationName] = value;
    },

    getAppliedMigrations : function() {
        if (undefined === this.appliedMigrations) {
            this.appliedMigrations = {};
        }
        return this.appliedMigrations;
    },

    getComponentDependers: function(component) {
        var dependers;
        if (this.componentDependerMap === null) {
            this.refreshComponentDependerMap();
        }
        
        dependers = this.componentDependerMap[component.id];
        return dependers || [];
    },

    hasResultReferences: function() {
        return this._hasResultReferences;
    },

    doesDstRootDependOnOtherRoot: function(rootId, otherRootId) {
        if (this._fullRootDependencies && this._fullRootDependencies[rootId]) {
            return (this._fullRootDependencies[rootId].indexOf(otherRootId) != -1);
        }
        return false;
    },

    getOtherDstRootsNeeded: function(rootId) {
        var rootsNeeded = [];
        var id;

        if (this.componentDependerMap === null) {
            this.refreshComponentDependerMap();
        }
        for (id in this._fullRootDependencies) {
            if (this._fullRootDependencies[id].indexOf(rootId) != -1) {
                rootsNeeded.push(id);
            }
        }
        if (rootsNeeded.indexOf(rootId) == -1) {
            rootsNeeded.push(rootId);
        }
        return rootsNeeded;
    },

  getDstRootDependents: function(rootId) {
    var rootsNeeded = [];


    if (this.componentDependerMap === null) {
      this.refreshComponentDependerMap();
    }

    if (this._fullRootDependencies[rootId]) {
        rootsNeeded = this._fullRootDependencies[rootId].slice(0);
    }

    return rootsNeeded;
  },

  refreshComponentDependerMap: function() {
    var _this = this;
    var rootId;

    this.componentDependerMap = {};
    this._dstRootDependerMap = {};
    this._hasResultReferences = false;

    if (!this.isPostDST()) {
      return;
    }

    var allComps = this.getAllComponents();
    this.getFormulas().forEach(function (formula) {
      var cx = formula.cx;
      var componentDependers;

      var references = formula.getResultsReferencesArray();
      var formulaReferences = formula.getFormulaReferencesArray();

      //Add any results references referred to by the formula references
      if (cx && formulaReferences && (formulaReferences.length > 0) && references) {
        formulaReferences.forEach(function (formulaReference) {
          var formRefComp;
          if (formulaReference.type === "cx") {
            formRefComp = allComps[formulaReference.id];
            if (formRefComp && formRefComp.getFormula()) {
              references = references.concat(formRefComp.getFormula().getResultsReferencesArray());
            }
          }
        });
      }

      if (cx && references && (references.length > 0)) {
        _this._hasResultReferences = true;
        references.forEach(function (reference) {
          var root, refComp, refRoot;

          if (reference.type === "cx") {
            componentDependers = _this.componentDependerMap[reference.id];
            if (!componentDependers) {
              componentDependers = {};
              _this.componentDependerMap[reference.id] = componentDependers;
            }
            if (!componentDependers[cx.id]) {
              componentDependers[cx.id] = cx;
            }
          }

          //Update dst root depency map
          root = cx.getDstRootComponent();
          refComp = allComps[reference.id];
          refRoot = refComp && refComp.getDstRootComponent();

          if (root && refRoot && root.id !== refRoot.id) {
            if (!_this._dstRootDependerMap[refRoot.id]) {
              _this._dstRootDependerMap[refRoot.id] = {};
            }
            _this._dstRootDependerMap[refRoot.id][root.id] = true;
          }
        });

      }
    });

    //Now clear all dependent formula information on the components
    this.components.forEach( function(component) {
      component.clearDependentFormulasCache();
    });

    this._fullRootDependencies = {};
    for (rootId in this._dstRootDependerMap) {
      if (this._dstRootDependerMap.hasOwnProperty(rootId)) {
        this._walkRootDependencies(rootId, {}, []);
      }
    }

  },

    _walkRootDependencies: function(rootId, visited, fullDependencies) {
        var dependId;
        var i;
        if (!this._fullRootDependencies[rootId]) {
            this._fullRootDependencies[rootId] = [];
        }
        if (this._dstRootDependerMap[rootId]) {
            for (dependId in this._dstRootDependerMap[rootId]) {
                if (this._dstRootDependerMap[rootId].hasOwnProperty(dependId)) {
                    if (!visited[dependId]) {
                        visited[dependId] = true;
                        fullDependencies = this._walkRootDependencies(dependId, visited, fullDependencies);

                        for (i=0; i < fullDependencies.length; i++) {
                            if (this._fullRootDependencies[rootId].indexOf(fullDependencies[i]) === -1) {
                                this._fullRootDependencies[rootId].push(fullDependencies[i]);
                            }
                        }
                        if (this._fullRootDependencies[rootId].indexOf(dependId) === -1) {
                            this._fullRootDependencies[rootId] = this._fullRootDependencies[rootId].concat([dependId]);
                        }
                    }
                }
            }
        }
        visited[rootId] = true;
        return this._fullRootDependencies[rootId];
    },

    isPostDST : function() {
        return (this.appliedMigrations && this.appliedMigrations["post_dst"]);
    },

    clearAllResolvedFormulas : function() {
        this.eachComponent(function(cx) {
            if (cx && cx.resolvedFormulas) {
                cx.resolvedFormulas = [];
            }
        }, false);
    },

    /**
     * opens the component tab if the sidebar is open
     */
    openComponentsTab : function()
    {
        if (page.paletteOpen){
            $("#control-palette").css("display", "none");
            $("#control-tab").removeClass("active");

            $("#component-palette").css("display", "inline");
            $("#component-tab").addClass("active");
        }
    }

});

;/****** c.klipfactory.js *******/ 

KlipFactory = $.klass({

    componentUUID : 1,

    layoutMap : {
        "default" : "StackedLayout"
    },

    componentMap :{
    },

    initialize : function()
    {
        // register all component classes by their factory ID for the
        // createComponent DSL
        // ----------------------------------------------------------
        var self = this;
        $.each(Component, function()
        {
            var c = new this();
            if (c.factory_id) self.componentMap[c.factory_id] = this;
        });
    },


    createKlip : function(dashboard, config)
    {
        var klip = new Klip(dashboard);
        klip.configure(config);
        klip.renderDom();
        klip.afterFactory();

        if(config.components){
            var len = config.components.length;
            for(var i = 0 ; i < len ; i++) {
                try{
                    var cx = this.createComponent( config.components[i], dashboard );
                    klip.addComponent(cx);
                }catch(e){
                    console.log("error creating klip",e);
                }
            }
        }

        klip.eachComponent( function(cx){
            cx.applyConditions();
            if ((dashboard.config.cacheDisabled || klip.cacheDisabled || klip.datasourceProps || cx.cacheDisabled) && cx.formulas && (!dashboard.config.workspaceMode || dashboard.isPublished())) {
                cx.setData([[]]);
                $.jStorage.deleteKey(cx.id);
            }
        },false);

        PubSub.publish("klip-created", [klip]);

        return klip;

    },

    newInstanceOf : function( cx_factory_id )
    {
        if ( !this.componentMap[cx_factory_id] ) throw "no cx type: " + cx_factory_id;
        var compClass = this.componentMap[cx_factory_id];
        return new compClass();
    },

    /**
     * //todo: does this really need dashboard param?  will know better when datasources are more thought out.
     * @param config
     * @param dashboard
     */
    createComponent : function(config,dashboard)
    {

//		if ( !this.componentMap[config.type] ) throw "no cx type: " + config.type;
//
//		var compClass = this.componentMap[config.type];
        var cx = this.newInstanceOf(config.type);
        var i;
        var deps = cx.getDependencies();

        if (deps != undefined) cx.dependenciesLoaded = false;


        // assign or create a unique componentId
        // -------------------------------------
        if (config.id) cx.id = config.id;
        else cx.id = SHA1.encode(Math.random()+"").substring(0,8) + "-" + this.componentUUID++;

        // create formulas and subscribe to data events.
        if (config.formulas)
        {
            for (i = 0; i < config.formulas.length; i++) {
                var f = this.createFormula(config.formulas[i], cx);
                DX.Manager.instance.registerFormula(f);
                cx.formulas.push(f);
            }
        }

        // create conditions
        if ( config.conditions )
        {
            var crLen = config.conditions.length;
            if( crLen > 0 )
            {
                cx.conditions = [];
                for( i = 0; i < crLen; i++)
                {
                    var cnd = this.createCondition(config.conditions[i]);
                    cx.conditions.push(cnd);
                }
            }
        }

        // configure & render
        // ------------------
        var storedData = null; //$.jStorage.get(cx.id);
        if ( storedData && !(dashboard && dashboard.config.imagingMode) ) {
            // Don't use cached data in imaging mode
            cx.setData( storedData );
        } else {
            if ( isPreview(dashboard) && config.data ) {
                cx.setData( config.data[0] );
            }
        }

        cx.configure(config);
        cx.renderDom();

        // once a component's dom is rendered it's el property should
        // be assigned.  we then attach a reference to the backing-object
        // for efficient lookups.  Some components, like proxy's do not
        // have any HTML elements associated with them so we need to check first.
        // -------------------------------------------------------------
        if ( cx.el ) cx.el.data("cx", cx);

        if (deps != undefined)
        {
            cx.dependenciesLoaded = false;
            require(deps,function(){
                cx.dependenciesLoaded = true;
                cx.afterFactory();
            });
        }
        else
        {
            cx.afterFactory();
        }


        return cx;
    },

    destroyComponent : function(cx)
    {
        if (!cx.formulas) return;
        DX.Manager.instance.unwatchFormulas(cx.formulas);
        cx.formulas = null;

//		for( var i in cx.formulas )
//		{
//			var f = cx.formulas[i];
//			DX.Manager.instance.unwatchFormulas(f);
//            cx.formulas[i] = null;
//		}
    },

    createLayout : function(config)
    {

    },


    createCondition : function(config)
    {
        return config;
    },


    createFormula: function(formulaConfig, component)
    {
        return new DX.Formula(formulaConfig.txt, formulaConfig.src, component, formulaConfig.ver);
    }

});

// singleton reference
// probably a bad idea to make additional instances since it will result in conflicting component ids
// ---------------------------------------------------------------------------------------------------
KlipFactory.instance = new KlipFactory();
;/****** c.mobile.js *******/ 

MobileDashboard = $.klass( Dashboard , {

	tabs : false,
	klips : false,

	initialize : function(config)
	{
		this.tabs = [];
		this.klips = [];
        var configDefaults = { mobile:true };

        this.config = $.extend({}, configDefaults, config);

        // setup for user property handling
        // ---------------------------------
        this.dashboardProps = [];
		this.queuedDashboardProps = [];
		this.sendUserProps = _.debounce(_.bind(this.sendUserPropsNow, this), 200);


		// normalize orientations to either portrait (0) or landscape
		if (window.orientation == undefined || window.orientation == 0 || window.orientation == 180) {
            this.config.orientation = 0;
        } else {
            this.config.orientation = 1;
        }

        var _this = this;
        $(window).bind("orientationchange", function() {
			// normalize orientations to either portrait (0) or landscape
			if (window.orientation == undefined || window.orientation == 0 || window.orientation == 180) {
                _this.config.orientation = 0;
            } else {
                _this.config.orientation = 1;
            }

			setTimeout( _.bind(_this.resizeActiveKlip,_this), 500);
		}).resize(function() {
            setTimeout( _.bind(_this.resizeActiveKlip,_this), 500);
        });
	},


	addTab : function(config) {
		if (!config.klips) config.klips = []; // lazy-init klips array
        this.tabs.push(config);

		config.$tabRoot = $("<li>")
			.addClass("mobile-tab")
			.attr("data-role", "list-divider")
			.attr("id", "tab-" + config.id)
			.html(config.name);

		config.$el = config.$tabRoot;

		$("#c-klips").append(config.$tabRoot);

		return config;
	},

    getTab : function(id)
    {
        var tab = false;
        for (var i = 0, len = this.tabs.length; i < len; i++) {
            tab = this.tabs[i];
            if (tab.id == id) break;
        }

        if (!tab) console.log("no tab found:" + id);

        return tab;
    },

    updateTabWidths : function($super)
    {

    },


	addKlip : function(config, tabId)
	{
		var tab = this.getTab(tabId);
        if (!tab) return;

        var k = KlipFactory.instance.createKlip(this, config);
		k.currentTab = tabId;

        var uid = tabId.substr(0,8) + "-" + config.id;

        tab.klips.push(k);
		this.klips.push(k);

		k.$listItem = $("<li>").html("<a href='#" + uid + "' data-transition='slide'>" + k.title + "</a>");

		tab.$tabRoot.after(k.$listItem);

		$(document.body).trigger("klip-added", k);
        k.handleEvent({ id:"added_to_tab", tab:tab, isMobileWeb:true });

		var _this = this;
		var $page = $("<div>")
			.attr("data-role", "page")
			.attr("id", uid)
			.attr("data-url", uid)
			.attr("data-backbtn", false)
			.append(
				$("<div>")
					.attr("data-role", "header")
					.html("<a onclick=\"document.location.hash='#'\" data-transition=\"slide\">&lt; back</a> <h1>" + k.title + "</h1>")
			)
			.append(
				$("<div>")
					.attr("role", "main")
                    .addClass("ui-content")
					.append(k.el)
			)
			.bind("pageshow", function() {
				_this.activeKlip = k;

				k.setVisible(true);
				k.setDimensions(k.el.closest(".ui-content").width());

				updateManager.queue(k);

				//Watch formulas for only those Klips which are being clicked from the list.
                DX.Manager.instance.watchFormulas([{ kid: k.id, fms: k.getFormulas() }]);
			});

		k.$mobileCard = $page;
		$(document.body).append($page);

	},

    resizeActiveKlip : function()
    {
        if (!this.activeKlip) return;
        this.activeKlip.setDimensions(this.activeKlip.el.closest(".ui-content").width());
    }

});;/****** c.page_controller.js *******/ 

/**
 * Provides a central point for component inter-communication and control.
 *
 * Actions are registered into a Page Controller's actionMap.
 *
 */
var PageController = $.klass({ // eslint-disable-line no-unused-vars

	actionMap: {},

	/**
	 *
	 */
	initialize : function() {
		// listen for all clicks in the document and handle instances where the target
		// has an actionId attribute
		// ------------------------------------------------------------------------
		var _this = this;

		// on page load init
		$(function() {
			$("body").click(function(evt) {
				// for some reason $('body').click( $.bind(this.onPageClick,this) ) doesn't work properly..
				_this.onPageClick(evt);
			});
		});
	},


	/**
	 * handle page clicks.  if the target element has an actionId attribute, it
	 * will be handled by the PageController.
	 * @param evt
	 */
	onPageClick : function(evt) {
		var target = $(evt.target);
		var actionId = target.attr("actionId");
		var args;

		if (!actionId) {
			// a little bit of a hack to deal with elements that have actionIds associated with them ,but have
			// decorative markup where the click first starts, eg: list menu items that have <span> tags for decoration
			// -----------------------------------------------------------------------------------------------
			if( target.attr("parentIsAction") ) {
				target = target.closest("[actionId]").eq(0);
				actionId = target.attr("actionId");
				if( !actionId ) throw "child promised actionId but none found";
			} else {
				return;
			}
		}

		// extract arguments from the element clicked -- they can either be
		// in a attribute called 'actionArgs' or in the data scope variable 'actionArgs'
		// -----------------------------------------------------------------------------
		args = target.data("actionArgs");
		if (!args) args = target.attr("actionArgs");

		this.invokeAction(actionId, args, evt);
	},

	/**
	 * adds an action to the action map, keying it by its 'id' property.
	 * if the action has a keybinding, it will register it.
	 * @param a
	 */
	addAction : function(a) {
		var _this = this;
		var i;

		this.actionMap[ a.id ] = a;
		if (a.key) {
            if (a.key instanceof Array) {
                for (i = 0; i < a.key.length; i++) {
                    $(document).bind("keydown", a.key[i], function(evt) {
                        $.bind(_this.invokeAction(a.id, null, evt));
                    });
                }
            } else {
                $(document).bind("keydown", a.key, function(evt) {
                    $.bind(_this.invokeAction(a.id, null, evt));
                });
            }
		}
	},


	/**
	 * adds all actions in the Actions namespace
	 */
	addAllActions : function(actions) {
		var _this = this;
		$.each(Actions, function() {
			var action = new this();
			_this.addAction(action);
		});

		if (actions) {
            actions.forEach(function(action) {
                this.addAction(action);
            }.bind(this));
		}
	},


	/**
	 * invokes an action by id
	 * @param id the id of the action to invoke
	 * @param args arbitrary argument(s) to pass to the action
	 * @param evt optional event which triggered the action
	 */
	invokeAction : function(id, args, evt) {
		var action = this.actionMap[id];
		var actionContext;

		if (!action) {
			console.error("unknown action: " + id); // eslint-disable-line no-console
			return;
		}

		actionContext = { event:evt, page:this , args:args };
		action.execute(actionContext);
	}
});


/**
 * action namespace -- where all actions should go
 */
var Actions = {};

var Action = $.klass({ // eslint-disable-line no-unused-vars

	id:false,

	execute: function(actionContext) {

	}

})

	/*

	 jQuery pub/sub plugin by Peter Higgins (dante@dojotoolkit.org)

	 Loosely based on Dojo publish/subscribe API, limited in scope. Rewritten blindly.

	 Original is (c) Dojo Foundation 2004-2009. Released under either AFL or new BSD, see:
	 http://dojofoundation.org/license for more information.

	 */


	;
(function(d) {

	// the topic/subscription hash
	var cache = {};

	d.publish = function(/* String */topic, /* Array? */args) {
		// summary:
		//		Publish some data on a named topic.
		// topic: String
		//		The channel to publish on
		// args: Array?
		//		The data to publish. Each array item is converted into an ordered
		//		arguments on the subscribed functions.
		//
		// example:
		//		Publish stuff on '/some/topic'. Anything subscribed will be called
		//		with a function signature like: function(a,b,c){ ... }
		//
		//	|		$.publish("/some/topic", ["a","b","c"]);
		d.each(cache[topic], function() {
			this.apply(d, args || []);
		});
	};

	d.subscribe = function(/* String */topic, /* Function */callback) {
		// summary:
		//		Register a callback on a named topic.
		// topic: String
		//		The channel to subscribe to
		// callback: Function
		//		The handler event. Anytime something is $.publish'ed on a
		//		subscribed channel, the callback will be called with the
		//		published array as ordered arguments.
		//
		// returns: Array
		//		A handle which can be used to unsubscribe this particular subscription.
		//
		// example:
		//	|	$.subscribe("/some/topic", function(a, b, c){ /* handle data */ });
		//
		if (!cache[topic]) {
			cache[topic] = [];
		}
		cache[topic].push(callback);
		return [topic, callback]; // Array
	};

	d.unsubscribe = function(/* Array */handle) {
		// summary:
		//		Disconnect a subscribed function for a topic.
		// handle: Array
		//		The return value from a $.subscribe call.
		// example:
		//	|	var handle = $.subscribe("/something", function(){});
		//	|	$.unsubscribe(handle);

		var t = handle[0];
		cache[t] && d.each(cache[t], function(idx) {
			if (this == handle[1]) {
				cache[t].splice(idx, 1);
			}
		});
	};

})(jQuery);;/****** c.tabPanelLibrary.js *******/ 

var dashboardTabLibrary = (function()
{

	// private variables
	// -----------------
	var tabs = [];
	var currentPage = 0;
	var searchTotal = 0;

	// references to jquery-created input elements
	// --------------------------------------------
	var $searchInput;
	var $sortInput;
	var $tabListInput;

	// on-load script:  probably want to do some initial rendering
	// of form elements
	// -----------------------------------------------------------
	$(function()
	{
		$searchInput = $("#tabSearchField");
		$sortInput = $("#tabSortSelect");
		$tabListInput = $("#tabListSelect");

		$tabListInput.change(function () { currentPage = 0; getTabs (); });
		$sortInput.change(function () { currentPage = 0; render (); });
		$searchInput.keyup(function () { currentPage = 0; render (); });
		$("#tabPageLeft").click(pageLeft);
		$("#tabPageRight").click(pageRight);
		$("#tabSearchPanel").click(clearSearch);
	});


	function render()
	{
		var start = currentPage * 12;
		var filteredTabs = searchTabs(tabs);
		sortTabs(filteredTabs);

        var $tabPanel = $("#tab-panel").empty();
        var $tabPageControls = $(".tabPageControl");

        if (filteredTabs.length == 0)
		{
            $tabPageControls.hide();
			$tabPanel.append($("<div class=\"italics\" style=\"color:#999999;\">You don't have access to any Dashboards for this selection.</div>"));
		}
		else
		{
            $tabPageControls.show();

            var upperLimit = start;
            var i, len, tempRow = "";
			for (i = start, len = start + 12; i < len; i++)
			{
				if ((i + 1) % 3 == 0 || i == filteredTabs.length) {
                    $tabPanel.append(tempRow);
				}

				if (i >= filteredTabs.length) {
					break;
				}

				if (i % 3 == 0 || i == start) {
					tempRow = $("<tr style='height:16px'>");
				}

				tempRow.append($("<td>")
                    .append($("<div id='add-tab-" + filteredTabs[i].publicId + "'>")
                        .addClass("action-add panel-add-tab")
                        .attr("actionId", "add_tab")
                        .attr("actionArgs", filteredTabs[i].publicId)
                        .text(filteredTabs[i].name)));

                upperLimit++;
            }

			$("#numPageTabs").empty().append((start + 1) + " to " + upperLimit + " of " + filteredTabs.length);

            searchTotal = filteredTabs.length;

            if (upperLimit == filteredTabs.length) {
				//disable page up
				$("#tabPageRight").addClass("disabled");
			} else {
				$("#tabPageRight").removeClass("disabled");
			}

			if (start == 0) {
				//disable page down
				$("#tabPageLeft").addClass("disabled");
			} else {
				$("#tabPageLeft").removeClass("disabled");
			}
		}
	}

	function sortTabs(tabsToFilter)
	{
		if ($sortInput.val() == "Last Updated")
		{
			tabsToFilter.sort(function(a, b)
			{
				if (a.lastUpdated < b.lastUpdated) return 1;
				if (a.lastUpdated > b.lastUpdated) return -1;
				return 0;
			});
		}
		else if ($sortInput.val() == "Name")
		{
			tabsToFilter.sort(function(a, b)
			{
				if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
				if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
				return 0;
			});
		}

	}


	function getTabs()
	{
		// do some ajax stuff, then render...
        if (typeof dashboard != "undefined")
        {

            $.ajax({
                type:"GET",
                data:{val:$tabListInput.val()},
                url:"/tabs/ajax_getDashboardTabs",
                success : function(resp)
                {

                    tabs = resp.sharedTabs;
                    render();
                },
                error : function(resp)
                {
                }
            });
        }
	}

	function pageLeft()
	{
		if (currentPage != 0)
		{
			currentPage--;
			render();
		}
	}

	function pageRight()
	{
		if ((currentPage+1) * 12 < searchTotal)
		{
			currentPage++;
			render();
		}
	}

	function clearSearch(){
		currentPage=0;
		$searchInput.val("");
		render();
	}

	function searchTabs(tabsToFilter){
		if (!$searchInput.val())
		{
			$("#tabSearchPanel").removeClass("clear");
			return tabsToFilter;
		}
		else
		{
			$("#tabSearchPanel").addClass("clear");
			var newList = [];
			var searchVal = $searchInput.val().toLowerCase();
			for (var $x = 0; $x < tabsToFilter.length; $x++)
			{
                if (tabsToFilter[$x].name.toLowerCase().indexOf(searchVal) != -1)
				{
					newList.push(tabsToFilter[$x]);
				}
			}
			return newList;
		}
	}


	// return an object that contains the 'publicly' accessible methods/properties
	return {
		"render" : render,
		"getTabs" : getTabs
	};
})();
;/****** c.tooltip_handler.js *******/ 

var TooltipHandler = $.klass({

	/** */
	tooltipIsVisible : false,

	/** */
	tooltipIsArmed: false,

	/** mouse is over tooltip */
	isOverToolip : false,

	/** the component that is armed for a tooltip */
	$overCx : false,
	/** the tooltip $el */
	$tooltip : false,

	/** flag to disable tooltip display */
	isEnabled : true,

	/** flag which indicates the tooltip has been recently displayed and should use the warmedUpDelays */
	isWarmedUp: false,

	/** callback that API users can put their own display function */
	onShow : false,

	/** number of pixels the mouse can stray from the arm position before the display timer is reset */
	sensitivity : 10,

	/** how long mouse must remain over a component to trigger a tooltip */
	showDelay : 1500,

	/** how long the muse must remain over a component to trigger a tooltip when 'warmed up' */
	warmedUpDelay : 300,

	/** how long the tooltip must remain hidden before it goes 'cold' */
	cooldownDelay : 600,

	/** animation speed when hiding */
	hideSpeed : 200,
	/** animation speed when showing */
	showSpeed : 400,

	/** */
	startPosition : {left:0,top:0},
	displayPosition : {left:0,top:0},
	tooltipPosition : {left:0,top:0},

	displayTimeout : false,
	cooldownTimeout : false,

	lastOverEvt : false,

	/** a pre-bound showTooltip fn */
	_showTooltip : false,
	/** a pre-bound hideTooltip fn */
	_hideTooltip : false,


	initialize : function()
	{
		$(document.body).mouseover( $.bind(this.onMouseOver,this) );
		$(document.body).mousemove( $.bind(this.onMouseMove,this) );

		this._showTooltip = $.bind(this.showTooltip,this);
		this._hideTooltip = $.bind(this.hideTooltip,this);

		this.$tooltip = $("#klip-tooltip");
	},

	/**
	 * util to climb up the parents of $t until a component is found.
	 * @param $t
	 */
	getParentComponent : function($t)
	{
		if ($t.hasClass("comp"))  return $t;
		var $parentQuery = $t.parents(".comp");
		if ($parentQuery.length != 0 ) return $parentQuery.eq(0);
		return false;
	},

	/**
	 * util to determine if $t is a child of the tooltip
	 * @param $t
	 * @returns true if $t is a child of the tooltip
	 */
	isTooltip : function($t)
	{
		return ( $t.is("#klip-tooltip") || $t.parents("#klip-tooltip").length > 0);
	},


	onMouseOver : function(evt)
	{
		if ( !this.isEnabled ) return;

		var $t = $(evt.target);
		var $pc = this.getParentComponent($t);

		// if our internal state is 'over a component'...
		// -------------------------------------
		if (this.$overCx)
		{
			if (this.isTooltip($t)) // ..and we just moved over the tooltip
			{
				this.isOverToolip = true; // set flag to prevent tooltip from closing if the mouse moves too far away.
				return;
			}

			if (this.isOverToolip) this.isOverToolip = false;


			if (!$pc) // .. we just moved over something that is not a component (dead space)
			{
				this.onComponentOut(this.$overCx);
				this.$overCx = false;
			}
			else if (this.$overCx.equals($pc)) // .. we just moved over something that is contained in the current component, eg. we're still over
			{
				// no action...for now.
			}
			else // .. we just moved over a *different* component
			{
				this.onComponentOut( this.$overCx );
				this.onComponentOver( $pc );
			}

		}
		// otherwise, we're not currently over a cx.
		else
		{
			if ($pc) // we just moved over a cx, update our internal state, and fire onComponentOver
			{
				this.$overCx = $pc;
				this.onComponentOver($pc);
			}
		}

	},


	onComponentOver : function($cx)
	{
		this.startTimer( this.isWarmedUp ? this.warmedUpDelay : this.showDelay , true );
	},

	onComponentOut : function($cx)
	{
		if ( this.tooltipIsVisible ) this.startTimer( this.hideDelay, false);
		else this.disarm();
	},


	/**
	 * clears the display timer
	 */
	disarm: function()
	{
		this.tooltipIsArmed = false;
		clearTimeout( this.displayTimeout );
	},

	/**
	 *
	 * @param delay
	 * @param display if true, the show callback will be fired, otherwise the hide callback is called
	 */
	startTimer : function(delay,display)
	{
		clearTimeout(this.displayTimeout);
		this.tooltipIsArmed = true;
		if ( display )	this.displayTimeout = setTimeout( this._showTooltip , delay );
		else this.displayTimeout = setTimeout( this._hideTooltip , delay );
	},

	/**
	 * primary responsibilites:  detect if the mouse has moved too far from the startPosition, and if so, restart the timer
	 * @param evt
	 */
	onMouseMove : function(evt)
	{
		if (! this.isEnabled ) return;

		var dx,dy;

		this.displayPosition = { left: evt.pageX,  top:evt.pageY };

		if ( this.tooltipIsVisible && ! this.isOverToolip )
		{
			dx =  Math.abs(evt.pageX-this.tooltipPosition.left),
			dy =  Math.abs(evt.pageY-this.tooltipPosition.top);

			if ( dx > this.sensitivity || dy > this.sensitivity )
			{
				this.hideTooltip();
			}
		}

		// if the tooltip is armed, check to see if the mouse position has exceeded the sensitivity threshold,
		// and if so, restart the display timer
		if ( !this.tooltipIsArmed ) return;

		dx =  Math.abs(evt.pageX-this.startPosition.left),
		dy =  Math.abs(evt.pageY-this.startPosition.top);

		if ( dx > this.sensitivity || dy > this.sensitivity )
		{
			this.startPosition = { left: evt.pageX, top: evt.pageY };
			this.startTimer(this.isWarmedUp ? this.warmedUpDelay : this.showDelay,true);
		}
	},

	showTooltip : function()
	{
		this.disarm(); // to prevent re-display or flickers.
		clearTimeout( this.cooldownTimeout );

		// record the position we're displaying at so we can hide the tooltip if the mouse
		// gets too far away
		// -----------------------------------------------------------------------------
		this.tooltipPosition = this.displayPosition;
		$("#tooltip-helper").offset( this.tooltipPosition );

		this.$tooltip
			.fadeIn( this.isWarmedUp ? 0 : 300 )
			.position( { my:"center top-5", at:"center" , of: $("#tooltip-helper") } );
//			.position( { my:'center top-5', at:'center bottom' , of: this.$overCx, collision:'none' } );

		$("#tooltip-helper").offset( {left:0,top:0} );
		this.tooltipIsVisible = true;
		this.isWarmedUp = true;
	},

	hideTooltip : function()
	{
		this.$tooltip.fadeOut(200);
		this.tooltipIsVisible = false;

		this.lastOverEvt.pageX = this.displayPosition.left;
		this.lastOverEvt.pageY = this.displayPosition.top;

		// if we're still over a component we can re-arm the tooltip
		if ( this.$overCx ) this.startTimer( this.showDelay , true );

		this.cooldownTimeout = setTimeout( $.bind( this.cooldown, this ) , this.cooldownDelay );
	},


	/**
	 * clears the warmedUp flag -- exists as a function so that we can pass it to a setTimeout call
	 */
	cooldown: function()
	{
		this.isWarmedUp = false;
	}


});;/****** c.visualizer.data_set_table.js *******/ 

Visualizer.DataSetTable = $.klass(Visualizer.DataSetVisualizerBase, {

    selectedColumnId: "",

    initialize: function($super, config) {
        $super(config);

        if (config && config.visualizerId) {
            this.visualizerId = config.visualizerId;
        }

        this.el.addClass("data-set-visualizer");
    },

    render: function() {

        if (!KF.company.hasFeature("data_modeller")) {
            return;
        }

        var modellerProps = {
            isEditMode: false,
            dataSet: this.data
        };

        require(["/js/build/vendor.packed.js"], function(Vendor) {
            require(["/js/build/dataset_visualizer_index.packed.js"], function(DataSetVisualizer) {
                DataSetVisualizer.render(
                    modellerProps,
                    this.el[0]
                );

                this.setupScrollHandler();
            }.bind(this));
        }.bind(this));

    },

    getPointer: function() {
        // TODO: implement getPointer.
    },

    setPointer: function(address) {
        this.selectedColumnId = (address ? address : "");
        this.render();
    },

    getDataForPointer: function(pointer) {
        // TODO: implement getDataForPointer.
        return false;
    },

    scrollToSelection: function(pointer) {
        return false;
    },

    setupScrollHandler: function() {
        var $columns = $(".vz-container .column-row-list");
        if ($columns) {
            $columns.off("scroll");

            $columns.scroll(
                this.dataSetColumnScrollHandler.bind(this)
            );
        }
    },

    dataSetColumnScrollHandler: function(evt) {
        var $columns = $(".vz-container .column-row-list");

        $columns.off("scroll");


        $columns.each(function() {
            if (this !== evt.target) {
                $(this).scrollTop($(evt.target).scrollTop());
            }
        });

        // Timeout required for smooth scrolling on firefox
        setTimeout(function() {
            $columns.scroll(
                this.dataSetColumnScrollHandler.bind(this)
            )
        }.bind(this), 20);
    }
});
;/****** c.visualizer.table.js *******/ 

/**
 * renders tabular data in a grid.
 * data should be in the format:
 *   [
 *      {col:'A',value:'text'},
 *      ...
 *   ]
 *
 */
Visualizer.Table = $.klass(Visualizer.DatasourceVisualizerBase, {

    colIds : [ "A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"],

	autoScrollTimer: undefined,
	truncate: true,

    CELL_REFERENCE_REGEX: /^([a-zA-Z]+)(\d+)/,

	initialize : function($super,config)
	{
		$super(config);

		var _this = this;

		PubSub.subscribe("row-decorated", function(evtName, args) {
			if (_this.config.visualizerId == args.visualizerId) {
				_this.decorateRow(args.rowIndex, {
					addClass: args.addClass,
					removeClass: args.removeClass
				});
			}
		});
	},


    /**
     * render the visualizer markup
     * @param $super
     */
    render : function($super,target)
    {
        $super();

		// find the max column count
		var nRows = this.data.length;
	    var _this = this;
		this.maxCols = 0;
		this.startingCell = 0;
		this.lastSelectedCell = 0;
		this.previousCell = 0;
		this.isDragging = false;
		while(--nRows > -1)
		{
			this.maxCols = Math.max(this.maxCols, this.data[nRows].length);
		}

		var table = $("<table>").addClass("data-vis-table").addClass("unselectable");
		this.renderHeader(table, this.data[0]);

		for( var i = 0 ; i < this.data.length; i++)
        {
            var row = this.data[i];
            this.renderRow(table, row, i);
			if (i == 199 && this.truncate) break;
        }

        // attach click handler for table
        // -------------------------------
		table.mouseover( $.bind(this.onMouseOver,this) );
		table.mousedown( $.bind(this.onMouseDown,this) );
		table.mousemove( $.bind(this.onMouseMove,this) );

		$(document).mouseup($.bind(this.onMouseUp,this));

		table.disableSelection();

        this.el.append(table);

		if ( this.truncate && this.data.length > 200 )
		{
			var numHidden = this.data.length - 200;
			this.el.append( $("<div>").addClass("truncatedRow").html("To make this page load faster, <span class='strong'>" + numHidden + "</span> rows are not being shown. <span class='strong link'>Show all items.</span>"));
			$(".truncatedRow").click(function() {
				_this.truncate = false;
				_this.render($super, "");
			});
		}
		if (this.config.scrollToData) {
            $("html, body").animate({scrollTop: this.el.height()}, "slow");
        }


    },

    /**
     *
     * @param table
     * @param headerRow
     */
    renderHeader : function(table, headerRow)
    {
        var th = $("<thead>");
        var tr = $("<tr>").addClass("row-header-index");
        th.append(tr);
        tr.append($("<th>").addClass("dead-space"));

		var i = -1;

        while(++i < this.maxCols)
        {
            var colId = this.getColdId(i);
            var pointer = this.getHeaderPointer(colId);
            var col = headerRow[i];
            var cell = $("<th>").html(colId).attr("p",pointer);
            tr.append(cell);
        }

        table.append(th);
    },

	getHeaderPointer : function(colId){
		return colId + ":" + colId;
	},


	getColdId : function(idx)
	{
		var col = "";
		var length = this.colIds.length;

		if ( idx > (length-1) )
		{
			var firstLetterIdx = Math.floor(idx  / length) - 1;
			var secondLetterIdx = idx % length;
			return this.colIds[firstLetterIdx] + this.colIds[secondLetterIdx];
		}
		else
		{
			return this.colIds[idx];
		}
	},

	/**
	 *
	 * @param table
	 * @param rowModel
	 * @param rowIdx
	 */
    renderRow : function(table, rowModel, rowIdx)
    {
        var row = $("<tr>");
        row.append($("<td>").text(rowIdx+1).attr("p",this.getRowPointer(rowIdx))); // append row id

		var i = -1;

//		for( var i = 0 ; i < rowModel.length ; i++)
		while(++i < this.maxCols)
		{
            var pointer = this.getColdId(i) + (rowIdx+1);
            var col = rowModel[i];

			var text = "";

			if (!col) text = "";
			else if ( _.isString(col) ) text = col;
			else text = col.v;


            var cell = $("<td>").text(text).attr("p",this.getCellPointer(i,rowIdx));
            row.append(cell);
        }

        table.append(row);
    },

	decorateRow: function(rowIndex, options) {
		var $tdsToDecorate = this.el.find("tr:eq(" + rowIndex + ") td");

		if (options.addClass) {
			$tdsToDecorate.addClass(options.addClass);
		}

		if (options.removeClass) {
			$tdsToDecorate.removeClass(options.removeClass);
		}
	},

	getRowPointer : function(rowIdx){
		return (rowIdx+1)+":"+(rowIdx+1);
	},

	getCellPointer : function(colIdx,rowIdx){
		return this.getColdId(colIdx) + (rowIdx+1);
	},


    /**
     * clears the current selection from the UI and updates it based on the new pointer
     * @param p
     */
    setPointer : function (p)
    {
        this.el.find(".selected").removeClass("selected");

	    if ( !p ) return;
        // is this a range selector?
        if (this.isRangeSelector (p))
        {
            var result;
            if ( result = this.isRowSelector(p) )
            {
                var rowIdx = parseInt(result);
                this.el.find("tr:eq("+rowIdx+") td").addClass("selected");
            }
            else if(result = this.isColSelector(p) )
            {
                var colIdx = this.getIdxForColId( result[1] );
                this.el.find("td:nth-child("+(colIdx+2)+")").addClass("selected");
                this.el.find("th:nth-child("+(colIdx+2)+")").addClass("selected");
            }
	        else
            {
	            var range = p.split(":");
	            this.setRange(range[0],range[1],false);
            }
        }
        // otherwise, this must be a cell
        else
        {
            this.el.find("[p='"+p+"']").addClass("selected");
        }
		this.scrollToSelection(p);

    },

	isRowSelector : function(p){
		return p.match(/^\d+/);
	},

	isColSelector : function(p){
		return p.match(/^([a-zA-Z]+):([a-zA-Z]+)/);
	},

	setRange : function (p1, p2, publish)
    {
		var result;
		var row1Idx = 0;
		var col1Idx = 0;
		var row2Idx = 0;
		var col2Idx = 0;
		if ( result = this.matchRangeElement(p1) )
		{
			col1Idx = this.getIdxForColId(result[1])+1;
			row1Idx = parseInt(result[2]);
		}
		if ( result = this.matchRangeElement(p2) )
		{
			col2Idx = this.getIdxForColId(result[1])+1;
			row2Idx = parseInt(result[2]);
		}

		this.el.find(".selected").removeClass("selected");
		if (Math.abs(row2Idx - row1Idx) > Math.abs (col2Idx - col1Idx))
		{
			// Select a column range from lower to higher value
			if (row2Idx < row1Idx)
			{
				var temp = row1Idx;
				row1Idx = row2Idx;
				row2Idx = temp;
			}
			col2Idx = col1Idx;
			this.el.find("td:nth-child("+(col1Idx+1)+")").slice(row1Idx-1, row2Idx).addClass("selected");
		}
		else
		{
			// Select a row range from lower to higher value
			if (col2Idx < col1Idx)
			{
				temp = col1Idx;
				col1Idx = col2Idx;
				col2Idx = temp;
			}
			row2Idx = row1Idx;
			this.el.find("tr:eq("+row1Idx+") td").slice(col1Idx, col2Idx+1).addClass("selected");
		}

		if (publish)
		{
			var range = this.buildRange(col1Idx,row1Idx,col2Idx,row2Idx);
        	PubSub.publish("pointer-selected", [range, this.datasourceInstance, this.config.visualizerId]);
		}
    },

	/**
	 * helper function for set range that differs in subclasses
	 * @param pointer
	 */
	matchRangeElement : function(pointer){
	    return pointer.match(this.CELL_REFERENCE_REGEX);
	},

	/**
	 * helper function for set range that differs in subclasses
	 * @param c1
	 * @param r1
	 * @param c2
	 * @param r2
	 */
	buildRange : function(c1,r1,c2,r2){
		return this.getColdId(c1-1)+r1+":"+this.getColdId(c2-1)+r2;
	},

	isRangeSelector : function (p)
    {
		if (!p) return false;
		return (p.indexOf(":") != -1);
	},

    /**
     * parses a pointer element, eg:  'B1' or 'B' and returns the col/row
     * @param s
     */
    parsePointerElement : function(s)
    {

    },


    /**
     *
     */
    getPointer : function()
    {

    },


	/**
	 *
	 * @param evt
	 */
	onMouseMove : function(evt)
	{
		if ( this.isDragging ){
			var tableoffset = this.el.offset();
			var mY = evt.pageY - tableoffset.top;

			var scrollDir = 0;
			var thresholdBottom = 20,
				thresholdTop = 40; // to account for header
			var ourHeight=  this.el.height();

			// is the mouse over the scroll threshshold?
			// ----------------------------------------
			if( (ourHeight - mY) < thresholdBottom ) scrollDir = 1;
			else if ( mY < thresholdTop ) scrollDir = -1;

			// if scrollDir is non-zero, begin the autoScroll interval.
			// otherwise, clear the inverval.  Once the interval is started
			// it must also be cleared onMouseUp
			// --------------------------------------------------------
			if ( scrollDir != 0 ){

				if (this.autoScrollTimer == undefined ){
					var ourEl = this.el;
					this.autoScrollTimer = setInterval(function(){
						var st = ourEl.scrollTop( ) || 0;
						ourEl.scrollTop( st + (20*scrollDir) );
					},100);
				}
			}else{
				clearInterval(this.autoScrollTimer);
				this.autoScrollTimer = undefined;
			}


		}
	},


    /**
     *
     * @param evt
     */

	onMouseDown : function(evt)
    {
		if ( !this.selectionEnabled ) return;

		this.mouseDown = true;
		var p = $(evt.target).attr("p");

		if (p)
		{
			// Indicate we started dragging
			if (!this.isRangeSelector (p))
			{
				this.isDragging = true;
				this.startingCell = p;
				this.lastSelectedCell = p;
				if (!evt.shiftKey) this.previousCell = p;
				this.el.addClass("dragging");
			}
			else{
				this.startingCell = p;
				this.lastSelectedCell = p;
			}
		}
    },

	onMouseOver : function(evt)
    {
		if ( !this.selectionEnabled ) return;

		//If we are dragging from a cell, highlight the selection but don't publish it
		if (this.isDragging && this.startingCell)
		{
			var p = $(evt.target).attr("p");
			if (p && !this.isRangeSelector (p))
			{
				this.setRange(this.startingCell, p, false);
				this.lastSelectedCell = p;
			}
		}
    },

	onMouseUp : function(evt)
    {

	    if ( this.autoScrollTimer ){
		    clearInterval(this.autoScrollTimer);
		    this.autoScrollTimer = undefined;
	    }

		if ( !this.selectionEnabled || !this.mouseDown ) return;
		this.mouseDown = false;

		// If we are finished dragging from a cell, set the range
		var p = $(evt.target).attr("p");
		if ( this.isRangeSelector(p) )
		{
			this.setPointer(p);
			PubSub.publish("pointer-selected", [p, this.datasourceInstance, this.config.visualizerId]);
		}
		else if ( this.startingCell == this.lastSelectedCell )
		{
			if (this.isRangeSelector(this.startingCell))
			{
				this.setPointer(this.startingCell);
				PubSub.publish("pointer-selected", [this.startingCell, this.datasourceInstance, this.config.visualizerId]);
			}
			else if (evt.shiftKey && this.previousCell)
			{
				this.setRange(this.previousCell, this.startingCell, true);
			}
			else
			{
				this.setPointer(this.startingCell);
				PubSub.publish("pointer-selected", [this.startingCell, this.datasourceInstance, this.config.visualizerId]);
			}
		}
		else if (this.lastSelectedCell && this.lastSelectedCell!=this.startingCell)
		{
			this.setRange(this.startingCell, this.lastSelectedCell, true);
		}
		this.isDragging = false;
		this.el.removeClass("dragging");

    },

    /**
     *
     * @param pointer
     */
    getDataForPointer : function(pointer)
    {

    },

    /**
     * given a column display id (eg, 'C') return the index (eg, '2')
     * only handles up to ZZZ (18278 columns ought to be enough for anybody)
     * @param colId
     * @returns the column numeric index, otherwise -1
     */
    getIdxForColId : function(colId)
    {
        var i;
        var idx = 0;
        var result = -1;
        var len = colId.length;

        if (colId) {
            colId = colId.toUpperCase();
        }

        if (len == 1)
        {
            idx = colId.charCodeAt(0)-65;
            if ((idx < 0) || (idx > 25))
            {
                return -1;
            }
            result = idx;
        }
        else if (len == 2)
        {
            idx = colId.charCodeAt(1)-65;
            if ((idx < 0) || (idx > 25))
            {
                return -1;
            }
            result = idx;
            idx = colId.charCodeAt(0)-65;
            if ((idx < 0) || (idx > 25))
            {
                return -1;
            }
            result += (idx + 1) * 26;
        }
        else if (len == 3)
        {
            idx = colId.charCodeAt(2)-65;
            if ((idx < 0) || (idx > 25))
            {
                return -1;
            }
            result = idx;
            idx = colId.charCodeAt(1)-65;
            if ((idx < 0) || (idx > 25))
            {
                return -1;
            }
            result = (idx + 1) * 26;
            idx = colId.charCodeAt(0)-65;
            if ((idx < 0) || (idx > 25))
            {
                return -1;
            }
            result += (idx + 1) * 26 * 26;
        }
        return result;
    },


	setData : function($super,data)
	{
		$super(data);
	},

	scrollToSelection:function(p)
	{

		var scrollDistY;//find selected data's y location
		var scrollDistX;//find selected data's x location
		var $scrollTarget;
		if (this.isRangeSelector(p))
		{
			var result;
			if (result = this.isRowSelector(p))
			{
				var rowIdx = parseInt(result);
				scrollDistY = this.el.find("tr:eq(" + rowIdx + ") td")[1].offsetTop;
				scrollDistX = 0;
			}
			else if (result = this.isColSelector(p))
			{

				var colIdx = this.getIdxForColId( result[1] );
                var xLocationCell=this.el.find("th:nth-child("+(colIdx+2)+")")[0];
				scrollDistX = xLocationCell?xLocationCell.offsetLeft:0;
				scrollDistY = 0;
			}
			else
			{
				var range = p.split(":");
				var el = this.el.find("[p='" + range[0] + "']")[0];
				if (el){
					scrollDistY = el.offsetTop;
					scrollDistX = el.offsetLeft;
				}
				else{
					scrollDistY = 0;
					scrollDistX = 0;
				}
			}
		}
		// otherwise, this must be a cell
		else
		{
			$scrollTarget = this.el.find("[p='" + p + "']")[0];
			if ( $scrollTarget )
			{
				scrollDistY = $scrollTarget.offsetTop;
				scrollDistX = $scrollTarget.offsetLeft;
			}
		}

		//scroll the visualizer vertically if necessary
		if ((this.el.scrollTop() < scrollDistY) && (scrollDistY < this.el.scrollTop() + this.el.height()))
		{
			//do nothing
		}
		else if (scrollDistY < this.el.height())
		{
			this.el.scrollTop(0);
		}
		else
		{
			this.el.scrollTop(scrollDistY - 50);
		}
		//scroll the visualizer horizontally if necessary
		if ((this.el.scrollLeft() < scrollDistX) && (scrollDistX < this.el.scrollLeft() + this.el.width() ))//&& (scrollDistX < this.el.scrollLeft() + this.el.width())
		{
			//do nothing
		}
		else if (scrollDistX < this.el.width()-400)
		{
			this.el.scrollLeft(0);
		}
		else
		{
			this.el.scrollLeft(scrollDistX);
		}
	}
});


Visualizer.Excel = $.klass(Visualizer.Table, {

	currentSheet : 0,
	workbook : false,


	setData : function(data)
	{
		this.workbook = data;
		if (this.workbook && this.workbook.length > 1) {
			this.populateSelect();
			$("#vizOptionsPanel").show();
		}

		this.changeSheet();
		this.render();
	},

	populateSelect : function()
	{
		var $curSheet = $("#curSheet");

		$curSheet.empty();
		if (this.workbook) {
			for (var i = 0; i < this.workbook.length; i++)
			{
				$curSheet.append($("<option>").val(i).text(this.workbook[i].name));
			}
		}

		if (this.currentSheet) {
			$curSheet.val(this.currentSheet);
		}
	},

	updateSelect : function()
	{
		$("#curSheet").val(this.currentSheet);
	},

	changeSheet : function()
	{
		if (!this.workbook) return;

		var $curSheet = $("#curSheet");
		var changed = false;

		if (this.currentSheet != $curSheet.val() && this.workbook.length > 1) {
			changed = true;
		}

		if ($curSheet.length > 0) {
			this.currentSheet = $curSheet.val();
		}

		if (this.currentSheet == null || this.workbook[this.currentSheet] == null) {
			this.currentSheet = 0;
		}

		if (typeof this.workbook[this.currentSheet] !== "undefined") {
			this.data = this.workbook[this.currentSheet].rows;
		} else {
			this.data = [];
		}

		if (changed) this.render();
	},

	getCellPointer : function(coldIdx, rowIdx)
	{
		if (!this.workbook) return "";

		var pointer = this.getColdId(coldIdx) + (rowIdx + 1);
		return this.workbook[this.currentSheet].name + "," + pointer;
	},

	getRowPointer : function(rowIdx)
	{
		if (!this.workbook) return "";

		var name = this.workbook[this.currentSheet].name + ",";
		return name + (rowIdx + 1) + ":" + (rowIdx + 1);
	},

	getHeaderPointer : function(colId)
	{
		if (!this.workbook) return "";

		var name = this.workbook[this.currentSheet].name + ",";
		return name + colId + ":" + colId;
	},

 	isRowSelector : function(p)
	{
		return p.substring(p.lastIndexOf(",") + 1).match(/^\d+/);
	},

	isColSelector : function(p)
	{
		return p.substring(p.lastIndexOf(",") + 1).match(/^([a-zA-Z]+):([a-zA-Z]+)/);
	},

	matchRangeElement : function(pointer)
	{
		if (pointer.indexOf(",") != -1) {
			return pointer.substring(pointer.lastIndexOf(",") + 1).match(this.CELL_REFERENCE_REGEX);
		} else {
			return pointer.match(this.CELL_REFERENCE_REGEX);
		}
	},

	buildRange : function(c1, r1, c2, r2)
	{
		if (!this.workbook) return "";

		var sheet = this.workbook[this.currentSheet].name + ",";
		return sheet + this.getColdId(c1 - 1) + r1 + ":" + this.getColdId(c2 - 1) + r2;
	},

	getIndexFromName : function(sheetName)
	{
		var idx = -1;

		if (this.workbook) {
			for (var i = 0; i < this.workbook.length; i++) {
				if (this.workbook[i].name == sheetName) {
					idx = i;
					break;
				}
			}
		}

		return idx;
	},

	setPointer : function($super, p)
	{
		if (p) {
			$("#curSheet").val(this.getIndexFromName(p.substring(0, p.lastIndexOf(","))));
			this.changeSheet();
		}

		$super(p);
	}

 });
;/****** c.visualizer.tree.js *******/ 

/* global KF:false, Visualizer:false, help:false, DX:false, page:false, ordinal:false */
/**
 * displays json data provided in the format
 *
 * node : {
 *	  name : "",
 *   text : "",
 *   attributes : [],
 *   children : [],
 *
 * }
 *
 *
 * the three visualizer can have one selected node at a time, and the resulting
 * xpath is created based on the selection.
 *
 * when we render the data we add additional structure to it.  this extra data is stored with the ~ prefix, eg:

 * node['~constrained']
 * node['~index']
 * node['~parent']
 * node['~selectedAttribute']
 *
 *
 *
 */

Visualizer.Tree = $.klass(Visualizer.DatasourceVisualizerBase,
{
	truncate:true,
    collectionDepthTracker:[],
    STARTING_NODE_NUMBER:20,
    NESTED_OBJECT_NUMBER:100,
    NESTED_OBJECT_INCREMENT:100,
    NODE_LIMIT_MULTIPLIER : 1.5,
    INCREMENT_NODE_BY:25,
    treeType: "XML", // Default tree type

    initialize : function($super, config)
	{
		this.selectedNode = false;
		$super(config);

		this.el.click(_.bind(this.onClick, this));

		if (this.selectionEnabled)
		{
			this.$pathBar = $("<div>").attr("id", "vz-treepath");
            this.$helpIcon = $(help.renderIcon({topic: "tree-path"}));

            this.$container.prepend(this.$pathBar);
		}
	},


    /**
     * Sets original path to the path from the definition
     * @param targetObj
     */
    initOriginalPath: function(targetObj) {
        this.path = this.buildPath(targetObj);
        this.pointer = this.buildAddress();
    },


	/**
	 * if node-text
	 *		if node == attribute  --> /@leaf
	 *		 else --> ../@leaf[index]
	 * if leaf ../leaf
	 * if attribute { add attribute constraint } /
	 * @param evt
	 */
	onClick : function(evt)
	{
		var $t = $(evt.target);

		// find the closest .tree-node -- this is the main context of the click action
		// --------------------------------------------------------------------------
		var $targetNode = $(evt.target).closest(".tree-node").eq(0);
		if ($t.hasClass("truncatedNode"))return;
		// if the expand/collapse arrow was click, change UI and then return fast
		// ---------------------------------------------------------------------
		if ($t.hasClass("node-child-toggle") || !$targetNode.hasClass("leaf"))
		{
			$t.closest(".tree-node").toggleClass("closed");
			return;
		}

		// if selection is disabled return early -- everything else below is related to picking a node
		if (!this.selectionEnabled) return;

        // if options button was clicked, show overlay and return
        // ------------------------------------------------------
        if ($t.hasClass("node-options")) {
            this.showSelectionOptions($t);
            return;
        }

		if ($targetNode)
		{
			var targetNode = $targetNode.data("node");

            if(typeof targetNode !== "undefined"){
                // if shift key was pressed, mark the node as constrained
                if (evt.shiftKey)
                {
                    targetNode["~constrained"] = !targetNode["~constrained"];
                    PubSub.publish("pointer-selected", [this.buildAddress(), this.datasourceInstance, this.config.visualizerId]);
                    return;
                }


                if ($targetNode.hasClass("leaf"))
                {
                    targetNode["~constrained"] = false;

                    // if the target node is an attribute, we are really selecting the parent (who owns the attribute)
                    // ------------------------------------------------------------------------------------------------
                    if ($targetNode.hasClass("node-attribute"))
                    {
                        var parent = targetNode["~parent"];
    //						parent['~selectedAttribute'] = targetNode;
                        this.initOriginalPath(parent);
                        this.setSelectedNode(parent, targetNode);
                    }
                    else
                    {
                        this.initOriginalPath(targetNode);
                        this.setSelectedNode(targetNode);
                    }


                    if ($t.hasClass("node-text"))
                    {
                        var current = targetNode;
                        while (current)
                        {
                            current["~constrained"] = true;
                            current = current["~parent"];
                        }
                    }
                }
            }

			PubSub.publish("pointer-selected", [this.buildAddress(), this.datasourceInstance, this.config.visualizerId]);
		}
	},

	/**
	 * when a node is selected rebuild the path, and update the UI
	 * @param n
	 * @param selectedAtt
	 */
	setSelectedNode : function(n, selectedAtt)
	{
		this.path = this.buildPath(n, selectedAtt);
        this.pointer = this.buildAddress();

		if (this.selectedNode) {
			if (this.selectedNode["~selectedAttribute"]) {
				this.selectedNode["~selectedAttribute"]["~el"].removeClass("selected");
				delete this.selectedNode["~selectedAttribute"];
			} else if (this.selectedNode["~el"]) {
                this.selectedNode["~el"].removeClass("selected");
            }

            this.el.find(".node-options").remove();
		}

        var $options = $("<a>")
            .addClass("node-options")
            .text("Selection Options");

		if (selectedAtt) {
            selectedAtt["~el"].addClass("selected");
            selectedAtt["~el"].children(".node-data").append($options);
        } else if (n["~el"]) {
            n["~el"].addClass("selected");
            n["~el"].children(".node-data").append($options);
        }

		this.selectedNode = n;
		if (selectedAtt) this.selectedNode["~selectedAttribute"] = selectedAtt;
	},

    showSelectionOptions : function($target)
    {
        var $root = $("<div>")
            .width(500)
            .append("<h2 style='margin-bottom:10px;'>Selection Options</h2>");

        var options = this.getSelectionOptions();

        var $list = $("<ul>");
        for (var i = 0; i < options.length; i++) {
            var option = options[i];
            var id = "option" + i;

            var selClass = "";
            if (this.pointer == option.path) {
                selClass = "selected";
                this.selectionPath = option.path;
                $root.find("#selection-preview").html(this.setSelectionPreview(this.selectionPath));
            }

            $list.append($("<li>").attr({id: id, path: option.path}).data("lock", option.lock).addClass(selClass).html(option.desc));
        }

        $root
            .append($("<div>").attr("id", "selection-list").html($list))
            .append($("<div>").html("Preview").css("padding-top","10px").addClass("small quiet"))
            .append($("<div>").attr("id", "selection-preview").css("white-space", "nowrap"));

        var _this = this;
        $root.click(function(evt){
            var $t = $(evt.target);
            if ($t.is("li") || $t.is("li span")){
                $("#selection-list li.selected").removeClass("selected");

                if (!$t.is("li")) $t = $t.parent();

                $t.addClass("selected");
                _this.selectionPath = $t.attr("path");
                _this.selectionLock = $t.data("lock");

                $(this).find("#selection-preview").html(_this.setSelectionPreview(_this.selectionPath));
            }
        });

        var $overlay = $.overlay($target, $root, {
            shadow:{
                opacity:0,
                onClick: function(){
                    _this.selectionPath = null;
                    $overlay.close();
                }
            },
            footer:{
                controls:[
                    {
                        text:"Cancel",
                        css:"rounded-button secondary",
                        onClick: function(evt) {
                            $overlay.close();
                            _this.selectionPath = null;
                        }
                    },
                    {
                        text:"Set Selection",
                        css:"rounded-button primary",
                        onClick: function(evt) {
                            if (_this.selectionPath) {
                                // lock elements
                                for (var i = 0; i < _this.selectionLock; i++) {
                                    _this.selectionLock[i].locked = true;
                                }

                                _this.pointer = _this.selectionPath;
                                PubSub.publish("pointer-selected", [_this.selectionPath, _this.datasourceInstance, _this.config.visualizerId]);
                            }

                            $overlay.close();
                            _this.selectionPath = null;
                        }
                    }
                ]
            }
        });
        $overlay.show();
    },

    getSelectionOptions : function()
    {
        var i;
        var address = this.buildAddress().replace(/\[\d+\]/g,"");

        var eltName = this.selectedNode.name;
        var eltIndex = this.selectedNode["~index"];
        var eltAttr = this.selectedNode["~selectedAttribute"];

        var attrStr = " ";
        if (eltAttr) {
            attrStr = " the <span>" + eltAttr.name + "</span> attribute of ";
        }

        var ancestors = [];
        this.getAncestors(this.selectedNode, ancestors);

        var ancestorPaths = [];
        var pathAddress = address;
        var locks = [];
        for (i = 0; i < ancestors.length; i++) {
            var ancestor = ancestors[i];

            var description = "Select" + attrStr + "all <span>" + eltName + "</span> elements in this <span>" + ancestor.name + "</span> element.";
            pathAddress = this.insertSelectors(pathAddress, [{name:ancestor.name,value:ancestor["~index"]}]);
            locks.unshift(ancestor);

            ancestorPaths.unshift({desc: description, path: pathAddress, lock: locks});
        }

        locks.unshift(this.selectedNode);
        ancestorPaths.unshift({desc: "Select" + attrStr + "only this <span>" + eltName + "</span> element.",
            path: this.insertSelectors(pathAddress, [{name:eltName,value:eltIndex}]),
            lock:locks});

        var attrPaths = [];
        if (!eltAttr && this.selectedNode.attributes) {
            var attr = this.selectedNode.attributes;

            for (var x in attr) {
                if (this.selectedNode["~parent"]) {
                    var parentName = this.selectedNode["~parent"].name;
                    var parentIndex = this.selectedNode["~parent"]["~index"];

                    attrPaths.push({desc: "Select all peer <span>" + eltName + "</span> elements with the <span>" + x + "</span> attribute.",
                        path: this.insertSelectors(address, [{name:eltName,value:"@"+x}]),
                        lock: []});
                    attrPaths.push({desc: "Select all peer <span>" + eltName + "</span> elements with the <span>" + x + "</span> attribute set to \"<span>" + attr[x] +"</span>\".",
                        path: this.insertSelectors(address, [{name:eltName,value:"@"+x+"=\""+attr[x]+"\""}]),
                        lock: []});
                    attrPaths.push({desc: "Select all <span>" + eltName + "</span> elements with the <span>" + x + "</span> attribute in this <span>" + parentName + "</span> element.",
                        path: this.insertSelectors(address, [{name:parentName,value:parentIndex}, {name:eltName,value:"@"+x}]),
                        lock: [this.selectedNode["~parent"]]});
                    attrPaths.push({desc: "Select all <span>" + eltName + "</span> elements with the <span>" + x + "</span> attribute set to \"<span>" + attr[x] + "</span>\" in this <span>" + parentName + "</span> element.",
                        path: this.insertSelectors(address, [{name:parentName,value:parentIndex}, {name:eltName,value:"@"+x+"=\""+attr[x]+"\""}]),
                        lock: [this.selectedNode["~parent"]]});
                }

                attrPaths.push({desc: "Select all <span>" + eltName + "</span> elements with the <span>" + x + "</span> attribute.",
                    path: "//" + eltName + "[@" + x + "]",
                    lock: []});
                attrPaths.push({desc: "Select all <span>" + eltName + "</span> elements with the <span>" + x + "</span> attribute set to \"<span>" + attr[x] + "</span>\".",
                    path: "//" + eltName + "[@" + x + "=\"" + attr[x] + "\"]",
                    lock: []});
            }
        }

        var options = [];

        // only this element
        if (ancestorPaths.length > 0) {
            options.push(ancestorPaths.shift());
        }

        // default: all peer elements
        options.push({desc: "Select" + attrStr + "all peer <span>" + eltName + "</span> elements.", path: address, lock: []});

        // the nth element in all parent elements
        var parent = ancestors.length > 0 ? ancestors[ancestors.length-1] : null;
        if (parent != null) {
            options.push({desc: "Select" + attrStr + "the <span>" + ordinal(eltIndex) + " " + eltName + "</span> element in all <span>" + parent.name + "</span> elements.",
                path: this.insertSelectors(address, [{name:eltName,value:eltIndex}]),
                lock: [this.selectedNode]});
        }

        // all selected elements anywhere
        options.push({desc: "Select" + attrStr + "all <span>" + eltName + "</span> elements.", path: "//" + eltName + (eltAttr ? "/@" + eltAttr.name : ""), lock: []});

        // in this parent, grandparent, etc. up to root
        for (i = 0; i < ancestorPaths.length; i++) {
            options.push(ancestorPaths[i]);
        }

        // attributes options
        for (i = 0; i < attrPaths.length; i++) {
            options.push(attrPaths[i]);
        }

        return options;
    },

    insertSelectors : function(path, selectors)

    {
        var pathArr = path.split("/");

        while (selectors.length > 0) {
            var selector = selectors.pop();

            var i = _.indexOf(pathArr, selector.name);

            if (i != -1) {
                pathArr[i] += "[" + selector.value + "]";
            }
        }

        return pathArr.join("/");
    },

    getAncestors : function(node, list)
    {
        if (node["~parent"]) {
            list.unshift(node["~parent"]);
            this.getAncestors(node["~parent"], list);
        }
    },

    setSelectionPreview : function(path)
    {
        var pointer = this.datasourceInstance.id + "@" + path + ";";

        DX.Manager.instance.query(pointer, function(result, msg){
            if(msg.error) {
                console.log(msg);
            } else {
                $("#selection-preview").html(result.join("<br/>"));
            }
        });
    },

	/**
	 *
	 * @param n
	 * @param selectedAtt
	 */
	buildPath : function(n, selectedAtt)
	{
		var path = [];

		if (selectedAtt)
		{
			path.unshift({ node: selectedAtt , isAttribute:true });
		}


		var parent = n;
		while (parent)
		{
			if (!parent["~pathexclude"]) path.unshift({ node: parent  });
			parent = parent["~parent"];
		}

		return path;
	},


	/**
	 * go from the selectedNode up to the root to create an xpath
	 */
	buildAddress : function()
	{

		var newPath = [];
		_.each(this.path, function(p)
		{
			var txt = "";
			if (p.node["~adr"])
			{
				txt = p.node["~adr"];
			}
			else
			{
				if (p.isAttribute) txt += "@";
				txt += p.node.name;
			}
			if (p.locked) txt += "[" + p.node["~index"] + "]";
			newPath.push(txt);
		});

		return "/" + newPath.join("/");
	},


	/**
	 *
	 * @param p
	 */
	setPointer : function(p)
	{
		if (p == this.pointer)return;

		if (p == false)
		{
			this.setSelectedNode(p);

		}
		else
		{
			var strArray = p.substr(1, p.length).split("/");

			var allNodes = [];//list of nodes to search
			var foundNode = false;//storage for target node
			var foundAttr = false;//storage for target attribute if applicable
			var addrArray =  p.substr(1, p.length).split("/");//string array of level names: path to node we are looking for

			// collect root-level node as starting point
			allNodes.push({"node":this.data,"depth":0});

			// find the previously selected node
			while (allNodes.length != 0)
			{
				var nextItem = allNodes.pop();
				var n = nextItem.node;
				var addrIndex = nextItem.depth;

				if (addrIndex == addrArray.length - 1)//we have reached the end of the path
				{
					foundNode = n;
					break;
				}
				else if(addrArray[addrIndex+1] && addrArray[addrIndex+1].charAt(0) == "@")
				{//if the next item is an attribute then this is our parent node
                    if(n["~el"]) {
                        foundNode = n;
                        var findAddr = p.replace("@", "").replace(/\[\d+\]/g,"");
                        foundAttr = foundNode["~el"].find(".tree-node.node-attribute.leaf[address=\"" + findAddr + "\"]").data("node");
                        break;
                    }
				}

				//get children nodes that match names with the next level
				var items = n.children? this.filterChildren(n.children,addrArray[addrIndex+1], "name"):[];
				if(items.length == 0) continue;//no children/relevant children so move on to next node

				//if the next level has a lock only look at the specific child
				if (addrArray[addrIndex+1].search(/\[\d+\]/) != -1)
				{
					var childIndex = addrArray[addrIndex+1].slice(addrArray[addrIndex+1].indexOf("[") + 1, addrArray[addrIndex+1].indexOf("]")) - 1;
					allNodes.push({"node":items[childIndex],"depth":addrIndex+1});
				}
				else{//otherwise add all nodes
					for(var $i = items.length-1;$i>-1;$i--){
						allNodes.push({"node":items[$i],"depth":addrIndex+1});
					}
				}

			}

			//call set selected node then scroll to it
			if (foundAttr)//if an attribute
			{
				this.setSelectedNode(foundNode, foundAttr);
				this.scrollToSelectedNode(foundNode, foundAttr);
			}
			else
			{
				this.setSelectedNode(foundNode);
				this.scrollToSelectedNode(foundNode);
			}


			//set locked status and rerender path bar
			for(var $k = 0;$k<strArray.length;$k++){
				if(strArray[$k].search(/\[\d+\]/)!= -1){
                    if (this.path[$k]) {
                        this.path[$k].locked = true;
                    }
				}
			}

            this.pointer = this.buildAddress();
            if (this.pointer == "/" || !foundNode["~el"]) this.pointer = p;
		}
	},

	render : function() {
        this.el.children(".tree-node").remove();//if we are re rendering we need to toss out the old
        this.el.disableSelection();

        this.renderPathBar();
        var dataNodes= this.data;
        if(this.treeType=="XML")  {
            dataNodes = this.convertNodes(this.data);
        }
        this.renderNode(dataNodes, false, this.el, 0, "top-node", false);

        this.el.find("a").attr("target", "_blank");
        if (this.config.scrollToData) {
            $("html, body").animate({scrollTop: this.el.height()}, "slow");
        }
    },


    renderPathBar : function() {
        var $pbar = this.$pathBar;
        var $path;
        var pathString = "Click an element to include it in your " + KF.company.get("brand", "widgetName");

        if ($pbar) {
            $path = $("<div class='pathText'>");
            $path.text(pathString);

            $pbar.empty()
                .append($("<div class='pathContainer'>")
                    .append($path)
                    .append(this.$helpIcon)
                );
        }
    },

    createNode: function(n){
        var $nodeData = $("<div>").addClass("node-data");
        var $nodeName = $("<span>").html(n.name).addClass("node-name");

        $nodeData.append($nodeName);

        var $textNode = $("<span>").addClass("node-text");
        if (n.text) {
            if(n.renderAsHtml) {
                $textNode.html(n.text);
            } else {
                $textNode.text(n.text);
            }
        }

        if(n.children || n.attributes) {
            var $nToggle = $("<div>").addClass("node-child-toggle");
            $nodeName.append($nToggle);
        }

        $nodeData.append($textNode);
        return $nodeData;
    },

    // method to convert xml nodes to add a unique identifier if a node has children or attribuets
    convertNodes : function (node, parentName, index) {

        if(node.qualifiedName) return node;
        if(index) {
            node.qualifiedName = node.name + index;
        } else {
            node.qualifiedName = node.name;
        }

        if(parentName) {
            node.qualifiedName = parentName + node.qualifiedName;
        }
        if(node.children) {
            var length = node.children.length;
            for(var i = 0; i< length; i++) {
                var child = node.children[i];
                if(child.children || node.children[i].attributes) {
                    node.children[i] = this.convertNodes(node.children[i], node.qualifiedName, i);
                }
            }
        }

        return node;

    },

    renderNode : function(n, parent, $parentEl, depth, className, showAll)
    {
        if (parent) n["~parent"] = parent;
        else n["~index"] = 1;

        var $nodeEl = $("<div>")
            .addClass("tree-node")
            .data("node", n);

        n["~el"] = $nodeEl;


        // address name..
        var address = "/" + n.name;
        if (parent)
        {
            var parentAddress = parent._address;
            if (parentAddress)	address = parentAddress + address;
        }

        n._address = address;

        $nodeEl.attr("address", address);


        if (className) $nodeEl.addClass(className);

        $nodeEl.append(this.createNode(n));

        if (n.attributes || n.children)
        {
            var $nodeChildren = $("<div>").addClass("node-children");

            if (n.attributes)
            {
                for (var k in n.attributes)
                {
                    var mockAtt = { name:k, text: n.attributes[k] };
                    this.renderNode(mockAtt, n, $nodeChildren, depth + 1, "node-attribute", true);
                }

                if (!n.children) $nodeEl.addClass("leaf");
            }

            if (n.children)
            {
                $nodeEl.css("cursor", "pointer");
                var _this = this;
                var isChildrenCollection = true;
                var increment;
                var nodeNumber;
                if(this.treeType == "JSON") {
                    isChildrenCollection = this.isChildrenCollection(n);
                }
                var name = n.qualifiedName;
                if(!name) {
                    name = n._address;
                }

                if (isChildrenCollection) {
                    increment = this.INCREMENT_NODE_BY;
                    nodeNumber = this.STARTING_NODE_NUMBER;
                } else {
                    increment = this.NESTED_OBJECT_INCREMENT;
                    nodeNumber = this.NESTED_OBJECT_NUMBER;
                }

                if (n.children.length > nodeNumber * this.NODE_LIMIT_MULTIPLIER) {
                    _this.collectionDepthTracker[name] = {start: 0, end:nodeNumber, childIndexTracker:{}};
                    _this.collectionDepthTracker[name].childIndexTracker = this.renderSiblings(n, 0, nodeNumber, depth, $nodeChildren, _this.collectionDepthTracker[name].childIndexTracker, increment);
                } else {
                    this.renderSiblings(n, 0, n.children.length, depth, $nodeChildren, {}, increment);
                }
            }

            $nodeEl.append($nodeChildren);
        }
        else
        {
            $nodeEl.addClass("leaf");
        }

        $parentEl.append($nodeEl);

        return $nodeEl;
    },

    /**
     * Detects if the object is a collection type by:
     * 1. Checks the name. For our json data, they contain [] in the name
     * 2. Checks if the children has similar keys
     * @param n
     * @returns {*}
     */
    isChildrenCollection : function(n) {
        if(!n || !n.children.length || n.children.length<2) {
            return false;
        }
        if((n.name && n.name.indexOf("[]") != -1 && n.name.indexOf("[]") > n.name.indexOf("/"))) {
            return true;
        }

        var child1 = n.children[0];
        var child2 = n.children[1];
        if(child1.children && child2.children) {
            return this.isTwoObjectsSimilarType(child1, child2);
        }
        return false;
    },

    /**
     * Kinda magic function to detect if the objects are similar. There has to be at least one same key
     * for the objects with less than 3 properties. Otherwise at least half the keys have to match.
     * @param first -first Object
     * @param second - second Object
     * @returns boolean value
     */
    isTwoObjectsSimilarType: function(first, second) {

        if(!first || !second || !first.children || !second.children || !first.children.length || !second.children.length) {
            // empty objects
            return false;
        }

        var obj1, obj2;

        if(first.children.length >= second.children.length) {
            obj1 = first;
            obj2 = second;
        } else {
            obj1 = second;
            obj2 = first;
        }

        if(obj1.children.length > 0 && obj2.children.length > 0 ) {
            var matchCounter = 0;
            _.keys(obj1).forEach(function(key) {
                if(_.has(obj2, key)) {
                    matchCounter++;
                }
            });
            var smallerObjectLength = obj2.children.length;

            if(smallerObjectLength < 3) {
                return matchCounter > 0;
            } else {
                return matchCounter > (smallerObjectLength/2);
            }
        }

        return false;
    },

    renderSiblings: function(n, start, end, depth, $nodeChildren, $childIndexMap, loadMoreIncrement) {
        var len = n.children.length;
        var moreItemCount = 0;
        var name = n.qualifiedName;
        var loadMoreNodeId;
        if(len < end) end = len;
        if(start >= end) return;
        var childIndexMap = {}; // keeps track of the index position of each child grouped by name
        if($childIndexMap) childIndexMap = $childIndexMap;
        for (var i = start; i < end; i++)
        {

            var child = n.children[i];
            var $child = this.renderNode(child, n, $nodeChildren, depth + 1);//render normally

            if (!childIndexMap[child.name]) childIndexMap[child.name] = 1;
            else childIndexMap[child.name]++;
            if (!child["~index"]) {
                child["~index"] = childIndexMap[child.name];
            }
            if (i != 0 && n.children[i].children) {
                $child.addClass("closed");
            }
        }

        if(len > end) {
            childIndexMap = this.renderLoadMoreButton(n, name, end, depth, $nodeChildren, childIndexMap, loadMoreIncrement);
        }

        return childIndexMap;
    },

    renderLoadMoreButton: function(n, name, end, depth, $nodeChildren, $childIndexMap, increment) {
        var len = n.children.length;
        var moreItemCount = 0;
        var loadMoreNodeId;
        var childIndexMap = {};
        var $loadMoreNode;
        if($childIndexMap) childIndexMap = $childIndexMap;

        loadMoreNodeId = "load-"+ name.replace(/[\W|\s]/g, "") + end;
        if((end + increment * this.NODE_LIMIT_MULTIPLIER) >= len) {
            moreItemCount =  len-end;
        } else {
            moreItemCount = increment;
        }

        $loadMoreNode = $("<div class='truncatedNode link'>Show the next "+moreItemCount+" elements (out of "+ len+" total).</div>")
            .attr("id", loadMoreNodeId).addClass("tree-node").addClass("leaf").css("cursor", "pointer");

        $loadMoreNode.off("click").on("click", function() {
            $("#" + loadMoreNodeId).remove();
            childIndexMap = this.renderSiblings(n, end, end+ moreItemCount, depth, $nodeChildren, childIndexMap, increment);
        }.bind(this));
        $nodeChildren.append($loadMoreNode);

        return childIndexMap;
    },

		filterChildren : function(childArray, lvlName, nameKey){
			var newItems = [];
			var nameToMatch = lvlName;
			if (lvlName.search(/\[\d+\]/) != -1)
			{
				nameToMatch = lvlName.replace(/\[\d+\]/,"");
			}
			for(var $i = 0;$i<childArray.length; $i++){
				if(childArray[$i][nameKey] ==nameToMatch) newItems.push(childArray[$i]);
			}
			return newItems;
		},

		scrollToSelection: function(p){
//		if(p != false){
//			var strArray = p.substr(1, p.length).split("/");
//			var scrollTo = this.findPath(strArray);
//
//			var current = scrollTo;
//			while (current)
//			{
//				current = current['~parent'];
//				if(current != undefined){
//					var curObj = $(current['~el']);
//					if(curObj.hasClass('closed')){
//						curObj.toggleClass("closed");
//					}
//				}
//			}
//			var scrollDistY =$(scrollTo['~el'])[0].offsetTop;
//			this.el.scrollTop(scrollDistY+50);
//
//		}

		},

	/**
	 * Scroll given node
	 * @param node
	 * @param attr(optional)
	 */
	scrollToSelectedNode:function (node, attr)
	{
		if (node == false || !node["~el"]) return;

		var current = node;
		var scrollDist = $(node["~el"])[0].offsetTop;
		while (current)
		{//collect scroll distance information
			current = current["~parent"];
			if (current != undefined)
			{//open each parent item if closed
				var curObj = $(current["~el"]);
				scrollDist += $(current["~el"])[0].offsetTop;
				if (curObj.hasClass("closed"))
				{
					curObj.toggleClass("closed");
				}
			}
		}
		if(attr){//if an attribute scroll to it
			scrollDist+=$(attr["~el"])[0].offsetTop;
		}

		//scroll the calculated distance
		if(scrollDist>0)this.el.scrollTop(scrollDist-75);


	},

    resize: function($super) {
        if (this.$container.is(":hidden")) return;

        var containerHeight = this.$container.height();
        var pathBarHeight = this.$pathBar ? this.$pathBar.outerHeight(true) : 0;
        var elMBP = this.el.outerHeight(true) - this.el.height();

        this.el.height(containerHeight - pathBarHeight - elMBP);
    }

});


Visualizer.Json = $.klass(Visualizer.Tree,
{

    treeType: "JSON",

	setData : function($super, data)
	{
		var jn = Visualizer.JsonToNode(null, "", data);
		$super(jn);
	},

    getSelectionOptions : function()
    {
        var i;
        var address = this.buildAddress().replace(/\[\d+\]/g,"");

        var eltName = this.selectedNode["~propname"];
        var eltIndex = this.selectedNode["~index"];

        var ancestors = [];
        this.getAncestors(this.selectedNode, ancestors);

        var parentArray = null;
        if (!this.selectedNode["~propname"]) {
            parentArray = ancestors[ancestors.length - 1];
            eltName = parentArray["~propname"];
        } else {
            ancestors.push(this.selectedNode);
        }

        var ancestorPaths = [];
        var pathAddress = address;
        var locks = [];
        for (i = 0; i < ancestors.length - 1; i++) {
            var ancestor = ancestors[i];
            var ancestorName = ancestor["~propname"];
            var ancestorIndex = ancestor["~index"];

            // found an array, make sure to get the proper index
            if (ancestor.name.search(/\[\]/) != -1) {
                ancestorIndex = ancestors[i+1]["~parent"]["~index"];
            }

            var description = "Select all <span>" + eltName + "</span> elements in this <span>" + ancestorName + "</span> element.";
            pathAddress = this.insertSelectors(pathAddress, [{name:ancestorName,value:ancestorIndex}]);
            locks.unshift(ancestor);

            ancestorPaths.unshift({desc: description, path: pathAddress, lock: locks});
        }

        locks.unshift(ancestors[ancestors.length - 1]);
        ancestorPaths.unshift({desc: "Select only this <span>" + eltName + "</span> element.",
            path: this.insertSelectors(pathAddress, [{name:eltName,value:eltIndex}]),
            lock:locks});

        var options = [];

        // only this element
        if (ancestorPaths.length > 0) {
            options.push(ancestorPaths.shift());
        }

        // default: all peer elements
        options.push({desc: "Select all peer <span>" + eltName + "</span> elements.", path: address, lock: []});

        // the nth element in parent array
        if (parentArray != null) {
            var parent = ancestors.length > 1 ? ancestors[ancestors.length-2] : null;
            if (parent != null) {
                options.push({desc: "Select the <span>" + ordinal(eltIndex) + " " + eltName + "</span> element in all <span>" + parent["~propname"] + "</span> elements.",
                    path: this.insertSelectors(address, [{name:eltName,value:eltIndex}]),
                    lock: [ancestors[ancestors.length - 1]]});
            }
        }

        // all selected elements anywhere
        options.push({desc: "Select all <span>" + eltName + "</span> elements.", path: "//" + eltName, lock: []});

        // in this parent, grandparent, etc. up to root
        for (i = 0; i < ancestorPaths.length; i++) {
            options.push(ancestorPaths[i]);
        }

        return options;
    },

    findParent : function(node)
    {
        if (node["~parent"]) {
            if (node["~parent"]["~propname"]) {
                return node["~parent"];
            } else {
                return this.findParent(node["~parent"]);
            }
        }

        return null;
    },

    getAncestors : function(node, list)
    {
        var parent = this.findParent(node);

        if (parent != null) {
            list.unshift(parent);
            this.getAncestors(parent, list);
        }
    },

	/**
	 * go from the selectedNode up to the root to create an xpath
	 */
	buildAddress : function()
	{
		var newPath = [];

        if (this.path) {
            var plen = this.path.length;
            for (var i = 0; i < plen; i++)
            {
                var p = this.path[i];
                if (!p.node["~propname"]) continue;
                var txt = p.node["~propname"];

                if (p.locked)
                {
                    // get the next path element's index
                    if (i != (plen - 1))
                    {
                        var nextindex = this.path[i + 1].node["~index"];
                        txt += "[" + nextindex + "]";
                    } else {
                        txt += "[" + p.node["~index"] + "]";
                    }
                }

                newPath.push(txt);

            }
        }

		return "/" + newPath.join("/");
	},

    /**
     *
     * @param p
     */
    setPointer : function(p)
    {
        var $i;
        if (p == this.pointer)return;

        if (p == false)
        {
            this.setSelectedNode(p);

        }
        else
        {
            var strArray = p.substr(1, p.length).split("/");

            var allNodes = [];//list of nodes to search
            var foundNode = false;//storage for target node
            var addrArray =  p.substr(1, p.length).split("/");//string array of level names: path to node we are looking for

            // collect root-level node as starting point
            allNodes.push({"node":this.data,"depth":0});

            // find the previously selected node
            while (allNodes.length != 0)
            {
                var nextItem = allNodes.pop();
                var n = nextItem.node;
                var addrIndex = nextItem.depth;
                    // skip over {}
                    if( typeof n !== "undefined") {
                        if ((!n["~propname"] || n["~propname"] == "") && n.children) {
                            // check if any of the children matches the level.
                            var itemsLevelN = n.children ? this.filterChildren(this.findChildren(n), addrArray[addrIndex], "~propname") : [];

                            // if nothing matches, fail-safe to check all node for the next level
                            if(!itemsLevelN || itemsLevelN.length<1) {
                                itemsLevelN = n.children;
                            }

                            for($i= itemsLevelN.length; $i > 0; $i--) {
                                allNodes.push({"node": itemsLevelN[$i-1], "depth": addrIndex});
                            }
                            continue;
                        }

                        //we have reached the end of the path
                        if (addrIndex == addrArray.length - 1) {
                            var propName = addrArray[addrIndex];
                            var index = false;
                            if (addrArray[addrIndex].search(/\[\d+\]/) != -1) {
                                propName = addrArray[addrIndex].substring(0, addrArray[addrIndex].indexOf("["));
                                index = addrArray[addrIndex].substring(addrArray[addrIndex].indexOf("[") + 1, addrArray[addrIndex].indexOf("]"));
                            }

                            if (n["~propname"] == propName) {
                                if (n["~type"] == "arr" && index) {
                                    if (n.children && 0 <= index && index < n.children.length) {
                                        foundNode = n.children[index - 1];
                                    }
                                }

                                if (!foundNode) foundNode = n;

                                break;
                            } else {
                                continue;
                            }
                       }

                        //get children nodes that match names with the next level
                        var items = n.children ? this.filterChildren(this.findChildren(n), addrArray[addrIndex + 1], "~propname") : [];
                        if (items.length == 0) continue;//no children/relevant children so move on to next node

                        //if the next level has a lock only look at the specific child
                        if (addrArray[addrIndex].search(/\[\d+\]/) != -1) {
                            var childIndex = addrArray[addrIndex].slice(addrArray[addrIndex].indexOf("[") + 1, addrArray[addrIndex].indexOf("]")) - 1;
                            allNodes.push({"node": items[childIndex], "depth": addrIndex + 1});
                        }
                        else {//otherwise add all nodes
                            for ($i = items.length - 1; $i > -1; $i--) {
                                allNodes.push({"node": items[$i], "depth": addrIndex + 1});
                            }
                        }
                    }
            }

            //call set selected node then scroll to it
            this.setSelectedNode(foundNode);
            this.scrollToSelectedNode(foundNode);

            //set locked status and rerender path bar
            var nodeNames = [];
            for(var $k = 0;$k<strArray.length;$k++){
                if(strArray[$k].search(/\[\d+\]/)!= -1){
                    nodeNames.push(strArray[$k].substring(0, strArray[$k].indexOf("[")));
                }
            }
            this.lockPath(nodeNames);

            this.pointer = this.buildAddress();
            if (this.pointer == "/") this.pointer = p;
        }
    },


    findChildren : function(node)
    {
        var childNodes = [];

        if (node.children) {
            for (var $i = 0; $i < node.children.length; $i++) {
                var child = node.children[$i];

                // skip over {}
                if (!child["~propname"] || child["~propname"] == "") {
                    if (child.children) {
                        for (var $j = 0; $j < child.children.length; $j++) {
                            childNodes.push(child.children[$j]);
                        }
                    }
                } else {
                    childNodes.push(child);
                }
            }
        }

        return childNodes;
    },

    lockPath : function(nodeNames)
    {
        for (var $i = 0; $i < this.path.length; $i++) {
            if (_.indexOf(nodeNames, this.path[$i].node["~propname"]) != -1) {
                this.path[$i].locked = true;
            }
        }
    }

});


Visualizer.JsonToNode = function(parent, propName, value, position)
{
	var n = { };
	if (parent) n["~parent"] = parent;
	n["~propname"] = propName;

    if(parent) {
        n.qualifiedName = parent.qualifiedName;
    } else {
        n.qualifiedName = "";
    }

	if ($.isPlainObject(value))
	{
		n.children = [];
		n.name = "{}";
        n.qualifiedName = (position==="undefined" || _.isNaN(position)) ? n.name : "{" +position + "}" + n.qualifiedName;
		n["~type"] = "obj";
		n["~adr"] = propName;

		if (propName) {
            n.name += " " + propName;
            n.qualifiedName += " " + propName;
        }
		else n["~pathbarexclude"] = true;

        var counter = 0;
		for (var p in value)
		{
			if (!value.hasOwnProperty(p)) continue;
			n.children.push(Visualizer.JsonToNode(n, p, value[p], counter));
            counter ++ ;
		}
	}
	else if ($.isArray(value))
	{

		n.name = "[]";
        n.qualifiedName = (position==="undefined" || _.isNaN(position)) ? n.name : "[" +position + "]"+ n.qualifiedName;
		n.children = [];
		n["~type"] = "arr";
		n["~adr"] = propName;
		if (propName) {
            n.name += " " + propName;
            n.qualifiedName += " " + propName;
        }
		else n["~pathbarexclude"] = true;

		for (var i = 0; i < value.length; i++)
		{
			var v = value[i];
			var c = Visualizer.JsonToNode(n, false, v, i);
			c.name = c.name ? c.name + " " + (i + 1) : (i + 1);
            c.qualifiedName = c.qualifiedName ? c.qualifiedName + " " + (i + 1) : (i + 1);
			c["~pathbarexclude"] = true;
			c["~index"] = i + 1;
			n.children.push(c);
		}
	}
	else
	{
		n.name = propName;
        n.qualifiedName = n.qualifiedName + " " + propName;
		if(value !=null)n.text = value.toString();
		n["~adr"] = propName;
	}


	return n;
};
;/****** c.visualizer_root.js *******/ 

var VisualizerTabPane = $.klass({


	initialize : function(config){

		this.visualizers = [];
		this.tabs = [];
		this.config = config;
		this.activeVisualizer = false;
		this.currentPointer = null;
		this.$el = $(config.el);
		this.$tabStrip = $("<ul>")
			.addClass("visualizer-tab-strip")
			.appendTo( this.$el );

		this.$el.addClass("visualizer-root").css("position","relative");

		this.$addDatasourceLink = $("<li>").html("<a actionId='add_datasource'>+ Add Data Source</a>").addClass("add-datasource-tab");

		this.$addDatasourceLink.appendTo(this.$tabStrip);

//		var _this=this;
//		PubSub.subscribe("workspace-resize",function(evtid,args){
//			if(args[0]=='height')_this.handleResize();
//		});
	},

	_isCurrentPointerInsideVisualizer: function(visualizer) {
		var id = visualizer.getDataProvider().id;

		return this.currentPointer && this.currentPointer.id === id;
	},

	addVisualizer: function(visualizer, options) {
		var pointerSelectedBeforeVisualizerLoaded = this._isCurrentPointerInsideVisualizer(visualizer);
		var isDataSetVisualizer = visualizer.dataSet;
		var isFirstVisualizer = this.visualizers.length === 0;

		options = options || {};
		options.showVisualizer = options.showVisualizer || isFirstVisualizer || pointerSelectedBeforeVisualizerLoaded;

		visualizer.$tab = visualizer.createTab();
		visualizer.$tab.on("click", _.bind(this.selectVisualizer, this, visualizer));

		$("#msg-no-datasources").hide();

		this.$el.append(visualizer.$container.show());
		this.visualizers.push(visualizer);

		visualizer.$tab.appendTo(this.$tabStrip);

		this.tabs.push(visualizer.$tab);

		// append the add button after each visualizer is add so it stays at the end of the tab strip
		this.$addDatasourceLink.appendTo(this.$tabStrip);

		if (options.showVisualizer) {
			if (this._isCurrentPointerInsideVisualizer(visualizer)) {
				this.selectPointer(this.currentPointer); // This will force a select*Visualizer call.
			} else {
				this.selectVisualizer(visualizer);
			}
		} else {
			visualizer.hide();
		}
	},

	removeVisualizer : function (v)
	{
		v.$container.remove();

		var index = _.indexOf(this.visualizers, v);
		this.visualizers.splice(index, 1);

		index = _.indexOf(this.tabs, v.$tab);
		this.tabs.splice(index, 1);

		v.$tab.remove();

		if (this.visualizers.length > 0) {
			this.selectVisualizer(this.visualizers[0]);
		} else {
			$("#msg-no-datasources").show();
			$("#vizOptionsPanel").hide();

            this.checkTabWidths();
            this.handleResize();
		}
	},

	removeAllVisualizers : function ()
	{
		var $i;
		for($i=this.visualizers.length-1;$i>-1;$i--){
			this.removeVisualizer(this.visualizers[$i]);
		}

		this.checkTabWidths();
	},

	getVisualizerByDatasource: function(dsid) {
		var results = this.visualizers.filter(function(visualizer) {
			return visualizer.datasourceInstance && (visualizer.datasourceInstance.id === dsid);
		});

		return results.length ? results[0] : false;
	},

	getVisualizerByDataSetId: function(dataSetId)
	{
		var results = this.visualizers.filter(function(visualizer) {
			return visualizer.dataSet && (visualizer.dataSet.id === dataSetId);
		});

		return results.length ? results[0] : false;
	},

	checkTabWidths : function ()
	{
		// Add extra vertical space to the datasource tab area if the tabs are too wide for one row

		var width = 0;
		for(var i = 0; i < this.tabs.length; i++)
		{
			// 8px is added to each tab width to account for padding
			width += this.tabs[i].width() + 8;
		}
		width += $(".add-datasource-tab").width();

		if (this.activeVisualizer)
		{
            if (this.$el && this.$el.width()) {
                var numRows = Math.ceil(width / this.$el.width());
                this.$tabStrip.css("bottom", -24 * numRows + "px");
                this.$el.css("margin-bottom", (24 * numRows) - 2 + "px");
            }
		}
	},

	clearAllSelections : function()
	{
		var vLen = this.visualizers.length;
		while(--vLen > -1)
			this.visualizers[vLen].setPointer(false);
	},


	selectPointer: function(pointer) {
		var id = pointer.id;
		var address = pointer.address;
		var isDataSet = this._isDataSetPointer(pointer);

		this.currentPointer = pointer;

		var targetVisualizer = isDataSet ?
			this._findDataSetVisualizer(id) :
			this._findDataSourceVisualizer(id);

		if (targetVisualizer) {
			targetVisualizer.setPointer(address);
			this.selectVisualizer(targetVisualizer);
		}
	},

    _isDataSetPointer: function(pointer) {
		return pointer.dt === "set"
	},

	_findDataSourceVisualizer: function(id) {
		var vLen = this.visualizers.length;
		var found = null;
		while (--vLen > -1) {
			if (this.visualizers[vLen].datasourceInstance && this.visualizers[vLen].datasourceInstance.id == id) {
				found = this.visualizers[vLen];
			}
		}

		return found;
	},

	_findDataSetVisualizer: function(id) {
		var vLen = this.visualizers.length;
		var found = null;
		while(--vLen > -1) {
			if (this.visualizers[vLen].dataSet && this.visualizers[vLen].dataSet.id == id) {
				found = this.visualizers[vLen];
			}
		}

		return found;
	},

	selectVisualizer: function(visualizer) {
		var dataProvider = visualizer.getDataProvider();
		// TODO: remove `!!visualizer.dataSet` workaround when DataSet share rights are handled on the server side.
		var canViewDataProvider = !!visualizer.dataSet || visualizer.getDataProvider().shared;

		if (this.activeVisualizer) {
			this.activeVisualizer.hide();
		}

		visualizer.activateTab();

		var $dsCreator = $("#datasource_creator");
		if (canViewDataProvider) {
			if(visualizer.pointer) visualizer.renderPathBar(false);
			visualizer.$container.show();
            $("#msg-no-access").hide();
			$dsCreator.html("");
        } else {
			visualizer.el.hide();
			if (dataProvider.creator) {
				$dsCreator.html(dataProvider.creator);
				$dsCreator.parent().show();
			} else {
				$dsCreator.parent().hide();
			}
            $("#msg-no-access").show();
			visualizer.$container.hide();
        }

        this.activeVisualizer = visualizer;

		var $vizOptionsPanel = $("#vizOptionsPanel");
		if (this.activeVisualizer.workbook && this.activeVisualizer.workbook.length > 1) {
			this.activeVisualizer.populateSelect();
			$vizOptionsPanel.show();
		} else {
			$vizOptionsPanel.hide();
		}

		this.checkTabWidths();
		this.handleResize();
	},

	scrollToSelection : function (){},

	handleResize: function(panelHeight)
    {
		var formulaBarHeight;
		var vizRootMBP = this.$el.outerHeight(true) - this.$el.height();
		var elementsToResize = [$("#msg-no-datasources"), $("#msg-no-access")];
		var resizeVisualizer = false;
		var i, len;
		var resizeEl, resizeElMBP;

		if (!panelHeight) {
			formulaBarHeight = this.config.formulaBarNode ? $(this.config.formulaBarNode).outerHeight(true) : 0;
            panelHeight = $("#tab-data").height() - formulaBarHeight;
        }

		if (this.activeVisualizer) {
			resizeVisualizer = true;
			elementsToResize.push(this.activeVisualizer.$container);
		}

		for (i = 0, len = elementsToResize.length; i < len; i++) {
			resizeEl = elementsToResize[i];
			resizeElMBP = resizeEl.outerHeight(true) - resizeEl.height();
			resizeEl.height(panelHeight - vizRootMBP - resizeElMBP);
		}

        if (resizeVisualizer) {
            this.activeVisualizer.resize();
        }
	}

});

VisualizerTabPane.truncateTabName = function(name) {
	var result = name.length > 50 ? name.substring(0, 50) + "..." : name;

	return result;
};

VisualizerTabPane.createTabContent = function (model) {
    var displayName = VisualizerTabPane.truncateTabName(model.name);
    var $tabContent = $("<span title='" + model.name + "'>")
        .append($("<div>")
            .addClass("tab-title")
            .text(displayName)
    );
    var $threeDotMenu = $("<div>").addClass("tab-three-dot");
    var $targetBox = $("<div>")
        .addClass("tab-container-three-dot")
        .click(function (e) {
            page.invokeAction("show_context_menu", model.menuConfig, e);
        });

    $targetBox.append($threeDotMenu);
    $tabContent.append($targetBox);

    return $tabContent;
};

VisualizerTabPane.DataSourceTab = function (visualizer) {
    var datasourceInstance = visualizer.datasourceInstance;
    var model = {
        name: datasourceInstance.name,
        menuConfig: {
            target: VisualizerTabPane,
            menuCtx: {
                id: datasourceInstance.id,
                isModelled: false,
                canEdit: KF.company.hasFeature("data_modeller") && datasourceInstance.shared
            }
        }
    };
    var $tab = $("<li>").data("visualizer", visualizer);
    $tab.append(VisualizerTabPane.createTabContent(model));
    return $tab;
};

VisualizerTabPane.DataSetTab = function (visualizer) {
    var dataSet = visualizer.dataSet;
    var model = {
        name: dataSet.name,
        menuConfig: {
            target: VisualizerTabPane,
            menuCtx: {
                id: dataSet.id,
                isModelled: true
            }
        }
    };
    var $tab = $("<li>").data("visualizer", visualizer);
    $tab.append(VisualizerTabPane.createTabContent(model));
    return $tab;
};

VisualizerTabPane.getContextMenuModel = function (ctx, menuConfig) {
  var model = [];

  if (ctx.isModelled) {
    if (KF.company.hasFeature("data_modeller_dev")) {
      model.push(
        {
          id: "menuitem-model",
          text: "Edit Modeled Data Source...",
          actionId: "edit_data_set",
          actionArgs: {
            dataSetId: ctx.id
          }
        },
        {id: "separator-a", separator: true});
    }
    model.push({id: "menuitem-remove", text: "Remove", actionId: "remove_data_set", actionArgs: ctx.id});
  } else {

    if (KF.user.hasPermission("dashboard.library")) {
      model.push(
        {id: "menuitem-about", text: "About Data Source", actionId: "view_datasource", actionArgs: ctx.id},
        {id: "separator-b", separator: true}
      );
    }
    if (ctx.canEdit && KF.company.hasFeature("data_modeller_dev")) {
      model.push(
        {
          id: "menuitem-model",
          text: "Create Modeled Data Source...",
          actionId: "edit_data_set",
          actionArgs: {datasourceId: ctx.id}
        },
        {id: "separator-a", separator: true}
      );
    }
    model.push({id: "menuitem-remove", text: "Remove", actionId: "remove_datasource", actionArgs: ctx.id});

  }
  return model;
};
;/****** c.workspace.js *******/ 

/* global PageController:false, WorkspaceKlipSaveManager:false, VisualizerTabPane:false, DrilldownControls:false,
    Props:false, AppliedActionsPane:false, ComponentPalette:false, ControlPalette:false, ContextMenu:false, Component:false,
    DX:false, FMT:false, Dashboard:false, CxFormula:false, Visualizer:false, KlipFactory:false, CXTheme:false, dashboard:false,
    page:false, eachComponent:false, customScrollbar:false, updateManager:false, KF: false, PropertyForm:false,
    ConditionsEditor:false, DST:false, global_dialogUtils:false, global_contentUtils:false */
var Workspace = $.klass(PageController, {

    /** the jQuery element for the tree menu */
    menuEl : false,

    /** the 'blue' tabs to edit properties, data, etc for the focused component/element */
    workspaceTabs: false,


    /** the Klip that is being edited in the workspace */
    activeKlip : false,

    /** the component that is focused/selected */
    activeComponent: false,

	/** the layout that is the target for user actions */
	activeLayout : false,


    /** is the root klip selected? */
    klipIsSelected : false,

	/** the dashboard this workspace is associated with */
	dashboard : false,

	/** cache of active datasources */
	datasourceInstances : false,

    appliedActionsPane: false,

	componentPropertyForm : false,

    layoutPropertyForm : false,

	visualizerTabPane : false,

	formulaBar : false,

    conditionsPane : false,

    drilldownControls : false,

    /**
     * uiVariables : {
     *   "name" : { name: "name", value: "", isUserProperty: false },
     *   ...
     * }
     */
    uiVariables : false,

    /** palette objects */
    compPalette : false,
    controlPalette : false,

    /** right click context menu for Klip/components */
    cxContextMenu : false,

    isAutosave: true,
    saveManager: null,

    initializeFormulas : true,

    dxManager : null,

    workspaceStartTime: null,
    workspaceEvents: null,

    MIN_PROPERTY_PANE_HEIGHT : 255,
    MIN_DATA_RESIZE_HEIGHT: 184,
    MAX_PROPERTY_PANE_HEIGHT_MULT : 0.55,

    /**
     * constructor.
     */
    initialize: function($super, config) {
		$super(config);

        this.workspaceStartTime = Date.now();
        this.workspaceEvents = {};

        this.dashboard = config.dashboard;

        this.dxManager = config.dxManager;

		this.visibleTabs = [];
        this.saveManager = new WorkspaceKlipSaveManager();

        this.initializeWorkspaceParts(config);
        this.initializeDomEventListeners();
        this.initializePubSubListeners();
        this.initializeInputs();
        this.registerDragAndDropHandlers();
        this.initializeAutosave();
        this.initializeUIVariables();
    },

    initializeWorkspaceParts: function(config) {
        if (config.formulaEditor) {
            this.formulaEditor = config.formulaEditor;
            this.formulaEditor.setApplyFormulaCallback(function(formulaText, formulaSource) {
                if (this.activeComponent) {
                    PubSub.publishSync("formula-changed", [ formulaText, formulaSource, true ] );
                }
            }.bind(this));
            this.formulaEditor.setEvaluateFormulaCallback(this.evaluate.bind(this));
        }

        this.visualizerTabPane = new VisualizerTabPane( {
            el: "#data-viz-tab-root",
            formulaBarNode: this.formulaEditor && this.formulaEditor.getWrapperDOMNode()
        });

        if (this.formulaEditor) {
            this.formulaEditor.setShowPointerCallback(this.showPointerInVisualizer.bind(this));
        }

        this.conditionsPane = new ConditionsEditor("#tab-conditions");
        this.drilldownControls = new DrilldownControls( { containerId:"dd-control-container" } );

        this.componentPropertyForm = new PropertyForm({
            $container: $("#tab-properties"),
            afterFieldUpdate:$.bind(this.onPropertyFormChange, this)
        });

        this.layoutPropertyForm = new PropertyForm({
            $container: $("#tab-layout"),
            afterFieldUpdate:$.bind(this.onLayoutFormChange, this)
        });

        this.appliedActionsPane = new AppliedActionsPane({ containerId:"applied-actions-container" });

        this.compPalette = new ComponentPalette({ containerId: "component-palette" });
        this.controlPalette = new ControlPalette({ containerId: "control-palette" });

        this.cxContextMenu = new ContextMenu({
            menuId:"cx-context-menu"
        });
    },

    initializeDomEventListeners: function() {
        var target, menuCtx, root;
        var _this = this;
        this.handleKlipSize = _.debounce( _.bind(this.handleKlipSize, this), 200 );
        this.onWindowResize = _.debounce( _.bind(this.onWindowResize, this), 200 );

        $(window).resize(function(evt) {
            _this.onWindowResize(evt);
        });

        $(window).bind("orientationchange", function(evt) {
            _this.onWindowResize(evt);
        });

        $("#c-workspace_menu").on("contextmenu", "a", function(evt) {
            evt.preventDefault();

            target = $(evt.target).closest("a").data("actionArgs");

            menuCtx = { fromTree:true };
            if (target.isKlip) {
                menuCtx.includePaste = true;

                page.invokeAction("select_klip", false, evt);
            } else {
                root = target.getRootComponent();
                if (target == root) menuCtx.isRoot = true;

                page.invokeAction("select_component", target, evt);
            }

            page.invokeAction("show_context_menu", { target:target, menuCtx:menuCtx }, evt);
        }).on("click contextmenu", ".dst-hint-component-icon-tree", function(evt) {
            evt.preventDefault();
            target = $(evt.target).siblings("a").data("actionArgs");

            if(target) {
                _this.renderActionDropdown(target, $(this));
            }
        }).on("click contextmenu", ".dst-hint-icon-filter, .dst-hint-sort-icon", function(evt) {
            var target = $(evt.target).siblings("a").data("actionArgs");
            var menuCtx = { fromTree:true };
            var classes;
            var root;
            evt.preventDefault();

            if(target) {
                classes = evt.target.classList;
                if (classes.contains("dst-hint-icon-filter")) {
                    menuCtx.fromFilterIcon = true;
                } else if(classes.contains("dst-hint-sort-icon")) {
                    menuCtx.fromSortIcon = true;
                }
                root = target.getRootComponent();
                if (target == root) menuCtx.isRoot = true;

                page.invokeAction("select_component", target, evt);
                page.invokeAction("show_context_menu", { target:target, menuCtx:menuCtx }, evt);
            }
        });

        $("#editor-message").on("click", function() {
            _this.hideMessage();
        });

        $("#property-tabs").on("click", "li", _.bind(this.onWorkspaceTabSelect, this) );

        $("#treeMenuContainer").resizable({
            handles:"e",
            helper:"resize-helper",
            minWidth: 70,
            maxWidth: 500,
            start: function(event, ui) {
                $(ui.helper).height(ui.originalSize.height);
                $(ui.helper).width(ui.originalSize.width);
            },
            stop: function(event, ui) {
                _this.widthResize(ui.size.width);
            }
        });

        $(".property-tab-pane").resizable({
            handles:"n",
            helper:"resize-helper",
            minHeight: this.MIN_PROPERTY_PANE_HEIGHT,
            start: function(event, ui) {
                $(ui.helper).width(ui.originalSize.width);
            },
            stop: function(event, ui) {
                _this.heightResize(ui.size.height);
            }
        });

        $("#data-resize").resizable({
            handles:"n",
            helper:"resize-helper",
            minHeight: this.MIN_DATA_RESIZE_HEIGHT,
            start: function(event, ui) {
                $(ui.helper).height(ui.originalSize.height);
                $(ui.helper).width(ui.originalSize.width);
            },
            stop: function(event, ui) {
                _this.dataTabResize(ui.size.height);
            }
        });
    },

    onKlipStatesChanged: function(eventName, args) {
        if ( args && args.state && args.state.busy === false ) {
            /*Once we've received a response from DPN, and the components have been updated,
              update the tree with the new formula states
             */
            if (this.activeKlip && this.activeKlip.isPostDST()) {
                this.updateMenu();
            }
        }
    },

    onFormulaChanging: function(eventName, args) {
        if (this.activeKlip && this.activeKlip.isPostDST()) {
            this.updateMenu();
        }
    },

    initializePubSubListeners: function() {
        var _this = this;
        var dpnReadyEventName = this.dxManager.getServiceAvailableEventName();

        PubSub.subscribe("formula-changed", _.bind(this.onFormulaChanged, this));
        PubSub.subscribe("formula-changing", _.bind(this.onFormulaChanging, this));

        PubSub.subscribe("klip_states_changed", _.bind(this.onKlipStatesChanged, this));

        PubSub.subscribe("layout_change", _.bind(this.onWindowResize, this) );

        PubSub.subscribe("data-tab-selected", function(evtName, args) {
            if (_this.selectedTab == "data") {
                $("#data-viz-tab-root").show();
                _this.handleDataPanelResize("data");
            }
        });

        /**
         * args[0] - selected variable name
         * args[1] - array of variables that changed {name:"", value:""}
         * args[2] - true, if variable should be selected in property list; false, otherwise
         * args[3] - true, if update variable from input control; false, otherwise
         */
        PubSub.subscribe("variables-changed", function(evtName, args) {
            var i, varName, varValue, fm;
            var varsChanged = args[1];
            if ( varsChanged && varsChanged.length > 0 ){
                // update the local info for changed var...otherwise we run into timing issues where the component's formula
                // can't be evaluated properly because old var info is still in this.uiVariables
                for (i = 0; i < varsChanged.length; i++) {
                    varName = varsChanged[i].name;
                    varValue = varsChanged[i].value;

                    if (_this.uiVariables[varName]) {
                        if (typeof varValue === "undefined") {
                            delete _this.uiVariables[varName];
                        } else {
                            _this.uiVariables[varName].value = varValue;
                        }
                    } else {
                        _this.uiVariables[varName] = {name:varName, value:varValue, isUserProperty:false};
                    }
                }

                // update the variable list in the data tab
                page.updateVariables(args[0], args[2]);

                // go through each component.
                // if component uses the variable: re-set its formula which will trigger
                //      re-evaluation with new variable data
                _this.activeKlip.eachComponent(function(cx){
                    if ( !cx.formulas ) return;

                    fm = cx.getFormula(0);

                    if (!fm) return;

                    for (i = 0; i < varsChanged.length; i++) {
                        if (fm.usesVariable(varsChanged[i].name)) {
                            _this.setComponentFormula(cx,fm.formula, fm.src, false, true);
                            break;
                        }
                    }
                });

                // change input control if variable was not changed from input control
                // in the first place
                if (!args[3]) {
                    _this.activeKlip.eachComponent(function(cx) {
                        if (cx.factory_id == "input") {
                            for (i = 0; i < varsChanged.length; i++) {
                                if (varsChanged[i].name == cx.propName) {
                                    cx.$control.val(encodeURI(varsChanged[i].value));
                                    cx.$control.change();
                                }
                            }
                        }
                    });
                }
            }

        });

        PubSub.subscribe(dpnReadyEventName, function(evtName, args) {
            if (args && this.initializeFormulas) {
                this.evaluateFormulas();
            }
        }.bind(this));

        PubSub.subscribe("dst-context-updated", function(evtName, args) {
            var dstRootComponent = args.dstRootComponent;

            if (this.selectedTab == Workspace.PropertyTabIds.APPLIED_ACTIONS ||
                this.selectedTab == Workspace.PropertyTabIds.PROPERTIES) {
                this.selectWorkspaceTab(this.selectedTab);
            }
            if (this.activeComponent && dstRootComponent == this.activeComponent.getDstRootComponent()) {
                this.addDstHintIconToComponent();
                this.updatePossibleComponentReferences();
            }
            this.updateMenu();
        }.bind(this));

        PubSub.subscribe("record-event", function(evtName, args) {
            var eventName = args.eventName;
            var count = args.count;
            var numRecordedEvents = this.workspaceEvents[eventName] || 0;

            this.workspaceEvents[eventName] = numRecordedEvents + count;
        }.bind(this));
    },

    initializeInputs: function() {
        var _this = this;

        // variables for input component
        Component.Input.VarResolver = function() {
            return _this.uiVariables;
        };
    },

    _handleDropComponentToPanel : function(cx, panel, currentCx, matrixInfo) {
        var newPosition = {x: matrixInfo.x, y: matrixInfo.y};
        var oldPosition = cx.layoutConfig;
        var parent = cx.parent;

        // don't need to add/remove components if just moving around in same panel
        if (panel != parent) {
            if (currentCx) {
                // there was a panel in a panel and moving cx into the cell the inner panel was in
                if (parent == currentCx) {
                    global_dialogUtils.confirm(global_contentUtils.buildWarningIconAndMessage("Are you sure you want to replace with the current component?"), {
                        title: "Replace Component",
                        okButtonText: "Replace"
                    }).then(function() {
                        if (cx.usePostDSTReferences()) {
                            //Remove cx from its parent first because we don't want to unwatch it
                            parent.removeComponent(cx, undefined, true);
                            //Remove current cx and unwatch any formulas
                            panel.removeComponent(currentCx);
                        } else {
                            panel.removeComponent(currentCx);
                        }

                        this._addComponentToPanelOnDrop(cx, parent, panel, newPosition);
                        this._updatePanelOnDrop(cx, panel, parent, matrixInfo);
                    }.bind(this));

                    return;
                } else {
                    //Swap components between panels
                    if (cx.usePostDSTReferences()) {
                        panel.removeComponent(currentCx, undefined, true);
                    } else {
                        panel.removeComponent(currentCx);
                    }

                    currentCx.layoutConfig = oldPosition;
                    parent.addComponent(currentCx, parent.isKlip ? {prev:cx.el} : undefined);

                    if (parent.isKlip) currentCx.setDimensions(parent.dimensions.w);
                }
            }

            this._addComponentToPanelOnDrop(cx, parent, panel, newPosition);
        } else {
            cx.layoutConfig = newPosition;

            if (currentCx) currentCx.layoutConfig = oldPosition;
        }

        this._updatePanelOnDrop(cx, panel, parent, matrixInfo);
    },

    _addComponentToPanelOnDrop: function(cx, parent, panel, newPosition) {
        page.setActiveComponent(false);

        cx.layoutConfig = newPosition;
        cx.el.css("margin-bottom", "");

        parent.removeComponent(cx, undefined, true);
        panel.addComponent(cx);
        cx.initializeForWorkspace(page);

        if (parent.isPanel) {
            parent.update();
        }

        page.setActiveComponent(cx);
        page.selectMenuItem("component-" + cx.id);
    },

    _updatePanelOnDrop: function(cx, panel, parent, matrixInfo) {
        var parentHasActiveCell = (parent.isPanel && parent.activeCell);
        var model;

        if (panel.activeCell || parentHasActiveCell) {
            panel.setActiveCell(matrixInfo.x, matrixInfo.y);
            page.setActiveLayout(panel);
        }

        model = panel.getLayoutConstraintsModel({ cx: cx });
        page.updatePropertySheet(model.props, model.groups, page.layoutPropertyForm);

        panel.invalidateAndQueue();
    },

    onDropComponentToPanel : function($target, klip, $prevSibling, evt, ui) {
        var panel = klip.getComponentById($target.closest(".comp").attr("id"));

        var matrixInfo = panel.getCellMatrixInfo($target);
        var currentCx = panel.getComponentAtLayoutXY(matrixInfo.x, matrixInfo.y);

        var idx = ui.helper.attr("cx-id");
        var cx = klip.getComponentById(idx);

        // can't drag panel into its own cell; no grid-ception
        // don't bother replacing cx by itself
        if (cx == panel || cx == currentCx) return;

        if (cx.factory_id == "separator") cx.el.css("height", "");

        this._handleDropComponentToPanel(cx, panel, currentCx, matrixInfo);

    },

    registerDragAndDropHandlers: function() {
        Workspace.RegisterDragDropHandlers("component", [
            {
                region:"layout",
                drop: this.onDropComponentToPanel.bind(this)

            },
            {
                region:"klip-body",
                drop: function($target, klip, $prevSibling, evt, ui) {
                    var idx = ui.helper.attr("cx-id");
                    var cx = klip.getComponentById(idx);
                    var parent = cx.parent;

                    if (parent.isKlip
                        && ($prevSibling.attr("id") == idx
                            || $prevSibling.attr("id") == cx.el.prev().attr("id"))) {
                        return;
                    }

                    if (cx.factory_id == "separator") cx.el.css("height", "");

                    page.setActiveComponent(false);

                    parent.removeComponent(cx, undefined, true);
                    klip.addComponent(cx, {prev: $prevSibling});
                    cx.initializeForWorkspace(page);
                    cx.setDimensions(klip.availableWidth);

                    page.updateMenu();
                    page.setActiveComponent(cx);
                    page.selectMenuItem("component-" + cx.id );

                    if (parent.isPanel) parent.invalidateAndQueue();
                }
            },
            {
                region:"preview",
                drop: function($target, klip, $prevSibling, evt, ui) {
                    var cx = klip.getComponentById(ui.helper.attr("cx-id"));
                    page.invokeAction("remove_component", {cx:cx, prompt:true}, {});
                }
            }
        ]);
    },

    initializeAutosave: function() {
        // Auto-save klip every 2 minutes
        setInterval (this.autosave.bind(this), 120000);
    },

    initializeUIVariables: function() {
        $.post("/workspace/ajax_getUIVariables", function(data) {
            var i;
            this.uiVariables = {};

            for (i = 0; i < data.variables.length; i++) {
                this.uiVariables[data.variables[i].name] = data.variables[i];
            }

            this.updateVariables();

            if (this._pendingFormulaEvaluations) {
                this._pendingFormulaEvaluations.forEach(function(currentRootComponent) {
                    this.evaluateAllComponentFormulas(currentRootComponent);
                }, this);
                this._pendingFormulaEvaluations = null;
            }
        }.bind(this));
    },

    autosave: function() {
        var klipJson, savePromise;

        if (!this.isAutosave || this.saveManager.isSaving) {
            return;
        }

        klipJson = JSON.stringify(this.activeKlip.serialize());
        savePromise = $.post("/workspace/ajax_autosave_klip", {id: this.activeKlip.id, schema: klipJson});

        this.saveManager.showUpdateUI(
            savePromise,
            "Auto-saving a revision...",
            "Auto-save complete.",
            { spinner: true }
        );
    },

    showMessage: function(message) {
        this.messageVisible = true;

        $("#editor-message").html(message).show();

        this.resizeMessage();
    },

    hideMessage: function() {
        $("#editor-message").hide();
        this.messageVisible = false;
    },

    resizeMessage: function() {
        var $editorMessage, messageHeight, parentHeight;

        if (!this.messageVisible) return;

        $editorMessage = $("#editor-message");
        messageHeight = $editorMessage.height();
        parentHeight = $editorMessage.parent().height();

        $editorMessage.css("margin-top", ((parentHeight - messageHeight) / 2) + "px");
    },


    evaluateFormulas : function(rootComponent) {
        // When we are evaluating the whole klip, we can skip evaluting peer components of all the sub-components since all components
        // will end up be evaluated. Evaluting peer compoennts in this case will cause a lot of duplicated evaluation.
        var skipPeers = !rootComponent;
        var currentRootComponent = rootComponent || this.activeKlip;

        this.initializeFormulas = false;

        if (this.uiVariables === false) {
            // need to wait until ui variables are initialized
            if (!this._pendingFormulaEvaluations) {
                this._pendingFormulaEvaluations = [];
            }
            this._pendingFormulaEvaluations.push(currentRootComponent);
        } else {
            // ui variables already initialized
            this.evaluateAllComponentFormulas(currentRootComponent, skipPeers);
        }
    },

    evaluateAllComponentFormulas: function(rootComponent, skipPeers) {
        var isPostDST = this.activeKlip.isPostDST();

        if (rootComponent != this.activeKlip) {
            this.updateVariableValue(rootComponent);
            if (!isPostDST) {
                this.evaluateComponentFormulas(rootComponent, skipPeers);
            }
        }

        eachComponent(rootComponent, function(component) {
            this.updateVariableValue(component);
            if (!isPostDST) {
                this.evaluateComponentFormulas(component, skipPeers);
            }
        }.bind(this));

        if (isPostDST) {
            DX.Manager.instance.watchFormulas([{ kid: this.activeKlip.id, fms: rootComponent.getFormulas() }]);
        }
    },

    updateVariableValue: function(cx) {
        if (cx.factory_id == "input" && this.uiVariables[cx.propName]) {
            cx.setControlValue(this.uiVariables[cx.propName].value);
        }
    },

    evaluateComponentFormulas: function(cx, skipPeers) {
        var fm;

        if (!cx.formulas) {
            return;
        }

        fm = cx.getFormula(0);

        if (!fm) {
            return;
        }

        this.setComponentFormula(cx, fm.formula, fm.src, skipPeers);
    },

    evaluate: function(formulaText, targetNodeSelector) {
        var dsts = {}, primaryDSTId, requiredContexts;
        var formula = this.activeComponent.getFormula(0);
        var vars = this.collectVariables(formula);
        var primaryDst, query;
        var dataSetId = this.activeComponent.getDataSetId();
        var requiredContextsClone = {};

        //Replace the formula references first so we are working with a fully expanded formula
        var processedFormula = DX.Formula.trim(DX.Formula.replaceFormulaReferences(formulaText, this.activeKlip, true), true)

        requiredContexts = formula ? DX.Formula.determineRequiredDSTContexts(formula.cx) : {};

        //Send a DST request for the evaluate only if the post-DST is enabled, and the selected formula text contains
        //results references.
        if (this.activeComponent.usePostDSTReferences() &&
            DX.Formula.getReferenceMatches(processedFormula, DX.Formula.RESULTS_REF).length > 0) {

            primaryDst = this.activeComponent.getDstContext();

            if (primaryDst) {
                primaryDSTId = primaryDst.id;
            }

            dsts = requiredContexts.dsts ? requiredContexts.dsts : [];
            if (requiredContexts.vars) {
                $.extend(vars,requiredContexts.vars);
            }

            //This looks unecessary, but is necessary, as these dsts are after the resolve operations have been inserted,
            //we need to find the one that matched the original primary dst.
            dsts.forEach(function(dst) {
                if (dst.id === primaryDSTId) {
                    primaryDst = dst;
                }
            });

            if (primaryDst) {
                this.removeDSTOperationsAfterResolveOnVC(primaryDst,formula.cx.getVirtualColumnId());

                //If we have a subset of the formula selected, replace the virtual column formula for the VC being referenced
                //by the selected portion. Note, we have to do this after getting the requriedDSTs to ensure we are only
                //modifying the already resolved DST copies, so we don't affect the formulas of the actual components.
                primaryDst.virtualColumns.forEach(function(vc) {
                    if (vc.id === formula.cx.getVirtualColumnId()) {
                        if (vc.formula !== formulaText) {
                            vc.formula = formulaText;
                            //We have to reapply the references to the formula subset
                            vc.formula = DX.Formula.putVCReferencesAndReplaceFormulaReferences(vc.formula, formula.cx);
                        }
                    }
                });
            }

            //Treat the evaluate as if it referred to the component from outside, so any results references will act as
            //external references
            processedFormula = DX.Formula.putVCReferencesAndReplaceFormulaReferences(processedFormula, formula.cx)
        } else if (dataSetId) {
            requiredContextsClone = $.extend(true, {}, requiredContexts);
            dsts = requiredContextsClone.dsts.map(function (dst) {
                dst.base = dataSetId;
                delete  dst["ops"]; // remove operations from dst so they don't get applied as a part of the DataSetColumn evaluation.
                return dst;
            });
        }

        query = {
            kid: this.activeComponent.getKlipInstanceId(),
            klip_pid: this.activeComponent.getKlipPublicId(),
            fm: processedFormula,
            vars: vars,
            dst: dsts,
            useNewDateFormat: this.activeComponent.useNewDateFormat()
        };

        this.dxManager.query(query, function(result, message) {
            require(["js_root/utilities/utils.evaluate"], function (EvaluateUtils) {
                EvaluateUtils.showEvaluationResults(result, message, targetNodeSelector);
            });
        })
    },

    removeDSTOperationsAfterResolveOnVC: function(dstContext, vcId) {
        var removalIdx = -1;
        var operation;
        var removedDimensions = {};
        var remainingVCs = [];
        var i;

        for (i = 0; i < dstContext.ops.length && removalIdx === -1; i++) {
            operation = dstContext.ops[i];

            if (operation.type === "resolve" && operation.dims.indexOf(vcId) !== -1) {
                removalIdx = i;
                break;
            }
        }

        if (removalIdx !== -1) {
            removalIdx++; //Skip past the last operation

            if (removalIdx < (dstContext.ops.length)) {

                //Check if the next operation is a format operation, of so we also need to include it (really should be part of the resolve)
                if (dstContext.ops[removalIdx].type === "format") {
                    removalIdx++;
                }

                //Remove all operations from the removalIDx on, but keep track of which resolved dimensions we removed
                for (i = removalIdx; i < dstContext.ops.length; i++) {
                    if (dstContext.ops[i].type === "resolve") {
                        dstContext.ops[i].dims.forEach(function(dim) {
                            removedDimensions[dim] = true;
                        })
                    }
                }

                dstContext.ops.splice(removalIdx,dstContext.ops.length - removalIdx);
            }
        }

        //Remove the VCs that are no longer resolved, since they would now be resolved before the DST, and would
        //conflict with the cached unprocessed dataset.
        if (Object.keys(removedDimensions).length > 0) {
            for (i = 0; i < dstContext.virtualColumns.length; i++) {
                if (!removedDimensions[dstContext.virtualColumns[i].id]) {
                    remainingVCs.push(dstContext.virtualColumns[i]);
                }
            }
            dstContext.virtualColumns = remainingVCs;
        }
    },

    getWorkspaceDuration: function() {
        var MILLISECONDS_IN_ONE_MINUTE = (1000 * 60);
        var millisBetweenDates = Date.now() - this.workspaceStartTime;
        var minutes = millisBetweenDates / MILLISECONDS_IN_ONE_MINUTE;

        return new FMT.num().format([minutes], { precision: 2 });
    },

    getWorkspaceEvents: function() {
        var allEvents = {};
        _.each(Workspace.EventNames, function(name) {
            allEvents[name] = 0;
        });

        return $.extend({}, allEvents, this.workspaceEvents);
    },

    /**
     * sets the active klip and initializes the workspace
     * @param k Klip
     */
    setActiveKlip: function(k) {
        var datasources, i;
        this.datasourceInstances = [];
        this.dataSets = [];
        this.activeKlip = k;

        k.setVisible(true);

        // fetch all the datasources in this klip.  Datasource in the klip will
        // only have a dsid, and no user-friendly name, so after getting the dsids
        // we'll ask the server for a batch description of all them.
        // ------------------------------------------------------------------------
//		var datasources = _.uniq(this.activeKlip.getDatasources());
        datasources = this.activeKlip.getDatasourceIds();

        if (datasources.length > 0) {
            this.visualizerTabPane.removeAllVisualizers();
        }

        for (i = 0; i < datasources.length; i++) {
            this.addDatasourceInstance({id: datasources[i]});
        }

        k.getDataSets().forEach(function(dataSet) {
            this.addDataSet(dataSet);
        }, this);

        this.updateMenu();
        k.initializeForWorkspace(this);

        if (this.initializeFormulas && this.dxManager.queryServiceAvailable()) {
            this.evaluateFormulas();
        }
    },

	/**
	 * makes an ajax request to the server for information about a list
	 * of datasource instance ids.  components/formulas make use of dsIds only
	 * and do not have any descriptive information assocaited with them, so when
	 * a klip/formula is added to the workspace we need to get more information about them
	 * @param dsids an array of DatasourceInstance ids
	 * @param callback a callback function that is invoked on ajax response.  is provided two parameters:  the datasource id, the datasource ojbect
	 */
	describeDatasourceInstances: function(dsids , callback) {
		var _this = this;
		$.post("/datasources/ajax_describe", {id:dsids} , function(data){
            var i, dsi;
			// go through all our datasources and lookup in the 'data' object
			// provided values of interest (currenlty just name)
			for(i = 0 ; i < _this.datasourceInstances.length ; i++) {
				dsi = _this.datasourceInstances[i];

				if (!data[dsi.id]) {
                    continue;  // if nothing in data, continue
                }

				dsi = $.extend(dsi,data[dsi.id]);
				if (callback) {
                    callback(dsi.id,data[dsi.id]);
                }
			}

			_this.updateMenu();
		},"json");
	},

	/**
	 *
	 */
	updateMenu: function() {
		this.renderMenu(false, this.buildMenu());
		if ( this.activeMenuValue ) this.selectMenuItem(this.activeMenuValue);

        customScrollbar($("#c-workspace_menu"),$("#treeMenuContainer"),"both");
    },

    updateVariables: function(selectedVarName, selectOption) {
        var _this = this;
        var variables = _this.uiVariables;
        var names = this.getVariableNames(variables);

        names.forEach(function(name){
            // When using DST in Klip editor, it will get variable values from Dashboard properties. Therefore when
            // variables are updated in workspace, we need to set their values in Dashboard object using Dashboard scope (3).
            _this.dashboard.setDashboardProp(Dashboard.SCOPE_DASHBOARD, name, variables[name].value);
        });

        this.renderVariables(names, variables, selectedVarName);

        if (_this.activeComponent.factory_id == "input" || _this.activeComponent.factory_id == "input_button") {
            _this.updatePropertySheet(
                _this.activeComponent.getPropertyModel(),
                _this.activeComponent.getPropertyModelGroups(),
                _this.componentPropertyForm
            );

            if (selectOption == "true") {
                $("#tab-properties select[name='propName']").val(selectedVarName).change();
            }
        }

        if (this.formulaEditor) {
            this.formulaEditor.setUIVariables(this.uiVariables);
        }
    },

    /**
     * Get a list of user and non-user variable names.
     * @returns {Array.<*>}
     */
    getVariableNames: function(variables) {
        var keys = Object.keys(variables);

        var userVariables = keys.filter(function(a) {
            return (a.indexOf("user.") >= 0 || a.indexOf("company.") >= 0);
        }).sort(function(a,b) {
            return a.localeCompare(b);
        });

        var nonUserVariables = keys.filter(function(a) {
            return !(a.indexOf("user.") >= 0 || a.indexOf("company.") >= 0);
        }).sort(function(a,b) {
            return a.localeCompare(b);
        });

        return userVariables.concat(nonUserVariables);
    },

    /**
     * sets the active component in the workspace.
     * @param c Component or false to clear
     * @param prevAction The action that occurred before calling this method
     */
    setActiveComponent: function (c, prevAction) {
        var draggableComp, displayTabs, activeTab, hasProperties, cxFormula, model, $dragHandle;

        if (this.activeLayout)  this.setActiveLayout(false);

        if (c == this.activeComponent) return; // if c is the same as the active cx, return
        if (this.klipIsSelected) this.selectActiveKlip(false); // if the klip is selected, clean up selection state by de-selecting the klip

        // unselect the active component
        // -----------------------------
        if (this.activeComponent) {
            if (this.formulaEditor && this.activeComponent === this.formulaEditor.getCurrentComponent()) {
                this.formulaEditor.onFocusLost();
            }

            this.activeComponent.setSelectedInWorkspace(false, c);

//            draggableComp = this.activeComponent;
//            if (!this.activeComponent.isDraggable) {
//                draggableComp = this.activeComponent.getRootComponent();
//            }
//
//            if (draggableComp.el && draggableComp.el.data("uiDraggable")) {
//                draggableComp.el.draggable("destroy");
//            }
        }

        $(".comp-drag-handle").remove();
        $(".dst-hint-icon-component").remove();

        if (c) {
            c.setSelectedInWorkspace(true, c);

            this.controlPalette.updateItems(c.getWorkspaceControlModel());

            this.activeComponent = c;
            this.conditionsPane.setTarget(c);
            displayTabs = [];
            activeTab = false;
            hasProperties = (c.getPropertyModel().length > 0);

            if (!c.dataDisabled && c.formulas) {
                cxFormula = c.getFormula(0);
                displayTabs.push("data");

                if (hasProperties) displayTabs.push(Workspace.PropertyTabIds.PROPERTIES);

                this.updateFormulaBar(cxFormula, c);

                activeTab = "data";
            } else {
                if (hasProperties) {
                    displayTabs.push(Workspace.PropertyTabIds.PROPERTIES);
                    activeTab = Workspace.PropertyTabIds.PROPERTIES;
                }
            }

            if (c.drilldownSupported) {
                displayTabs.push("drilldown");
            }

            if (c.parent.isPanel) {
                model = this.getLayoutConstraintsModel(c);
                if (model) {
                    displayTabs.push("layout");
                    this.updatePropertySheet(
                        model.props,
                        model.groups,
                        this.layoutPropertyForm
                    );
                }

                if (!activeTab && this.selectedTab == "layout") activeTab = "layout";
            }


            // add the drag handle to component or root
            // -------------------------------------------------------
            draggableComp = c;
            $dragHandle = $("<div>").addClass("comp-drag-handle");

            if (!c.isDraggable) {
                draggableComp = c.getRootComponent();
                $dragHandle.addClass("root-handle");
            }

            draggableComp.el.append($dragHandle);
            Workspace.EnableDraggable(draggableComp.el, "component",
                {
                    handle: ".comp-drag-handle",
                    appendTo: "body",
                    zIndex: 100,
                    cursor: "move"
                }
            );


            // handle indicating DST actions
            // -------------------------------------------------------
            if (c.getDstRootComponent()) {
                if (KF.company.hasFeature("applied_actions")) {
                    displayTabs.push(Workspace.PropertyTabIds.APPLIED_ACTIONS);
                }

                this.addDstHintIconToComponent();
                if (c.hasDstActions()) {
                    this.selectMenuItem("component-" + draggableComp.id);
                }
            }

            // if a cx supports reactions, display the conditions tab
            // -------------------------------------------------------
            if (c.getSupportedReactions()) displayTabs.push("conditions");

            this.displayWorkspaceTabs(displayTabs, activeTab);

            this.focusFirstPropertyInput();
        } else {
            this.activeComponent = false;

            this.controlPalette.updateItems([]);
        }

        PubSub.publish("cx-selected", [this.activeComponent]);
//        PubSub.publish("data-tab-selected", ["component"]);
    },

    addDstHintIconToComponent: function() {
        var rootComponent = this.activeComponent.getRootComponent();
        var dstRootComponent = this.activeComponent.getDstRootComponent();
        var $dstHintHandle = rootComponent.el.find(".dst-hint-icon-component");
        var _this = this;

        if (dstRootComponent.hasDstActions() || dstRootComponent.hasSubComponentsWithDstContext()) {
            if ($dstHintHandle.length === 0) {
                $dstHintHandle = $("<div title='View applied actions'>").addClass("dst-hint-icon-component")
                    .on("click contextmenu", function () {
                        _this.renderActionDropdown(_this.activeComponent, $(this));
                    });
                rootComponent.el.find(".comp-drag-handle").after($dstHintHandle);
            }
        } else {
            $dstHintHandle.remove();
        }
    },

    updatePossibleComponentReferences: function() {
        if (this.activeComponent.usePostDSTReferences() && this.formulaEditor) {
            this.formulaEditor.setPossibleComponentReferences(this.getPossibleComponentReferencesModel(this.activeComponent.getDstHelper().canReferToDstPeers()));
        }
    },


    renderActionDropdown: function (component,clickedIcon) {
        var rootComponent, _this, adjustedTopMargin, adjustedLeftMargin,
            actionContainer, containerPosition, width;

        if (!component || !clickedIcon) return;

        rootComponent = component.getDstRootComponent();
        if (!rootComponent) return;

        _this = this;
        adjustedTopMargin=14;
        adjustedLeftMargin=14;

        actionContainer = $("<div>").addClass("action-dropdown-container");
        containerPosition = clickedIcon.offset();
        width = clickedIcon.outerWidth();

        this.cxContextMenu.hide();

        require(["/js/build/vendor.packed.js"], function( Vendor) {
            require(["/js/build/applied_actions_main.packed.js"], function ( AppliedAction) {
                AppliedAction.render(rootComponent, {
                    workspaceRef: _this,
                    isDropdown: true,
                    container: actionContainer[0],
                    callback: _this.actionDropdownRenderCallback(actionContainer[0], _this)
                });
            });
        });


        $(document.body).append(actionContainer);

        actionContainer.css({
            top: containerPosition.top + adjustedTopMargin + "px",
            left: (containerPosition.left + width - adjustedLeftMargin) + "px"
        });

        PubSub.publish("record-event", { eventName: Workspace.EventNames.APPLIED_ACTION_EVENT, count: 1 });
    },

    actionDropdownRenderCallback: function (actionContainer,_this) {
        var container = $(actionContainer);
        $(document).on("mouseup.actionDropdown", function (e) {

            // if the target of the click isn't the container or one its decendants
            if (!container.is(e.target) && container.has(e.target).length === 0) {
                _this.closeActionContainer(container);
            }
        });

        $(window).on("keydown.actionDropdown", function (e) {
            if (e.keyCode === 27) {
                _this.closeActionContainer(container);
            }
        });
    },

    closeActionContainer: function (actionContainer) {
        var container = $(actionContainer);
        container.remove();
        $(document).off("mouseup.actionDropdown");
        $(window).off("keydown.actionDropdown");
    },

    updateFormulaBar: function(cxFormula, component) {
        if (cxFormula) {
            cxFormula.convertToTextOnlyFormat();
        }

        this.formulaEditor.setFormula(cxFormula, component, this.getPossibleComponentReferencesModel(component.getDstHelper().canReferToDstPeers()));

        if (!cxFormula) {
            // This is needed to ensure that some components, such as tables,
            // are rendered properly after being dropped onto the klip from
            // the palette. It essentially triggers the same key behaviour that
            // was implicitly triggered by the call to `builder.clearFormula()`
            // when the old Formula Bar was enabled.
            // TODO: figure out a better way to do this
            this.activeComponent.clearFormula(0);
        }
    },

    /**
     *  deselects whatever is currently selected and sets focus back to the klip
     */
    selectActiveKlip: function(select) {
        if (select) {
            this.setActiveComponent(false);
            this.setActiveLayout(false);
            this.klipIsSelected = true;
            $("#klip-preview .c-dashboard").addClass("selected-component");

			this.selectMenuItem("klip");

			this.displayWorkspaceTabs([Workspace.PropertyTabIds.PROPERTIES], Workspace.PropertyTabIds.PROPERTIES);
        } else {
            this.klipIsSelected = false;
            $("#klip-preview .c-dashboard").removeClass("selected-component");
        }
    },


	/**
	 * sets the active layout component (which can be different than the active component).
	 * there are times when both a component and layout can be targeted by different events -- specificially, when the 'Position'
	 * tab is updated.
	 * @param cx
	 */
	setActiveLayout: function(cx) {
		if (!cx){
			if(this.activeLayout){
				this.activeLayout.setActiveCell(false);
			}
			this.activeLayout = false;
		}else{
			this.activeLayout = cx;
		}

	},

	/**
	 * generate a constraint propery model to be used for the 'Position' property sheet based on the given component.
	 *
	 * @param cx
	 */
	getLayoutConstraintsModel: function(cx) {
		var panel = cx.parent.isPanel ? cx.parent : false;

        if (!panel.isPanel) return;

		return panel.getLayoutConstraintsModel( { cx : cx } );
	},

	/**
	 * updates the form fields in a property sheet based on the
	 * provided model and groups
	 */
	updatePropertySheet: function(model,groups, cform) {
        var ids, i;

        // resort model array based on groups if available
        if (groups && groups.length) {
            ids = [];
            $.each(model, function(i,a){
                ids.push(a.id);
            });
            model.sort(function (a,b) {
                var aindex = _.indexOf(groups,a.group);
                var bindex = _.indexOf(groups,b.group);
                if (aindex < bindex) return -1;
                else if (aindex > bindex) return 1;
                else {
                    aindex = _.indexOf(ids,a.id);
                    bindex = _.indexOf(ids,b.id);
                    return (aindex < bindex?-1:aindex == bindex?0:1);
                }

            });
        }

		cform.removeAll();

		for (i = 0; i < model.length; i++) {
			cform.addWidget(model[i]);
		}

		cform.render();
	},

	/**
	 *
	 * @param changeEvt
	 * @param rawEvt
	 */
	onPropertyFormChange: function(changeEvt,rawEvt) {
		var config = {};
        var argName;

        config["~propertyChanged"] = true;

		if (changeEvt.name.indexOf("fmt_arg_") ==   0) {
			argName = changeEvt.name.substring( "fmt_arg_".length );
			config["fmtArgs"] = {};
			config["fmtArgs"][argName] = changeEvt.value;
		} else {
			config[ changeEvt.name ] = changeEvt.value;
		}

		if (this.klipIsSelected) {
			this.activeKlip.configure( config );
			this.activeKlip.update();
		} else if (this.activeComponent) {
			this.activeComponent.configure( config );
			if(changeEvt.widget.ignoreUpdates != true)updateManager.queue(this.activeComponent);
		}
	},

	/**
	 *
	 * @param changeEvt
	 * @param rawEvt
	 */
	onLayoutFormChange: function(changeEvt,rawEvt) {
		var config = {};
        config["~propertyChanged"] = true;
		config[ changeEvt.name ] = changeEvt.value;


		if (this.activeLayout) {
			this.activeLayout.onLayoutPropertiesChange(changeEvt);
		} else if ( this.activeComponent && !this.activeComponent.isPanel ){
			this.activeComponent.parent.onLayoutPropertiesChange( changeEvt, this.activeComponent );
		}
	},


	/**
	 * display the specified tabs, and hides the rest
	 */
	displayWorkspaceTabs: function(tabIds, selectId) {
		var nTabs = tabIds.length;
        var changeTabs = true;

        var i, t;

        // check if the current list of tabs matches
        if (nTabs == this.visibleTabs.length) {
            changeTabs = false;
			while (--nTabs > -1 && !changeTabs) {
                changeTabs = (_.indexOf(this.visibleTabs, tabIds[nTabs]) == -1);
			}
		}

        if (changeTabs) {
            this.visibleTabs = tabIds;

            $(".property-tabs li").hide();
            $(".property-tab-pane").hide();

            for (i = 0; i < tabIds.length; i++) {
                t = tabIds[i];
                $("li[tabid=" + t + "]").show();
            }
        }

        // try to stay on the same tab
        if (this.selectedTab && (_.indexOf(tabIds, this.selectedTab) != -1)) {
            selectId = this.selectedTab;
        } else {
            if (!selectId) {
                if (tabIds.length == 0) {
                    // If there are no visible tabs, display the blank Properties tab (ie, an empty blue box)
                    selectId = Workspace.PropertyTabIds.PROPERTIES;
                } else {
                    // choose the first tab, if nothing else
                    selectId = tabIds[0];
                }
            }
        }

		this.selectWorkspaceTab(selectId);
	},

    /**
     * A simple function to determine if a tab is visible
     * @param tabId
     * @returns {*}
     */
    isTabVisible: function(tabId) {
        var tab = $("li[tabid=" + tabId + "]");
        if(tab) {
            return tab.is(":visible");
        }
        return false;
    },

    /**
     * Toggles the display of single tab
     * @param tabId
     * @param toShow
     */
    toggleSingleTabDisplay: function(tabId, toShow) {
        var tab = $("li[tabid=" + tabId + "]");
        if(tab) {
            if(toShow) {
                if(this.isTabVisible(tabId)) return;
                tab.show();
            } else {
                if(!this.isTabVisible(tabId)) return;
                tab.hide();
            }
        }
    },

    /**
     * creates the model for the workspace navigation menu by inspecting the activeKlip and its
     * components
     */
    buildMenu: function() {
        var menuModel = [];

        if (this.activeKlip) {
            menuModel.push(this.activeKlip.getEditorMenuModel());
        }

        return menuModel;
    },

    /**
     *
     * @param parent
     * @param items
     */
    renderMenu: function(parent, items) {
        var isRoot = false;
        var item, actionLink, currentLine, currentListItem, i, linkedToValue, childList;

        if (!parent) {
            isRoot = true;
            parent = $("<ul>").attr("id", "workspace-menu");
        }

        for (i in items) {
            item = items[i];
            actionLink = $("<a>").html(item.text).attr({
                actionId: item.actionId,
                title: item.tooltip || item.text
            });
            if (this.activeKlip && this.activeKlip.isPostDST()) {
                //currentLine holds the component text name and all adjacent icons
                currentLine = $("<span>");
                this.renderRootState(actionLink, item);
                currentLine.append(actionLink);

                //currentListItem holds children nodes as well
                currentListItem = $("<li>").append(currentLine);
                this.renderFormulaState(currentLine, item);
                this.renderDstHintForMenuItemInTree(currentLine, item);
            } else {
                currentListItem = $("<li>").append(actionLink);
                this.renderRootState(actionLink, item);
                this.renderDstHintForMenuItemInTree(currentListItem, item);
            }


            // if there are action arguments, attach them to the <a>'s data scope,
            // the PageController will then automatically pass the arguments on to
            // the Action handler.
            // ------------------------------------------------------------------
            if (item.actionArgs) {
                actionLink.data("actionArgs", item.actionArgs);
            }

            // in order to easily find a menu item we add a linkedTo attribute
            // which we can select on (eg, $([linkedTo=xxx]) in other parts of the UI
            // --------------------------------------------------------------
            switch (item.actionId) {
                case "select_component":
                    linkedToValue = "component-" + item.actionArgs.id; // actionArgs should always be a Component for 'select_component' actions
                    break;
                case "select_klip":
                    linkedToValue = "klip";
                    break;
				default:
					linkedToValue = item.linkedTo;
					break;
            }

            if (linkedToValue) actionLink.attr("linkedTo", linkedToValue);

            // insert this item into the current root <ul>
            // ------------------------------------------
            parent.append(currentListItem);


            // if there are child menu items for this item, create a new <ul> to hold
            // them and then process them recursively
            // ----------------------------------------------------------------------
            if (item.items) {
                if (item.items.length > 0) {
                    childList = $("<ul>");
                    this.renderMenu(childList, item.items);
                    currentListItem.append(childList);
                }
            }
        }

        // finally, if this was the root menu item we should put into the document
        // and turn it into a treeview
        // -----------------------------------------------------------------------
        if (isRoot) {
            $("#c-workspace_menu").empty().append(parent);
            parent.treeview();
            this.menuEl = parent;
        }
    },

    renderRootState: function(treeLink, menuItem) {
        var component = (menuItem && menuItem.actionArgs && this.activeKlip.getComponentById(menuItem.actionArgs.id));
        if (!component) return;

        if (component.isDstRoot && component.canMakeDstRoot) {
            treeLink.addClass("is-dst-root")
        }
    },

    renderFormulaState: function(treeNode, menuItem) {
        var component = this.activeKlip.getComponentById(menuItem.actionArgs.id);
        var $errorMsg;

        if (!component) return;

        if (this.formulaEditor && this.formulaEditor.isInErrorState(component.id)) {
            $errorMsg = $("<span title='An error occurred while trying to calculate.' class='component-icon-tree-formula-error'></span>");
            treeNode.append($errorMsg);
        }
    },

    renderDstHintForMenuItemInTree: function(treeNode, menuItem) {
        var component = this.activeKlip.getComponentById(menuItem.actionArgs.id);
        var virtualColumnId;
        var filterOperationData;
        var sortOperationData;
        var visualCueTitle;
        var sortOrder;
        var lastSpanClass;
        var $dstHandler;
        var sortDirectionHintClass;

        if (!component || (!component.hasDstActions() && !component.hasSubComponentsWithDstContext()) || !component.getDstRootComponent()) return;

        lastSpanClass = "dst-hints-"+component.getVirtualColumnId() + "-last";

        if (component.isDstRoot) {
            $dstHandler = $("<span title='View applied actions' class='dst-hint-component-icon-tree  dst-hint-icon-tree-node " + lastSpanClass + "'></span>");
            treeNode.append($dstHandler);
            return;
        }

        virtualColumnId = component.getVirtualColumnId();

        filterOperationData = component.findDstOperationByTypesAndDimensionId(["filter", "top_bottom"], virtualColumnId);
        sortOperationData = component.findDstOperationByTypeAndDimensionId("sort", virtualColumnId);

        if(filterOperationData){
            treeNode.append($("<span title='Filter is applied'>")
                .addClass("dst-hint-icon-tree dst-hint-icon-filter dst-hint-icon-tree-node " + (sortOperationData ? "" : lastSpanClass)));
        }

        if(sortOperationData){
            sortDirectionHintClass = "";
            sortOrder = sortOperationData.operation.sortBy[0].dir;
            if(sortOrder == 1) {
                sortDirectionHintClass = "dst-hint-icon-sort-asc";
            } else if (sortOrder == 2) {
                sortDirectionHintClass = "dst-hint-icon-sort-desc";
            }
            visualCueTitle = component.getTitleBySortOrder(sortOrder);
            treeNode.append($("<span title='Sorted "+visualCueTitle+"' class='dst-hint-icon-tree "+sortDirectionHintClass+" dst-hint-sort-icon dst-hint-icon-tree-node "+ lastSpanClass +"'></span>"));
        }

    },

	/**
	 * selects the menu item based on its linkedTo attribute value.  Selecting a menu
	 * item is purely cosmetic and does not result in panel/ui changes
	 * @param linkedToVal
	 */
    selectMenuItem: function (linkedToVal) {
        var menu = this.menuEl;
        var menuItem = menu.find("[linkedTo=" + linkedToVal + "]");
        var dstHintClass, dstHints;
        var target, menuContext, threeDots;

        this.activeMenuValue = linkedToVal;

        menu.find(".active").removeClass("active");
        menuItem.addClass("active");
        menu.find(".three-dots").remove();

        if (this.activeComponent) {
            target = this.activeComponent;
            menuContext = {
                isRoot: (target == target.getRootComponent()),
                fromTree: true
            };

            if (this.activeKlip && !this.activeKlip.isPostDST()) {
                if (this.activeComponent.hasDstActions() && menuItem.parent()) {
                    dstHintClass = ".dst-hints-" + this.activeComponent.getVirtualColumnId() + "-last";
                    dstHints = menuItem.parent().find(dstHintClass);

                    if (dstHints && dstHints.length == 1) {
                        menuItem = dstHints;
                    }
                }
            }
        } else {
            target = this.activeKlip;
            menuContext = {
                includePaste: true
            }
        }

        threeDots = $("<span class='three-dots' title='More actions'>");
        if (this.activeKlip && !this.activeKlip.isPostDST()) {
            menuItem.after(threeDots);
        } else {
            menuItem.parent().append(threeDots);
        }

        threeDots
            .on("contextmenu", function (event) {
                event.preventDefault();
                event.stopPropagation();
                page.invokeAction("show_context_menu", {target: target, menuCtx: menuContext}, event);
            })
            .attr("actionId", "show_context_menu")
            .data("actionArgs", {
                target: target,
                menuCtx: menuContext
            });
    },

	/**
	 *
	 * @param cxs
     * @param stack
	 */
	selectFirstDataComponent: function (cxs, stack) {
        var len, cx, i, dfsResult;

		if ( !cxs ){
			cxs = this.activeKlip.components;
			stack = [];
		}

		len = cxs.length;
		for(i = 0; i < len; i++){
			cx = cxs[i];
			if ( cx.getData != undefined && !cx.hideInWorkspace) {
                return cx; // only data cxs should have a getData method...
            }
			if ( _.indexOf(stack,cx) == -1 ) {
				if ( cx.components && cx.components.length != 0){
					stack.push(cx);
					dfsResult = this.selectFirstDataComponent(cx.components, stack);
					if ( dfsResult ) {
                        return dfsResult;
                    }
				}
			}
		}

		if ( stack.length != 0 ){
			cx = stack.pop();
			return this.selectFirstDataComponent( cx.components,stack );
		}

		return false;
	},


	/**
	 * event handler for tab click.
	 * @param evt
	 */
	onWorkspaceTabSelect: function(evt) {
		var tabId = $(evt.target).attr("tabId");
		if (!tabId || this.selectedTab == tabId) return;
		this.selectWorkspaceTab(tabId);
	},


	showPointerInVisualizer: function(pointer) {
        var dataId, dataset;
        var _dst = new DST();
        if (pointer) {
            dataId = pointer.id;

            if(_dst.isDataSetPointer(pointer)) {
                if (dataset = this.activeKlip.getDataSet(dataId)) {
                    pointer.address = _dst.getDataSetVCIdFromName(dataset, pointer.address);
                    this.visualizerTabPane.selectPointer(pointer);
                } else {
                    this.activeKlip.addDataSet(dataId);
                    this.addDataSet(
                        {id: dataId},
                        {showVisualizer: true},
                        function () {
                            this.visualizerTabPane.selectPointer(pointer);
                        }.bind(this)
                    );
                }
            } else {
                if (this.activeKlip.getDatasource(dataId)) {
                    this.visualizerTabPane.selectPointer(pointer);
                } else {
                    this.activeKlip.addDatasource(dataId);
                    this.addDatasourceInstance(
                        {id: dataId},
                        {showVisualizer: true},
                        function () {
                            this.visualizerTabPane.selectPointer(pointer);
                        }.bind(this)
                    );
                }
            }
        } else {
            this.visualizerTabPane.clearAllSelections();
        }
    },


	/**
	 *
	 * @param evtName
	 * @param args 0 new formula
	 */
	onFormulaChanged : function(evtName,args) {
        var f;
        var src;
        var isEdit;
        var cx;
        var oldFormula;
        var dstRootComponent;
        var groupInfo;
        var aggregateInfo;
        var currentAggregation;
        var dst;
        var virtualColumnId;
        var aggregations = {};

		if ( this.activeComponent ) {
			f = args[0];
			src = args[1];
            isEdit = args[2];
			cx = args[3] ? args[3] : this.activeComponent;

            if (!isEdit) {
                oldFormula = cx.getFormula(0);
            }

            // Updates DST before applying formula to the components
            if(oldFormula && oldFormula.cx &&
                oldFormula.cx.id == cx.id && oldFormula.formula != f) {
                this.updateDstOnFormulaChange(cx);
            }

            // Updates formulas on the component
            if ( !f || f.length == 0 ) {
                cx.clearFormula(0);
            } else {
                this.setComponentFormula(cx,f,src);
            }

            // update formulas that reference this one
            this.updateFormulaReferences(cx.id);

            dstRootComponent = cx.getDstRootComponent();

            if (f) {
                // if there is already a Group DST, need to set the default aggregation
                // for new virtual columns
                groupInfo = cx.getGroupInfo();
                aggregateInfo = cx.getAggregateInfo();
                if ((groupInfo.isGrouped && !groupInfo.isGroupedByThisComponent) || aggregateInfo.isAggregated) {

                    virtualColumnId = cx.getVirtualColumnId();
                    currentAggregation = cx.getDstAggregation(virtualColumnId);
                    dst = cx.getAvailableDst();

                    if (!currentAggregation && dst.aggregation) {

                        aggregations[virtualColumnId] = cx.getAggregation(true);
                        dstRootComponent.updateDstAggregations(aggregations);
                    }
                }
            }
		}
	},

    // In case the formula changes for a virtual column, update them here
    updateDstOnFormulaChange : function(cx) {
        var filterDst = cx.getFilterOrTopBottomOp();
        if(filterDst) {
            cx.deleteDstOperation(filterDst.orderIndex);
        }
    },

	/**
	 * sets the formula on a component, and submits the formula
	 * for execution.
	 * @param cx
	 * @param txt
	 * @param src
	 */
	setComponentFormula : function(cx, txt, src, skipPeers, varChange) {
        var version = this.formulaEditor && DX.Formula.VERSIONS.TYPE_IN;
        var componentsToSubmit = [];
        var peers;
        var vars;

        var dstVariables, fvars, dstRootComponent, hasDstActions;

        cx.setFormula(0, txt, src, version);

        if (cx.usePostDSTReferences() && this.activeKlip && !varChange) {
            //Formula changed, we need to figure out the dependencies again
            this.activeKlip.refreshComponentDependerMap();
        }

        if (cx.usePostDSTReferences() && !cx.sendQueryRequestInsteadOfWatch()) {
            //cx.sendQueryRequestInsteadOfWatch is true only for result rows (non-custom) against pDPN
            this.updateDataSourcePropsOnKlip(cx.getFormula(0));
            return cx.updateDSTFormula(true);
        }

        // dstVariables will capture all variables for the component and all its peers when DST is applied.
        dstVariables = {};
        fvars = {};
        dstRootComponent = cx.getDstRootComponent();
        hasDstActions = cx.hasDstActions(true);
        // when a formula is submitted, DST peers should also be submitted to enforce
        // consistent evaluation -- collect all the components that should be
        // sent for evaluation
        // -------------------------------------------------------------------------
        if (hasDstActions) {
            peers = cx.getDstPeerComponents();
            if (peers) {
                peers.forEach(function (peer) {
                    vars = this.collectVariables(peer.getFormula(0));
                    $.extend(dstVariables, vars);

                    if (!skipPeers) {
                        componentsToSubmit.push(peer);
                    }
                }.bind(this));
            } else {
                vars = this.collectVariables(cx.getFormula(0));
                $.extend(dstVariables, vars);

                if (!skipPeers) {
                    componentsToSubmit.push(cx);
                }
            }
            fvars = dstRootComponent.collectDstFilterVariables();
            $.extend(dstVariables, fvars);
        }

        if (!hasDstActions || skipPeers) {
            componentsToSubmit.push(cx);
        }

        componentsToSubmit.forEach(function (componentToSubmit) {
            var formula = componentToSubmit.getFormula(0);
            var variables;
            if (hasDstActions) {
                // When we have DST actions applies, we will use variables from all peers.
                variables = dstVariables;
            } else {
                // Without DST, we will just use the variables referenced by the formula.
                variables = this.collectVariables(formula);
            }

            this.updateDataSourcePropsOnKlip(formula);

            // submit the formula for evaluation
            // ---------------------------------
            this.submitFormula(componentToSubmit, formula, variables);
        }.bind(this));
    },

    submitFormula: function(componentToSubmit, formula, variables) {
        CxFormula.submitFormula({
            cx: componentToSubmit,
            formula: formula,
            vars: variables,
            isWorkspace: true
        });
    },

    updateDataSourcePropsOnKlip: function(formula) {
        var datasourceInstance, datasourceProps;
        if (formula && formula.datasources) {
            formula.datasources.forEach(function (datasource) {
                datasourceInstance = this.getDatasourceInstance(datasource);

                if (datasourceInstance &&
                    datasourceInstance.isDynamic &&
                    _.isArray(datasourceInstance.dynamicProps)) {
                    if (!this.activeKlip.datasourceProps) {
                        this.activeKlip.datasourceProps = [];
                    }
                    datasourceProps = this.activeKlip.datasourceProps;

                    datasourceInstance.dynamicProps.forEach(function (propName) {
                        if (datasourceProps.indexOf(propName) == -1) {
                            datasourceProps.push(propName);
                        }
                    }.bind(this));
                }
            }.bind(this));
        }
    },

    updateFormulaReferences: function(componentId) {
        if (this.activeKlip) {
            this.activeKlip.eachComponent(function(currentComponent) {
                var currentComponentFormula = currentComponent.formulas && currentComponent.getFormula(0);
                var i, currentReference;

                if (currentComponentFormula && currentComponentFormula.references) {
                    for (i = 0; i < currentComponentFormula.references.length; i++) {
                        currentReference = currentComponentFormula.references[i];
                        if (currentReference.type == "cx" && currentReference.id == componentId) {
                            this.setComponentFormula(currentComponent, currentComponentFormula.formula, currentComponentFormula.src);
                            this.updateFormulaReferences(currentComponent.id);
                            break;
                        }
                    }
                }
            }.bind(this));
        }
    },

    collectVariables: function(f) {
        var i;
        var vars = {};
        var varNames = [];
        var dsi, varName;

        if (!f) {
            return {};
        }

        // if the formala has variables: lookup each variable in our current list of uiVariables
        // and assign the value to the query object so that they can be evaluated on dpn
        $.merge(varNames, f.variables);
        if ( this.activeKlip.datasourceProps ){
            $.merge(varNames,this.activeKlip.datasourceProps);
        }

        if ( f.datasources ){
            for(i = 0 ; i < f.datasources.length; i++) {
                dsi = this.getDatasourceInstance(f.datasources[i]);
                if ( dsi.isDynamic ) {
                    $.merge(varNames,dsi.dynamicProps);
                }
            }
        }


        for(i = 0 ; i < varNames.length ; i++) {
            varName = varNames[i];
            vars[varName] = this.uiVariables[varName] ? this.uiVariables[varName].value : undefined;
        }

        return vars;
    },

	/**
	 *
	 * @param tabId
	 */
	selectWorkspaceTab: function(tabId) {
        var activeComponent, enableInput;

		if ((!KF.company.hasFeature("applied_actions")) && tabId == Workspace.PropertyTabIds.APPLIED_ACTIONS) {
            return;
        }

        this.selectedTab = tabId;

		$("#property-tabs .active").removeClass("active");
		$(".property-tab-pane").hide();

		$("#tab-" + tabId).show();
		$("#property-tabs li[tabid="+tabId+"]").addClass("active");

		switch (tabId){
			case "data":

                this.handleDataTabResize();

                PubSub.publish("data-tab-selected", ["tab"]);

                if (this.formulaEditor) {
                    this.formulaEditor.refresh();

                    PubSub.publish("formula-editor-viewed");
                }

				break;
			case "conditions":
				this.conditionsPane.updateConditionList();
				this.conditionsPane.updateProperties();
				this.conditionsPane.handleResize();
				break;
			case Workspace.PropertyTabIds.PROPERTIES:
            {
                activeComponent = this.activeComponent || this.activeKlip;
                this.updatePropertySheet(activeComponent.getPropertyModel(), activeComponent.getPropertyModelGroups(), this.componentPropertyForm);
                break;
            }
            case "layout":
                this.layoutPropertyForm.reflow();
                break;
            case "drilldown":
            {
                enableInput = $("#tab-drilldown input[name='enableDD']").data("actionArgs", this.activeComponent);

                if (this.activeComponent.drilldownEnabled) {
                    enableInput.attr("checked", "checked");
                    $("#dd-control-area").show();

                    this.drilldownControls.render(this.activeComponent);
                } else {
                    enableInput.removeAttr("checked");
                    $("#dd-control-area").hide();
                }

                break;
            }
            case Workspace.PropertyTabIds.APPLIED_ACTIONS:
                this.appliedActionsPane.render(this.activeComponent);
                break;
		}

        if (tabId == "layout") {
            if (!this.activeLayout) {
                this.setActiveLayout(this.activeComponent.parent.isPanel ? this.activeComponent.parent : false);
            }

            if (this.activeLayout && this.activeComponent.layoutConfig) {
                this.activeLayout.setActiveCell(this.activeComponent.layoutConfig.x, this.activeComponent.layoutConfig.y);
            }
        } else {
            if (this.activeLayout) this.activeLayout.setActiveCell(false);
        }

        if (tabId != "drilldown") {
            if (this.activeComponent.drilldownEnabled && this.activeComponent.drilldownStack.length > 0) {
                this.activeComponent.popDrilldownLevel();
            }
        }

		this.focusFirstPropertyInput();
	},

	/**
	 * adds a new datasource to the workspace.  when a DS is added an event is broadcast
	 * to the UI.
	 * @dsObj id
	 * @dsObj name
	 */
	addDatasourceInstance: function(dsObj, options, dataRenderedCallback) {
        var dataViz;

        // only add a datasource once
		// ---------------------------
		if (this.getDatasourceInstance(dsObj.id) ) return;

		this.datasourceInstances.push( dsObj );

		// if the datasource being added does not have a name, we should query for its
		// description
		// ---------------------------------------------------------------------------
		dataViz = this.visualizerTabPane; // local ref for nested callbacks

		if (!dsObj.name) {

			this.describeDatasourceInstances( [dsObj.id] ,function(dsid,dataDesc){
                dsObj.visualizer = Visualizer.createforFormat( dsObj.format, { visualizerId: Workspace.VISUALIZER_ID } );

                if (dsObj.visualizer) {
                    dsObj.visualizer.setDatasourceInstance(dsObj, dataRenderedCallback);
                    dataViz.addVisualizer(dsObj.visualizer, options);
                }
			});
		} else {
            dsObj.visualizer = Visualizer.createforFormat(dsObj.format, { visualizerId: Workspace.VISUALIZER_ID });
            dsObj.visualizer.setDatasourceInstance(dsObj, dataRenderedCallback);
            dataViz.addVisualizer(dsObj.visualizer, options);
            this.updateMenu();
		}

        if (this.formulaEditor) {
            this.formulaEditor.setDatasourceInstances(this.datasourceInstances);
        }
	},

    getDataSets: function() {
        return this.dataSets;
    },

    updateDataSet: function(newDataSet) {
        var i, dataset, success;
        if(this.dataSets) {
            for (i = 0; i < this.dataSets.length; i++) {
                dataset = this.dataSets[i];
                if (dataset.id === newDataSet.id) {
                    if (newDataSet.dstContext) {
                        $.extend(dataset, newDataSet);
                        dataset.dstContext = newDataSet.dstContext;
                        success =  this.activeKlip.updateDataSet({id: dataset.id, dstContext: dataset.dstContext});
                        this.activeKlip.eachComponent(function(currentComponent) {
                            if(currentComponent.isUsingDataSet(newDataSet.id)){
                                this.evaluateFormulas(currentComponent);
                            }
                        }.bind(this));

                        return !!success;
                    }
                }
            }
        }
        return false;
    },

    addDataSet: function(dataSet, options, dataRenderedCallback) {
        var _dst = new DST();

        if (this.getDataSet(dataSet.id)) {
            // Prevent duplicate DataSet adds.
            return;
        }

        _dst._getDataSetDST(dataSet.id).then(function(result) {
            dataSet.dstContext = (dataSet.dstContext) ? dataSet.dstContext: result;
            this.dataSets.push(dataSet);
            this.activeKlip.addDataSet({id: dataSet.id, dstContext: dataSet.dstContext});

            dataSet.visualizer = Visualizer.createforFormat("dataset", { visualizerId: Workspace.VISUALIZER_ID });
            return dataSet.visualizer.setDataSet(dataSet, dataRenderedCallback).then(function(fullDataSet) {
                $.extend(dataSet, fullDataSet);

                this.visualizerTabPane.addVisualizer(dataSet.visualizer, options);
                this.updateMenu();

                if (this.formulaEditor && KF.company.isModellerFeatureOn()) {
                    this.formulaEditor.setDataSetInstances(this.dataSets);
                    this.formulaEditor.convertDataSetIdsToNames();
                }

                return fullDataSet;
            }.bind(this));
        }.bind(this));
    },

	getDatasourceInstance: function(dsid) {
        var dsi;
		var len = this.datasourceInstances.length;
		while(--len != -1) {
			dsi = this.datasourceInstances[len];
			if ( dsi.id == dsid) return dsi;
		}
		return false;
	},

    getDataSet: function(dataSetId) {
        var results = this.dataSets.filter(function(dataSet) {
            return dataSet.id === dataSetId;
        });

        return results.length ? results[0] : false;
    },

    removeDatasourceInstance : function(dsid){
        var dsi = this.getDatasourceInstance(dsid);
        this.datasourceInstances.splice(_.indexOf(this.datasourceInstances, dsi), 1);
    },

    removeDataSet: function(dataSetId) {
        var dataSet = this.getDataSet(dataSetId);

        if (dataSet) {
            this.dataSets.splice(_.indexOf(this.dataSets, dataSet), 1);
        }
    },

    buildRefValues: function() {
        var klipMenuItem = { text:KF.company.get("brand", "widgetName"), reference:false , items:[] };
        var i, c;

        for (i in this.activeKlip.components) {
            c = this.activeKlip.components[i];
            if (c.visibleInWorkspace){
                klipMenuItem.items.push( c.getReferenceValueModel() );
            }
        }

        return [ klipMenuItem ];
    },

    renderVariables: function(names, variables, selectedVarName) {
        $("#variableNames").empty();

        names.forEach(function(name){
            var currentListItem = $("<li class='ellipsis'>").attr({actionId: "vf_assign_variable"}).html(name);

            currentListItem.data("actionArgs", variables[name]);
            currentListItem.attr("linkedTo", "variable-" + name);

            if (name == selectedVarName) {
                currentListItem.addClass("varSoftSelect");
            }

            if (!variables[name].isUserProperty) {
                currentListItem.append($("<span class='remove-var' actionId='remove_variable'>")
                    .attr("title", "Delete this variable from the " + KF.company.get("brand", "widgetName") + " Editor")
                    .data("actionArgs", name)
                    .append($("<img src='/images/list-delete.png' parentIsAction='true'>")));
            }

            $("#variableNames").append(currentListItem);
        });
    },

    setSelectableReference: function(selectionState, cxId) {
        var menuItem = $("#data-reference_values li[linkedTo=\"component-"+cxId+"\"]");

        if (selectionState) {
            menuItem.removeClass("noReference");
        } else {
            menuItem.addClass("noReference");
        }
    },
	applyAllMigrations: function(config, msgs) {
		var resolvedSchema = this._applyMigrations(config, msgs, Workspace.PreMigrations);
		resolvedSchema = this._applyMigrations(resolvedSchema, msgs, Workspace.Migrations);
		return resolvedSchema;
	},

    _applyMigrations: function(config, msgs, migrations) {
        var i, j;
        for (j = 0; j < migrations.length; j++) {
            if (migrations[j].applyTo(config)) {
                if (migrations[j].notifyUser(config)) {
                    // Make sure we don't have duplicated messages.
                    if (msgs.indexOf(migrations[j].description) == -1) {
                        msgs.push(migrations[j].description);
                    }
                }
                config = migrations[j].migrate(config);
            }
        }

        if (config.components) {
            for (i = 0; i < config.components.length; i++) {
                for (j = 0; j < migrations.length; j++) {
                    if (migrations[j].applyTo(config.components[i])) {
                        if (migrations[j].notifyUser(config)) {
                            // Make sure we don't have duplicated messages.
                            if (msgs.indexOf(migrations[j].description) == -1) {
                                msgs.push(migrations[j].description);
                            }
                        }
                        config.components[i] = migrations[j].migrate(config.components[i]);
                    }
                }

                this._applyMigrations(config.components[i], msgs, migrations);
            }
        }
        return config;
    },

	/**
	 * Function to resize entire workspace. Called on page load, window resize, etc
	 * @param treeWidth -->optional if set use this width for the tree view container otherwise use a percentage
	 * @param propHeight -->optional if set use this height for the property container otherwise use a percentage
	 */
    setWorkspaceDimensions: function(treeWidth, propHeight) {
        var $root = $("#c-root"),
            $mainDiv = $(".panel-grey"),
            $workspace = $("#workspace"),
            $propertyPane = $(".property-tab-pane"),
            preferredDimensions = $.cookie("resizeDims", { path:"/" }),
            availableHeight,
            availableWidth,
            wsHeight,
            maxPropHeight,
            newPropHeight,
            preferredHeight;

        // set height on container elements. If propHeight is not given use maximum possible space
        availableHeight = $(window).height() - $("#c-footer").outerHeight(true) - $("#panel-button-div").outerHeight(true);
        wsHeight = availableHeight - ($workspace.outerHeight(true) - $workspace.height()) /* workspace vertical MBP */;
        $workspace.height(wsHeight);

        // set width on container elements. If treeWidth is not given use maximum possible space.
        availableWidth = $(window).width() - ($root.outerWidth(true) - $root.width()) /* root MBP */ - ($mainDiv.outerWidth(true) - $mainDiv.width()) /* main div MBP */;
        $mainDiv.width(availableWidth);
        $workspace.width(availableWidth - ($workspace.outerWidth(true) - $workspace.width())) /* workspace horizontal MBP */;
        $propertyPane.width($workspace.width() - ($propertyPane.outerWidth(true) - $propertyPane.width()) /* property pane MBP */);
        $("#data-resize").width($propertyPane.width());

        //Get the preferred height from the cookie (set the last time someone resized manually)
        preferredHeight = propHeight;
        if (preferredDimensions) {
            preferredHeight = parseInt(preferredDimensions.substr(preferredDimensions.indexOf("-") + 1)); //Take the number after the dash
        }

        maxPropHeight = Math.max(this.MIN_PROPERTY_PANE_HEIGHT /* minimum manually resize amount */, wsHeight * this.MAX_PROPERTY_PANE_HEIGHT_MULT);
        newPropHeight = preferredHeight ? Math.min(preferredHeight, maxPropHeight)  : maxPropHeight;  //Don't let the property pane be larger than the max after a window resize

        this.setHeight(newPropHeight);
        this.setWidth(treeWidth ? treeWidth : $workspace.width() * 0.2);
    },

    /**
     * Resize property tabs and their children elements
     * @param propHeight
     */
    setHeight: function(propHeight) {
        var $workspace = $("#workspace"),
            $treeContainer = $("#treeMenuContainer"),
            $propertyTabs = $(".property-tabs"),
            $propertyPanes = $(".property-tab-pane"), // get the tabs
            $currentPane = $(".property-tab-pane:visible"); // get the active tab

        var $openPalette, paletteTitleHeight;

        var availableHeight = $workspace.height() - propHeight;
        availableHeight -= $propertyTabs.outerHeight(true);

        // adjust the height of the top region to match the remaining height
        $treeContainer.height(availableHeight);
        $("#klipPreviewContainer").height(availableHeight);
        $("#paletteContainer").height(availableHeight);
        $("#topRegion").height(availableHeight);

        // check if any element needs a scrollbar
        customScrollbar($("#c-workspace_menu"), $treeContainer, "both");

        if (this.paletteOpen) {
            $openPalette = $("#" + this.paletteOpen + "-palette");
            paletteTitleHeight = $openPalette.find(".palette-title").outerHeight(true);
            $openPalette.find(".palette-item-container").height(availableHeight - paletteTitleHeight);
            customScrollbar($openPalette.find(".palette-items"), $openPalette.find(".palette-item-container"), "v");
        }

        // re-position klip
        this.handleKlipSize();

        // set height on the resizable property panes
        $propertyPanes.css("height", propHeight - ($currentPane.outerHeight(true) - $currentPane.height()) /* current pane MBP */);

        // set the max height of the property panes
        $propertyPanes.resizable("option", "maxHeight", $workspace.height() - $propertyTabs.outerHeight(true));

        this.handleDataTabResize();

        // let other elements+children know of the resize so they can adjust as necessary
        PubSub.publish("workspace-resize", ["height", propHeight]);

    },

	/**
	 * Resize property tabs and their children elements
	 * @param propHeight
	 */
	heightResize: function(propHeight) {
        var $treeContainer = $("#treeMenuContainer"),
            $currentPane = $(".property-tab-pane:visible"); // get the active tab

		this.setHeight(propHeight);

		// record new "preferred" dimensions
		$.cookie("resizeDims", $treeContainer.outerWidth()+"-"+$currentPane.outerHeight(true), { path:"/" });
	},

    /**
     * Resize the klip preview area
     * @param treeWidth
     */
    setWidth: function(treeWidth) {
        var $treeContainer = $("#treeMenuContainer"),
            $previewContainer = $("#klipPreviewContainer");

        // set width of containers
        $treeContainer.width(treeWidth - ($treeContainer.outerWidth() - $treeContainer.width()));
        $previewContainer.width($("#workspace").width() - ($treeContainer.outerWidth(true) + $("#paletteContainer").outerWidth(true)) - 5 /* extra space */);

        // check if any element needs a scrollbar
        customScrollbar($("#c-workspace_menu"), $treeContainer, "both");
        Workspace.previewScrollAPI = customScrollbar($("#klip-preview"), $previewContainer, "both");

        // let any other elements, that need to resize, know about the width change
        PubSub.publish("workspace-resize", ["width", treeWidth]);
    },

	/**
	 * Resize the klip preview area
	 * @param treeWidth
	 */
	widthResize: function(treeWidth) {
        var $treeContainer = $("#treeMenuContainer");

        this.setWidth(treeWidth);

		// record new dimensions
        $.cookie("resizeDims", $treeContainer.outerWidth()+"-"+$(".property-tab-pane:visible").outerHeight(true), { path:"/" });
	},

    handleDataTabResize: function() {
        var $dataTab = $("#tab-data");
        var minHeight = this.MIN_DATA_RESIZE_HEIGHT; // after a property panel resize the resizable area in the data tab needs to be updated
        var $formulaBar = this.getFormulaBarTextNode();

        if ($dataTab.is(":visible") && $formulaBar.outerHeight() + minHeight > $dataTab.height()) {
            this.dataTabResize(minHeight);
        } else {
            this.dataTabResize($dataTab.height() - $formulaBar.height());
        }
    },

	/**
	 * Resize the data tab contents including the formula bar and visualizer
	 * @param newHeight
	 */
	dataTabResize: function(newHeight) {
        var MIN_FORMULA_BAR_HEIGHT_PX = 26;
        var MIN_FORMULA_EDITOR_HEIGHT_PX = 32;
        var $formulaBar = this.getFormulaBarTextNode();
        var formulaBarMBP = $formulaBar.outerHeight(true) - $formulaBar.height();
        var formulaBarHeight = $("#tab-data").height() - newHeight;
        var minHeight = this.formulaEditor ? MIN_FORMULA_EDITOR_HEIGHT_PX : MIN_FORMULA_BAR_HEIGHT_PX;

		if (formulaBarHeight > minHeight) { // formula bar must be greater than one bar height
            $formulaBar.height(formulaBarHeight);
		} else {
            $formulaBar.height(minHeight);
			newHeight -= (minHeight - formulaBarHeight);
		}
        newHeight -= formulaBarMBP;

        $("#data-resize").height(newHeight);
        this.resizeDataPanels(); // update data panel, literal panel, etc
	},

	/*
	 Helper function to automatically size the formula bar, reacts to new nodes, etc
	 */
	updateFormulaSize: function() {
        var $formulaBar = this.getFormulaBarNode();

		this.dataTabResize($("#tab-data").height() - $formulaBar.height());
	},

	/*
	   Helper function to keep klip positioned, reacts to klip size changes, etc
	 */
	handleKlipSize: function() {
        var $preview = $("#klip-preview"),
            $previewContainer = $("#klipPreviewContainer");

        if (!Workspace.isDragging) {
            // keep klip as vertically centered as possible using padding
            $preview.css("padding-top", "");
            if ($preview.outerHeight(true) < $previewContainer.height()) {
                $preview.css("padding-top", ($previewContainer.height() - $preview.outerHeight(true)) / 2);
            }
        }

		// check if a scroll bar is needed
		Workspace.previewScrollAPI = customScrollbar($preview, $previewContainer, "both");

        if ($preview.find(".c-dashboard").outerWidth(true) < $previewContainer.width() && Workspace.previewScrollAPI) {
            $previewContainer.find(".jspPane").css("left", "");
        }
    },

	/*
	   Helper function to resize all data tab panels
	 */
	resizeDataPanels: function() {
        var $formulaBar = this.getFormulaBarNode();
        var panelHeight = $("#tab-data").height() - $formulaBar.outerHeight(true);

        this.dataResize(panelHeight);

        if (!this.formulaEditor) {
            this.literalResize(panelHeight);
            this.optionResize(panelHeight);
            this.functionResize(panelHeight);
            this.exprResize(panelHeight);
            this.referenceResize(panelHeight);
            this.variableResize(panelHeight);
        }
	},

    handleDataPanelResize: function(panelType) {
        var $formulaBar = this.getFormulaBarNode();
        var panelHeight = $("#tab-data").height() - $formulaBar.outerHeight(true);

        this[panelType + "Resize"](panelHeight);
    },

    dataResize: function(panelHeight) {
        this.visualizerTabPane.checkTabWidths();
        this.visualizerTabPane.handleResize(panelHeight); // update visualizer
    },

	literalResize: function(panelHeight) {
		var $literalMenu = $("#literal-menu");
        var lel = $literalMenu.find(".panel-section");

        var literalMenuMBP, lelMBP;

		if (lel.is(":hidden")) return;

        literalMenuMBP = $literalMenu.outerHeight(true) - $literalMenu.height();
        lelMBP = lel.outerHeight(true) - lel.height();

		lel.height(panelHeight - literalMenuMBP - lelMBP);
	},

    optionResize: function(panelHeight) {
        var $optionMenu = $("#option-menu");
        var oel = $optionMenu.find(".panel-section");

        var optionMenuMBP, oelMBP, oelHeight;

        if (oel.is(":hidden")) return;

        optionMenuMBP = $optionMenu.outerHeight(true) - $optionMenu.height();
        oelMBP = oel.outerHeight(true) - oel.height();

        oel.height(panelHeight - optionMenuMBP - oelMBP);

        oelHeight = oel.height() - oel.find(".panel-header").outerHeight(true);
        $(".resize-content").height(oelHeight);
    },

	functionResize: function(panelHeight) {
		var $functionMenu = $("#function-menu");
        var fel = $functionMenu.find(".panel-section");

        var functionMenuMBP, felMBP, felHeight;

		if (fel.is(":hidden")) {
            return;
        }

        functionMenuMBP = $functionMenu.outerHeight(true) - $functionMenu.height();
        felMBP = fel.outerHeight(true) - fel.height();

		fel.height(panelHeight - functionMenuMBP - felMBP);

		felHeight = fel.height() - fel.find(".panel-header").outerHeight(true);
		$(".resize-content").height(felHeight);
	},

    referenceResize: function(panelHeight) {
        var $referenceMenu = $("#reference-menu");
        var rel = $referenceMenu.find(".panel-section");

        var referenceMenuMBP, relMBP, relHeight;

        if (rel.is(":hidden")) return;

        referenceMenuMBP = $referenceMenu.outerHeight(true) - $referenceMenu.height();
        relMBP = rel.outerHeight(true) - rel.height();

        rel.height(panelHeight - referenceMenuMBP - relMBP);

        relHeight = rel.height() - rel.find(".panel-header").outerHeight(true);
        $(".resize-content").height(relHeight);
    },

    exprResize: function(panelHeight) {
        var $expressionMenu = $("#expression-menu");
        var eel = $expressionMenu.find(".panel-section");

        var expressionMenuMBP, eelMBP;

        if (eel.is(":hidden")) return;

        expressionMenuMBP = $expressionMenu.outerHeight(true) - $expressionMenu.height();
        eelMBP = eel.outerHeight(true) - eel.height();

        eel.height(panelHeight - expressionMenuMBP - eelMBP);
    },

    variableResize: function(panelHeight) {
        var variableMenuMBP, velMBP, velHeight;

        var $variableMenu = $("#variable-menu");
        var vel = $variableMenu.find(".panel-section");

        if (vel.is(":hidden")) return;

        variableMenuMBP = $variableMenu.outerHeight(true) - $variableMenu.height();
        velMBP = vel.outerHeight(true) - vel.height();

        vel.height(panelHeight - variableMenuMBP - velMBP);

        velHeight = vel.height() - vel.find(".panel-header").outerHeight(true);
        $(".resize-content").height(velHeight);
    },

	onWindowResize: function(evt) {
        // only want the browser level resizes which correspond to window in everything but ie 8
        if (evt && evt.target && evt.target != window && evt.target != document) return;

        this.setWorkspaceDimensions($("#treeMenuContainer").outerWidth(), $(".property-tab-pane:visible").outerHeight(true));

        this.resizeMessage();
	},

	/*
	 Helper function to focus the first visible input field on the property tab
	 */
	focusFirstPropertyInput: function() {
		var firstPropertyInput = $("#tab-properties").find("input:first:visible");
		if(firstPropertyInput) firstPropertyInput.focus().select();
	},

    getFormulaBarNode: function() {
        return this.formulaEditor && $(this.formulaEditor.getWrapperDOMNode());
    },

    getFormulaBarTextNode: function() {
        return this.formulaEditor && $(this.formulaEditor.getTextWrapperDOMNode());
    },

    getPossibleComponentReferencesModel: function(canReferToDstPeers, rootComponent, validReferencesMap) {
        var currentComponent = rootComponent || this.activeKlip;
        var childComponents = currentComponent.components;
        var model, possibleRef, possibleResultsRef, invalidRefToDstPeer, invalidRefToDstRoot, dependent;

        if(!validReferencesMap) validReferencesMap = {};

        if (rootComponent) {
            possibleRef = false;
            possibleResultsRef = false;
            invalidRefToDstPeer = false;
            invalidRefToDstRoot = false;
            if (currentComponent!==this.activeComponent) {
                possibleRef = !this.isReferringToActiveComponent(currentComponent, validReferencesMap);
                if (possibleRef) {
                    if (canReferToDstPeers) {
                        possibleResultsRef = true;
                    } else {
                        //The active component is grouped. It cannot have a result reference to any DST peers
                        //It also cannot have a formula reference to a result reference of a DST peer
                        possibleResultsRef = !this.activeComponent.isInSameDSTAs(currentComponent);
                        invalidRefToDstPeer = !possibleResultsRef;
                        possibleRef = !this.isFormulaRefToResultsRefOfActiveDSTRoot(currentComponent);
                    }
                    invalidRefToDstRoot = this.isReferringToActiveDSTRoot(currentComponent);
                    possibleResultsRef = possibleResultsRef && !invalidRefToDstRoot;
                    dependent = currentComponent.isDependentComponent();
                    possibleRef = (possibleRef && (!dependent || !invalidRefToDstRoot));
                }
            }

            model = {
                component: currentComponent,
                isPossibleFormulaReference: possibleRef,
                isPossibleResultsReference: possibleResultsRef,
                invalidRefToDstPeer: invalidRefToDstPeer,
                invalidRefToDstRoot: invalidRefToDstRoot
            };
        } else {
            model = {
                component: null,
                isPossibleFormulaReference: false,
                isPossibleResultsReference: false,
                invalidRefToDstPeer: false,
                invalidRefToDstRoot: false
            };
        }


        if (childComponents && childComponents.length > 0) {
            model.childComponents = [];
            childComponents.forEach(function(childComponent, index) {
                var childModel;

                if ((childComponent !== this.activeComponent && childComponent.visibleInWorkspace) ||
                    (childComponent == this.activeComponent && childComponent.canHaveDataSlots)) {

                    childModel = this.getPossibleComponentReferencesModel(canReferToDstPeers, childComponent, validReferencesMap);
                    model.childComponents.push(childModel);
                }
            }, this);
        }

        return model;
    },

    isFormulaRefToResultsRefOfActiveDSTRoot: function(component) {
        var i, comp;

        for (i in component._allDependencies) {
            comp = this.activeKlip.getComponentByVCId(i);
            if (this.activeComponent.isInSameDSTAs(comp)) {
                return true;
            }
        }
        return false;
    },

    isReferringToActiveDSTRoot: function(component) {
        var dstRoot, activeDstRoot;
        if (!component.usePostDSTReferences()) {
            return false;
        }

        dstRoot = component.getDstRootComponent();
        activeDstRoot = this.activeComponent.getDstRootComponent();

        if (this.activeKlip && dstRoot && activeDstRoot) {
            return this.activeKlip.doesDstRootDependOnOtherRoot(activeDstRoot.id, dstRoot.id);
        }
        return false;
    },


    isReferringToActiveComponent: function(component, validReferencesMap) {
        var refersToActiveComponent = false;
        var componentFormula = component && component.getFormula && component.getFormula(0);
        var componentFormulaReferences = null;
        if (componentFormula) {
            componentFormulaReferences = componentFormula.getFormulaReferencesArray().concat(componentFormula.getResultsReferencesArray());
        }
        if (componentFormulaReferences) {
            refersToActiveComponent = componentFormulaReferences.some(function(reference) {
                var refersDirectlyToActive = reference.id === this.activeComponent.id;
                var refersIndirectlyToActive, referenceComponent;
                var refersToActive;

                if(validReferencesMap[reference.id]) {
                    return false;
                }

                if (!refersDirectlyToActive) {
                    referenceComponent = this.activeKlip.getComponentById(reference.id);
                    refersIndirectlyToActive = this.isReferringToActiveComponent(referenceComponent, validReferencesMap);
                }

                refersToActive = refersDirectlyToActive || refersIndirectlyToActive;

                if(!refersToActive) {
                    validReferencesMap[reference.id] = true;
                }

                return refersToActive;
            }, this);
        }

        return refersToActiveComponent;
    },

    hasDataProviders: function() {
        var hasDatasources = this.datasourceInstances && this.datasourceInstances.length > 0;
        var hasDataSets = this.dataSets && this.dataSets.length > 0;

        return hasDatasources || hasDataSets;
    }
});

Workspace.EventNames = {
    GROUP_EVENT: "Group in Editor",
    ACTION_ROOT_EVENT: "Action root event in Editor",
    AGGREGATE_EVENT: "Group in Editor",
    SORT_EVENT: "Sort in Editor",
    FILTER_EVENT: "Filter in Editor",
    AGGREGATION_EVENT: "Aggregate in Editor",
    APPLIED_ACTION_EVENT: "Applied Actions in Editor"
};

Workspace.PropertyTabIds = {
    PROPERTIES: "properties",
    APPLIED_ACTIONS: "applied-actions"
};

Workspace.VISUALIZER_ID = "workspace";

Workspace.Yes = function() {
    return true;
};

Workspace.No = function() {
    return false;
};

Workspace.eachComponentFromConfig = function (config, callback) {
    var i;
    var breakSignal = false;
    for (i = 0; config.components && i < config.components.length; i++) {
        breakSignal = callback(config.components[i], config);
        if (breakSignal) {
            break;
        }
        Workspace.eachComponentFromConfig(config.components[i], callback);
    }
};

Workspace.NotifyUserPostDST = function(klipConfig) {
    var foundFrah = false;
    Workspace.eachComponentFromConfig(klipConfig, function(comp) {
        if (comp.firstRowIsHeader && comp.firstRowIsHeader === true) {
            foundFrah  = true;
            return true;
        }
        return false;
    });
    return foundFrah;

};
/**
 * The date migrations need to be applied before any other migrations
 * The reason is that the first migration relies on the saved data in each component to do the conversion
 * As soon as a component is created from a config, that data is cleared out (to prepare for the newly evaluated data)
 * The date filter migration needs to happen while the fmt is still "dat", so it needs to happen before as well.
 */
Workspace.PreMigrations = [
    {
        description: "<span class='strong' style='color:#3BA150'>NEW!</span> This Klip has been updated to use new Date/Time formats. You may notice some changes. <span class='link' onclick='help.toggle(\"klips/date-migration\");'>More information...</span><br/><br/><span>Note: If data is missing from your Klips, try formatting your data as Text.</span>",
        notifyUser: Workspace.Yes,
        applyTo: function(cxConfig) {
            // Only migrated when format type is "dat" and the new date format feature is turned on.
            return cxConfig.fmt == FMT.type_dat && KF.company.isNewDateFormatFeatureOn();
        },
        migrate: function(cxConfig) {
            var dateInputFormat = this.getFmtArgs(cxConfig, "dateInputFormat");
            var dateInputFormatCustom = this.getFmtArgs(cxConfig, "dateInputFormatCustom");
            var dateOutputFormat = this.getFmtArgs(cxConfig, "dateFormat");
            var dateOutputFormatCustom = this.getFmtArgs(cxConfig, "dateFormatCustom");
            var firstIntValue = this.getFirstIntegerValue(cxConfig);
            var hasAutoFmt = cxConfig.autoFmt;
            var skipMigratingOutputCustom = false;

            if (this.isOldAutoDetect(hasAutoFmt, dateInputFormat, dateInputFormatCustom)) {
                // If we are using the old auto detect feature (explicitly or implicitly, we will migrate to epoch-ms
                // if the data contains integer value, or migrate to the new Automatically set format option.
                if (firstIntValue) {
                    this.setFmtArgs(cxConfig, "dateInputFormat", "epoch-ms");
                    cxConfig.autoFmt = false;
                } else {
                    cxConfig.autoFmt = true;
                    if (cxConfig.fmtArgs && cxConfig.fmtArgs.dateInputFormat == "default") {
                        delete cxConfig.fmtArgs.dateInputFormat;
                    }
                }
            }
            if (dateInputFormatCustom) {
                // For any custom input format, we will migrate the format string from Date.js to Java.
                this.setFmtArgs(cxConfig, "dateInputFormatCustom", FMT.convertDateJsFormatToJava(dateInputFormatCustom));
            }
            if (this.isDefaultOutputFormat(hasAutoFmt, dateOutputFormat, dateOutputFormatCustom)) {
                // when dateOutputFormat is empty string or when autoFmt=false and dateOutputFormat is undefined, we need to reset to default.
                this.setFmtArgs(cxConfig, "dateFormat", Props.Defaults.dateFormatJava[Props.Defaults.dateFormatDefaultIndex].value);
                this.setFmtArgs(cxConfig, "dateFormatCustom", Props.Defaults.dateFormatJava[Props.Defaults.dateFormatDefaultIndex].value);
                skipMigratingOutputCustom = true;
            } else if (dateOutputFormat && dateOutputFormat != FMT.DATE_INPUT_FORMAT_CUSTOM) {
                // For any date output format that's not custom, we will migrate the format type from Date.js to Java.
                this.setFmtArgs(cxConfig, "dateFormat", FMT.convertDateJsFormatToJava(dateOutputFormat));
                if (!dateOutputFormatCustom || dateOutputFormatCustom == "") {
                    this.setFmtArgs(cxConfig, "dateFormatCustom", FMT.convertDateJsFormatToJava(dateOutputFormat));
                    skipMigratingOutputCustom = true;
                }
            }
            if (!skipMigratingOutputCustom && dateOutputFormatCustom && dateOutputFormatCustom != "") {
                // For any non-empty custom output format, we will migrate the format string from Date.js to Java.
                this.setFmtArgs(cxConfig, "dateFormatCustom", FMT.convertDateJsFormatToJava(dateOutputFormatCustom));
            }
            cxConfig.fmt = FMT.type_dat2;
            return cxConfig;
        },
        isDefaultOutputFormat: function(hasAutoFmt, outputFormat, outputFormatCustom) {
            // The logic to decide default date output format is different between auto detect on and off due to the override.
            // this logic follows the current behavior in the application.
            if (!hasAutoFmt) {
                if (!outputFormat || outputFormat == "") {
                    // When auto detect if off, when output format is "" or undefined, use default.
                    return true;
                }
                if (outputFormat == "custom" && (!outputFormatCustom || outputFormatCustom == "")) {
                    // when output format is custom and custom format is "", use default.
                    return true;
                }
            } else {
                // When auto detection is on, and either outputFormat or outputFormatCustom is "" which override the auto detect value
                // We will have to fall back to default output format.
                if (outputFormat == "" || outputFormatCustom == "") {
                    return true;
                }
            }
            return false;
        },
        getFirstIntegerValue: function(cxConfig) {
            if (cxConfig && cxConfig.data && cxConfig.data.length > 0) {
                return  FMT.getFirstIntegerValue(cxConfig.data[0]);
            }
            return false;
        },
        isOldAutoDetect: function(hasAutoFmt, dateInputFormat, dateInputFormatCustom) {
            if (hasAutoFmt && dateInputFormat != "default" && dateInputFormat != "") {
                // When autoFmt is on and the dateInputFormat is NOT specifically set to default, it will NOT be the old auto detect.
                return false;
            }
            if (!dateInputFormat || dateInputFormat == "default") {
                // If we don't set a input format type or the type is default, we are using the old auto detect.
                return true;
            }
            if (dateInputFormat == "custom" && (!dateInputFormatCustom || dateInputFormatCustom == "")) {
                // Another possibility is we are using the custom input format type but the custom format string is empty or non-exist.
                return true;
            }
            // All other situation, we are using explicitly defined input format, either GA or custom format string.
            return false;
        },
        getFmtArgs: function(cxConfig, name) {
            if (cxConfig && cxConfig.fmtArgs) {
                return cxConfig.fmtArgs[name];
            }
            return null;
        },
        setFmtArgs: function(cxConfig, name, value) {
            if (cxConfig && name && value) {
                if (!cxConfig.fmtArgs) {
                    cxConfig.fmtArgs = {};
                }
                cxConfig.fmtArgs[name] = value;
            }
        }
    },
    {
        description: "Hide series which are all nulls or zeros",
        notifyUser: Workspace.No,
        applyTo : function(config)  {
            var i, shouldMigrate, curCx, shouldApply;
            if(!config.type) {// If config is a klip schema
                shouldMigrate = config.appliedMigrations && !config.appliedMigrations["post_dst"] && !config.appliedMigrations["hideNullSeries"];
                if (shouldMigrate) {
                    Workspace.eachComponentFromConfig(config, function(chartCx) {
                        if (chartCx.type === "chart_series" || chartCx.type === "chart_scatter") {
                            for (i = 0; i < chartCx.components.length; i++) {
                                if(chartCx.components[i].fmt === FMT.type_dat2) { // Klips which had the date migration already applied will not be migrated
                                    shouldApply = false;
                                    return false;
                                }

                                if (chartCx.components[i].type === "scatter_data") {
                                    curCx = chartCx.components[i]; // set curCx to scatter_data
                                } else {
                                    curCx = chartCx; // set curCx to chart root
                                }
                                if (curCx.components[i].data.length > 0 &&
                                    curCx.components[i].data[0].length === 0 &&
                                    curCx.components[i].type === "series_data"
                                ) {
                                    shouldApply = true;
                                    return true;
                                }
                            }
                        }
                    });
                }
            }
            return !!shouldApply;
        },
        migrate : function(klipConfig) {
            var i, curCx;
            Workspace.eachComponentFromConfig(klipConfig, function(chartCx) {
                if (chartCx.type === "chart_series" || chartCx.type === "chart_scatter") {
                    for (i = 0; i < chartCx.components.length; i++) {
                        if(chartCx.components[i].fmt === FMT.type_dat2) { // Klips which had the date migration already applied will not be migrated
                            return false;
                        }

                        if (chartCx.components[i].type === "scatter_data") {
                            curCx = chartCx.components[i]; // set curCx to scatter_data
                        } else {
                            curCx = chartCx; // set curCx to chart root
                        }
                        if (curCx.components[i].data.length > 0 &&
                            curCx.components[i].data[0].length === 0 &&
                            curCx.components[i].type === "series_data"
                        ) {
                            chartCx.hideNullSeries = true;
                        }
                    }
                }
            });
            if(!klipConfig.appliedMigrations) {
                klipConfig.appliedMigrations = {};
            }
            klipConfig.appliedMigrations.hideNullSeries = true;
            return klipConfig;
        }
    },
    {
        // Migrate DST filters on pre-formatted date values.
        notifyUser: Workspace.No,
        applyTo: function(cxConfig) {
            // Only migrated when format type is "dat" and the new date format feature is turned on.
            return KF.company.isNewDateFormatFeatureOn() && cxConfig.dstContext && cxConfig.dstContext.ops;
        },
        migrate : function(cxConfig) {
            var comp = KlipFactory.instance.createComponent(cxConfig);
            var i, op, cx, aggregation;
            var groupingIndex = -1;
            var filterAfterGrouping = [];
            var afterGrouping = false;
            var ops = cxConfig.dstContext.ops || {};
            var aggs = cxConfig.dstContext.aggs || {};
            for (i = 0; i < ops.length; i++) {
                op = ops[i];
                // note: component with aggregate option doesn't support filter so we don't need to handle it here.
                if (op.type === "group") {
                    afterGrouping = true;
                    groupingIndex = i;
                }
                // For date, we only need to migrate member and condition filter.
                if (op.type === "filter" && (op.variation === "member" || op.variation === "condition")) {
                    cx = comp.getDescendantByVCId(op.filterBy);
                    aggregation = aggs[op.filterBy];

                    if (cx && cx.fmt === FMT.type_dat) {
                        // We will apply migration for filter before grouping. If filter is after grouping, only migrate for supported aggregation type.
                        if (!afterGrouping || this.supportedAggregations(aggregation)) {
                            // Set the useSourceData flag so DPN knows how to filter on source value.
                            op.useSourceData = true;
                            if (afterGrouping) {
                                // Save the filters after grouping and remove it from the current operations list. We need to insert the filter before grouping.
                                filterAfterGrouping.push(op);
                                ops.splice(i, 1);
                            }
                        }
                    }
                }
            }
            if (groupingIndex >= 0 && filterAfterGrouping.length > 0) {
                ops.splice.apply(ops, [groupingIndex, 0].concat(filterAfterGrouping));
            }
            return cxConfig;
        },
        supportedAggregations : function(aggregation) {
            // We only need to migrate if the we have a filter on a non-aggregated date component or aggregated with first or last aggregation type.
            return aggregation === null || aggregation === undefined || aggregation === "first" || aggregation === "last";
        }
    }
];

Workspace.Migrations = [
    {
        /**
         * A description of what the migration did
         */
        description: "Your upgraded gauge can now be vertical or semi-circular and use indicators.",

        /**
         * true if the user should be notified about the migration; false, otherwise
         */
        notifyUser: Workspace.Yes,

        /**
         * @param cxConfig - component config
         * @return {Boolean} - true if should apply the associated migration, false otherwise
         */
        applyTo : function(cxConfig) {
            return cxConfig.type == "gauge_bar";
        },

        /**
         * @param cxConfig - component config
         * @return {*} - the new component config
         */
        migrate : function(cxConfig) {
            var gauge_bar = KlipFactory.instance.createComponent(cxConfig);
            var gauge = KlipFactory.instance.createComponent({type:"gauge"});

            // transfer data
            var gb_current = gauge_bar.getChildByRole("current");
            var g_current = gauge.getChildByRole("current");

            var gb_target, g_target, gb_min, g_min, gb_max, g_max;

            g_current.fmt = gb_current.fmt;
            g_current.fmtArgs = gb_current.fmtArgs;
            g_current.conditions = gb_current.conditions;
            g_current.formulas = gb_current.formulas;
            g_current.data = gb_current.data;
            g_current.font_style = gb_current.font_style;
            g_current.font_colour = gb_current.font_colour;

            gb_target = gauge_bar.getChildByRole("target");
            g_target = gauge.getChildByRole("target");

            g_target.fmt = gb_target.fmt;
            g_target.fmtArgs = gb_target.fmtArgs;
            g_target.conditions = gb_target.conditions;
            g_target.formulas = gb_target.formulas;
            g_target.data = gb_target.data;
            g_target.font_style = gb_target.font_style;
            g_target.font_colour = gb_target.font_colour;

            gb_min = gauge_bar.getChildByRole("min");
            g_min = gauge.getChildByRole("min");

            g_min.formulas = gb_min.formulas;
            g_min.data = gb_min.data;
            g_min.show_value = false;

            gb_max = gauge_bar.getChildByRole("max");
            g_max = gauge.getChildByRole("max");

            g_max.formulas = gb_max.formulas;
            g_max.data = gb_max.data;
            g_max.show_value = false;


            // handle gauge type
            switch (gauge_bar.gaugeType) {
                // nothing
                case "s": {
                    break;
                }
                // current > target -> green, current < target -> red, otherwise nothing
                case "ou": {
                    gauge.conditions = [
                        {
                            predicates: [{
                                left: {cx: g_current.id},
                                type: "gt",
                                right: {cx: g_target.id}
                            }],
                            reactions: [{
                                type: "color",
                                value: "#71B37C"
                            }]
                        },
                        {
                            predicates: [{
                                left: {cx: g_current.id},
                                type: "lt",
                                right: {cx: g_target.id}
                            }],
                            reactions: [{
                                type: "color",
                                value: "#E14D57"
                            }]
                        }
                    ];
                    break;
                }
                // current > target -> green, otherwise nothing
                case "oo": {
                    gauge.conditions = [
                        {
                            predicates: [{
                                left: {cx: g_current.id},
                                type: "gt",
                                right: {cx: g_target.id}
                            }],
                            reactions: [{
                                type: "color",
                                value: "#71B37C"
                            }]
                        }
                    ];
                    break;
                }
                // current < target -> red, otherwise nothing
                case "uo": {
                    gauge.conditions = [
                        {
                            predicates: [{
                                left: {cx: g_current.id},
                                type: "lt",
                                right: {cx: g_target.id}
                            }],
                            reactions: [{
                                type: "color",
                                value: "#E14D57"
                            }]
                        }
                    ];
                    break;
                }
            }

            return gauge.serialize();
        }
    },
    {
        /* This migration must come before the "Delete threshold properties" migration */
        description: "Your property thresholds have been migrated to indicators.",
        notifyUser: Workspace.Yes,
        applyTo : function(cxConfig) {
            if (cxConfig.fmtArgs) {
                return cxConfig.fmtArgs.threshold == "active";
            }

            return false;
        },
        migrate : function(cxConfig) {
            var cx = KlipFactory.instance.createComponent(cxConfig);

            if (cx.fmtArgs.threshold == "active") {
                cx.conditions = [
                    {
                        predicates: [{
                            left: {"self": 1},
                            type: cx.fmtArgs.thresholdCondition.replace("_", ""),
                            right: {"raw": cx.fmtArgs.thresholdvalue.toString()}
                        }],
                        reactions: [{
                            type: "color",
                            value: cx.fmtArgs.thresholdcolour
                        }]
                    }
                ];
            }

            delete cx.fmtArgs["threshold"];
            delete cx.fmtArgs["thresholdCondition"];
            delete cx.fmtArgs["thresholdvalue"];
            delete cx.fmtArgs["thresholdcolour"];

            return cx.serialize();
        }
    },
    {
        description: "Delete threshold properties",
        notifyUser: Workspace.No,
        applyTo : function(cxConfig) {
            if (cxConfig.fmtArgs) {
                return cxConfig.fmtArgs.threshold
                    || cxConfig.fmtArgs.thresholdCondition
                    || cxConfig.fmtArgs.thresholdvalue
                    || cxConfig.fmtArgs.thresholdcolour;
            }

            return false;
        },
        migrate : function(cxConfig) {
            var cx = KlipFactory.instance.createComponent(cxConfig);

            delete cx.fmtArgs["threshold"];
            delete cx.fmtArgs["thresholdCondition"];
            delete cx.fmtArgs["thresholdvalue"];
            delete cx.fmtArgs["thresholdcolour"];

            return cx.serialize();
        }
    },
	{
		/* Migrate datasources from forumlas to klips */
		description: "Adding datasources to klip workspace property",
		notifyUser: Workspace.No,
		applyTo : function(cxConfig) {
            return (!cxConfig.workspace || !cxConfig.workspace.datasources) && !cxConfig.type;
		},
		migrate : function(cxConfig) {
			var cx = KlipFactory.instance.createKlip(dashboard,cxConfig);
			var dsList = cx.getDatasources();
            var i;

			if(!cxConfig.workspace) {
                cxConfig.workspace = {};
            }

			cxConfig.workspace.datasources = [];

			for(i = 0; i<dsList.length; i++){
				if(_.indexOf(cxConfig.workspace.datasources,dsList[i])==-1)
					cxConfig.workspace.datasources.push(dsList[i]);
			}

			return cxConfig;
		}
	},
    {
        description: "Changing font_colour to new colour classes",
        notifyUser: Workspace.No,
        applyTo : function(cxConfig) {
            return cxConfig.font_colour == "regular"
                || cxConfig.font_colour == "base"
                || cxConfig.font_colour == "medium"
                || cxConfig.font_colour == "light";
        },
        migrate : function(cxConfig) {
            switch (cxConfig.font_colour) {
                case "regular":
                case "base":
                    cxConfig.font_colour = "cx-color_444";
                    break;
                case "medium":
                    cxConfig.font_colour = "cx-color_888";
                    break;
                case "light":
                    cxConfig.font_colour = "cx-color_ccc";
                    break;
            }

            return cxConfig;
        }

    },
	{
		description: "Updating series chart stack setting",
		notifyUser: Workspace.No,
		applyTo : function(cxConfig) {
			return (cxConfig.type == "chart_series" && cxConfig.stack);
		},
		migrate : function(cxConfig) {
			if(cxConfig.stack) {
				cxConfig.stackBars = 1;
				cxConfig.stack = false;
			}

			return cxConfig;
		}
	},
    {
        description: "Changing hex codes to theme colours (components)",
        notifyUser: Workspace.No,
        applyTo : function(cxConfig) {
            switch (cxConfig.type) {
                case "chart_pie":
                {
                    if (cxConfig.sliceColors) return true;
                    break;
                }
                case "label":
                {
                    if (cxConfig.fmtArgs &&
                        ((cxConfig.fmtArgs.lineColor && cxConfig.fmtArgs.lineColor.indexOf("#") != -1)
                            || (cxConfig.fmtArgs.fillColor && cxConfig.fmtArgs.fillColor.indexOf("#") != -1)
                            || (cxConfig.fmtArgs.spotColor && cxConfig.fmtArgs.spotColor.indexOf("#") != -1)
                            || (cxConfig.fmtArgs.minSpotColor && cxConfig.fmtArgs.minSpotColor.indexOf("#") != -1)
                            || (cxConfig.fmtArgs.maxSpotColor && cxConfig.fmtArgs.maxSpotColor.indexOf("#") != -1)
                            || (cxConfig.fmtArgs.barColor && cxConfig.fmtArgs.barColor.indexOf("#") != -1)
                            || (cxConfig.fmtArgs.negBarColor && cxConfig.fmtArgs.negBarColor.indexOf("#") != -1)
                            || (cxConfig.fmtArgs.winColor && cxConfig.fmtArgs.winColor.indexOf("#") != -1)
                            || (cxConfig.fmtArgs.lossColor && cxConfig.fmtArgs.lossColor.indexOf("#") != -1)
                            || (cxConfig.fmtArgs.drawColor && cxConfig.fmtArgs.drawColor.indexOf("#") != -1))) {
                        return true;
                    }
                    break;
                }
                case "range":
                case "scatter_data":
                case "series_data":
                {
                    if (cxConfig.color && cxConfig.color.indexOf("#") != -1) return true;
                    break;
                }
                case "sparkline":
                {
                    if ((cxConfig.lineColor && cxConfig.lineColor.indexOf("#") != -1)
                        || (cxConfig.fillColor && cxConfig.fillColor.indexOf("#") != -1)
                        || (cxConfig.spotColor && cxConfig.spotColor.indexOf("#") != -1)
                        || (cxConfig.minSpotColor && cxConfig.minSpotColor.indexOf("#") != -1)
                        || (cxConfig.maxSpotColor && cxConfig.maxSpotColor.indexOf("#") != -1)
                        || (cxConfig.barColor && cxConfig.barColor.indexOf("#") != -1)
                        || (cxConfig.negBarColor && cxConfig.negBarColor.indexOf("#") != -1)
                        || (cxConfig.winColor && cxConfig.winColor.indexOf("#") != -1)
                        || (cxConfig.lossColor && cxConfig.lossColor.indexOf("#") != -1)
                        || (cxConfig.drawColor && cxConfig.drawColor.indexOf("#") != -1)) {
                        return true;
                    }
                    break;
                }
                case "tile_map":
                {
                    if ((cxConfig.startColor && cxConfig.startColor.indexOf("#") != -1)
                        || (cxConfig.endColor && cxConfig.endColor.indexOf("#") != -1)) {
                        return true;
                    }
                    break;
                }
            }

            return false;
        },
        migrate : function(cxConfig) {
            var i;

            switch (cxConfig.type) {
                case "chart_pie":
                {
                    if (cxConfig.sliceColors) {
                        for (i = 0; i < cxConfig.sliceColors.length; i++) {
                            cxConfig.sliceColors[i] = CXTheme.migrateThemeColour(cxConfig.sliceColors[i]);
                        }
                    }
                    break;
                }
                case "label":
                {
                    if (cxConfig.fmtArgs) {
                        if (cxConfig.fmtArgs.lineColor) cxConfig.fmtArgs.lineColor = CXTheme.migrateThemeColour(cxConfig.fmtArgs.lineColor);
                        if (cxConfig.fmtArgs.fillColor) cxConfig.fmtArgs.fillColor = CXTheme.migrateThemeColour(cxConfig.fmtArgs.fillColor);
                        if (cxConfig.fmtArgs.spotColor) cxConfig.fmtArgs.spotColor = CXTheme.migrateThemeColour(cxConfig.fmtArgs.spotColor);
                        if (cxConfig.fmtArgs.minSpotColor) cxConfig.fmtArgs.minSpotColor = CXTheme.migrateThemeColour(cxConfig.fmtArgs.minSpotColor);
                        if (cxConfig.fmtArgs.maxSpotColor) cxConfig.fmtArgs.maxSpotColor = CXTheme.migrateThemeColour(cxConfig.fmtArgs.maxSpotColor);
                        if (cxConfig.fmtArgs.barColor) cxConfig.fmtArgs.barColor = CXTheme.migrateThemeColour(cxConfig.fmtArgs.barColor);
                        if (cxConfig.fmtArgs.negBarColor) cxConfig.fmtArgs.negBarColor = CXTheme.migrateThemeColour(cxConfig.fmtArgs.negBarColor);
                        if (cxConfig.fmtArgs.winColor) cxConfig.fmtArgs.winColor = CXTheme.migrateThemeColour(cxConfig.fmtArgs.winColor);
                        if (cxConfig.fmtArgs.lossColor) cxConfig.fmtArgs.lossColor = CXTheme.migrateThemeColour(cxConfig.fmtArgs.lossColor);
                        if (cxConfig.fmtArgs.drawColor) cxConfig.fmtArgs.drawColor = CXTheme.migrateThemeColour(cxConfig.fmtArgs.drawColor);
                    }
                    break;
                }
                case "range":
                case "scatter_data":
                case "series_data":
                {
                    if (cxConfig.color) cxConfig.color = CXTheme.migrateThemeColour(cxConfig.color);
                    break;
                }
                case "sparkline":
                {
                    if (cxConfig.lineColor) cxConfig.lineColor = CXTheme.migrateThemeColour(cxConfig.lineColor);
                    if (cxConfig.fillColor) cxConfig.fillColor = CXTheme.migrateThemeColour(cxConfig.fillColor);
                    if (cxConfig.spotColor) cxConfig.spotColor = CXTheme.migrateThemeColour(cxConfig.spotColor);
                    if (cxConfig.minSpotColor) cxConfig.minSpotColor = CXTheme.migrateThemeColour(cxConfig.minSpotColor);
                    if (cxConfig.maxSpotColor) cxConfig.maxSpotColor = CXTheme.migrateThemeColour(cxConfig.maxSpotColor);
                    if (cxConfig.barColor) cxConfig.barColor = CXTheme.migrateThemeColour(cxConfig.barColor);
                    if (cxConfig.negBarColor) cxConfig.negBarColor = CXTheme.migrateThemeColour(cxConfig.negBarColor);
                    if (cxConfig.winColor) cxConfig.winColor = CXTheme.migrateThemeColour(cxConfig.winColor);
                    if (cxConfig.lossColor) cxConfig.lossColor = CXTheme.migrateThemeColour(cxConfig.lossColor);
                    if (cxConfig.drawColor) cxConfig.drawColor = CXTheme.migrateThemeColour(cxConfig.drawColor);
                    break;
                }
                case "tile_map":
                {
                    if (cxConfig.startColor) cxConfig.startColor = CXTheme.migrateThemeColour(cxConfig.startColor);
                    if (cxConfig.endColor) cxConfig.endColor = CXTheme.migrateThemeColour(cxConfig.endColor);
                    break;
                }
            }

            return cxConfig;
        }
    },
    {
        description: "Changing hex codes to theme colours (conditions)",
        notifyUser: Workspace.No,
        applyTo : function(cxConfig) {
            var i, condition, j, reaction;

            if (cxConfig.conditions) {
                for (i = 0; i < cxConfig.conditions.length; i++) {
                    condition = cxConfig.conditions[i];

                    if (condition.reactions) {
                        for (j = 0; j < condition.reactions.length; j++) {
                            reaction = condition.reactions[j];

                            if (reaction.type == "color" && reaction.value && reaction.value.indexOf("#") != -1) {
                                return true;
                            }
                        }
                    }
                }
            }

            return false;
        },
        migrate : function(cxConfig) {
            var i, condition, j, reaction;

            if (cxConfig.conditions) {
                for (i = 0; i < cxConfig.conditions.length; i++) {
                    condition = cxConfig.conditions[i];

                    if (condition.reactions) {
                        for (j = 0; j < condition.reactions.length; j++) {
                            reaction = condition.reactions[j];

                            if (reaction.type == "color" && reaction.value) {
                                reaction.value = CXTheme.migrateThemeColour(reaction.value);
                            }
                        }
                    }
                }
            }

            return cxConfig;
        }
    },
    {
        description: "Your mini chart has been upgraded.",
        notifyUser: Workspace.Yes,
        applyTo : function(cxConfig) {
            return cxConfig.type == "sparkline";
        },
        migrate : function(cxConfig) {
            var sparkline = KlipFactory.instance.createComponent(cxConfig);
            var miniSeries = KlipFactory.instance.createComponent({type:"mini_series"});

            var sLabel = sparkline.getChildByRole("last-value");

            var mLabel = miniSeries.getChildByRole("last-value");
            var mSeries = miniSeries.getChildByRole("series");

            var propsToTransfer;

            miniSeries.removeComponent(mLabel);
            miniSeries.addComponent(sLabel);

            miniSeries.size = sparkline.size;
            miniSeries.lastValueSeries = (sparkline.displayLastValue ? 1 : "hidden");

            switch (sparkline.lineFormat) {
                case "l":
                    mSeries.lineFormat = "l";
                    break;
                case "lp":
                    mSeries.lineFormat = "l";
                    mSeries.includeHighLowPoints = true;
                    break;
                case "lpa":
                    mSeries.includeHighLowPoints = true;
            }

            if (sparkline.chartRangeMinOption == "cust" || sparkline.chartRangeMaxOption == "cust") mSeries.chartRangeOption = "cust";

            mSeries.posBarColor = sparkline.barColor;

            propsToTransfer = ["formulas", "data", "chartType", "winThreshold", "minCustomVal", "maxCustomVal",
                "lineColor", "fillColor", "spotColor", "minSpotColor", "maxSpotColor", "negBarColor", "winColor", "lossColor", "drawColor"];

            _.each(propsToTransfer, function(p) {
                mSeries[p] = sparkline[p];
            });

            return miniSeries.serialize();
        }
    },
    {
        description: "Upgrade mini chart formats with new properties.",
        notifyUser: Workspace.No,
        applyTo : function(cxConfig) {
            if (cxConfig.fmtArgs) {
                if (cxConfig.fmtArgs.chartLineFormat == "lp" || cxConfig.fmtArgs.chartLineFormat == "lpa") return true;
                if (cxConfig.fmtArgs.chartRangeMinOption || cxConfig.fmtArgs.chartRangeMaxOption) return true;
                if (cxConfig.fmtArgs.barColor) return true;
            }

            return false;
        },
        migrate : function(cxConfig) {
            if (cxConfig.fmtArgs.chartLineFormat == "lp") {
                cxConfig.fmtArgs.chartLineFormat = "l";
                cxConfig.fmtArgs.includeHighLowPoints = true;
            } else if (cxConfig.fmtArgs.chartLineFormat == "lpa") {
                cxConfig.fmtArgs.chartLineFormat = "la";
                cxConfig.fmtArgs.includeHighLowPoints = true;
            }

            if (cxConfig.fmtArgs.chartRangeMinOption || cxConfig.fmtArgs.chartRangeMaxOption) {
                if (cxConfig.fmtArgs.chartRangeMinOption == "cust" || cxConfig.fmtArgs.chartRangeMaxOption == "cust") {
                    cxConfig.fmtArgs.chartRangeOption = "cust";
                }

                delete cxConfig.fmtArgs["chartRangeMinOption"];
                delete cxConfig.fmtArgs["chartRangeMaxOption"];
            }

            if (cxConfig.fmtArgs.barColor) {
                cxConfig.fmtArgs.posBarColor = cxConfig.fmtArgs.barColor;

                delete cxConfig.fmtArgs["barColor"];
            }

            return cxConfig;
        }
    },
    {
        description: "Upgrade image component.",
        notifyUser: Workspace.No,
        applyTo : function(cxConfig) {
            return (cxConfig.type == "image"
                && (cxConfig.scale == "fit" || cxConfig.scale == "fill" || cxConfig.max_height));
        },
        migrate : function(cxConfig) {
            if (cxConfig.scale == "fit") {
                cxConfig.scale = "contain";
            } else if (cxConfig.scale == "fill") {
                cxConfig.scale = "cover";
            }

            if (cxConfig.max_height) {
                cxConfig.height = "cust";
                cxConfig.customHeightVal = cxConfig.max_height;

                delete cxConfig["max_height"];
            }

            return cxConfig;
        }
    },
    {
        description: "Upgrade table column component.",
        notifyUser: Workspace.No,
        applyTo : function(cxConfig) {
            return (cxConfig.type == "table_col" && cxConfig.border);
        },
        migrate : function(cxConfig) {
            switch (cxConfig.border) {
                case "left-heavy":
                    cxConfig.borderLeft = "2";
                    break;
                case "left-none":
                    cxConfig.borderLeft = "0";
                    break;
                case "right-heavy":
                    cxConfig.borderRight = "2";
                    break;
                case "right-none":
                    cxConfig.borderRight = "0";
                    break;
            }

            delete cxConfig["border"];

            return cxConfig;
        }
    },
    {
        description: "Your map component has been upgraded.",
        notifyUser: Workspace.Yes,
        applyTo : function(cxConfig) {
            var i, comp;

            if (cxConfig.type == "tile_map" && cxConfig.components) {
                for (i = 0; i < cxConfig.components.length; i++) {
                    comp = cxConfig.components[i];
                    if (comp.role == "tile_ids") {
                        return true;
                    }
                }
            }

            return false;
        },
        migrate : function(cxConfig) {
            var ids, values, descs, links
            var regionCx, idCx, colorCx, descCx, linkCx;
            var map = KlipFactory.instance.createComponent(cxConfig);
            map.afterFactory();

            ids = map.getChildByRole("tile_ids");
            values = map.getChildByRole("tile_values");
            descs = map.getChildByRole("tile_desc");
            links = map.getChildByRole("tile_xref");

            regionCx = map.getChildByRole("tile_regions");
            idCx = regionCx.getChildByRole("region_id");
            colorCx = regionCx.getChildByRole("region_color");
            descCx = regionCx.getChildByRole("region_desc");
            linkCx = regionCx.getChildByRole("region_xref");

            // transfer data
            regionCx.xrefEnabled = cxConfig.xrefEnabled;

            idCx.id = ids.id;
            idCx.formulas = ids.formulas;
            idCx.data = ids.data;

            colorCx.id = values.id;
            colorCx.fmt = values.fmt;
            colorCx.fmtArgs = values.fmtArgs;
            colorCx.formulas = values.formulas;
            colorCx.data = values.data;
            colorCx.colorsEnabled = cxConfig.colorsEnabled;
            colorCx.startColor = cxConfig.startColor;
            colorCx.endColor = cxConfig.endColor;
            colorCx.showValues = cxConfig.showValues;

            descCx.id = descs.id;
            descCx.formulas = descs.formulas;
            descCx.data = descs.data;

            linkCx.id = links.id;
            linkCx.formulas = links.formulas;
            linkCx.data = links.data;
            linkCx.xrefInParent = cxConfig.xrefInParent;

            // remove old sub-components
            map.removeComponent(ids);
            map.removeComponent(values);
            map.removeComponent(descs);
            map.removeComponent(links);

            return map.serialize();
        }
    },
    {
        description: "You can now sort series values as well as axes on Bar/Line Charts. <span class='link' onclick='help.toggle(\"klips/dst-sort\");'>More information...</span>",
        notifyUser: Workspace.Yes,
        applyTo: function(cxConfig) {
            var i, len;
            var comp;

            if (cxConfig.type == "chart_series" && cxConfig.components) {
                for (i = 0, len = cxConfig.components.length; i < len; i++) {
                    comp = cxConfig.components[i];
                    if (comp.type == "chart_axis" && comp.role == "axis_x" && comp.sort) {
                        return true;
                    }
                }
            }

            return false;
        },
        migrate: function(cxConfig) {
            var seriesChart = KlipFactory.instance.createComponent(cxConfig);
            var xAxis = seriesChart.getChildByRole("axis_x");
            var dstContext = { ops:[] };

            dstContext.ops.push({
                type: "sort",
                sortBy: [
                    {
                        dim: xAxis.getVirtualColumnId(),
                        dir: parseInt(xAxis.sort)
                    }
                ]
            });

            seriesChart.dstContext = dstContext;

            delete xAxis.sort;

            return seriesChart.serialize();
        }
    },
    {
        description: "Sorting for Pie Charts has moved. <span class='link' onclick='help.toggle(\"klips/dst-sort\");'>More information...</span>",
        notifyUser: Workspace.Yes,
        applyTo: function(cxConfig) {
            return cxConfig.type == "chart_pie" && cxConfig.sort;
        },
        migrate: function(cxConfig) {
            var pieChart = KlipFactory.instance.createComponent(cxConfig);
            var dstContext = { ops:[] };
            var currentSort = parseInt(pieChart.sort);
            var direction = (currentSort == 1 || currentSort == 4) ? 1 : 2;
            var sortedComponentRole = (currentSort == 1 || currentSort == 2) ? "labels" : "data";
            var sortedComponent = pieChart.getChildByRole(sortedComponentRole);

            if (sortedComponent) {
                dstContext.ops.push({
                    type: "sort",
                    sortBy: [
                        {
                            dim: sortedComponent.getVirtualColumnId(),
                            dir: direction
                        }
                    ]
                });

                pieChart.dstContext = dstContext;
            }

            delete pieChart.sort;

            return pieChart.serialize();
        }
    },
    {
        description: "<span class='strong' style='color:#3BA150'>NEW!</span> Klipfolio can now automatically detect data formats. <span class='link' onclick='help.toggle(\"klips/auto-format\");'>More information...</span>",
        notifyUser: Workspace.Yes,
        applyTo: function(cxConfig) {
            var cx;
            var dataComponents;
            var scatterData;
            var i;
            if (cxConfig.type == "chart_scatter") {
                cx = KlipFactory.instance.createComponent(cxConfig);
                if (cx && cx.isAutoFormatFeatureOn()) {
                    scatterData = cx.getChildrenByRole("scatter_data");
                    if (scatterData) {
                        for (i = 0; i < scatterData.length; i++) {
                            dataComponents = this.getDataComponentWithoutAutofmt(scatterData[i]);

                            if (dataComponents.length > 0) {
                                return true;
                            }
                        }
                    }
                }
            }
            return false;
        },
        migrate: function(cxConfig) {
            var cx = KlipFactory.instance.createComponent(cxConfig);
            var scatterData = cx.getChildrenByRole("scatter_data");
            var i;
            var dataComponents;
            if (scatterData) {
                for (i = 0; i < scatterData.length; i++) {
                    dataComponents = this.getDataComponentWithoutAutofmt(scatterData[i]);
                    if (dataComponents) {
                        dataComponents.forEach(function(dataComponent){
                            var role;
                            var axis;
                            if (dataComponent.role == "data_x") {
                                role = "axis_x";
                            } else if (dataComponent.role == "data_y") {
                                role = "axis_y";
                            } else if (dataComponent.role == "data_z") {
                                role = "axis_z";
                            }
                            if (role) {
                                axis = cx.getChildByRole(role);
                                if (axis) {
                                    dataComponent.fmt = axis.fmt;
                                    dataComponent.fmtArgs = axis.fmtArgs;
                                    dataComponent.autoFmt = false;
                                }
                            }
                        });
                    }
                }
            }
            return cx.serialize();
        },
        getDataComponentWithoutAutofmt: function(cx) {
            var allData = cx.getChildrenByPropertyFilter("role", function(value) {
                return (value.indexOf("data_") == 0 && value.indexOf("data_labels") == -1); //exclude data_labels from autoFmt migration meant for axis data values
            });
            var dataList = [];
            if (allData) {
                allData.forEach(function(data) {
                    if (data && !data.isAutoFormatMigrationDone()) {
                        dataList.push(data);
                    }
                });
            }
            return dataList;
        }
    },
    {
        description: "<span class='strong' style='color:#3BA150'>NEW!</span> Klipfolio can now automatically detect data formats. <span class='link' onclick='help.toggle(\"klips/auto-format\");'>More information...</span>",
        notifyUser: Workspace.Yes,
        applyTo: function(cxConfig) {
            var cx;
            var seriesList;
            if (cxConfig.type == "chart_series") {
                cx = KlipFactory.instance.createComponent(cxConfig);
                if (cx && cx.isAutoFormatFeatureOn()) {
                    seriesList = this.getSeriesWithoutAutofmt(cx);
                    if (seriesList.length > 0) {
                        return true;
                    }
                }
            }
            return false;
        },
        migrate: function(cxConfig) {
            var cx = KlipFactory.instance.createComponent(cxConfig);
            var seriesList = this.getSeriesWithoutAutofmt(cx);
            if (seriesList) {
                seriesList.forEach(function(series){
                    var axis = cx.getChildById(series.axis);
                    if (axis) {
                        series.fmt = axis.fmt;
                        series.fmtArgs = axis.fmtArgs;
                        series.autoFmt = false;
                    }
                });
            }
            return cx.serialize();
        },
        getSeriesWithoutAutofmt: function(cx) {
            var allSeries = cx.getChildrenByRole("series");
            var seriesList = [];
            if (allSeries) {
                allSeries.forEach(function(series) {
                    if (series && !series.isAutoFormatMigrationDone()) {
                        seriesList.push(series);
                    }
                });
            }
            return seriesList;
        }
    },
	{
		/* Migrate formulas to type-in format */
		description: "Converting formulas to Type-In format",
		notifyUser: Workspace.No,
		applyTo : function(klipConfig) {
            var klip;
            var migrationApplied = klipConfig.appliedMigrations && klipConfig.appliedMigrations["textOnlyFormulas"];
            var foundOldFormulas = false;

            if (!klipConfig.type && !migrationApplied) { // this is in fact a klip config and not a component config
                klip = KlipFactory.instance.createKlip(dashboard, klipConfig);
                klip.eachComponent(function(component) {
                    var formulas = component.getFormulas();
                    if (formulas) {
                        foundOldFormulas = formulas.some(function(formula) {
                            return formula.version !== DX.Formula.VERSIONS.TYPE_IN;
                        });
                    }

                    return foundOldFormulas; //stop iterating as soon as an old formula is found
                });
            }

            return foundOldFormulas;
		},
		migrate : function(klipConfig) {
			var klip = KlipFactory.instance.createKlip(dashboard, klipConfig);
            klip.eachComponent(function(component) {
                var formulas = component.getFormulas();
                if (formulas) {
                    formulas.forEach(function(formula) {
                        formula.convertToTextOnlyFormat();
                    });
                }
            });
            klip.setAppliedMigration("textOnlyFormulas", true);
            return klip.serialize();
		}
	},
    {
        /* Migrate component references to be applied after DST actions instead of before */
        description: "<span class='strong' style='color:#3BA150'>NEW!</span> You can now customize individual column headers. To enable this we’ve made changes to the way the <span class='strong'>Use first values as column headers</span> Table property works. <span class='link' onclick='help.toggle(\"klips/post-dst-migration\");'>More information...</span>",
        notifyUser: Workspace.NotifyUserPostDST,
        applyTo : function(klipConfig) {
            var migrationApplied = klipConfig.appliedMigrations && klipConfig.appliedMigrations["post_dst"];
            if (!klipConfig.type && !migrationApplied && KF.company.isPostDSTFeatureOn()) { // this is in fact a klip config and not a component config
                return true;
            }
            return false;
        },
        migrate : function(klipConfig) {
            var klip = KlipFactory.instance.createKlip(dashboard, klipConfig);
            klip.eachComponent(function(component) {
                component.getDstHelper().migratePostDST();
            });
            klip.setAppliedMigration("post_dst", true);
            return klip.serialize();
        }
    },
    {
        // Migrate hidden data columns
        description: "Migrate hidden data",
        notifyUser: Workspace.No,
        applyTo : function(klipConfig) {
            var migrationApplied = klipConfig.appliedMigrations && klipConfig.appliedMigrations["separate_root_dsts"];
            if (!klipConfig.type && !migrationApplied && KF.company.isVCFeatureOn()) { // this is in fact a klip config and not a component config
                return true;
            }
            return false;
        },

        migrate : function(klipConfig) {
            var klip = KlipFactory.instance.createKlip(dashboard, klipConfig);
            klip.eachComponent(function(component) {
                component.getDstHelper().migrateSeparateRoots();
            });
            klip.setAppliedMigration("separate_root_dsts", true);
            return klip.serialize();
        }

    },
    {
        // Migrate font style to new values
        description: "Migrate font styles",
        notifyUser: Workspace.No,
        applyTo: function(cxConfig) {
            return (cxConfig.font_style === "regular" || cxConfig.font_style === "bolditalic");
        },
        migrate: function(cxConfig) {
            var cx = KlipFactory.instance.createComponent(cxConfig);

            switch (cx.font_style) {
                case "regular":
                    cx.font_style = "";
                    break;
                case "bolditalic":
                    cx.font_style = "bold,italic";
                    break;
            }

            return cx.serialize();
        }
    },

  {
        "description": "Migrate non custom result rows to use result references",
        notifyUser: Workspace.No,
        applyTo : function(klipConfig) {
          var migrationApplied = klipConfig.appliedMigrations && klipConfig.appliedMigrations["result_rows2"];
          if (!klipConfig.type && !migrationApplied && KF.company.isVCFeatureOn()) { // this is in fact a klip config and not a component config
            return true;
          }
          return false;
        },

        migrate : function(klipConfig) {
          var klip = KlipFactory.instance.createKlip(dashboard, klipConfig);
          klip.eachComponent(function(component) {
              if (component.role === "result") {
                component.migrateResultRowsToUseResultReferences();
              }

          });
          klip.setAppliedMigration("result_rows2", true);
          delete klip.appliedMigrations["result_rows"];
          return klip.serialize();
        }

  }
];



Workspace.isDragging = false;
Workspace.$prevHover = false;
Workspace.previewScrollAPI = false;

Workspace.EnableDraggable = function($el, name, options) {
    $el.attr("drag-element", name)
        .draggable({
            addClasses: false,
            helper: "clone",
            start : function() {
                Workspace.isDragging = true;
                PubSub.publish("dragging-element", true);

                // If the klip is empty, hide the Klip empty message and show the Drag component here message
                // by enlarging the klip height to fit the message height
                if (!$(".klip-body").children(":not(.klip-empty)").length) {
                    $(".klip-component-drop-message").show();
                }

                page.cxContextMenu.hide();
            },
            drag: function (evt, ui) {
                var $helper = $(ui.helper);
                var $componentDropMessage = $(".klip-component-drop-message");
                var el, $currentHover, regionId, prevRegionId;
                var $previewContainer, previewPosition;
                var top, right, bottom, left;
                var distance, step;

                // we need to hide the thing being dragged, otherwise elementFromPoint will return it
                // instead of the thing we are over
                // ----------------------------------------------------------------------------------
                $helper.hide();
                $componentDropMessage.hide();
                el = document.elementFromPoint(evt.originalEvent.clientX, evt.originalEvent.clientY);
                $helper.show();
                if (!$(".klip-body").children(":not(.klip-empty)").length) {
                    $componentDropMessage.show();
                }

                // greedy: the thing we are currently over might be a child a drop target
                // so find the closest drop-target.   this is expensive and it might be worth
                // making this an option that can be disabled
                // -------------------------------------------------------------------------
                $currentHover = $(el).closest(".drop-target");
                regionId = $currentHover.closest("[drop-region]").attr("drop-region");

                if (Workspace.$prevHover) {
                    // if we're over something new tell the previous region that we are exiting it
                    // -----------------------------------------------------------------------------
                    if (Workspace.$prevHover.get(0) != el) {
                        Workspace.$prevHover.removeClass("drop-target-valid");
                        prevRegionId = Workspace.$prevHover.closest("[drop-region]").attr("drop-region");
                        Workspace.HandleDragDrop("exit", prevRegionId, Workspace.$prevHover, evt, ui);
                    }
                }

                if ($currentHover.hasClass("drop-target")) {
                    // if we left the previous handler, tell this handler we're entering it
                    // --------------------------------------------------------------------
                    if (!Workspace.$prevHover || (Workspace.$prevHover.get(0) != el) ){
                        Workspace.HandleDragDrop("enter", regionId, $currentHover, evt, ui);
                    }

                    $currentHover.addClass("drop-target-valid");
                    Workspace.HandleDragDrop("drag", regionId, $currentHover, evt, ui);
                }

                // the circle of life continues...
                Workspace.$prevHover = $currentHover;

                // Enable scrolling while dragging
                // ----------------------------------------
                if (Workspace.previewScrollAPI) {
                    $previewContainer = $("#klipPreviewContainer");
                    previewPosition = $previewContainer.position();

                    top = previewPosition.top;
                    right = previewPosition.left + $previewContainer.width();
                    bottom = previewPosition.top + $previewContainer.height();
                    left = previewPosition.left;

                    distance = 50;
                    step = 10;
                    if (evt.originalEvent.clientY <= top + distance) {      // top
                        Workspace.previewScrollAPI.scrollByY(-step);
                    }

                    if (evt.originalEvent.clientY >= bottom - distance) {   // bottom
                        Workspace.previewScrollAPI.scrollByY(step);
                    }

                    if (evt.originalEvent.clientX <= left + distance) {     // left
                        Workspace.previewScrollAPI.scrollByX(-step);
                    }

                    if (evt.originalEvent.clientX >= right - distance) {    // right
                        Workspace.previewScrollAPI.scrollByX(step);
                    }
                }

            },
            stop: function (evt, ui) {
                var $handler;

                if (Workspace.$prevHover && Workspace.$prevHover.hasClass("drop-target")) {
                    $handler = Workspace.$prevHover.closest("[drop-region]");

                    if ($handler) {
                        $handler.removeClass("drop-target-valid");
                        Workspace.HandleDragDrop("drop", $handler.attr("drop-region"), Workspace.$prevHover, evt, ui);
                    }
                }

                $(".klip-component-drop-message").hide();

                Workspace.isDragging = false;
                PubSub.publish("dragging-element", false);
            }
        }).draggable("option", options);

};

Workspace.RegisterDragDropHandlers = function(dragElement, handlers) {
    var i, h;
    for (i = 0; i < handlers.length; i++) {
        h = Workspace.GetDropTargetHandler(handlers[i].region);

        if (!h.accept) h.accept = [];

        h.accept.push(dragElement);
        h["drop-" + dragElement] = handlers[i].drop;
    }
};

Workspace.HandleDragDrop = function(evtType, regionId , $dropTarget, evt, ui) {
    var h = Workspace.GetDropTargetHandler(regionId);

    var dragElement, returnObj;

    if (h) {
        dragElement = ui.helper.attr("drag-element");

        if (h.accept && h.accept.indexOf(dragElement) == -1) return;

        returnObj = false;
        if (h[evtType]) returnObj = h[evtType]($dropTarget, evt, ui);

        if (evtType == "drop" && h["drop-" + dragElement]) {
            h["drop-" + dragElement]($dropTarget, page.activeKlip, returnObj, evt, ui);
        }
    }
};

Workspace.GetDropTargetHandler = function(regionId) {
    var handler = false;

    _.each(Workspace.DragDropTargetHandlers, function(h) {
        if (h.region == regionId) {
            handler = h;
        }
    });

    return handler;
};

Workspace.DragDropTargetHandlers = [
    {
        region: "layout"
    },
    {
        region: "klip"
    },
    {
        region: "klip-body",
        drop: function($target) {
            var $shim = $target.find("#insert-shim");
            var $prevSibling = $shim.prev();

            $shim.remove();

            //Append the klip empty message back into the klip body
            $(".klip-empty").appendTo(".klip-body");

            return $prevSibling;
        },
        drag: function($target, evt, ui) {
            Workspace.InsertKlipShim($target, ui);
            $(".klip-resizable").trigger("setDefaultMinWidth");
        },
        enter: function($target, evt, ui) {
            var shimPaddingTop = $target.css("padding-top");
            var shimPaddingBottom = 0;

            var $insertShim = $("<div>").attr("id","insert-shim");

            // hide empty message and move it out of klip-body in order for the shim
            // to be placed correctly inside the body
            $(".klip-empty").hide().appendTo(".klip");
            $(".klip-component-drop-message").hide();

            if (!$target.children(":not(#insert-shim):not(.klip-empty)").length) {
                shimPaddingTop = 90;
                shimPaddingBottom = 98;
            }
            $insertShim.css({ "margin-top":shimPaddingTop, "margin-bottom":shimPaddingBottom })

            $target.append($insertShim);
            Workspace.InsertKlipShim($target, ui);
        },
        exit: function($target) {
            $target.find("#insert-shim").remove();

            // Show the Drag component here and klip empty messages if there are no components in the preview
            if ($target.children(":not(#insert-shim)").length == 0) {
                $(".klip-component-drop-message").show();
                $(".klip-empty").appendTo(".klip-body").show();
            }
        }
    },
    {
        region: "preview"
    }
];

Workspace.InsertKlipShim = function($target, ui) {
    var y =  ui.offset.top;

    var i, c, klipPaddingTop, klipPaddingBottom;

    var childCoords = [];
    $target.children(":not(#insert-shim)").each(function() {
        var coord = {
            $el: $(this),
            height: $(this).height(),
            top: $(this).offset().top
        };

        childCoords.push(coord);
    });

    if (childCoords.length > 0) {
        childCoords.sort(function(a, b) {
            if (a.top < b.top) {
                return -1;
            } else if (a.top > b.top) {
                return 1;
            } else {
                return 0;
            }
        });

        klipPaddingTop = $target.css("padding-top");
        klipPaddingBottom = $target.css("padding-bottom");

        for (i = 0; i < childCoords.length; i++) {
            c = childCoords[i];

            if (y < (c.top + c.height / 2)) {
                c.$el.before($("#insert-shim").css({"margin-top":(i == 0 ? 0 : klipPaddingTop), "margin-bottom":klipPaddingBottom}));
                break;
            }
        }
    }
};
;/****** c.workspace_klip_save_manager.js *******/ 

WorkspaceKlipSaveManager = $.klass({
    isSaving: false,

    showUpdateUI: function(savePromise, beforeMessage, afterMessage, options) {
        options = options || {};

        this.isSaving = true;
        this.disableSaveActions();
        var showMessagePromise = this.showSaveMessage(beforeMessage, options);
        var _this = this;

        $.when(showMessagePromise, savePromise).
            done(function() {
                _this.hideSaveMessage(afterMessage);
            }).fail(function(jqXHR, textStatus, errorThrown) {
                _this.hideSaveMessage("Error saving " + KF.company.get("brand", "widgetName") + ".");
                var resErrCode = jqXHR.status;
                var resErrText = jqXHR.statusText;
                // Pop-up a validation message if form data is too large (i.e  > 3 MB )to save the klip.
                if (resErrCode == 500 && resErrText.toLowerCase().indexOf("form too large") != -1) {
                    _this.formDataTooLargeErrorMessage();
                }
            }).always(function() {
                _this.isSaving = false;
                _this.enableSaveActions();
            });

        return savePromise;
    },

    showSaveMessage : function(text, options) {
        options = options || { spinner: false };

        var fadeInPromise = $("#autoSaveText")
            .html(text)
            .fadeTo(750, 1)
            .delay(250)
            .promise();

        if (options.spinner) {
            $("#saveKlipSpinner").fadeTo(750, 1);
        }


        return fadeInPromise;
    },

    disableSaveActions: function() {
        $("#btnContainer button, #saveNewKlip, #cancelKlipChanges").attr("disabled", true);
    },

    enableSaveActions: function() {
        $("#btnContainer button, #saveNewKlip, #cancelKlipChanges").attr("disabled", false);
    },

    hideSaveMessage : function(afterText)
    {
        var _this = this;

        $("#autoSaveText").html(afterText);

        $("#saveKlipSpinner").fadeTo(150, 0);
        $("#autoSaveText").delay(250).fadeTo(350, 0);
    },

    formDataTooLargeErrorMessage: function () {
        var widgetName = KF.company.get("brand", "widgetName");
        if(typeof mixPanelTrack == "function") {
          mixPanelTrack("Form data too large");
           }
        var $content = $("<div class='expired-dialog'>")
            .width(400)
            .append($("<h3 style='margin-bottom:10px;'>This " + widgetName + " is too complex to save.</h3>"))
            .append($("<div class='gap-south'>Large, complex " + widgetName + "s run slowly for users. To reduce the size of your " + widgetName + ", remove components (if it's a multi-component " + widgetName + ") or simplify formulas.</div>"));

        var $overlay = $.overlay(null, $content, {
            onClose: function () {
                $content.appendTo(document.body).hide();
                $overlay.hide();
            },
            shadow: true,
            footer: {
                controls: [
                    {
                        text: "Edit "+widgetName,
                        css: "rounded-button primary",
                        onClick: function () {
                            $overlay.close();
                        }
                    }
                ]
            }
        });
        $overlay.show();
    }
});;