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

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

// 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) {
    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,
                    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);

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

        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 });
        } else {
            $.post("/dashboard/ajax_setProps", { props:data });
        }
    },

    getRootPath : function() {
        return "/";
    },

    getLocationOrigin : function() {
        return getLocationOrigin();
    },

    getAssetInfo : function(assetId, successCallback) {
        if (!assetId) return;

        $.ajax({
            type: "POST",
            url: "/assets/ajax_assetInfo/" + assetId,
            async: false,
            success: successCallback,
            dataType: "json",
            data: {publishedProfileId: this.config.publishedProfileId}
        });
    },

    sortKlipsByOffset: function(klipA, klipB) {
        var offsetA = klipA.el.offset();
        var offsetB = klipB.el.offset();

        var x1 = offsetA.left;
        var y1 = offsetA.top;
        var x2 = offsetB.left;
        var y2 = offsetB.top;

        if ((x1 < x2 && y1 <= y2) || (x1 >= x2 && y1 < y2)) {
            return -1;
        } else if ((x1 <= x2 && y1 > y2) || (x1 > x2 && y1 >= y2)) {
            return 1;
        } else {
            return 0;
        }
    },

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

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

        if (this.containerClassName) this.el.addClass(this.containerClassName);

        this.toolbarEl = $("<div>").addClass("klip-toolbar").append($("<div class='klip-toolbar-handle'>"));

        var titleText = this.getVariableKlipName();
        this.toolbarEl.append($("<span>").addClass("klip-toolbar-title").html(titleText.length === 0 ? "&nbsp;" : titleText));

        var warningIcon = $("<div>").addClass("klip-button klip-button-warning").attr("title", "Warnings").hide();
        var annotationsIcon = $("<div>").addClass("klip-button klip-button-annotation").attr("title", "Comments");
        var configIcon = $("<div>").addClass("klip-button klip-button-config").attr("title", "Menu");

        this.toolbarEl.append(
            $("<div>").addClass("klip-toolbar-buttons")
                .append(configIcon)
                .append(KF.user.hasPermission("dashboard.annotation.view") ? annotationsIcon : "")
                .append(KF.user.hasPermission("dashboard.ds_warning.view") ? warningIcon : "")
        );
        this.el.append(this.toolbarEl);

        // attach hover & click event handlers to the toolbar
        if (!this.dashboard.config.workspaceMode && !this.dashboard.config.mobile) {
            this.toolbarEl
                .click($.bind(this.onToolbarClick, this))
                .disableSelection();
        }

        if (this.fixedHeight) {
            this.el.height(this.fixedHeight);
        }


        this.bodyEl = $("<div>").addClass("klip-body");
        var sampleDataButton = this.generateSampleDataButton();
        if (sampleDataButton) {
            this.el.append(sampleDataButton);
        }

        if (this.dashboard.config.workspaceMode) {
            this.el.addClass("drop-target").attr("drop-region", "klip");
            this.bodyEl.addClass("drop-target").attr("drop-region", "klip-body").css("min-height", "37px");
        }

        this.messageEl = $("<div>").addClass("klip-message").append($("<div class='inner-message quiet x-small'>"));

        this.$emptyMessage = $("<div>").addClass("klip-empty")
            .append($("<div>").addClass("klip-empty-arrow"))
            .append($("<div>").addClass("klip-empty-icon"))
            .append($("<h2>").addClass("klip-empty-header").text("This " + KF.company.get("brand", "widgetName") + " is empty."))
            .append($("<div>").addClass("klip-empty-subheader").text("Drag in a component to start previewing!"));

        this.$componentDropMessage = $("<div>").addClass("klip-component-drop-message")
            .append($("<p>").addClass("klip-component-drop-header").text("Drag component here"));

        if (this.dashboard.config.workspaceMode) {
            this.$emptyMessage.hide().appendTo(this.bodyEl);
        }
        this.$componentDropMessage.hide();

        this.el.append(this.bodyEl)
            .append(this.messageEl);
        return this.el;
    },

    resetDom : function() {
        this.setDimensions(this.KLIP_MIN_WIDTH_ZERO_STATE);
        this.$klipResizable.trigger("setZeroStateMinWidth");
        this.$emptyMessage.appendTo(this.bodyEl).show();
        this.openComponentsTab();
    },

    afterFactory : function() {
        if(this.usesSampleData && this.hasSampleData === false) {
            var $message = $("<div>");
            $message.text("This " + KF.company.get("brand", "widgetName") + " doesn't have any data yet.");
            this.addMessage({$message:$message}, "info");
        }
    },

    /**
     * adds a component to the Klip with an optional constraint that tells the layout
     * manager how to position the component
     * @param c
     * @param layoutConstraint
     */
    addComponent : function(c, layoutConstraint) {
        if (this.components.length == 0) this.bodyEl.css("min-height", "");

        if (layoutConstraint) {
            var $prev = layoutConstraint.prev;

            if ($prev) {
                if ($prev.length == 0) {
                    this.components.splice(0, 0, c);
                    this.bodyEl.prepend(c.el);
                } else {
                    var prevCx = this.getComponentById($prev.attr("id"));
                    var prevIdx = _.indexOf(this.components, prevCx);

                    this.components.splice(prevIdx + 1, 0, c);

                    $prev.after(c.el);
                }
            }
        } else {
            this.components.push(c);
            this.bodyEl.append(c.el);
        }

        this.setComponentPadding();

        c.parent = this;
        c.onKlipAssignment();
    },

    /**
     * removes a component from the Klip
     * @param c
     * @param detach - if true, detach the component el from the Klip instead of removing it, and do not unwatch its formulas (used when about to re-add)
     */
    removeComponent : function(c, nextActive, detach) {
        var len = this.components.length;

        while(--len != -1) {
            if( this.components[len] == c) {
                this.components.splice(len,1);

                c.onKlipRemoval();

                if (detach) {
                    c.el.detach();
                } else {
                    c.el.remove();
                    if (c.usePostDSTReferences()) {
                        c.destroy();
                    }
                }

                if (this.components.length == 0) {
                    this.resetDom();
                }

                this.setComponentPadding();

                return;
            }
        }
    },

    removeComponents : function() {
        var len = this.components.length;
        var c;
        while (--len != -1) {
            c = this.components[len];

            this.removeComponent(c);
        }
    },


    setComponentPadding : function() {
        var _this = this;
        var numComponents;
        var visibleComponents = this.components;

        if (!isWorkspace()) {
            visibleComponents = this.components.filter(function(component) {
                return (component.visible && component.el);
            });
        }

        numComponents = visibleComponents.length;

        visibleComponents.forEach(function(component, i) {
            if(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();

        if (!this.isVisible) return;

        var len = this.components.length;

        if (len > 0) {
            if (!callback) {
                callback = _.bind(this.displayMessages, this);
            }

            var queueOpts = { r:"klip-update", batchId: this.id, batchCallback: callback };

            while (--len != -1) {
                updateManager.queue(this.components[len], queueOpts);
            }
        } else {
            if (callback) {
                callback();
            } else {
                this.displayMessages();
            }
        }
    },

    setTitleSize: function() {
        this.toolbarEl.removeClass("klip-toolbar-xx-small klip-toolbar-x-small klip-toolbar-small klip-toolbar-new-small klip-toolbar-medium klip-toolbar-large klip-toolbar-x-large klip-toolbar-xx-large");
        this.toolbarEl.addClass("klip-toolbar-"+ this.titleSize);
    },

    /**
     * Maps the old Titles Sizes to the new ones.(Refer : Props.Defaults.klipTitleSizeMap )
     * @param config
     */
    updateOldTitleSizes: function (config) {
        if (config && config.titleSize && (config.titleSize === "small" || config.titleSize === "small-medium")) {
            this.titleSize = Props.Defaults.klipTitleSizeMap[config.titleSize];
        }
    },

    updateTitle : function() {
        var titleText = this.getVariableKlipName();

        if (this.hasDynamicTitle) {
            // update mobileUI title if applicable
            // -----------------------------------
            if (this.$listItem) this.$listItem.find("a").text(titleText);
            if (this.$mobileCard) this.$mobileCard.find("h1").text(titleText);
        }

        this.toolbarEl.find(".klip-toolbar-title").html(titleText.length === 0 ? "&nbsp;" : titleText);

        this.toolbarEl.toggleClass("hidden", !this.showToolbar);
    },

    getVariableKlipName : function() {
        var titleText = this.title ? this.title : "";

        if (this.hasDynamicTitle) {
            titleText = replaceMarkers(/\{([^}]*)\}/, this.title, _.bind(this.getKlipProp, this));
        }

        return titleText;
    },


    setVisible : function(isVis) {
        if ( this.isVisible == isVis ) return;
        this.isVisible = isVis;
        this.el.toggle(isVis);
        this.handleEvent( isVis ? {id:"klip-show"} : {id:"klip-hide"} );
//		if (isVis) this.update();
//        if(isVis && this.dashboard.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
        });

        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);
        sortTabs(filteredTabs);

        var $tabPanel = $("#tab-panel").empty();
        var $tabPageControls = $(".tabPageControl");

        if (filteredTabs.length == 0) {
            $tabPageControls.hide();
            $tabPanel.append($("<div class=\"italics\" style=\"color:#999999;\">You don't have access to any Dashboards for this selection.</div>"));
        } else {
            $tabPageControls.show();

            var upperLimit = start;
            var i, len, tempRow = "";
            for (i = start, len = start + 12; i < len; i++) {
                if ((i + 1) % 3 == 0 || i == filteredTabs.length) {
                    $tabPanel.append(tempRow);
                }

                if (i >= filteredTabs.length) {
                    break;
                }

                if (i % 3 == 0 || i == start) {
                    tempRow = $("<tr style='height:16px'>");
                }

                tempRow.append($("<td>")
                    .append($("<div id='add-tab-" + filteredTabs[i].publicId + "'>")
                        .addClass("action-add panel-add-tab")
                        .attr("actionId", "add_tab")
                        .attr("actionArgs", filteredTabs[i].publicId)
                        .text(filteredTabs[i].name)));

                upperLimit++;
            }

            $("#numPageTabs").empty().append((start + 1) + " to " + upperLimit + " of " + filteredTabs.length);

            searchTotal = filteredTabs.length;

            if (upperLimit == filteredTabs.length) {
                //disable page up
                $("#tabPageRight").addClass("disabled");
            } else {
                $("#tabPageRight").removeClass("disabled");
            }

            if (start == 0) {
                //disable page down
                $("#tabPageLeft").addClass("disabled");
            } else {
                $("#tabPageLeft").removeClass("disabled");
            }
        }
    }

    function sortTabs(tabsToFilter) {
        if ($sortInput.val() == "Last Updated") {
            tabsToFilter.sort(function(a, b) {
                if (a.lastUpdated < b.lastUpdated) return 1;
                if (a.lastUpdated > b.lastUpdated) return -1;
                return 0;
            });
        } else if ($sortInput.val() == "Name") {
            tabsToFilter.sort(function(a, b) {
                if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
                if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
                return 0;
            });
        }

    }


    function getTabs() {
        // do some ajax stuff, then render...
        if (typeof dashboard != "undefined") {

            $.ajax({
                type:"GET",
                data:{val:$tabListInput.val()},
                url:"/tabs/ajax_getDashboardTabs",
                success : function(resp) {

                    tabs = resp.sharedTabs;
                    render();
                },
                error : function(resp) {
                }
            });
        }
    }

    function pageLeft() {
        if (currentPage != 0) {
            currentPage--;
            render();
        }
    }

    function pageRight() {
        if ((currentPage+1) * 12 < searchTotal) {
            currentPage++;
            render();
        }
    }

    function clearSearch(){
        currentPage=0;
        $searchInput.val("");
        render();
    }

    function searchTabs(tabsToFilter){
        if (!$searchInput.val()) {
            $("#tabSearchPanel").removeClass("clear");
            return tabsToFilter;
        } else {
            $("#tabSearchPanel").addClass("clear");
            var newList = [];
            var searchVal = $searchInput.val().toLowerCase();
            for (var $x = 0; $x < tabsToFilter.length; $x++) {
                if (tabsToFilter[$x].name.toLowerCase().indexOf(searchVal) != -1) {
                    newList.push(tabsToFilter[$x]);
                }
            }
            return newList;
        }
    }


    // return an object that contains the 'publicly' accessible methods/properties
    return {
        "render" : render,
        "getTabs" : getTabs
    };
})();
;/****** c.tooltip_handler.js *******/ 

var TooltipHandler = $.klass({ //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"],

    autoScrollTimer: undefined,
    truncate: true,

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

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

        var _this = this;

        PubSub.subscribe("row-decorated", function(evtName, args) {
            if (_this.config.visualizerId == args.visualizerId) {
                _this.decorateRow(args.rowIndex, {
                    addClass: args.addClass,
                    removeClass: args.removeClass
                });
            }
        });
    },


    /**
     * render the visualizer markup
     * @param $super
     */
    render : function($super,target) {
        $super();

        // find the max column count
        var nRows = this.data.length;
        var _this = this;
        this.maxCols = 0;
        this.startingCell = 0;
        this.lastSelectedCell = 0;
        this.previousCell = 0;
        this.isDragging = false;
        while(--nRows > -1) {
            this.maxCols = Math.max(this.maxCols, this.data[nRows].length);
        }

        var table = $("<table>").addClass("data-vis-table").addClass("unselectable");
        this.renderHeader(table, this.data[0]);

        for( var i = 0 ; i < this.data.length; i++) {
            var row = this.data[i];
            this.renderRow(table, row, i);
            if (i == 199 && this.truncate) break;
        }

        // attach click handler for table
        // -------------------------------
        table.mouseover( $.bind(this.onMouseOver,this) );
        table.mousedown( $.bind(this.onMouseDown,this) );
        table.mousemove( $.bind(this.onMouseMove,this) );

        $(document).mouseup($.bind(this.onMouseUp,this));

        table.disableSelection();

        this.el.append(table);

        if ( this.truncate && this.data.length > 200 ) {
            var numHidden = this.data.length - 200;
            this.el.append( $("<div>").addClass("truncatedRow").html("To make this page load faster, <span class='strong'>" + numHidden + "</span> rows are not being shown. <span class='strong link'>Show all items.</span>"));
            $(".truncatedRow").click(function() {
                _this.truncate = false;
                _this.render($super, "");
            });
        }
        if (this.config.scrollToData) {
            $("html, body").animate({scrollTop: this.el.height()}, "slow");
        }


    },

    /**
     *
     * @param table
     * @param headerRow
     */
    renderHeader : function(table, headerRow) {
        var th = $("<thead>");
        var tr = $("<tr>").addClass("row-header-index");
        th.append(tr);
        tr.append($("<th>").addClass("dead-space"));

        var i = -1;

        while(++i < this.maxCols) {
            var colId = this.getColdId(i);
            var pointer = this.getHeaderPointer(colId);
            var 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']
 *
 *
 *
 */

Visualizer.Tree = $.klass(Visualizer.DatasourceVisualizerBase, {
    truncate:true,
    collectionDepthTracker:[],
    STARTING_NODE_NUMBER:20,
    NESTED_OBJECT_NUMBER:100,
    NESTED_OBJECT_INCREMENT:100,
    NODE_LIMIT_MULTIPLIER : 1.5,
    INCREMENT_NODE_BY:25,
    treeType: "XML", // Default tree type

    initialize : function($super, config) {
        this.selectedNode = false;
        $super(config);

        this.el.click(_.bind(this.onClick, this));

        if (this.selectionEnabled) {
            this.$pathBar = $("<div>").attr("id", "vz-treepath");
            this.$helpIcon = $(help.renderIcon({topic: "tree-path"}));

            this.$container.prepend(this.$pathBar);
        }
    },


    /**
     * Sets original path to the path from the definition
     * @param targetObj
     */
    initOriginalPath: function(targetObj) {
        this.path = this.buildPath(targetObj);
        this.pointer = this.buildAddress();
    },


    /**
     * if node-text
     *		if node == attribute  --> /@leaf
     *		 else --> ../@leaf[index]
     * if leaf ../leaf
     * if attribute { add attribute constraint } /
     * @param evt
     */
    onClick : function(evt) {
        var $t = $(evt.target);

        // find the closest .tree-node -- this is the main context of the click action
        // --------------------------------------------------------------------------
        var $targetNode = $(evt.target).closest(".tree-node").eq(0);
        if ($t.hasClass("truncatedNode"))return;
        // if the expand/collapse arrow was click, change UI and then return fast
        // ---------------------------------------------------------------------
        if ($t.hasClass("node-child-toggle") || !$targetNode.hasClass("leaf")) {
            $t.closest(".tree-node").toggleClass("closed");
            return;
        }

        // if selection is disabled return early -- everything else below is related to picking a node
        if (!this.selectionEnabled) return;

        // if options button was clicked, show overlay and return
        // ------------------------------------------------------
        if ($t.hasClass("node-options")) {
            this.showSelectionOptions($t);
            return;
        }

        if ($targetNode) {
            var targetNode = $targetNode.data("node");

            if(typeof targetNode !== "undefined"){
                // if shift key was pressed, mark the node as constrained
                if (evt.shiftKey) {
                    targetNode["~constrained"] = !targetNode["~constrained"];
                    PubSub.publish("pointer-selected", [this.buildAddress(), this.datasourceInstance, this.config.visualizerId]);
                    return;
                }


                if ($targetNode.hasClass("leaf")) {
                    targetNode["~constrained"] = false;

                    // if the target node is an attribute, we are really selecting the parent (who owns the attribute)
                    // ------------------------------------------------------------------------------------------------
                    if ($targetNode.hasClass("node-attribute")) {
                        var parent = targetNode["~parent"];
    //						parent['~selectedAttribute'] = targetNode;
                        this.initOriginalPath(parent);
                        this.setSelectedNode(parent, targetNode);
                    } else {
                        this.initOriginalPath(targetNode);
                        this.setSelectedNode(targetNode);
                    }


                    if ($t.hasClass("node-text")) {
                        var current = targetNode;
                        while (current) {
                            current["~constrained"] = true;
                            current = current["~parent"];
                        }
                    }
                }
            }

            PubSub.publish("pointer-selected", [this.buildAddress(), this.datasourceInstance, this.config.visualizerId]);
        }
    },

    /**
     * when a node is selected rebuild the path, and update the UI
     * @param n
     * @param selectedAtt
     */
    setSelectedNode : function(n, selectedAtt) {
        this.path = this.buildPath(n, selectedAtt);
        this.pointer = this.buildAddress();

        if (this.selectedNode) {
            if (this.selectedNode["~selectedAttribute"]) {
                this.selectedNode["~selectedAttribute"]["~el"].removeClass("selected");
                delete this.selectedNode["~selectedAttribute"];
            } else if (this.selectedNode["~el"]) {
                this.selectedNode["~el"].removeClass("selected");
            }

            this.el.find(".node-options").remove();
        }

        var $options = $("<a>")
            .addClass("node-options")
            .text("Selection Options");

        if (selectedAtt) {
            selectedAtt["~el"].addClass("selected");
            selectedAtt["~el"].children(".node-data").append($options);
        } else if (n["~el"]) {
            n["~el"].addClass("selected");
            n["~el"].children(".node-data").append($options);
        }

        this.selectedNode = n;
        if (selectedAtt) this.selectedNode["~selectedAttribute"] = selectedAtt;
    },

    showSelectionOptions : function($target) {
        var $root = $("<div>")
            .width(500)
            .append("<h2 style='margin-bottom:10px;'>Selection Options</h2>");

        var options = this.getSelectionOptions();

        var $list = $("<ul>");
        for (var i = 0; i < options.length; i++) {
            var option = options[i];
            var id = "option" + i;

            var selClass = "";
            if (this.pointer == option.path) {
                selClass = "selected";
                this.selectionPath = option.path;
                $root.find("#selection-preview").html(this.setSelectionPreview(this.selectionPath));
            }

            $list.append($("<li>").attr({id: id, path: option.path}).data("lock", option.lock).addClass(selClass).html(option.desc));
        }

        $root
            .append($("<div>").attr("id", "selection-list").html($list))
            .append($("<div>").html("Preview").css("padding-top","10px").addClass("small quiet"))
            .append($("<div>").attr("id", "selection-preview").css("white-space", "nowrap"));

        var _this = this;
        $root.click(function(evt){
            var $t = $(evt.target);
            if ($t.is("li") || $t.is("li span")){
                $("#selection-list li.selected").removeClass("selected");

                if (!$t.is("li")) $t = $t.parent();

                $t.addClass("selected");
                _this.selectionPath = $t.attr("path");
                _this.selectionLock = $t.data("lock");

                $(this).find("#selection-preview").html(_this.setSelectionPreview(_this.selectionPath));
            }
        });

        var $overlay = $.overlay($target, $root, {
            shadow:{
                opacity:0,
                onClick: function(){
                    _this.selectionPath = null;
                    $overlay.close();
                }
            },
            footer:{
                controls:[
                    {
                        text:"Cancel",
                        css:"rounded-button secondary",
                        onClick: function(evt) {
                            $overlay.close();
                            _this.selectionPath = null;
                        }
                    },
                    {
                        text:"Set Selection",
                        css:"rounded-button primary",
                        onClick: function(evt) {
                            if (_this.selectionPath) {
                                // lock elements
                                for (var i = 0; i < _this.selectionLock; i++) {
                                    _this.selectionLock[i].locked = true;
                                }

                                _this.pointer = _this.selectionPath;
                                PubSub.publish("pointer-selected", [_this.selectionPath, _this.datasourceInstance, _this.config.visualizerId]);
                            }

                            $overlay.close();
                            _this.selectionPath = null;
                        }
                    }
                ]
            }
        });
        $overlay.show();
    },

    getSelectionOptions : function() {
        var i;
        var address = this.buildAddress().replace(/\[\d+\]/g,"");

        var eltName = this.selectedNode.name;
        var eltIndex = this.selectedNode["~index"];
        var eltAttr = this.selectedNode["~selectedAttribute"];

        var attrStr = " ";
        if (eltAttr) {
            attrStr = " the <span>" + eltAttr.name + "</span> attribute of ";
        }

        var ancestors = [];
        this.getAncestors(this.selectedNode, ancestors);

        var ancestorPaths = [];
        var pathAddress = address;
        var locks = [];
        for (i = 0; i < ancestors.length; i++) {
            var ancestor = ancestors[i];

            var description = "Select" + attrStr + "all <span>" + eltName + "</span> elements in this <span>" + ancestor.name + "</span> element.";
            pathAddress = this.insertSelectors(pathAddress, [{name:ancestor.name,value:ancestor["~index"]}]);
            locks.unshift(ancestor);

            ancestorPaths.unshift({desc: description, path: pathAddress, lock: locks});
        }

        locks.unshift(this.selectedNode);
        ancestorPaths.unshift({desc: "Select" + attrStr + "only this <span>" + eltName + "</span> element.",
            path: this.insertSelectors(pathAddress, [{name:eltName,value:eltIndex}]),
            lock:locks});

        var attrPaths = [];
        if (!eltAttr && this.selectedNode.attributes) {
            var attr = this.selectedNode.attributes;

            for (var x in attr) {
                if (this.selectedNode["~parent"]) {
                    var parentName = this.selectedNode["~parent"].name;
                    var parentIndex = this.selectedNode["~parent"]["~index"];

                    attrPaths.push({desc: "Select all peer <span>" + eltName + "</span> elements with the <span>" + x + "</span> attribute.",
                        path: this.insertSelectors(address, [{name:eltName,value:"@"+x}]),
                        lock: []});
                    attrPaths.push({desc: "Select all peer <span>" + eltName + "</span> elements with the <span>" + x + "</span> attribute set to \"<span>" + attr[x] +"</span>\".",
                        path: this.insertSelectors(address, [{name:eltName,value:"@"+x+"=\""+attr[x]+"\""}]),
                        lock: []});
                    attrPaths.push({desc: "Select all <span>" + eltName + "</span> elements with the <span>" + x + "</span> attribute in this <span>" + parentName + "</span> element.",
                        path: this.insertSelectors(address, [{name:parentName,value:parentIndex}, {name:eltName,value:"@"+x}]),
                        lock: [this.selectedNode["~parent"]]});
                    attrPaths.push({desc: "Select all <span>" + eltName + "</span> elements with the <span>" + x + "</span> attribute set to \"<span>" + attr[x] + "</span>\" in this <span>" + parentName + "</span> element.",
                        path: this.insertSelectors(address, [{name:parentName,value:parentIndex}, {name:eltName,value:"@"+x+"=\""+attr[x]+"\""}]),
                        lock: [this.selectedNode["~parent"]]});
                }

                attrPaths.push({desc: "Select all <span>" + eltName + "</span> elements with the <span>" + x + "</span> attribute.",
                    path: "//" + eltName + "[@" + x + "]",
                    lock: []});
                attrPaths.push({desc: "Select all <span>" + eltName + "</span> elements with the <span>" + x + "</span> attribute set to \"<span>" + attr[x] + "</span>\".",
                    path: "//" + eltName + "[@" + x + "=\"" + attr[x] + "\"]",
                    lock: []});
            }
        }

        var options = [];

        // only this element
        if (ancestorPaths.length > 0) {
            options.push(ancestorPaths.shift());
        }

        // default: all peer elements
        options.push({desc: "Select" + attrStr + "all peer <span>" + eltName + "</span> elements.", path: address, lock: []});

        // the nth element in all parent elements
        var parent = ancestors.length > 0 ? ancestors[ancestors.length-1] : null;
        if (parent != null) {
            options.push({desc: "Select" + attrStr + "the <span>" + ordinal(eltIndex) + " " + eltName + "</span> element in all <span>" + parent.name + "</span> elements.",
                path: this.insertSelectors(address, [{name:eltName,value:eltIndex}]),
                lock: [this.selectedNode]});
        }

        // all selected elements anywhere
        options.push({desc: "Select" + attrStr + "all <span>" + eltName + "</span> elements.", path: "//" + eltName + (eltAttr ? "/@" + eltAttr.name : ""), lock: []});

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

        // attributes options
        for (i = 0; i < attrPaths.length; i++) {
            options.push(attrPaths[i]);
        }

        return options;
    },

    insertSelectors : function(path, selectors) {
        var pathArr = path.split("/");

        while (selectors.length > 0) {
            var selector = selectors.pop();

            var i = _.indexOf(pathArr, selector.name);

            if (i != -1) {
                pathArr[i] += "[" + selector.value + "]";
            }
        }

        return pathArr.join("/");
    },

    getAncestors : function(node, list) {
        if (node["~parent"]) {
            list.unshift(node["~parent"]);
            this.getAncestors(node["~parent"], list);
        }
    },

    setSelectionPreview : function(path) {
        var pointer = this.datasourceInstance.id + "@" + path + ";";

        DX.Manager.instance.query(pointer, function(result, msg){
            if(msg.error) {
                console.log(msg); // 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(/\[\d+\]/g,"");
                        foundAttr = foundNode["~el"].find(".tree-node.node-attribute.leaf[address=\"" + findAddr + "\"]").data("node");
                        break;
                    }
                }

                //get children nodes that match names with the next level
                var items = n.children? this.filterChildren(n.children,addrArray[addrIndex+1], "name"):[];
                if(items.length == 0) continue;//no children/relevant children so move on to next node

                //if the next level has a lock only look at the specific child
                if (addrArray[addrIndex+1].search(/\[\d+\]/) != -1) {
                    var childIndex = addrArray[addrIndex+1].slice(addrArray[addrIndex+1].indexOf("[") + 1, addrArray[addrIndex+1].indexOf("]")) - 1;
                    allNodes.push({"node":items[childIndex],"depth":addrIndex+1});
                } else {
                    //otherwise add all nodes
                    for(var $i = items.length-1;$i>-1;$i--){
                        allNodes.push({"node":items[$i],"depth":addrIndex+1});
                    }
                }

            }

            //call set selected node then scroll to it
            if (foundAttr) {
                //if an attribute
                this.setSelectedNode(foundNode, foundAttr);
                this.scrollToSelectedNode(foundNode, foundAttr);
            } else {
                this.setSelectedNode(foundNode);
                this.scrollToSelectedNode(foundNode);
            }


            //set locked status and rerender path bar
            for(var $k = 0;$k<strArray.length;$k++){
                if(strArray[$k].search(/\[\d+\]/)!= -1){
                    if (this.path[$k]) {
                        this.path[$k].locked = true;
                    }
                }
            }

            this.pointer = this.buildAddress();
            if (this.pointer == "/" || !foundNode["~el"]) this.pointer = p;
        }
    },

    render : function() {
        this.el.children(".tree-node").remove();//if we are re rendering we need to toss out the old
        this.el.disableSelection();

        this.renderPathBar();
        var dataNodes= this.data;
        if(this.treeType=="XML")  {
            dataNodes = this.convertNodes(this.data);
        }
        this.renderNode(dataNodes, false, this.el, 0, "top-node", false);

        this.el.find("a").attr("target", "_blank");
        if (this.config.scrollToData) {
            $("html, body").animate({scrollTop: this.el.height()}, "slow");
        }
    },


    renderPathBar : function() {
        var $pbar = this.$pathBar;
        var $path;
        var pathString = "Click an element to include it in your ";

        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(/\[\d+\]/g,"");

        var eltName = this.selectedNode["~propname"];
        var eltIndex = this.selectedNode["~index"];

        var ancestors = [];
        this.getAncestors(this.selectedNode, ancestors);

        var parentArray = null;
        if (!this.selectedNode["~propname"]) {
            parentArray = ancestors[ancestors.length - 1];
            eltName = parentArray["~propname"];
        } else {
            ancestors.push(this.selectedNode);
        }

        var ancestorPaths = [];
        var pathAddress = address;
        var locks = [];
        for (i = 0; i < ancestors.length - 1; i++) {
            var ancestor = ancestors[i];
            var ancestorName = ancestor["~propname"];
            var ancestorIndex = ancestor["~index"];

            // found an array, make sure to get the proper index
            if (ancestor.name.search(/\[\]/) != -1) {
                ancestorIndex = ancestors[i+1]["~parent"]["~index"];
            }

            var description = "Select all <span>" + eltName + "</span> elements in this <span>" + ancestorName + "</span> element.";
            pathAddress = this.insertSelectors(pathAddress, [{name:ancestorName,value:ancestorIndex}]);
            locks.unshift(ancestor);

            ancestorPaths.unshift({desc: description, path: pathAddress, lock: locks});
        }

        locks.unshift(ancestors[ancestors.length - 1]);
        ancestorPaths.unshift({desc: "Select only this <span>" + eltName + "</span> element.",
            path: this.insertSelectors(pathAddress, [{name:eltName,value:eltIndex}]),
            lock:locks});

        var options = [];

        // only this element
        if (ancestorPaths.length > 0) {
            options.push(ancestorPaths.shift());
        }

        // default: all peer elements
        options.push({desc: "Select all peer <span>" + eltName + "</span> elements.", path: address, lock: []});

        // the nth element in parent array
        if (parentArray != null) {
            var parent = ancestors.length > 1 ? ancestors[ancestors.length-2] : null;
            if (parent != null) {
                options.push({desc: "Select the <span>" + ordinal(eltIndex) + " " + eltName + "</span> element in all <span>" + parent["~propname"] + "</span> elements.",
                    path: this.insertSelectors(address, [{name:eltName,value:eltIndex}]),
                    lock: [ancestors[ancestors.length - 1]]});
            }
        }

        // all selected elements anywhere
        options.push({desc: "Select all <span>" + eltName + "</span> elements.", path: "//" + eltName, lock: []});

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

        return options;
    },

    findParent : function(node) {
        if (node["~parent"]) {
            if (node["~parent"]["~propname"]) {
                return node["~parent"];
            } else {
                return this.findParent(node["~parent"]);
            }
        }

        return null;
    },

    getAncestors : function(node, list) {
        var parent = this.findParent(node);

        if (parent != null) {
            list.unshift(parent);
            this.getAncestors(parent, list);
        }
    },

    /**
     * go from the selectedNode up to the root to create an xpath
     */
    buildAddress : function() {
        var newPath = [];

        if (this.path) {
            var plen = this.path.length;
            for (var i = 0; i < plen; i++) {
                var p = this.path[i];
                if (!p.node["~propname"]) continue;
                var txt = p.node["~propname"];

                if (p.locked) {
                    // get the next path element's index
                    if (i != (plen - 1)) {
                        var nextindex = this.path[i + 1].node["~index"];
                        txt += "[" + nextindex + "]";
                    } else {
                        txt += "[" + p.node["~index"] + "]";
                    }
                }

                newPath.push(txt);

            }
        }

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

    /**
     *
     * @param p
     */
    setPointer : function(p) {
        var $i;
        if (p == this.pointer)return;

        if (p == false) {
            this.setSelectedNode(p);
        } else {
            var strArray = p.substr(1, p.length).split("/");

            var allNodes = [];//list of nodes to search
            var foundNode = false;//storage for target node
            var addrArray =  p.substr(1, p.length).split("/");//string array of level names: path to node we are looking for

            // collect root-level node as starting point
            allNodes.push({"node":this.data,"depth":0});

            // find the previously selected node
            while (allNodes.length != 0) {
                var nextItem = allNodes.pop();
                var n = nextItem.node;
                var addrIndex = nextItem.depth;
                    // skip over {}
                    if( typeof n !== "undefined") {
                        if ((!n["~propname"] || n["~propname"] == "") && n.children) {
                            // check if any of the children matches the level.
                            var itemsLevelN = n.children ? this.filterChildren(this.findChildren(n), addrArray[addrIndex], "~propname") : [];

                            // if nothing matches, fail-safe to check all node for the next level
                            if(!itemsLevelN || itemsLevelN.length<1) {
                                itemsLevelN = n.children;
                            }

                            for($i= itemsLevelN.length; $i > 0; $i--) {
                                allNodes.push({"node": itemsLevelN[$i-1], "depth": addrIndex});
                            }
                            continue;
                        }

                        //we have reached the end of the path
                        if (addrIndex == addrArray.length - 1) {
                            var propName = addrArray[addrIndex];
                            var index = false;
                            if (addrArray[addrIndex].search(/\[\d+\]/) != -1) {
                                propName = addrArray[addrIndex].substring(0, addrArray[addrIndex].indexOf("["));
                                index = addrArray[addrIndex].substring(addrArray[addrIndex].indexOf("[") + 1, addrArray[addrIndex].indexOf("]"));
                            }

                            if (n["~propname"] == propName) {
                                if (n["~type"] == "arr" && index) {
                                    if (n.children && 0 <= index && index < n.children.length) {
                                        foundNode = n.children[index - 1];
                                    }
                                }

                                if (!foundNode) foundNode = n;

                                break;
                            } else {
                                continue;
                            }
                       }

                        //get children nodes that match names with the next level
                        var items = n.children ? this.filterChildren(this.findChildren(n), addrArray[addrIndex + 1], "~propname") : [];
                        if (items.length == 0) continue;//no children/relevant children so move on to next node

                        //if the next level has a lock only look at the specific child
                        if (addrArray[addrIndex].search(/\[\d+\]/) != -1) {
                            var childIndex = addrArray[addrIndex].slice(addrArray[addrIndex].indexOf("[") + 1, addrArray[addrIndex].indexOf("]")) - 1;
                            allNodes.push({"node": items[childIndex], "depth": addrIndex + 1});
                        } else {
                            //otherwise add all nodes
                            for ($i = items.length - 1; $i > -1; $i--) {
                                allNodes.push({"node": items[$i], "depth": addrIndex + 1});
                            }
                        }
                    }
            }

            //call set selected node then scroll to it
            this.setSelectedNode(foundNode);
            this.scrollToSelectedNode(foundNode);

            //set locked status and rerender path bar
            var nodeNames = [];
            for(var $k = 0;$k<strArray.length;$k++){
                if(strArray[$k].search(/\[\d+\]/)!= -1){
                    nodeNames.push(strArray[$k].substring(0, strArray[$k].indexOf("[")));
                }
            }
            this.lockPath(nodeNames);

            this.pointer = this.buildAddress();
            if (this.pointer == "/") this.pointer = p;
        }
    },


    findChildren : function(node) {
        var childNodes = [];

        if (node.children) {
            for (var $i = 0; $i < node.children.length; $i++) {
                var child = node.children[$i];

                // skip over {}
                if (!child["~propname"] || child["~propname"] == "") {
                    if (child.children) {
                        for (var $j = 0; $j < child.children.length; $j++) {
                            childNodes.push(child.children[$j]);
                        }
                    }
                } else {
                    childNodes.push(child);
                }
            }
        }

        return childNodes;
    },

    lockPath : function(nodeNames) {
        for (var $i = 0; $i < this.path.length; $i++) {
            if (_.indexOf(nodeNames, this.path[$i].node["~propname"]) != -1) {
                this.path[$i].locked = true;
            }
        }
    }

});


Visualizer.JsonToNode = function(parent, propName, value, position) {
    var n = { };
    if (parent) n["~parent"] = parent;
    n["~propname"] = propName;

    if(parent) {
        n.qualifiedName = parent.qualifiedName;
    } else {
        n.qualifiedName = "";
    }

    if ($.isPlainObject(value)) {
        n.children = [];
        n.name = "{}";
        n.qualifiedName = (position==="undefined" || _.isNaN(position)) ? n.name : "{" +position + "}" + n.qualifiedName;
        n["~type"] = "obj";
        n["~adr"] = propName;

        if (propName) {
            n.name += " " + propName;
            n.qualifiedName += " " + propName;
        } else {
            n["~pathbarexclude"] = true;
        }

        var counter = 0;
        for (var p in value) {
            if (!value.hasOwnProperty(p)) continue;
            n.children.push(Visualizer.JsonToNode(n, p, value[p], counter));
            counter ++ ;
        }
    } else if ($.isArray(value)) {
        n.name = "[]";
        n.qualifiedName = (position==="undefined" || _.isNaN(position)) ? n.name : "[" +position + "]"+ n.qualifiedName;
        n.children = [];
        n["~type"] = "arr";
        n["~adr"] = propName;
        if (propName) {
            n.name += " " + propName;
            n.qualifiedName += " " + propName;
        } else {
            n["~pathbarexclude"] = true;
        }

        for (var i = 0; i < value.length; i++) {
            var v = value[i];
            var c = Visualizer.JsonToNode(n, false, v, i);
            c.name = c.name ? c.name + " " + (i + 1) : (i + 1);
            c.qualifiedName = c.qualifiedName ? c.qualifiedName + " " + (i + 1) : (i + 1);
            c["~pathbarexclude"] = true;
            c["~index"] = i + 1;
            n.children.push(c);
        }
    } else {
        n.name = propName;
        n.qualifiedName = n.qualifiedName + " " + propName;
        if(value !=null)n.text = value.toString();
        n["~adr"] = propName;
    }


    return n;
};
;/****** c.visualizer_root.js *******/ 

/* 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 tabProperties = $("#tab-properties");
        var firstPropertyInput = tabProperties.find("input:first:visible");
        var is_chrome = (typeof window.chrome === "object" && navigator.appVersion.indexOf("Edge") === -1);
        var timeoutMS = 100;

        if(firstPropertyInput) {
            firstPropertyInput.focus().select();
        }

        // TODO: Remove setTimeout (ref: https://klipfolio.myjetbrains.com/youtrack/issue/saas-12744)
        if(is_chrome) {
            setTimeout(function() {
                var isCurrentlyVisible = tabProperties.is(":visible");
                if(isCurrentlyVisible) {
                    tabProperties.hide().show();
                } else {
                    tabProperties.show();
                    setTimeout(function() {
                        tabProperties.hide();
                    }, timeoutMS);
                }
            }, timeoutMS);
        }
    },

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