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

var Palette = $.klass({ // eslint-disable-line no-unused-vars
    /** 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 *******/ 

/* global VisualizerTabPane:false, page:false */

/**
 * 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' data-pendo='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) );
	},

    renderPermissionError: function(creator) {
        var div = $("<div style='width:380px;height:60px;position:absolute;left:50%;top:50%;margin-left:-190px;margin-top:-30px;text-align:center;'>");
        div.append($("<div style='font-size:0.8rem;margin-bottom:8px;font-weight:bold;line-height:1.5'>").html("This Data Source has not been shared with you."));
        if (creator) {
            div.append($("<div style='line-height:0.8rem;line-height:1.5;'>").html("Data source creator: <span id='datasource_creator'>"+creator+"</span>"));
        }

        this.el.empty();
		this.el.append(div);
    },


    /**
     * 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);
        this._dynamicDSMsg = (options && options.dynamicDSMsg !== "") ? options.dynamicDSMsg : "" ;
    },

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

        if (dsi.shared) {
            this.refresh(callback, errorCallback);
        } else {
            this.renderPermissionError(dsi.creator);
            if (errorCallback) {
                errorCallback();
            }
        }
    },

    /**
     * asks the server for new data for this datasource
     */
    refresh: function(callback, errorCallback) {
        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){
            var isDynamic = _this.datasourceInstance.isDynamic;

            if (errorCallback && errorCallback instanceof Function) {
                errorCallback(err);
            }
            if (_this._dynamicDSMsg !== "" && isDynamic && err.status === 500) {
                _this.renderError(_this._dynamicDSMsg);
            } else {
              _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();

        if (this.dataSet.shared) {
            require(["js_root/build/vendor.packed"], function(Vendor) {
                require(["js_root/build/model_main.packed"], function (DataSetModel) {
                    DataSetModel.invalidateCache();
                    DataSetModel
                        .load(_this.dataSet.id, {columns: true})
                        .then(function (dataSetMetadata) {
                            _this.updateDataSetDefinition(dataSetMetadata);
                            refreshPromise.resolve(dataSetMetadata);
                            return dataSetMetadata;
                        })
                        .fail(function(error) {
                            refreshPromise.reject(error)
                        });
                });
            });
        } else {
            refreshPromise.resolve(this.dataSet);
        }

        return refreshPromise;
    },

    updateDataSetDefinition: function(updatedDefinition) {
        $.extend(this.dataSet, updatedDefinition);
    },

    setDataSet: function(dataSet, callback) {
        var completionPromise = $.Deferred();
        var _this = this;

        this.dataSet = dataSet;

        this.refresh(callback).then(function(result) {
            if (dataSet.shared) {
                _this.render();
                _this.resize();
            } else {
                _this.renderPermissionError(dataSet.creator);
            }

            completionPromise.resolve(result);
        });

        return completionPromise;
    }
});

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({ // eslint-disable-line no-undef

    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,{ // eslint-disable-line no-undef

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


GridLayout = $.klass(LayoutManager,{ // eslint-disable-line no-undef

    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) {
        var i;
        var cd;

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

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

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

            // render table cells
            // ------------------
            for(i = 0 ; i < this.rows; i++){
                $tr = $("<tr>");
                for( 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++){
                _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, dashboard:false, TWEEN:false ListPage: false */
/* eslint-disable vars-on-top */

// if there is browser no logging mechanism, make stub to consume messages
if ( typeof(console) == "undefined" ){
	window.console = {
		log : function(msg){
		}
	};
}

function escapeHtml(unsafe) {
	if (typeof unsafe === "object" || unsafe === undefined)
		return;

	return unsafe
		.replace(/&/g, "&amp;")
		.replace(/</g, "&lt;")
		.replace(/>/g, "&gt;")
		.replace(/"/g, "&quot;")
		.replace(/'/g, "&#039;");
}

// eslint-disable-next-line
var expiredDialog = false;
$(function() {

    checkLocalStorageSupport(); // for safari private browsing mode

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

            try {
                // eslint-disable-next-line
                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'>") // eslint-disable-line
                    .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>"));

                // eslint-disable-next-line
                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
 */
// eslint-disable-next-line
var $KF_GLOBAL_TEXT_CONVERTER = null;

function safeText (input, isHighchartsFormatter) { // eslint-disable-line no-unused-vars
    if(KF.company.hasFeature("saas_11282")) {

        //TODO: remove the unescape, customer's text should be presented exactly as written (customers not being able to use html tags)
        input = _.unescape(input);

        if(isHighchartsFormatter) { //highcharts formatter does its own encoding with the exception of completely stripping out < and >.
            return input.replace(/</g, "&lt;").replace(/>/g, "&gt;");
        }
        return _.escape(input);
    }

    //TODO: have everyone use the feature and no one go through this section of code
    if ($KF_GLOBAL_TEXT_CONVERTER == null) {
        $KF_GLOBAL_TEXT_CONVERTER = $("<div></div>");
    }
    input = "" + input; // force string
    var div = $KF_GLOBAL_TEXT_CONVERTER; // eslint-disable-line

    if (input.indexOf("&") != -1) {
        var ar = input.split("&"); // eslint-disable-line
        var en, i; // eslint-disable-line
        var result = ""; // eslint-disable-line
        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;
    }
    // eslint-disable-next-line
    var html = div.text(input);
    html = div.outerHTML();
    return html.substring(5,html.length - 6);
}

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

    if (agreementVisible) return;
    agreementVisible = true;

    var $content = $("#termsDialog"); // eslint-disable-line

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

    // eslint-disable-next-line
    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).
// eslint-disable-next-line
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]); // eslint-disable-line
//		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]; // eslint-disable-line
		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; // eslint-disable-line
// eslint-disable-next-line
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 insecureUrlRegex = new RegExp("http:\/\/");

function isUnsecureUrl(url) {
    var context = dashboard.getContextString();
    if(context && (context === "editor" || context === "desktop")) {
        return insecureUrlRegex.exec(url);
    }
    return false;
}

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

function clearRelevantStatus(msg){
    if($status && (!msg || $status[0].innerText.trim() == msg.trim())){
        $status.hide();
    }
}

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); // eslint-disable-line

    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) clearRelevantStatus();
            $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();
        }
    };
}

// eslint-disable-next-line
function checkForUsageLimit(limitKey, limitName, spinnerBlockId, onAddResource, productFamily = "K1") {
    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,
                    limitKey: limitKey,
                    limitCount: data.limit,
                    productFamily: productFamily,
                    addResource: function () {
                        if (onAddResource) {
                            onAddResource();
                        }
                    },
                    onFinished: function () {
                        if (spinnerBlockId) {
                            spinnerBlockButton(spinnerBlockId, false);
                        }
                    }
                });
            });
        } else {
            if (onAddResource) {
                onAddResource();
            }
        }
    });
}

function sendPlanLimitReachedMixpanelEvent(limitKey, limitName, topUpPurchaseAvailable){
    $.post("/dashboard/ajax_getCurrentLimitUsage", {limitKey: limitKey}, function (data) {
        if (data.usage >= data.limit && data.limit >= 0){
            require(["js_root/content/limits/limit.mixpanel.helper"], function (helper) {
                helper.sendPlanLimitReachedMixpanelMessage(limitName, data.limit, topUpPurchaseAvailable);
            });
        }
    })
}

function sendPlanUpdatedToMixPanel(source, changeType, paymentMethod) {
    mixPanelTrack("Plan Updated", {
        "Source": source,
        "Plan Change Type": changeType,
        "Payment Method": paymentMethod
    });
}
function sendAssetImportedMessageToMixPanel(assetType, sourceOfImport){
    mixPanelTrack("Asset Imported", {
        "Asset Imported From" : sourceOfImport,
        "Asset Type": assetType
    });
}

function checkTrialUserLimit(onAddResource) {
    $.post("/users/ajax_reachedTrialUserLimit", function (data) {
        if (data.reachedLimit) {
            require(["js_root/build/users_main.packed"], function (Users) {
                Users.render({
                    type: "USER_LIMIT"
                });
            });
        } else {
            if (onAddResource) {
                onAddResource();
            }
        }
    });
}

function partnerClientAddedMixPanel(dashboardsAssigned, usersAssigned, accountStatus, featuresEnabled, directBilled){
    mixPanelTrack("Client Added", {
        "Dashboards Assigned": dashboardsAssigned,
        "Users Assigned": usersAssigned,
        "Client Status": accountStatus,
        "Features Enabled": featuresEnabled,
        "Billed Direct": directBilled,
        "Number of Clients": KF.company.get("numClients") + 1
    });
}

function partnerClientReconfiguredMixPanel(dashboardsAssigned, usersAssigned, accountStatus, featuresEnabled, directBilled){
    mixPanelTrack("Client Reconfigured", {
        "Dashboards Assigned": dashboardsAssigned,
        "Users Assigned": usersAssigned,
        "Client Status": accountStatus,
        "Features Enabled": featuresEnabled,
        "Billed Direct": directBilled
    });
}

function partnerClientDeleteMixPanel(payload){
    mixPanelTrack("Client Deleted", {
        "PIDs": payload["deletedClient"]
    });
}

function extractCompanyStateFromEnum(num) {
    switch(num) {
        case 1:
            return "Expired";
        case 2:
            return "Grace";
        case 3:
            return "Arrears";
        case 4:
            return "Disabled";
        case 5:
            return "Active";
        case 6:
            return "Trial";
        case 7:
            return "Setup";

        default:
            return null;
    }
}

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

function spinnerBlockButtonWhenPolling(block) {
	var $button = $(".for_polling");
	$button.removeClass("primary");
	blockButton($button, block)
}

function blockButton($button, block) {
	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();
	}
}

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

	for ( i = 0; i < values.length; i++){
		k = values[i];
		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) ) { // eslint-disable-line no-cond-assign
		var extractedValues = match[1].split("|"); // eslint-disable-line
		var replaceText; // eslint-disable-line

		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; // eslint-disable-line
        var subText = loopText.substring(0, endIndex); // eslint-disable-line

        // 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, csrfToken: window.KF.csrfToken},
		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); // eslint-disable-line no-console
        }
    }
}

/**
 * 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; // eslint-disable-line
		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); // eslint-disable-line
    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 : "_"); // eslint-disable-line
    var periodReplacement = (replacements && replacements.period ? replacements.period : "_"); // eslint-disable-line

    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; // eslint-disable-line

    // 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(); // eslint-disable-line

        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
/* eslint-disable */
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;};
/* eslint-enable */

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)){ // eslint-disable-line no-cond-assign
        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){
        // eslint-disable-next-line
        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++) { // eslint-disable-line
            var row = rows[i]; // eslint-disable-line

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

        var countList = []; // eslint-disable-line
        _.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 = {}; // eslint-disable-line
        _.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]; // eslint-disable-line

                    var isDistinct = true; // eslint-disable-line
                    var j = 0; // eslint-disable-line
                    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); // eslint-disable-line

        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 isEdge() { // eslint-disable-line
	return /edge/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+. // eslint-disable-line

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 - elRect.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>") // eslint-disable-line
            .addClass("btn-scroll left-scroll")
            .css("display", "none")
            .data("delta", 1);

        var $rightButton = $("<div>") // eslint-disable-line
            .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 contentLeft = contentRect.left - elRect.left;
    var newLeft = contentLeft;

    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 = contentLeft + (leftRect.right - itemRect.left);
    } else if (itemRect.right > rightRect.left) {
        newLeft = contentLeft - (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;
})();

// eslint-disable-next-line
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;
}

// eslint-disable-next-line no-unused-vars
function findDefaultAggregationRule(aggregationRules) {
    return aggregationRules.filter(function(rule) {
        return rule.default;
    })[0];
}

// Focus change listener on check-template-tokens class
// eslint-disable-next-line no-unused-vars
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."); // eslint-disable-line no-console
        }
    }
}

function sendGoogleAnalytics(hitType, eventCategory, eventAction, eventLabel, eventValue) {
    if (eventValue){
        ga("send", {
            hitType: hitType,
            eventCategory: eventCategory,
            eventAction: eventAction,
            eventLabel: eventLabel,
            eventValue: eventValue
        });
    } else {
        ga("send", {
            hitType: hitType,
            eventCategory: eventCategory,
            eventAction: eventAction,
            eventLabel: eventLabel
        });
    }
}

function getModalListPager($importListContainer, asset, timeUtils){
    return new ListPager({
        listId:"tabImportList",
        container: $importListContainer.find(".listPager"),
        blockAreaSelector: ".dialog-content",
        limit:15,
        page:1,
        params:{
            show:"all"
        },
        onData:function (data, pager) {
            var $results = $importListContainer.find(".library-results");
            var row, cell;
            var i;

            $results.find("tr:gt(0)").remove();
            asset.checkAccess(data.count, $results, $importListContainer.find(".no-library-items"));

            for (i = 0; i < data.records.length; i++) {
                row = $("<tr>").addClass("auto-client_import_dashboards-dashboard");
                cell = $("<td>").html("<input pid=\""+data.records[i].publicId+"\" type=\"checkbox\" class=\"importCheck\" format = \""+data.records[i].assetType+"\"/>");
                row.append(cell);
                cell = $("<td>").html(data.records[i].name).addClass("strong");
                row.append(cell);
                cell = $("<td>").html(data.records[i].numKlips);
                row.append(cell);
                cell = $("<td>").html("<div>"+timeUtils.generateDateSpan(data.records[i].lastUpdated)+"</div>");
                row.append(cell);
                cell = $("<td>").html("" + data.records[i].createdBy);
                row.append(cell);
                if (data.records[i].shared)cell = $("<td>").html("<span class=\"shared-check pad-left\"></span>");
                else cell = $("<td>").html("");
                row.append(cell);
                $results.append(row);
            }

        },
        dataUrl:"/tabs/ajax_tabListsContents"
    });
}

function canImport(){
    const isClient = KF.company.get("accountType") === "Client";
    const hasClients = KF.company.get("numClients") > 0;
    const hasImportPermission = KF.company.get("user").permissions.includes("tab.import");

    return (hasClients || isClient) && hasImportPermission;
}

function canAddDashboard(){
    return KF.company.get("user").permissions.includes("tab.build");
}

function canShareDashboard() {
    return KF.company.get("user").permissions.includes("tab.share");
}

function canDeleteDashboard() {
    return KF.company.get("user").permissions.includes("tab.delete");
}

// See same filtering logic in #BizopsService.isKlipfolioInternalUser().
function isKlipfolioInternalUser(email) {
    if (email) {
        var emailLowerCase = email.toLowerCase();
        return emailLowerCase.endsWith("@klipfolio.com") || emailLowerCase.startsWith("kftester.automated");
    } else {
        return false;
    }
}

function hasCustomStyleFeature() {
    return KF.company.hasFeature("theming");
}
;/****** c.applied_actions_pane.js *******/ 

/* global ButtonDropDown:false, customScrollbar:false */

var AppliedActionsPane = $.klass({ // eslint-disable-line no-unused-vars
    containerId: "",

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

    filterButtons: null,

    rootComponent: null,

    initialize: function(config) {
        var _this;

        $.extend(this, config);

        _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) {
        var _this;
        if (!component) return;

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

        require(["js_root/build/vendor.packed"], function() {
            require(["js_root/build/applied_actions_main.packed"], 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 *******/ 

/* eslint-disable vars-on-top */

/**
 * 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({ // eslint-disable-line no-unused-vars
    /** 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 *******/ 

/* eslint-disable vars-on-top */

/**
 * 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, Palette:false, Workspace:false */

var ComponentPalette = $.klass(Palette, {

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

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

        $super(config);

        _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) {
        var i;
        var paletteItem;
        var $item;

        for (i = 0; i < this.items.length; i++) {
            paletteItem = this.items[i];

            $item = $("<div>")
                .addClass("palette-item")
                .attr("item-id", paletteItem.id)
                .attr("data-pendo", "palette-item")
                .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) {
        var i;

        for (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;
        var i;

        if (item) {
            if (item.selectionIdx) {
                for (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 *******/ 

/* eslint-disable vars-on-top */
/* global Palette:false */

var ControlPalette = $.klass(Palette, { // eslint-disable-line no-unused-vars

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

/* eslint-disable vars-on-top */
/* 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, DashboardWarningSystem:false, eachComponent:false, parseQueryString:false,
    getLocationOrigin:false, replaceMarkers:false, fixHeaderToTop:false, toggleHorizontalScroll:false,
    DashboardLayout:false, Dashboard:false, DashboardGridLayout:false, clearSelections:false
    isWorkspace:false, getScrollPosition:false, setScrollPosition:false, newRelicNoticeError:false,
    GridLayoutManager:false */

Dashboard = $.klass({ // eslint-disable-line no-undef, no-global-assign

    /** 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,
    numKlipsBusyByDashboard: false,

    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,

    imagingBackend : "",

    /**
     * @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.numKlipsBusyByDashboard = {};

        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.dsWarningSystem = new DsWarningSystem({ dashboard: this });

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

			const canDownloadReports = KF.company.hasFeature("downloadable_reports");
			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 users...", 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: (canDownloadReports ? "download_pdf_klip" : "upgrade_for_pdf_dialog"), id:"menuitem-download_pdf_klip", iconCss: canDownloadReports ? null : "icon-lock", iconPosition:"right" },
								{ text:"Image...", actionId: (canDownloadReports ? "download_image_klip" : "upgrade_for_image_download_dialog"), id:"menuitem-download_image_klip", iconCss: canDownloadReports ? null : "icon-lock", iconPosition:"right" },
								{ 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:"Configure", actionId:"configure-metric-view", id:"menuitem-configure-metricView"},
					{ text:"About", actionId:"about-metric-view", id:"menuitem-about-metricView"},
					{ 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 users...", 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() {
                    _this.onAddDashboardClick();
                })
                .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");
                    }

                    _this.setTabOrder(newOrder);
                }
            });
        }


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

    },

    onAddDashboardClick: function() {
        var hasOneProductUI = KF.company.hasFeature("one_product_ui");
        var tabPanelActive = $("#panel-add_tab").is(":visible");
        const scrollContainer = hasOneProductUI ? document.getElementById("c-main") : window;

        if(hasOneProductUI) {
            scrollContainer.scrollTop = 0;
            page.invokeAction("add_tab");
        } else {
            if (!tabPanelActive) {
                // Close the Add Klip/Layout toolbar if it is open
                this.closeToolbar();

                this.openTabPanel();

                scrollContainer.scrollTo(0, 0);
            } else {
                this.closeTabPanel();
            }
        }
    },

    setTabOrder: function(newOrder) {
        var _this = this;

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

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

            _this.tabs = newTabs;

            _this.updateTabListMenu();
        });
    },

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

                if(dashboard) {
                    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"];

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

            if (klip) {
                dashboardId = klip.currentTab;

                if (_this.numKlipsBusyByDashboard[dashboardId] === undefined) {
                    _this.numKlipsBusyByDashboard[dashboardId] = 0;
                }

                numKlipsBusyBefore = _this.numKlipsBusyByDashboard[dashboardId];

                if (busy !== undefined) {
                    stateSet = klip.setBusy(busy);

                    if (stateSet) {
                        busyCount = (busy ? 1 : -1);

                        _this.numKlipsBusy += busyCount;
                        _this.numKlipsBusyByDashboard[dashboardId] += busyCount;
                    }
                }

                // this shouldn't happen but just in case
                if (_this.numKlipsBusyByDashboard[dashboardId] < 0) {
                    _this.numKlipsBusyByDashboard[dashboardId] = 0;
                }

                if (numKlipsBusyBefore === 0 && _this.numKlipsBusyByDashboard[dashboardId] > 0) {
                    PubSub.publish("dashboard_busy", { busy: true, dashboardId: dashboardId });
                } else if (_this.numKlipsBusyByDashboard[dashboardId] === 0) {
                    PubSub.publish("dashboard_busy", { busy: false, dashboardId: dashboardId });
                }
            }
        });

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

    /**
     * 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");
        var maySeeLockedFeatures = KF.company.get("isDirectBilled") && !KF.company.hasFeature("whitelabel");

        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");

            if (KF.user.hasPermission("dashboard.library")) {
                this.menuModel.tab.push("menuitem-about_tab");
            }

            if (KF.user.hasPermission("klip.library")) {
                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 || maySeeLockedFeatures) {
                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 || maySeeLockedFeatures) {
                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,
            csrfToken: window.KF.csrfToken
        }, function() {
            page.invokeAction("tv_small_tabs");
        });
    },
    initDashboardWarningSystem : function(DashboardWarningSystem){
        this.dashboardWarningSystem = new DashboardWarningSystem({dashboard: this});
    },
    /**
     * 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 (tab.klips.length > 0) {
                    if (_this.dsWarningSystem) {
                        _this.dsWarningSystem.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 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;

        PubSub.publish("tab-selected", {
            selectedTabId: t.id
        });

        $("#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) {
                $("#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();
        }

        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 {
                if (KF.company.hasFeature("one_product_ui")) {
                    this.toolbar.tabsEl.find("li").each(function (i) {
                        // display every single button except Connect button, which is taken care by template creation flow.
                        $(this).attr("id") !== "tb-tab-connect" ?  $(this).show() : true;
                    });
                }
                this.toolbar.el.show();
            }
        } else {
            if (initial) {
                this.toolbar.el.slideUp(200);
            } else {
                if (KF.company.hasFeature("one_product_ui")) {
                    this.toolbar.tabsEl.find("li").each(function (i) {
                        // when we want to hide all the buttons, we still want to show full screen mode button for read only dashboards
                        $(this).attr("id") !== "tb-tab-fullscreen" ? $(this).hide() : $(this).show();
                    });
                    this.toolbar.el.show(); //will only shows the fullscreen button in one product ui
                } else {
                    this.toolbar.el.hide(200);
                }
            }
        }

        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(), dashboardId: tab.id });
        });

        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;
                }

                // check if tab already exists (if added through the "+" button)
                if(!dashboard.getTabById(parsedPermalink.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 $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();
            }
        } 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.dsWarningSystem.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.buttonId === "tb-tab-fullscreen" && !KF.company.hasFeature("fullscreen_mode")) {
            tbButton.append($("<img src='/images/skin-w/premium-lock-white.svg' style='margin-left:8px; width:10px; height:12px;'/>"));
            tbButton.attr("actionId", "upgrade_for_feature_dialog");
        }

        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.data("actionArgs", config.actionArgs);
            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
                    }
                };
            } else if (layoutId != "grid" && currentLayoutId == "grid") {
                // migrate from custom to template layout
                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){
                var 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 formula;

        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) {
        var i;
        var len;
        var tab;
        var tabName;

        if (!tabs) return;

        for (i = 0, len = tabs.length; i < len; i++) {
            tab = tabs[i];

            // update variables in tab names
            if (tab.hasDynamicTitle) {
                tabName = this.getVariableTabName(tab);

                if (tabName) {
                    PubSub.publish("tab-name-updated", {
                        tabId: tab.id,
                        name: 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, csrfToken: window.KF.csrfToken });
        } else {
            $.post("/dashboard/ajax_setProps", { props:data, csrfToken: window.KF.csrfToken });
        }
    },

    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;
        }
    },

    getTabMetricViews: function(tab) {
        var metricViews = [];

        if (tab && tab.klips) {
            tab.klips.forEach(function (klip) {
                if (klip.isMetricView) metricViews.push(klip);
            });
        }

        return metricViews;
    }

});


/**
 *
 */
DashboardLayout = $.klass({ // eslint-disable-line no-undef, no-global-assign

    layoutEl : false,
    defaultRegionId: false,
    dashboard: false,
    id: false,
    currentTabId: false,
    layoutState: false,
    numPages: 1,

    initialize : function(config) {
        var r;

        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
        }

        r = this.layoutEl.attr("default-region");
        if (r) {
            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() {
        var r;

        if (!this.defaultRegionId) {
            r = this.layoutEl.attr("default-region");
            if (r) {
                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 , { // eslint-disable-line no-undef, no-global-assign

    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 ) { // eslint-disable-line no-unused-vars
    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 *******/ 

/* eslint-disable vars-on-top */
/* global help:false, customScrollbar:false */

var DrilldownControls = $.klass({ // eslint-disable-line no-unused-vars
    /** 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 *******/ 

/* eslint-disable vars-on-top */
/* global help:false */

/**
 * 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({ // eslint-disable-line no-unused-vars
    /** 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({ // eslint-disable-line no-undef

    $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); // eslint-disable-line no-console
            }

        }

        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); // eslint-disable-line no-console
                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;

        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, clickSupportTicket:false */
var HelpSystem = $.klass({

    $root: false,

    width: 340,

	defaultPage : "help-index",
	whatsNewPage : "whats-new/whats-new-productupdates",
	currentPage : false,
	previousHelpPage: null,
	previousHelpSection: null,
	toggleWhatsNewPage: false,
	previousPages: [],
	source: null,
	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") && KF.company.hasFeature("one_product_ui")) {
            if (this.isTourAvailable()) {
                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'/>"));

        if(KF.company.hasFeature("support_ticket")){
            this.$helpbar.append($("<span title='Message us' class='help-bar-icon help-message'/>")
                .on("click", function() {
                    if (typeof clickSupportTicket === "function") {
                        clickSupportTicket();
                    }
                }));
        }

        this.$helpbar.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.source = this.getPathName(window);

		this.setupMixpanelListeners();
	},

	getPathName: function(window) {
		try {
			return window.location.pathname;
		} catch (error) {
			return null;
		}
	},

    isTourAvailable: function(){
        return isWorkspace() || this.isIntroTourAvailable(window);
    },

    isIntroTourAvailable: function(window){
        try {
            return window.location.pathname.toString().includes("/dashboard") || window.location.pathname.toString() === "/";
        } catch (error) {
            return false;
        }
    },

    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,
					"Source": _this.source
				});
            }
        });

        $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,
					"Source": _this.source
				});
			}
		});
	},

    onResize : function() {
        var windowHeight;

        if (this.visible) {
            windowHeight = $(window).height();
            this.$root.height(windowHeight);
            this.$body.height(windowHeight - this.$menu.outerHeight(true));
        }
    },

    useLegacyHelp: function() {
        return isWorkspace() /* Klip editor */ || this.source.includes("/workspace/edit_model") /* Modeller */;
    },

    toggle: function(page, section) {
        if (KF.user.hasGoals() && !this.useLegacyHelp()) {
            PubSub.publish("TOGGLE_HELP_PAGE", { page: page, section: section });
        } else {
            this.toggleWhatsNewPage = page == this.whatsNewPage;
            if (!page && this.previousHelpPage && this.previousHelpPage != this.whatsNewPage && KF.company.hasFeature("one_product_ui")) { //return to last opened page
                page = this.previousHelpPage;
                section = this.previousHelpSection;
            } else {
                page = page ? page : this.defaultPage;
            }

            if (this.visible && this.currentPage == page) {
                this.hide();
            } else {
                this.show(page, false, false, section);
            }

            this.toggleWhatsNewPage = false;
            const selectedSection = $(".accordion > .selected");
            if (selectedSection.length > 0) {
                this.previousHelpSection = selectedSection[0].innerText;
            }
        }
    },

    show : function(page, fast, fromBack, section) {
        var windowHeight;
        var _this = this;

        if (KF.user.hasGoals() && !this.useLegacyHelp()) {
            PubSub.publish("TOGGLE_HELP_PAGE", { page: page, section: section });
        } else {
            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",
                        "Source": _this.source
                    });
                }
            }
            $.cookie("htopic", page, { path:"/" });

            if (!this.visible) {
                this.visible = true;

                $("#c-root").css( "margin-right" , this.width);
                $("#modeller-main-container").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 (!this.toggleWhatsNewPage) {
                    if (!fromBack && this.currentPage && this.previousHelpPage != page && !(this.previousPages.length == 0 && this.currentPage == this.whatsNewPage)) {
                        // Don't store the current page in the history if the user clicked the Back button.
                        this.previousPages.push(this.currentPage);
                    }
                    this.previousHelpPage = page;
                }
                this.currentPage = page;
                this.$body.scrollTop(0);
            }

            if (this.previousPages.length == 0 && page != this.defaultPage && page != this.whatsNewPage) {
                //add the default page as the previous page if the page was changed or reloaded with the help page open to a dive-in section
                this.previousPages.push(this.defaultPage);
            }

            if (this.previousPages.length == 0 || this.toggleWhatsNewPage) {
                $("#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", "");
        $("#modeller-main-container").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,
						"Source": _this.source
					});
				}

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

/* eslint-disable vars-on-top */
/* global page:false, isWorkspace:false, isPreview:false, removeItemFromArray:false, bindValues:false
    updateManager:false, Props:false, replaceMarkers:false, isIElt11:false, eachComponent:false, hasCustomStyleFeature:false */

Klip = $.klass({ // eslint-disable-line no-undef

    /** 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,
    isMetricView : 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();
        }

        this.isMetricView = config.components && config.components.length && config.components[0].type === "metric_view";
    },

    /**
     * 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;
    },

    generateMetricViewMenuModel: function(tabRights, metricId) {
        this.toolbarEl.find(".klip-button-annotation").remove();
        if (tabRights.edit) {
            this.menuModel = ["menuitem-about-metricView", "menuitem-remove_klip"];
            this.addConfigureOptionToMetricView(metricId, this.menuModel);
        } else {
            this.toolbarEl.find(".klip-button-config").remove();
        }
    },

    addConfigureOptionToMetricView: function(metricId, menuModel) {
        require(["js_root/build/vendor.packed"], function (Vendor) { //Ensure vendor bundle is loaded first
            require(["js_root/build/metrics_lib.packed"], function (MetricsLib) {
                this.unshiftConfigToMenuModel(MetricsLib, metricId, menuModel)
            }.bind(this));
        }.bind(this))
    },

    unshiftConfigToMenuModel: function(MetricsLib, metricId, menuModel){
        MetricsLib.userHasMetricAccess(metricId).then(function(result){
            if(result) {
                menuModel.unshift("menuitem-configure-metricView");
            }
        });
    },

    generateMenuModel : function(baseModel, rights, tabRights) {
        var removedItems;

        if (this.isMetricView) {
            var metricId = this.components.length > 0 ? this.components[0].metricId : null;
            this.generateMetricViewMenuModel(tabRights, metricId);
            return;
        }

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

        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(component.el) {
                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 existingVarNames = [];
        var _this = this;
        var i, len, name, value;
        var dstFilterVariableNames = {};
        var klipVariableProps = {
            variables: [],
            variableCount: 0
        };

        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];

                        _this._addUniqueVariables(klipVariableProps, existingVarNames, name, scope);
                    }
                }
            }

            var internalPropNames = cx.getInternalPropNames();
            if (internalPropNames) {
                for (i = 0, len = internalPropNames.length; i < len; i++) {
                    name = internalPropNames[i];
                    value = _this.getKlipProp(name);

                    klipVariableProps.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 (variable) {
            _this._addUniqueVariables(klipVariableProps, existingVarNames, variable, scope);
        });

        if (this.datasourceProps) {
            this.datasourceProps.forEach(function(datasourceProp) {
                _this._addUniqueVariables(klipVariableProps, existingVarNames, datasourceProp, scope);
            });
        }

        return {variables: klipVariableProps.variables, variableCount: klipVariableProps.variableCount};
    },

    _addUniqueVariables: function (klipVariableProps, existingVariables, newVariable, scope) {
        var _this = this;
        if (existingVariables.indexOf(newVariable) == -1) {
            klipVariableProps.variables.push(_this._expandProp(newVariable, scope));
            klipVariableProps.variableCount++;
        }
        existingVariables.push(newVariable);
    },

    _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();
        this.setKlipClassName();

        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);
    },

    setKlipClassName: function () {
        if (this.containerClassName) this.el.addClass(this.containerClassName);
    },

    /**
     * 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.dsWarningSystem) this.dashboard.dsWarningSystem.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.dsWarningSystem.hide();
            this.dashboard.dsWarningSystem.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 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;
    },

    hasDatasources : function() {
        if (this.getDatasources().length > 0 || this.getDatasourceIds().length > 0) {
            return true;
        }
        return false;
    },

    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);
    },

    addDataSets: function (dataSets) {
        if (dataSets && dataSets.length > 0) {
            for (var i = 0; i < dataSets.length; i++) {
                this.addDataSet({id: dataSets[i]})
            }
        }
    },

    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
        });

        if(hasCustomStyleFeature()) {
            model.push({
                type:"text",
                id:"containerClassName",
                value: this.containerClassName,
                label:widgetName + " Class(es)",
                group:"padding",
                regexRule:"^[\\w- ]*$",
                errorMessage: "Invalid identifier. Must be alphanumeric.",
                help:{ link:"klips/klip-classes" }
            });
        }

        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){
            page.paletteOpen = "component";
            $("#control-palette").css("display", "none");
            $("#control-tab").removeClass("active");

            $("#component-palette").css("display", "inline");
            $("#component-tab").addClass("active");
        }
    },

    getMetricViewComponent : function() {
        if (!_.isEmpty(this.components) && this.components[0].factory_id === "metric_view") {
            return this.components[0];
        } else {
            return null;
        }
    },

    setTitle : function(title) {
        this.title = title;
        this.updateTitle();
    },

    persistChanges: function() {
        page.invokeAction("persist_klip_schema_update", {klip: this})
    }
});

;/****** c.klipfactory.js *******/ 

/* eslint-disable vars-on-top */
/* global Component:false, Klip:false, SHA1:false, DX:false, isPreview:false */

KlipFactory = $.klass({ // eslint-disable-line no-undef

    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); // eslint-disable-line no-console
                }
            }
        }

        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();
    },

    getNextId : function() {
		return (SHA1.encode(Math.random()+"").substring(0,8) + "-" + this.componentUUID++);
	},

    /**
     * //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 = this.getNextId();

        // 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(); // eslint-disable-line no-undef
;/****** c.mobile.js *******/ 

/* eslint-disable vars-on-top */
/* global Dashboard:false, KlipFactory:false, updateManager:flase, DX:false */

MobileDashboard = $.klass( Dashboard , { // eslint-disable-line no-undef

    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); // eslint-disable-line no-console

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

/* eslint-disable vars-on-top */

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

    // 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);
        if (filteredTabs) {
            sortTabs(filteredTabs);
        } else {
            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({ //eslint-disable-line no-unused-vars

    /** */
    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) {
        var $parentQuery;

        if ($t.hasClass("comp"))  return $t;
        $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) {
        var $t, $pc;

        if ( !this.isEnabled ) return;

        $t = $(evt.target);
        $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* componentthis.onComponentOut( this.$overCx );
                this.onComponentOver( $pc );
            }

        } else {
            // otherwise, we're not currently over a cx.
            if ($pc) {
                // we just moved over a cx, update our internal state, and fire onComponentOverthis.$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) {
        var dx,dy;

        if (! this.isEnabled ) return;

        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, { // eslint-disable-line no-undef

    selectedColumnId: "",

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

        if (config && config.visualizerId) {
            this.visualizerId = config.visualizerId;
        }

        this.el.addClass("data-set-visualizer");
    },

    render: function() {
        require(["js_root/build/vendor.packed"], function(Vendor) {
            require(["js_root/build/dataset_visualizer_index.packed"], function(DataSetVisualizer) {
                DataSetVisualizer.render(
                    this.dataSet,
                    this.el[0]
                );
            }.bind(this));
        }.bind(this));
    },

    getPointer: function() {
        // TODO: implement getPointer.
    },

    setPointer: function(address) {
        PubSub.publish("data-set-column-selected", {id: address});
    },

    getDataForPointer: function(pointer) {
        // TODO: implement getDataForPointer.
        return false;
    },

    scrollToSelection: function(pointer) {
        return false;
    }
});
;/****** c.visualizer.table.js *******/ 

/* eslint-disable vars-on-top */

/**
 *
 * renders tabular data in a grid.
 * data should be in the format:
 *   [
 *      {col:'A',value:'text'},
 *      ...
 *   ]
 *
 */
Visualizer.Table = $.klass(Visualizer.DatasourceVisualizerBase, { // eslint-disable-line no-undef

    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"],

    DATASOURCE_PREVIEW_TEXT : "This preview displays a sample portion of your data.",

    autoScrollTimer: undefined,
    truncate: true,
    previewOnly : false,

    CELL_REFERENCE_REGEX: /^([a-zA-Z]+)(\d+)/,

    initialize : function($super,config) {
        $super(config);
		this.previewOnly = !!(config && config.previewOnly);
        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;
			if (this.previewOnly) {
				this.el.append($("<div>")
                    .addClass("previewOnly")
                    .html(this.DATASOURCE_PREVIEW_TEXT)
                );
			} else {
				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 cell = $("<th>").html(colId).attr("p",pointer);
            tr.append(cell);
        }

        table.append(th);
    },

    getHeaderPointer : function(colId){
        return colId + ":" + colId;
    },


    getColdId : function(idx) {
        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 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 (  this.isRowSelector(p) ) {
                result = this.isRowSelector(p);
                var rowIdx = parseInt(result);
                this.el.find("tr:eq("+rowIdx+") td").addClass("selected");
            } else if( this.isColSelector(p) ) {
                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);
            }
        } else {
            // otherwise, this must be a cell
            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 ( this.matchRangeElement(p1) ) {
            result = this.matchRangeElement(p1);
            col1Idx = this.getIdxForColId(result[1])+1;
            row1Idx = parseInt(result[2]);
        }
        if ( this.matchRangeElement(p2) ) {
            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 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 ( this.isRowSelector(p)) {
                result = this.isRowSelector(p);
                var rowIdx = parseInt(result);
                scrollDistY = this.el.find("tr:eq(" + rowIdx + ") td")[1].offsetTop;
                scrollDistX = 0;
            } else if (this.isColSelector(p)) {
                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;
                }
            }
        } else {
            // otherwise, this must be a cell
            $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, { // eslint-disable-line no-undef

    currentSheet : 0,
    workbook : false,


    setData : function(data) {
        this.workbook = data;
        if (this.workbook && this.workbook.length > 1) {
            this.populateSelect();
            $("#vizOptionsPanel").show();
        }

        this.onChangeSheet();
        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);
    },

    onChangeSheet: function() {
        var $curSheet;

        if (!this.workbook) return;

        $curSheet = $("#curSheet");
        if($curSheet) {
            this.selectSheet($curSheet.val());
        }
    },

    selectSheet: function(sheetIndex) {
        var changed = false;
        if (this.currentSheet != sheetIndex && this.workbook.length > 1) {
            changed = true;
        }

        if (sheetIndex >= 0) {
            this.currentSheet = sheetIndex;
        }

        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, callback) {
        var sheetIndex;
        if (p) {
            if(p.address) {
                p = p.address;
            }
            sheetIndex = this.getIndexFromName(p.substring(0, p.lastIndexOf(",")));
            if(callback && sheetIndex >= 0) {
                callback(sheetIndex);
            }
            $("#curSheet").val(sheetIndex);
            this.onChangeSheet();
        }

        $super(p);
    }

 });
;/****** c.visualizer.tree.js *******/ 

/* eslint-disable vars-on-top */
/* global KF:false, Visualizer:false, help:false, DX: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']
 *
 *
 *
 */

const SPECIAL_XPATH_TERMS = [" ","'","\"","/",".","*","@",":","(",")","[","]","+","-","div","mod"];

//For an address that contains an element inside of an array, denotes all parts of the address that specify an index
//Eg for "/parent[1]/childArray[2]/element", the regex would signify "/parent/childArray/element" [1] and [2], inside the address
const SQUARE_BRACES_AND_DIGITS_REGEX = new RegExp(/\[\d+\]/g);

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(SQUARE_BRACES_AND_DIGITS_REGEX,"");

        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); // eslint-disable-line no-console
            } 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(SQUARE_BRACES_AND_DIGITS_REGEX,"");
                        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 ";

        if (this.config && this.config.visualizerId && this.config.visualizerId === "modeller") {
            pathString += "Model";
        } else {
            pathString += 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 name = n.qualifiedName;
        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(SQUARE_BRACES_AND_DIGITS_REGEX,"");
        var parent;

        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: []});

        
        parent = this.getSelectedNodeParent(ancestors);

        //Special selection option, to use kf:fill_elements
        if (parent != null && this.selectedNode["~adr"]) {
            const paddedAddress = this.buildPaddedAddress().replace(SQUARE_BRACES_AND_DIGITS_REGEX,"");
            options.push({desc: `Select all <span>${eltName}</span> elements in all <span>${parent["~propname"]}</span> elements, adding blanks for missing <span>${eltName}</span> elements.`,
                path: this.insertSelectors(paddedAddress, [{name:eltName,value:eltIndex}]),
                lock: [ancestors[ancestors.length - 1]]});
        }

        //Special selection option, to use kf:names
        if (parent != null && this.selectedNode["~adr"]) {
            const namedAddress = this.buildNamedAddress().replace(SQUARE_BRACES_AND_DIGITS_REGEX,"");
            options.push({desc: `Select all field names in <span>${parent["~propname"]}</span>.`,
                path: this.insertSelectors(namedAddress, [{name:eltName,value:eltIndex}]),
                lock: [ancestors[ancestors.length - 1]]});
        }

        // the nth element in parent array
        if (parentArray != null) {
            parent = this.getSelectedNodeParent(ancestors)
            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
        var special = Visualizer.containsSpecialTerms(eltName);
        var eltNameTxt = eltName;
        if (special) {
            eltNameTxt = Visualizer.buildPathForSpecialTerms(eltName);
        }
        options.push({desc: "Select all <span>" + eltName + "</span> elements.", path: "//" + eltNameTxt, lock: []});

        // in this parent, grandparent, etc. up to root
        for (i = 0; i < ancestorPaths.length; i++) {
            options.push(ancestorPaths[i]);
        }

        //All children in this parent (parent/*)
        parent = this.getSelectedNodeParent(ancestors)
        if (parent != null) {
            var wildcardAddress = this.buildWilcardAddress();
            options.push({desc: "Select everything in <span>" + parent["~propname"] + "</span> elements.",
                path: this.insertSelectors(wildcardAddress, [{name:eltName,value:eltIndex}]),
                lock: [ancestors[ancestors.length - 1]]});
        }

        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);
        }
    },

    /**
     * Given the ancestors for a selected JSON node, return the most immediate parent node name, or null, if there is none.
     * Note that for JSON data, the ancestor immediately above will be just the specific index of the parent, instead of the parent node itself.
     * Thus, it is necessary to look one level further
     *  Eg for "/parent/child", "child" would have ancestors with names [parent, parent {1}, child]
     * 
     * @param ancestors: A list of ancestors for the selected JSON node, ordered from most distant to most immediate
     * 
     * @return: The most immediate ancestor node to the selected JSON node, or null, if there is none
     */
    getSelectedNodeParent : function(ancestors) {
        return ancestors.length > 1 ? ancestors[ancestors.length-2] : null;
    },
        

    /**
     * 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"];

                //Account for special characters in the path
                var special = Visualizer.containsSpecialTerms(txt);
                if (special) {
                    txt = Visualizer.buildPathForSpecialTerms(txt);
                }

                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("/");
    },

    /**
     * go from the selectedNode up to the root to create an xpath, with the selectedNode being replaced with a wildcard(*)
     * JSON specifically
     */
    buildWilcardAddress : function() {
        const newPath = [];

        const path = this.path || "";
        const pathLength = path.length;

        for (let index = 0; index < pathLength; index++) {

            const isLastIndex = index === (pathLength - 1);

            //If we are on the selectedNode, return the wildcard
            if (isLastIndex) {
                newPath.push("*");
                break;
            }

            const pathSegment = this.path[index];

            //If the current level has no name, continue
            if (!pathSegment.node["~propname"]) {
                continue;
            } 

            let text = pathSegment.node["~propname"];

            //Account for special characters in the path, if any
            if (Visualizer.containsSpecialTerms(text)) {
                text = Visualizer.buildPathForSpecialTerms(text);
            }

            //If the current level is an element inside of an array...
            if (pathSegment.locked) {
                text += Visualizer.processLockedElement(pathSegment,index,isLastIndex,this.path)
            }

            newPath.push(text);

        }

        return "/" + newPath.join("/");
    },

    /**
     * Goes from the selectedNode up to the root to create an xpath, and uses the kf:fill_elements function to pad the values.
     * Constructs a final path with the given structure:
     *  kf:fill_elements(path of all parent nodes, "final child name")
     *
     */
    buildPaddedAddress : function() {
        const newPath = [];
        let finalNodePath;

        const path = this.path || "";
        const pathLength = path.length;

        for (let index = 0; index < pathLength; index++) {

            const isLastIndex = index === (pathLength - 1);
            const pathSegment = this.path[index];

            //If the current level has no name, continue
            if (!pathSegment.node["~propname"]) {
                continue;
            }
            let text = pathSegment.node["~propname"];

            //Check the final node in the path
            //Since kf:fill_elements will not work when the final node is inside of an array, we should check do this before checking if the final node is locked
            //Also, the second parameter of kf:fill_elements(the final node) is already in quotations,special characters do not need to be accounted for there. However, both types of quotes need to be accounted for, in similar fashion to regular xpath
            if (isLastIndex) {
                finalNodePath = Visualizer.buildXPathString(text);
                break;
            }



            //Account for special characters in the path
            if (Visualizer.containsSpecialTerms(text)) {
                text = Visualizer.buildPathForSpecialTerms(text);
            }

            //If the current level is an element inside of an array...
            if (pathSegment.locked) {
                text += Visualizer.processLockedElement(pathSegment,index,isLastIndex,this.path)
            }

            newPath.push(text);

        }

        return "kf:fill_elements(" + newPath.join("/") + "," + finalNodePath + ")";
    },

    /**
     * go from the selectedNode up to the root to create an xpath, and use the kf:names function to retrieve the name(key) values of the parent, instead of the actual values.
     * JSON specifically
     */
    buildNamedAddress : function() {
        const newPath = [];

        const path = this.path || "";
        const pathLength = path.length;

        for (let index = 0; index < pathLength; index++) {

            const isLastIndex = index === (pathLength - 1);

            //If we are on the selectedNode, stop
            if (isLastIndex) {
                break;
            }

            const pathSegment = this.path[index];

            //If the current level has no name, continue
            if (!pathSegment.node["~propname"]) {
                continue;
            } 

            let text = pathSegment.node["~propname"];

            //Account for special characters in the path, if any
            if (Visualizer.containsSpecialTerms(text)) {
                text = Visualizer.buildPathForSpecialTerms(text);
            }

            //If the current level is an element inside of an array...
            if (pathSegment.locked) {
                text += Visualizer.processLockedElement(pathSegment,index,isLastIndex,this.path)
            }

            newPath.push(text);

        }

        return "kf:names(" + 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;
};

/**
 * Checks to see if the given path contains any special XPath terms that need to be accounted for, or starts with a number
 * @param path: The given path to check
 */
Visualizer.containsSpecialTerms = function(path) {

    if (!(path && (typeof path)==="string")) {
        return false;
    }

    //Check if the first character of the string is a number
    if (path.length>0 && path[0] <="9" && path[0] >="0") {
        return true;
    }

    //Check if there are any other special xpath terms in the path
    return SPECIAL_XPATH_TERMS.some((term) => path.includes(term));
};

/**
 * Given some text, convert this text to a string that can be read in XPath
 * @param text: The given string to convert
 */
Visualizer.buildXPathString = function(text) {

    if (!(text && (typeof text)==="string")) {
        return "";
    }

    //Capture exact name inside of double quotes
    //This is done when the text contains single quotes, but not double quotes
    //Also default case, if text has no quotes at all
    if ( (text.includes("'") && !text.includes("\"")) || (!text.includes("'") && !text.includes("\"")) ) {
        return `"${text}"`;
    }

        //Capture exact name inside of single quotes
    //When the text itself contains a double quote, since XPath 1 cannot escape characters, we simply use single quotes to surround the string
    else if (text.includes("\"") && !text.includes("'")) {
        return `'${text}'`;
    }

    //In the rare case of a string containing both a single and double quote, since we cannot escape either, we need to come up with a string put together using a concat of multiple substrings
    else if (text.includes("\"") && text.includes("'")) {
        const doubleQuotedPath = Visualizer.buildPathForSingleAndDoubleQuotes(text);
        return doubleQuotedPath;
    }
};

/**
 * Given a locked element(eg, an Element that is inside an array, not a Dict), return the XPath that corresponds to that specific element of the array
 * @param currentPathSegment: The current path element we are examining, which is assumed to be locked
 * @param index: Index of the current path element, in the path
 * @param isLastIndex: If the current index is the last of the path
 * @param path: The entire path
 */
Visualizer.processLockedElement = function(currentPathSegment, index, isLastIndex, path) {
    if (isLastIndex){
        return `[${currentPathSegment.node["~index"]}]`;
    } else {
        const nextIndex = path[index + 1].node["~index"];
        return `[${nextIndex}]`;
    }
}

/**
 * Given an xpath level that contains a special character(or starts with a number), returns an xpath expression that accounts for it
 * @param path: The given path to check for special terms in
 */
Visualizer.buildPathForSpecialTerms = function(path) {

    //Take the path, and format it to work wtih XPath 1
    const formattedPath = Visualizer.buildXPathString(path);

    return `*[name()=${formattedPath}]`;
};

/**
 * Given an xpath level that contains both single and double quotes in it, compose an xpath expression that can account for this, by combining multiple substrings with different surrounding quotes.
 * For example, giving the following string:
 *      My "name" is 'Josh'
 *  both of the following are invalid:
 *      "My "name" is 'Josh'"
 *      'My "name" is 'Josh''
 *
 *  To get around this, we come up with a combination of substrings, that will be put together later on.
 *  For example:
 *      'My "name" is '
 *      "'Josh'"
 *  Notice how the first string is surrounded via single quotes, and the second with double quotes. As long as we alternate the quotes used for surrounding the text, we can deal with any combination of both types of quotes
 *
 * @param path: The given path to check
 */
Visualizer.buildPathForSingleAndDoubleQuotes = function(path) {

    /**
     * Given a single or double quote, return the other one
     * @param current
     */
    function otherQuote(current) {
        if (current === "\"") {
            return "'";
        }
        return "\"";
    }

    //Find the first quote that appears in the path, so we know which surrounding quote to use(the other one)
    let currentQuote;
    if (path.indexOf("'") > path.indexOf("\"")) {
        currentQuote = "'";
    } else {
        currentQuote = "\"";
    }

    let output = currentQuote;

    //Go through every character in the string
    //When we encounter the character we are using as the surrounding quote inside the path, start a new substring, using the 'other quote' as the surrounding quote
    for (var i = 0; i < path.length; i++) {
        output += path[i];

        //If the current character inside the path is our surrounding quote, start a new substring, and use the other surrounding quote
        if (path[i] === currentQuote) {
            output += "," + otherQuote(currentQuote) + currentQuote;
            currentQuote = otherQuote(currentQuote);
        }
    }

    return `concat(${output}${currentQuote})`;
}
;/****** c.visualizer_root.js *******/ 

/* global page:false */

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 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) {
        var index;

        v.$container.remove();

        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;
        var i;
        var numRows;

        for(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()) {
                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);
        var targetVisualizer;

        this.currentPointer = pointer;

        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;
        var $vizOptionsPanel;

        if (this.activeVisualizer) {
            this.activeVisualizer.hide();
        }

        visualizer.activateTab();

        if (canViewDataProvider) {
            if(visualizer.pointer) visualizer.renderPathBar(false);
            visualizer.$container.show();
        } else {
            visualizer.$container.show();
            visualizer.renderPermissionError(dataProvider.creator);
        }

        this.activeVisualizer = visualizer;

        $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")];
        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: 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.user.hasPermission("datasource.library")) {
        model.push(
            {id: "menuitem-about", text: "About Modelled Data Source", actionId: "view_modelled_datasource", actionArgs: ctx.id},
            {id: "separator-b", separator: true}
        );
    }

    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("datasource.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));

        PubSub.subscribe("dataset-updated", function(event, data) {
            var dataSet;

            if(data[0] && data[0].dataSet && data[0].columns){
                dataSet = {
                    id: data[0].dataSet.id,
                    columns: data[0].columns
                };

                this.formulaEditor.onDataSetUpdated(dataSet);
            }

        }.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 newDst, vcFormula;

        //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)
        }

        //Create a dummy dst which just has the formula in its virtual columns
        //This is necessary for model references
        newDst = DST.createDummyDST(this.activeComponent.getKlipInstanceId(), processedFormula);
        dsts.unshift(newDst);

        //The formula is now the pointer to the virtual column
        vcFormula = DST.getVCReferenceText(newDst.id, newDst.virtualColumns[0].id);


        query = {
            kid: this.activeComponent.getKlipInstanceId(),
            klip_pid: this.activeComponent.getKlipPublicId(),
            fm: vcFormula,
            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");
    },

    describeDataSets: function(dataSetIds, callback) {
        var promise = $.Deferred();
        var _this = this;

        $.post("/dataSets/ajax_describe", {ids: dataSetIds}, function(data) {
            _this.dataSets.forEach(function(dataSet) {
                if (!data[dataSet.id]) {
                    return;
                }

                dataSet = $.extend(dataSet, data[dataSet.id]);
                promise.resolve(dataSet);
            });

            _this.updateMenu();
        }, "json");

        return promise;
    },

    /**
     *
     */
    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_root/build/vendor.packed"], function() {
            require(["js_root/build/applied_actions_main.packed"], 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")
                .attr("data-pendo", "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);
    },

    getDataSetColIdFromName: function(dataset, name) {
        var col;
        if( !(dataset && dataset.columns) ) {
            return "";
        }

        col = _.find(dataset.columns, function(col){
            return col.name == name
        });

        return col ? col.id : "";
    },

    isDataSetPointer: function(pointer){
        return pointer && pointer.dt === "set";
    },

    showPointerInVisualizer: function(pointer) {
        var dataId, foundDataSet;
        if (pointer) {
            dataId = pointer.id;

            if(this.isDataSetPointer(pointer)) {
                foundDataSet = _.find(this.dataSets, function (dataset) {
                    return dataset.id === dataId;
                });

                if (foundDataSet) {
                    pointer.address = this.getDataSetColIdFromName(foundDataSet, pointer.address);
                    this.visualizerTabPane.selectPointer(pointer);
                } else {
                    this.activeKlip.addDataSet({id : 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;
    },

    addDataSet: function(dataSet, options, dataRenderedCallback) {
        var _this = this;

        if (this.getDataSet(dataSet.id)) {
            // Prevent duplicate DataSet adds.
            return;
        }

        _this.dataSets.push(dataSet);

        if (!dataSet.name) {
            this.describeDataSets([dataSet.id]).then(function(describedDataSet) {
                dataSet.visualizer = Visualizer.createforFormat("dataset", { visualizerId: Workspace.VISUALIZER_ID });
                dataSet.visualizer.setDataSet(dataSet, dataRenderedCallback).then(function(fullDataSet) {
                    $.extend(dataSet, fullDataSet);
                    _this.visualizerTabPane.addVisualizer(dataSet.visualizer, options);

                    if (_this.formulaEditor) {
                        _this.formulaEditor.setDataSetInstances(_this.dataSets);
                    }
                });
            });
        }
    },

    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;
        var invalidDatePickerRef;

        if(!validReferencesMap) validReferencesMap = {};

        if (rootComponent) {
            possibleRef = false;
            possibleResultsRef = false;
            invalidRefToDstPeer = false;
            invalidRefToDstRoot = false;
            invalidDatePickerRef = 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);
					invalidDatePickerRef = currentComponent.isDatePickerInternalComponent();
                    possibleResultsRef = possibleResultsRef && !invalidRefToDstRoot && !invalidDatePickerRef;
                    dependent = currentComponent.isDependentComponent();

                    possibleRef = (possibleRef && !invalidDatePickerRef && (!dependent || !invalidRefToDstRoot));
                }
            }

            model = {
                component: currentComponent,
                isPossibleFormulaReference: possibleRef,
                isPossibleResultsReference: possibleResultsRef,
                invalidRefToDstPeer: invalidRefToDstPeer,
                invalidRefToDstRoot: invalidRefToDstRoot,
				invalidDatePickerRef: invalidDatePickerRef
            };
        } else {
            model = {
                component: null,
                isPossibleFormulaReference: false,
                isPossibleResultsReference: false,
                invalidRefToDstPeer: false,
                invalidRefToDstRoot: false,
				invalidDatePickerRef: 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 *******/ 

/* global mixPanelTrack:false */

WorkspaceKlipSaveManager = $.klass({ // eslint-disable-line no-undef
    isSaving: false,

    showUpdateUI: function(savePromise, beforeMessage, afterMessage, options) {
        var showMessagePromise;
        var _this;
        var resErrCode, resErrText;

        options = options || {};

        this.isSaving = true;
        this.disableSaveActions();
        showMessagePromise = this.showSaveMessage(beforeMessage, options);
        _this = this;

        $.when(showMessagePromise, savePromise).
            done(function() {
                _this.hideSaveMessage(afterMessage);
            }).fail(function(jqXHR, textStatus, errorThrown) {
                _this.hideSaveMessage("Error saving " + KF.company.get("brand", "widgetName") + ".");
                resErrCode = jqXHR.status;
                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) {
        var fadeInPromise = $("#autoSaveText")
            .html(text)
            .fadeTo(750, 1)
            .delay(250)
            .promise();

        options = options || { spinner: false };

        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) {
        $("#autoSaveText").html(afterText);

        $("#saveKlipSpinner").fadeTo(150, 0);
        $("#autoSaveText").delay(250).fadeTo(350, 0);
    },

    formDataTooLargeErrorMessage: function () {
        var widgetName = KF.company.get("brand", "widgetName");
        var $content, $overlay;

        if(typeof mixPanelTrack == "function") {
          mixPanelTrack("Form data too large");
        }

        $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>"));

        $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();
    }
});;