/*

TO DO:

- send event when the contents of an element change - this should be handled by elements of type "image"
- add lots of elements (as many as possible from Bootstrap)
- create test file with empty editor
- create test file for saving/loading editor state
- create test file for editor and content side-by-side
- create how to use guide
- create API docs

*/

// Squares
// Description: Interactive and embeddable HTML content builder.
// Author: Nikolay Dyankov
// License: MIT

/*

Usage

This script is meant to be embedded in a back-end site builder or similar project.
The usage scenario is the following (for now):

1. Add a class "squares" to the containers that should have editable content
2. Call an API to get the current state of the editor to store it.
3. Call an API to get the generated HTML content for the front-end
4. Include the "squares.css" file in the front-end
5. Insert the previously generated HTML code

*/

;(function ($, window, document, undefined) {

    var editorWindow = undefined, registeredElements = new Array(), registeredControls = new Array(), editors = new Array();

    // =========================================================================
    // [API]

    // Create an editor with previously stored settings in JSON format.
    // The "host" parameter is the root element of the editor. It contains
    // (or will contain a reference to the JS class instance).
    $.squaresInitWithSettings = function(host, settings) {
        // If the host already has an editor attached, remove the editor from the editors array
        if (host.data.editor) {
            for (var i=0; i<editors.length; i++) {
                if (editors[i].id == host.data.editor.id) {
                    editors.splice(i, 1);
                }
            }
        }

        // Init the new editor
        var squaresInstance = new Squares(host, settings);
        editors.push(squaresInstance);
    };

    // Gets the current state as JS object of an editor, selected by its host
    $.squaresGetCurrentSettings = function(host) {
        return host.data.editor.getCompressedSettings();
    };

    // Called at the end to generate the final HTML code to be inserted in the
    // front-end.
    $.squaresGenerateHTML = function(host) {
        return host.data.editor.generateHTML();
    };

    /*
    Adds a new element to the catalog.
    Required options for Element registration:
    - name: sematic name for the Element
    - iconClass: complete class name from Font Awesome
    - content(): callback function which returns HTML code to be rendered
    - (optional) extendOptions - array containing additional controls for
    the element. For example:

    */
    $.squaresRegisterElement = function(options) {
        registeredElements.push(options);
    };

    /*
    Registers a control that can be added to the element settings window
    Required options for Control registration:
    - type: int, float, text, color, etc
    - getValue: getter for the value of the control
    - setValue: setter for the value of the control
    - HTML: returns the HTML of the control
    - init: create events associated with this specific control element, etc
    */

    $.squaresRegisterControl = function(options) {
        registeredControls.push(options);
    }

    $.squaresShowEditorWindow = function(x, y) {
        editorWindow.show(x, y);
    }
    $.squaresHideEditorWindow = function() {
        editorWindow.hide();
    }
    $.squaresGetEditorWindowContents = function() {
        return editorWindow.getContentHTML();
    }
    $.squaresExtendElementDefaults = function(extendedDefaults) {
        elementDefaultSettings = $.extend(true, {}, elementDefaultSettings, extendedDefaults);
    }

    // BOILERPLATE CODE FOR IMPLEMENTING REQUIRED API FUNCTIONS ****************
    // *************************************************************************

    $.squaresUpdatedContent = function(newContent) {

    }

    // [END API]
    // =========================================================================

    $(document).ready(function() {
        // On document load, loop over all elements with the "squares" class
        // and initialize a Squares editor on them.
        $('.squares').each(function() {
            var squaresInstance = new Squares(this);
            editors.push(squaresInstance);
            $(this).data.editor = squaresInstance;
        });
    });

    // The bulk of the functionality goes here.
    // Squares is the "root" class.
    var squaresDefaultSettings = {
        containers: []
    };

    function Squares(host, settings) {
        var that = this;

        // "host" is the direct parent of the embedded editor
        this.host = $(host);
        this.id = Math.floor(Math.random() * 9999) + 1;
        this.settings = $.extend(true, {}, squaresDefaultSettings);

        this.contentRoot = undefined;
        this.root = undefined;
        this.Window = undefined;

        // Drag general flags
        this.ix = 0; // initial dragged object x
        this.iy = 0; // initial dragged object x
        this.iex = 0; // initial event x
        this.iey = 0; // initial event y

        // Drag container flags
        this.shouldStartDraggingContainer = false;
        this.didStartDraggingContainer = false;
        this.draggingContainer = false;

        // Drag container vars
        this.draggedContainerIndex = 0;
        this.draggedContainer = undefined;
        this.dummyContainer = undefined;
        this.containerReorderMap = undefined;
        this.newIndexOfDraggedContainer = 0;

        // Reorder elements
        this.shouldStartDraggingElement = false;
        this.didStartDraggingElement = false;
        this.draggingElement = false;
        this.draggedElementIndex = -1;
        this.draggedElementContainerIndex = -1;
        this.elementDragMap = undefined;
        this.dummyElement = undefined;
        this.newIndexOfDraggedElement = -1;
        this.draggedElementWidth = -1;

        // Selected Element ID
        this.selectedElementID = undefined;

        // Form Settings

        this.loadSettings(settings);
        this.init();
    };
    Squares.prototype.loadSettings = function(settings) {
        // When settings are loaded, we make sure containers and elements
        // have the correct prototype.
        if (settings) {
            // Iterate over all containers
            if (settings.containers) {
                for (var i=0; i<settings.containers.length; i++) {
                    var c = settings.containers[i];

                    // Add a container and store a reference
                    var newContainer = this.appendContainer();

                    // Iterate over all elements of the container
                    if (c.settings) {
                        for (var j=0; j<c.settings.elements.length; j++) {
                            var e = c.settings.elements[j];
                            
                            if (e.settings) {
                                // Get the catalog index of the element with the same name
                                // and insert it in the container
                                for (var k=0; k<registeredElements.length; k++) {
                                    if (e.settings.name == registeredElements[k].name) {
                                        newContainer.insertElement(k, j, e.options);
                                    }
                                }
                            }
                            
                        }
                    }
                }
            }
        }
    };
    Squares.prototype.init = function () {
        // Save a reference in the host to the Editor
        this.host.data.editor = this;

        // Insert a container to hold everything
        this.host.html('');
        this.host.append('<div class="sq-root-container"></div>');
        this.root = this.host.find('.sq-root-container');

        // Insert a container to hold all the user generated content
        this.host.find('.sq-root-container').append('<div class="sq-content"></div>');
        this.contentRoot = this.host.find('.sq-content');

        this.contentRoot.attr('id', 'sq-editor-' + this.id);

        this.addUI();
        this.addEvents();
        this.redraw();

        // Editor window
        editorWindow = new EditorWindow();
        // editorWindow.hide();
    };
    Squares.prototype.redraw = function () {
        // This is the global redraw function.
        // It is called only when a change in hierarchy is made.
        // It is responsible for creating the root element for each
        //      container and element, telling those objects that they have a new
        //      root element, and calling the "render" function on them.
        this.contentRoot.html('');

        for (var i=0; i<this.settings.containers.length; i++) {
            var c = this.settings.containers[i];

            // Append a container
            var html = '<div class="sq-container" data-index="'+ i +'" id="'+ c.id +'"></div>';

            this.contentRoot.append(html);

            // Set the container's "root" object
            var containerRoot = $('#' + c.id);

            // Call the render() function of the container
            c.render();
            c.appendEditorControls();

            for (var j=0; j<c.settings.elements.length; j++) {
                var e = c.settings.elements[j];

                // Append an element to the container
                var html = '<div class="sq-element" data-index="' + j + '" id="'+ e.id +'"></div>';
                containerRoot.append(html);

                // Call the render() function of the element
                e.render();
                e.appendEditorControls();
            }

            containerRoot.append('<div class="squares-clear"></div>');
        }

        // If there are no containers, hide the "elements button"
        // and send updatedcontents event
        if (this.settings.containers.length == 0) {
            this.root.find('.sq-add-elements').hide();
        } else {
            this.root.find('.sq-add-elements').show();
        }

        // Re-select the currently selected element
        if (this.selectedElementID) {
            this.selectElement(this.selectedElementID);
        }
    };
    Squares.prototype.addEvents = function() {
        var self = this;

        // Button for appending a new container
        this.host.find('.sq-add-container').off('click');
        this.host.find('.sq-add-container').on('click', function() {
            self.appendContainer();
            self.redraw();
        });

        // Delete container button
        $(document).off('mouseout', '#sq-editor-' + this.id + ' .sq-container');
        $(document).on('mouseout', '#sq-editor-' + this.id + ' .sq-container', function(e) {
            if ($(e.target).closest('.sq-container-confirm-delete').length == 0 && !$(e.target).hasClass('sq-container-confirm-delete') &&
                $(e.target).closest('.sq-container-delete').length == 0 && !$(e.target).hasClass('sq-container-delete')) {
                $('.sq-container-confirm-delete').hide();
            }
        });
        $(document).off('click', '#sq-editor-' + this.id + ' .sq-container-delete');
        $(document).on('click', '#sq-editor-' + this.id + ' .sq-container-delete', function() {
            $(this).siblings('.sq-container-confirm-delete').show();
        });
        $(document).off('click', '#sq-editor-' + this.id + ' .sq-container-confirm-delete');
        $(document).on('click', '#sq-editor-' + this.id + ' .sq-container-confirm-delete', function() {
            self.deleteContainer($(this).data('container-id'));
            self.redraw();
        });

        // Reorder containers and elements functionality

        // Containers
        $(document).off('mousedown', '#sq-editor-'+ self.id +' .sq-container-move');
        $(document).on('mousedown', '#sq-editor-'+ self.id +' .sq-container-move', function(e) {
            // If there is just one container, then don't do anything
            if (self.settings.containers.length <= 1) return;

            self.iex = e.pageX;
            self.iey = e.pageY;
            self.shouldStartDraggingContainer = true;
            self.draggedContainerIndex = $(e.target).closest('.sq-container').data('index');
            self.draggedContainer = self.host.find('.sq-container[data-index='+ self.draggedContainerIndex +']');
        });


        // Elements
        $(document).off('mousedown', '#sq-editor-'+ self.id +' .sq-element');
        $(document).on('mousedown', '#sq-editor-'+ self.id +' .sq-element', function(e) {
            // If there is just one container with one element, then don't do anything
            if (self.settings.containers.length == 1 && self.settings.containers[0].settings.elements.length == 1) return;

            self.iex = e.pageX;
            self.iey = e.pageY;
            self.shouldStartDraggingElement = true;
            self.draggedElement = $(this);
            self.draggedElementIndex = $(this).data('index');
            self.draggedElementContainerIndex = $(this).closest('.sq-container').data('index');
        });

        $(document).off('mousemove.'+ self.id);
        $(document).on('mousemove.'+ self.id, function(e) {
            // Drag container
            if (self.shouldStartDraggingContainer && !self.didStartDraggingContainer) {
                self.startDraggingContainer(e);
            }

            if (self.draggingContainer) {
                self.dragContainer(e);
            }

            // Drag element
            if (self.shouldStartDraggingElement && !self.didStartDraggingElement) {
                self.startDraggingElement(e);
            }

            if (self.draggingElement) {
                self.dragElement(e);
            }
        });
        $(document).off('mouseup.'+ self.id);
        $(document).on('mouseup.'+ self.id, function(e) {
            if (self.draggingContainer) {
                self.endDraggingContainer(e);
            }
            if (self.draggingElement) {
                self.endDraggingElement(e);
            }

            // Clean up
            self.shouldStartDraggingContainer = false;
            self.didStartDraggingContainer = false;
            self.draggingContainer = false;

            self.draggedContainerIndex = 0;
            self.draggedContainer = undefined;
            self.dummyContainer = undefined;

            self.shouldStartDraggingElement = false;
            self.didStartDraggingElement = false;
            self.draggingElement = false;
            self.draggedElementIndex = -1;
            self.draggedElementContainerIndex = -1;
        });

        // [end] Reorder containers functionality

        // Delete element button
        $(document).off('click.' + this.id, '#sq-delete-element-button');
        $(document).on('click.' + this.id, '#sq-delete-element-button', function() {
            var elementID = $(this).data('element-id');

            // Search for the element
            for (var i=0; i<self.settings.containers.length; i++) {
                var c = self.settings.containers[i];

                for (var j=0; j<c.settings.elements.length; j++) {
                    if (c.settings.elements[j].id == elementID) {
                        c.removeElementAtIndex(j);
                        self.redraw();
                    }
                }
            }
        });
    };
    Squares.prototype.startDraggingContainer = function(e) {
        if (Math.abs(e.pageX - this.iex) > 5 || Math.abs(e.pageY - this.iey) > 5) {
            this.draggingContainer = true;
            this.didStartDraggingContainer = true;

            // Create a virtual map of the current containers, where
            // every possible position of the dragged container is
            // precalculated
            this.containerReorderMap = new Array();
            var draggedContainerY = this.draggedContainer.outerHeight()/2;

            for (var i=0; i<this.settings.containers.length; i++) {
                var y = draggedContainerY;

                // Add the height of all previous containers to calculate
                // the new virtual Y position of the dragged container
                // for the current index
                for (var j=i-1; j>=0; j--) {
                    var index = j;

                    // The height of the dragged container must not be
                    // included in the calculation.
                    // If the current index is the index of the dragged
                    // container, then increase the index
                    if (j >= this.draggedContainerIndex) {
                        index++;
                    }

                    var c = this.host.find('.sq-container[data-index='+ index +']');
                    y += c.outerHeight();
                }

                this.containerReorderMap.push(y);
            }

            // Position the container absolutely
            this.ix = this.draggedContainer.position().left;
            this.iy = this.draggedContainer.position().top;

            this.draggedContainer.css({
                position: 'absolute',
                left: this.ix,
                top: this.iy,
                width: this.draggedContainer.width()
            });

            this.draggedContainer.addClass('sq-dragging');

            // Insert a dummy container
            this.draggedContainer.after('<div id="sq-dummy-container"></div>');
            this.dummyContainer = $('#sq-dummy-container');
            this.dummyContainer.css({
                width: this.draggedContainer.outerWidth(),
                height: this.draggedContainer.outerHeight()
            });
        }
    }
    Squares.prototype.dragContainer = function(e) {
        this.draggedContainer.css({
            left: this.ix + e.pageX - this.iex,
            top: this.iy + e.pageY - this.iey
        });

        var y = this.draggedContainer.position().top + this.draggedContainer.outerHeight()/2;
        var closestDeltaY = 999999;
        var closestIndex = undefined;

        for (var i=0; i<this.containerReorderMap.length; i++) {
            if (Math.abs(y - this.containerReorderMap[i]) < closestDeltaY) {
                closestDeltaY = Math.abs(y - this.containerReorderMap[i]);
                closestIndex = i;
            }
        }

        // If the closest index changed, move the dummy container to the
        // new position.
        if (closestIndex != this.newIndexOfDraggedContainer) {
            this.newIndexOfDraggedContainer = closestIndex;

            this.dummyContainer.remove();

            if (this.newIndexOfDraggedContainer < this.draggedContainerIndex) {
                this.host.find('.sq-container[data-index='+ this.newIndexOfDraggedContainer +']').before('<div id="sq-dummy-container"></div>');
            } else {
                this.host.find('.sq-container[data-index='+ this.newIndexOfDraggedContainer +']').after('<div id="sq-dummy-container"></div>');
            }

            this.dummyContainer = $('#sq-dummy-container');
            this.dummyContainer.css({
                width: this.draggedContainer.outerWidth(),
                height: this.draggedContainer.outerHeight()
            });
        }
    }
    Squares.prototype.endDraggingContainer = function(e) {
        // Switch places of containers
        if (this.draggedContainerIndex != this.newIndexOfDraggedContainer) {
            var a = this.settings.containers[this.draggedContainerIndex];
            this.settings.containers.splice(this.draggedContainerIndex, 1);
            this.settings.containers.splice(this.newIndexOfDraggedContainer, 0, a);
        }

        this.redraw();
    }
    Squares.prototype.startDraggingElement = function(e) {
        if (Math.abs(e.pageX - this.iex) > 5 || Math.abs(e.pageY - this.iey) > 5) {
            this.draggingElement = true;
            this.didStartDraggingElement = true;

            // Save the starting position of the draggedElement
            this.ix = this.draggedElement.offset().left;
            this.iy = this.draggedElement.offset().top;

            // Create a virtual map of all possible positions of the element
            // in each container
            this.elementDragMap = new Array();

            var draggedElementObject = this.settings.containers[this.draggedElementContainerIndex].settings.elements[this.draggedElementIndex];

            this.draggedElementWidth = getWidthOfElementInGrid(draggedElementObject.controls['layout']['column_span'].getVal());
            this.draggedElementWidth = this.draggedElement.outerWidth();

            var dummyElementHTML = '<div id="sq-dummy-element-tmp" style="width: '+ this.draggedElementWidth +'px; height: '+ this.draggedElement.outerHeight() +'px;"></div>';

            this.draggedElement.hide();
            for (var i=0; i<this.settings.containers.length; i++) {
                var c = this.settings.containers[i];

                // If the container doesn't have any elements, insert just one
                // dummy element and move on to next container
                if (c.settings.elements.length == 0) {
                    this.host.find('.sq-container[data-index='+i+']').append(dummyElementHTML);
                    var el = $('#sq-dummy-element-tmp');
                    this.elementDragMap.push({ x: el.offset().left + el.width()/2, y: el.offset().top + el.height()/2, containerIndex: i, elementIndex: 0 });
                    $('#sq-dummy-element-tmp').remove();
                }

                for (var j=0; j<c.settings.elements.length; j++) {
                    this.host.find('.sq-container[data-index='+i+']').find('.sq-element[data-index='+j+']').before(dummyElementHTML);
                    var el = $('#sq-dummy-element-tmp');
                    this.elementDragMap.push({ x: el.offset().left + el.width()/2, y: el.offset().top + el.height()/2, containerIndex: i, elementIndex: j });
                    $('#sq-dummy-element-tmp').remove();

                    if (j == c.settings.elements.length - 1) {
                        this.host.find('.sq-container[data-index='+i+']').find('.sq-element[data-index='+j+']').after(dummyElementHTML);
                        var el = $('#sq-dummy-element-tmp');
                        this.elementDragMap.push({ x: el.offset().left + el.width()/2, y: el.offset().top + el.height()/2, containerIndex: i, elementIndex: j + 1 });
                        $('#sq-dummy-element-tmp').remove();
                    }
                }
            }

            this.draggedElement.show();

            // Insert a dummy element
            this.draggedElement.after('<div id="sq-dummy-element"><div id="sq-dummy-element-inner"></div></div>');
            this.dummyElement = $('#sq-dummy-element');
            this.dummyElement.css({
                width: this.draggedElementWidth,
                height: this.draggedElement.outerHeight(),
                margin: this.draggedElement.css('margin'),
                padding: 0
            });

            // Position the element absolutely

            var draggedElementWidth = this.draggedElement.outerWidth();
            var draggedElementHeight = this.draggedElement.outerHeight();
            var draggedElementHTML = this.draggedElement.clone().attr('id', 'sq-dragged-element').wrap('<div>').parent().html();
            
            this.draggedElement.hide();

            $('body').prepend(draggedElementHTML);
            this.draggedElement = $('#sq-dragged-element');

            this.draggedElement.css({
                position: 'absolute',
                left: this.ix,
                top: this.iy,
                width: draggedElementWidth,
                height: draggedElementHeight
            });
            this.draggedElement.addClass('sq-dragging');
        }
    }
    Squares.prototype.dragElement = function(e) {
        this.draggedElement.css({
            left: this.ix + e.pageX - this.iex,
            top: this.iy + e.pageY - this.iey
        });

        // Find the closest virtual position to the mouse position
        var closestIndex = 0;
        var closestDistance = 999999;

        for (var i=0; i<this.elementDragMap.length; i++) {
            var d = Math.abs(e.pageX - this.elementDragMap[i].x) + Math.abs(e.pageY - this.elementDragMap[i].y);
            if (d < closestDistance) {
                closestDistance = d;
                closestIndex = i;
            }
        }

        if (closestIndex != this.newIndexOfDraggedElement) {
            this.newIndexOfDraggedElement = closestIndex;

            // Remove the current dummy element
            $('#sq-dummy-element').remove();

            // Insert a new dummy element at the container/element index
            var containerIndex = this.elementDragMap[this.newIndexOfDraggedElement].containerIndex;
            var elementIndex = this.elementDragMap[this.newIndexOfDraggedElement].elementIndex;
            var c = this.host.find('.sq-container[data-index='+ containerIndex +']');
            // If the index of the dummy element is bigger than the number
            // of elements in that container, insert the dummy at the end
            if (this.settings.containers[containerIndex].settings.elements.length == 0) {
                c.prepend('<div id="sq-dummy-element"><div id="sq-dummy-element-inner"></div></div>');
            } else if (elementIndex == this.settings.containers[containerIndex].settings.elements.length) {
                var lastElementIndex = this.settings.containers[containerIndex].settings.elements.length - 1;
                var el = c.find('.sq-element[data-index='+ lastElementIndex +']');
                el.after('<div id="sq-dummy-element"><div id="sq-dummy-element-inner"></div></div>');
            } else {
                var el = c.find('.sq-element[data-index='+ elementIndex +']');
                el.before('<div id="sq-dummy-element"><div id="sq-dummy-element-inner"></div></div>');
            }

            this.dummyElement = $('#sq-dummy-element');
            this.dummyElement.css({
                width: this.draggedElementWidth,
                height: this.draggedElement.outerHeight(),
                margin: this.draggedElement.css('margin'),
            });
        }
    }
    Squares.prototype.endDraggingElement = function(e) {
        this.draggedElement.remove();

        // Move the element to the new index
        var newContainerIndex = this.elementDragMap[this.newIndexOfDraggedElement].containerIndex;
        var newElementIndex = this.elementDragMap[this.newIndexOfDraggedElement].elementIndex;

        var oldElementIndex = this.draggedElementIndex;
        var oldContainerIndex = this.draggedElementContainerIndex;

        var el = this.settings.containers[oldContainerIndex].settings.elements[oldElementIndex];
        this.settings.containers[oldContainerIndex].settings.elements.splice(oldElementIndex, 1);
        this.settings.containers[newContainerIndex].settings.elements.splice(newElementIndex, 0, el);

        this.redraw();
    }
    Squares.prototype.addUI = function() {
        this.appendAddContainerButton();
        // this.appendAddElementsButton();
    };
    Squares.prototype.appendAddContainerButton = function() {
        var addContainerButtonHTML = '<div class="sq-add-container"><i class="fa fa-plus"></i></div>';
        // var addContainerButtonHTML = '<div class="sq-add-container"><i class="fa fa-plus"></i> <span>Add Container</span></div>';

        this.root.append(addContainerButtonHTML);
    };
    Squares.prototype.appendAddElementsButton = function() {
        var addElementsButtonHTML = '<div class="sq-add-elements"><i class="fa fa-cube"></i></div>';

        this.root.append(addElementsButtonHTML);
    };
    Squares.prototype.appendContainer = function() {
        var c = new Container(this.host);
        this.settings.containers.push(c);

        return c;
    };
    Squares.prototype.deleteContainer = function(id) {
        // Find out the index of the container
        var index = 0;

        for (var i=0; i<this.settings.containers.length; i++) {
            if (this.settings.containers[i].id == id) {
                index = i;
            }
        }

        this.settings.containers.splice(index, 1);
    };
    Squares.prototype.addElement = function(containerIndex, elementIndex, elementCatalogIndex, elementControlOptions) {
        var self = this;

        // Add element to container at index
        self.settings.containers[containerIndex].insertElement(elementCatalogIndex, elementIndex, elementControlOptions);

        // Redraw
        self.redraw();
    };
    Squares.prototype.getCompressedSettings = function() {
        var settings = $.extend(true, {}, this.settings);

        // Compress element settings
        for (var i=0; i<settings.containers.length; i++) {
            // var c = $.extend(true, {}, settings.containers[i]);
			var c = {
				id: settings.containers[i].id,
				settings: $.extend(true, {}, settings.containers[i].settings)
			};

            for (var j=0; j<c.settings.elements.length; j++) {
                var e = $.extend(true, {}, c.settings.elements[j]);

                e.settings = subtract(e.settings, elementDefaultSettings);
                e.settings = clean(e.settings);

                // Get the current values of the controls
                var options = e.getCurrentOptions();
                options = subtract(options, e.defaults);
                options = clean(options);

                c.settings.elements[j] = {
                    settings: e.settings,
                    options: options
                };
            }

            settings.containers[i] = c;
        }

        return settings;
    }
    Squares.prototype.generateHTML = function() {
        // function generating the HTML code that will be used in the end product

        var html = '';

        for (var i=0; i<this.settings.containers.length; i++) {
            var c = this.settings.containers[i];

            html += c.generateHTML();
        }

        // Strip slashes
        html = html.replace(/\\(.)/mg, "$1");

        // Replace line breaks with <br>
        html = html.replace(/\n/mg, "<br>");

        return html;
    }
    Squares.prototype.selectElement = function(elementID) {
        this.selectedElementID = elementID;

        $('.sq-element-selected').removeClass('sq-element-selected');
        $('#' + this.selectedElementID).addClass('sq-element-selected');
    }

    // The "Container" class servs literally as a container
    // for Element objects, similar to Bootstrap's "row" class.
    // It will have settings only for layout.

    var containerDefaultSettings = {
        elements: []
    };

    function Container(host) {
        // this.root is the highest element in the container's hierarchy.
        // it will contain data-index attribute, used to reference this element
        this.id = 'sq-container-' + Math.floor(Math.random() * 99999) + 1;

        this.settings = $.extend(true, {}, containerDefaultSettings);
        this.host = host;
    }
    Container.prototype.insertElement = function(elementCatalogIndex, index, options) {
        var e = new Element(registeredElements[elementCatalogIndex], options, this);
        this.settings.elements.splice(index, 0, e);
    }
    Container.prototype.removeElementAtIndex = function(i) {
        this.settings.elements.splice(i, 1);
        editorWindow.openFirstTab();
        editorWindow.removeElementSettings();
    }
    Container.prototype.render = function() {
        // Nothing to render for now
    }
    Container.prototype.appendEditorControls = function() {
        var html = '';
        html += '     <div class="sq-container-move"></div>';
        html += '     <div class="sq-container-delete"><i class="fa fa-times" aria-hidden="true"></i></div>';
        html += '     <div class="sq-container-confirm-delete" data-container-id="'+ this.id +'">Delete</div>';

        $('#' + this.id).append(html);
    }
    Container.prototype.generateHTML = function() {
        // function generating the HTML code that will be used in the end product

        var html = '';

        html += '<div class="squares-container">';

        for (var i=0; i<this.settings.elements.length; i++) {
            var e = this.settings.elements[i];
            html += e.generateHTML();
        }

        html += '<div class="squares-clear"></div>'
        html += '</div>';

        return html;
    }

    // The element object will represent a single piece of content.
    // Image, text, video, etc.
    // It will have settings for layout and styling

    var elementDefaultSettings = {
        name: 'Untitled Element',
        iconClass: 'fa fa-cube',
        controls: [],
        defaultControls: {
            layout: {
                box_model: {
                    name: 'Box Model',
                    type: 'box model',
                    default: {
                        margin: {
                            top: 0,
                            bottom: 0,
                            left: 0,
                            right: 0
                        },
                        padding: {
                            top: 10,
                            bottom: 10,
                            left: 10,
                            right: 10
                        }
                    }
                },
                use_grid: {
                    name: 'Use Grid System',
                    type: 'switch',
                    default: 1
                },
                column_span: {
                    name: 'Grid Settings',
                    type: 'grid system',
                    group: 'Layout Grid',
                    default: {
                        xs: {
                            use: 0,
                            class: 'sq-col-xs-12',
                            visible: 0
                        },
                        sm: {
                            use: 0,
                            class: 'sq-col-sm-12',
                            visible: 0
                        },
                        md: {
                            use: 0,
                            class: 'sq-col-md-12',
                            visible: 1
                        },
                        lg: {
                            use: 1,
                            class: 'sq-col-lg-12',
                            visible: 1
                        },
                    }
                },
                width: {
                    name: 'Width',
                    type: 'int',
                    group: 'Layout Manual',
                    default: '100'
                },
                auto_width: {
                    name: 'Auto Width',
                    type: 'switch',
                    group: 'Layout Manual',
                    default: 1
                },
                height: {
                    name: 'Height',
                    type: 'int',
                    group: 'Layout Manual',
                    default: '100'
                },
                auto_height: {
                    name: 'Auto Height',
                    type: 'switch',
                    group: 'Layout Manual',
                    default: 1
                }
            },
            style: {
                background_color: {
                    name: 'Background Color',
                    type: 'color',
                    default: '#ffffff'
                },
                background_opacity: {
                    name: 'Background Opacity',
                    type: 'slider',
                    options: {
                        min: 0,
                        max: 1
                    },
                    default: '0'
                },
                opacity: {
                    name: 'Opacity',
                    type: 'slider',
                    options: {
                        min: 0,
                        max: 1
                    },
                    default: '1'
                },
                box_shadow: {
                    name: 'Box Shadow',
                    type: 'text',
                    default: 'none'
                },
                border_width: {
                    name: 'Border Width',
                    type: 'slider',
                    options: { min: 0, max: 20, type: 'int' },
                    default: '0'
                },
                border_style: {
                    name: 'Border Style',
                    type: 'select',
                    options: [ 'none', 'hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset' ],
                    default: 'none'
                },
                border_color: {
                    name: 'Border Color',
                    type: 'color',
                    default: '#000000'
                },
                border_opacity: {
                    name: 'Border Opacity',
                    type: 'slider',
                    options: {
                        min: 0,
                        max: 1
                    },
                    default: '1'
                },
                border_radius: {
                    name: 'Border Radius',
                    type: 'slider',
                    options: { min: 0, max: 100, type: 'int' },
                    default: '0'
                },
            },
            font: {
                font_family: {
                    name: 'Font Family',
                    type: 'text',
                    default: 'sans-serif'
                },
                font_size: {
                    name: 'Font Size',
                    type: 'text',
                    format: 'int',
                    default: '14'
                },
                font_weight: {
                    name: 'Font Weight',
                    type: 'text',
                    default: 'normal'
                },
                font_style: {
                    name: 'Font Style',
                    type: 'select',
                    options: [ 'normal', 'italic', 'oblique', 'initial', 'inherit' ],
                    default: 'normal'
                },
                line_height: {
                    name: 'Line Height',
                    type: 'text',
                    format: 'int',
                    default: '22'
                },
                text_color: {
                    name: 'Text Color',
                    type: 'color',
                    default: '#000000'
                },
                text_align: {
                    name: 'Text Align',
                    type: 'select',
                    options: ['left', 'right', 'center', 'justify', 'justify-all', 'start', 'end', 'match-parent', 'inherit', 'initial', 'unset'],
                    default: 'left'
                },
                text_decoration: {
                    name: 'Text Decoration',
                    type: 'select',
                    options: ['none', 'underline'],
                    default: 'none'
                },
                text_transform: {
                    name: 'Text Transform',
                    type: 'select',
                    options: [ 'none', 'capitalize', 'uppercase', 'lowercase', 'initial', 'inherit' ],
                    default: 'none'
                },
                text_shadow: {
                    name: 'Text Shadow',
                    type: 'text',
                    default: ''
                }
            },
            general: {
                id: {
                    name: 'ID',
                    type: 'text',
                    default: ''
                },
                classes: {
                    name: 'Classes',
                    type: 'text',
                    default: ''
                },
                css: {
                    name: 'CSS',
                    type: 'text',
                    default: ''
                }
            }
        },
        defaultControlGroupIcons: {
            general: 'fa fa-wrench',
            layout: 'fa fa-th-large',
            font: 'fa fa-font',
            style: 'fa fa-paint-brush'
        },
        content: function() {
            return '';
        }
    };

    function Element(settings, options, container) {
        // this.root is the highest element in the container's hierarchy.
        // it will contain data-index attribute, used to reference this element
        this.id = 'sq-element-' + Math.floor(Math.random() * 99999) + 1;

        // Settings are used only for initialization
        this.settings = $.extend(true, {}, elementDefaultSettings, settings);

        // This array will contain only the default values for each option and
        // it will be used only for compressing the generated JSON
        this.defaults = new Array();

        // Array containing all control objects
        // all options of this element should be accessed from here
        this.controls = new Array();

        // Create a reference to the content() function, so 'this' within that function
        // refers to the Element object and it has access to its controls
        this.content = this.settings.content;

        // Temporary variable until a better solution is found
        this.fontStyles = '';

        // Reference to its parent container
        this.container = container;

        this.init(options);
    }
    Element.prototype.init = function(options) {
        // Merge the custom controls with the default controls
        this.settings.controls = $.extend(true, {}, this.settings.controls, this.settings.defaultControls);

        // Merge the custom control group icons with the default control group icons
        this.settings.controlGroupIcons = $.extend(true, {}, this.settings.controlGroupIcons, this.settings.defaultControlGroupIcons);

        // Remove the default style controls if the option is specified
        if (this.settings.useStyleControls === false) {
            this.settings.controls.style = undefined;
        }
        // Remove the default text style controls if the option is specified
        if (this.settings.useFontControls === false) {
            this.settings.controls.font = undefined;
        }

        // Create associative array from this.settings.controls containing default values
        // Used only for compression
        for (var g in this.settings.controls) {
            if (this.settings.controls.hasOwnProperty(g)) {
                var group = this.settings.controls[g];

                if (!this.defaults[g]) {
                    this.defaults[g] = {};
                }

                for (var op in group) {
                    if (group.hasOwnProperty(op)) {
                        var option = group[op];

                        this.defaults[g][op] = option.default;
                    }
                }
            }
        }

        // Create controls
        for (var g in this.settings.controls) {
            if (this.settings.controls.hasOwnProperty(g)) {
                var group = this.settings.controls[g];

                for (var op in group) {
                    if (group.hasOwnProperty(op)) {
                        var option = group[op];

                        // Get a control from the registered controls
                        // of the corresponding type
                        var controlOptions = undefined;

                        for (var i=0; i<registeredControls.length; i++) {
                            if (registeredControls[i].type == option.type) {
                                controlOptions = registeredControls[i];
                            }
                        }

                        // Check if there is a value in the init options
                        var v = option.default;

                        if (options !== undefined && options[g] !== undefined && options[g][op] !== undefined) {
                            if (typeof(options[g][op]) == 'object') {
                                v = $.extend(true, {}, option.default, options[g][op]);
                            } else {
                                v = options[g][op];
                            }
                        }

                        if (this.controls[g] === undefined) {
                            this.controls[g] = {};
                        }

                        var self = this;

                        this.controls[g][op] = new SquaresControl(controlOptions, option.name, option.group, g, option.options, function() {
                            self.updateForm();
                            self.render();
                            self.appendEditorControls();
                        });

                        this.controls[g][op].setVal(v);
                    }
                }
            }
        }
    }
    Element.prototype.getSettingsForm = function() {
        // Loop over all controls and get the HTML from each control
        // Also add a label with the name of the control
        var html = '';

        // Create tabs
        html += '<div id="sq-window-settings-sidebar">';
        var groupCount = 0;
        for (var g in this.controls) {
            var icon = '<i class="fa fa-toggle-on" aria-hidden="true"></i>';

            if (this.settings.controlGroupIcons[g]) {
                icon = '<i class="'+ this.settings.controlGroupIcons[g] +'" aria-hidden="true"></i>';
            }

            html += '<div class="sq-window-settings-sidebar-button" data-tab-index="'+ groupCount +'" data-tab-group="sq-element-settings-tab-group" data-tab-button>';
            html += '   <div class="sq-window-settings-sidebar-button-icon">'+ icon +'</div>';
            html += '   <div class="sq-window-settings-sidebar-button-title">'+ g +'</div>';
            html += '</div>';
            groupCount++;
        }

        // Append delete element tab button
        html += '<div class="sq-window-settings-sidebar-button" data-tab-index="'+ groupCount +'" data-tab-group="sq-element-settings-tab-group" data-tab-button>';
        html += '   <div class="sq-window-settings-sidebar-button-icon"><i class="fa fa-trash-o" aria-hidden="true"></i></div>';
        html += '   <div class="sq-window-settings-sidebar-button-title">Delete</div>';
        html += '</div>';

        html += '</div>';


        // Create content for each tab
        html += '<div class="sq-settings-window-content-wrap">';

        var groupCount = 0;
        for (var g in this.controls) {
            html += '<div class="sq-window-content" data-tab-content data-tab-index="'+ groupCount +'" data-tab-group="sq-element-settings-tab-group">';

            var tabGroup = this.controls[g];
            groupCount++;

            for (var c in tabGroup) {
                var control = tabGroup[c];

                html += '<div class="sq-form-control '+ control.elementClass +'">';

                if (control.customLabel) {
                    html += control.HTML();
                } else {
                    html += '<label for="'+ control.elementID +'">'+ control.name +'</label>';
                    html += control.HTML();
                }

                html += '</div>';
            }

            html += '</div>';
        }

        // Create content for the delete element tab
        html += '<div class="sq-window-content" data-tab-content data-tab-index="'+ groupCount +'" data-tab-group="sq-element-settings-tab-group">';
        html += '   <div class="sq-form-control">';
        html += '       <p>Delete Element?</p>';
        html += '       <div id="sq-delete-element-button" data-element-id="'+ this.id +'">Delete</div>';
        html += '   </div>';
        html += '</div>';

        html += '</div>';

        return html;
    }
    Element.prototype.loadOptions = function() {
        // Load the current options of the element in the settings window

        for (var g in this.controls) {
            var tabGroup = this.controls[g];

            for (var c in tabGroup) {
                var control = tabGroup[c];
                control.loadVal();
            }
        }

        this.updateForm();
    }
    Element.prototype.updateForm = function() {
        if (this.controls['layout']['use_grid'].getVal() == 1) {
            $('.' + this.controls['layout']['width'].elementClass).hide();
            $('.' + this.controls['layout']['column_span'].elementClass).show();
        } else {
            $('.' + this.controls['layout']['width'].elementClass).show();
            $('.' + this.controls['layout']['column_span'].elementClass).hide();
        }
    }
    Element.prototype.generateStyles = function() {
        var css = '';
        // =====================================================================
        // Layout
        // =====================================================================

        var o = this.controls['layout'];

        // Box Model
        if (isNumeric(o['box_model'].getVal().margin.top)) {
            css += 'margin-top: ' + o['box_model'].getVal().margin.top + 'px; ';
        }
        if (isNumeric(o['box_model'].getVal().margin.bottom)) {
            css += 'margin-bottom: ' + o['box_model'].getVal().margin.bottom + 'px; ';
        }
        if (isNumeric(o['box_model'].getVal().margin.left)) {
            css += 'margin-left: ' + o['box_model'].getVal().margin.left + 'px; ';
        }
        if (isNumeric(o['box_model'].getVal().margin.right)) {
            css += 'margin-right: ' + o['box_model'].getVal().margin.right + 'px; ';
        }

        if (isNumeric(o['box_model'].getVal().padding.top)) {
            css += 'padding-top: ' + o['box_model'].getVal().padding.top + 'px; ';
        }
        if (isNumeric(o['box_model'].getVal().padding.bottom)) {
            css += 'padding-bottom: ' + o['box_model'].getVal().padding.bottom + 'px; ';
        }
        if (isNumeric(o['box_model'].getVal().padding.left)) {
            css += 'padding-left: ' + o['box_model'].getVal().padding.left + 'px; ';
        }
        if (isNumeric(o['box_model'].getVal().padding.right)) {
            css += 'padding-right: ' + o['box_model'].getVal().padding.right + 'px; ';
        }

        if (parseInt(o['use_grid'].getVal(), 10) == 1) {
            // Grid system

        } else {
            // Width
            if (parseInt(o['auto_width'].getVal(), 10) == 1) {
                css += 'width: auto; ';
            } else {
                if (o['width'].getVal() !== '' && !isNaN(o['width'].getVal())) {
                    css += 'width: '+ o['width'].getVal() +'px; ';
                }
            }

            // Height
            if (parseInt(o['auto_height'].getVal(), 10) == 1) {
                css += 'height: auto; ';
            } else {
                if (o['height'].getVal() !== '' && !isNaN(o['height'].getVal())) {
                    css += 'height: '+ o['height'].getVal() +'px; ';
                }
            }
        }

        // css += 'float: left; ';

        // =====================================================================
        // Text
        // =====================================================================
        var o = this.controls['font'];

        if (o) {
            // Font Family
            if (o['font_family'].getVal() !== '') {
                css += 'font-family: ' + o['font_family'].getVal() + '; ';
                this.fontStyles += 'font-family: ' + o['font_family'].getVal() + '; ';
            }

            // Font Size
            if (isNumeric(o['font_size'].getVal())) {
                css += 'font-size: ' + o['font_size'].getVal() + 'px; ';
                this.fontStyles += 'font-size: ' + o['font_size'].getVal() + 'px; ';
            }

            // Font Weight
            if (o['font_weight'].getVal() !== '') {
                css += 'font-weight: ' + o['font_weight'].getVal() + '; ';
                this.fontStyles += 'font-weight: ' + o['font_weight'].getVal() + '; ';
            }

            // Font Style
            if (o['font_style'].getVal() !== '') {
                css += 'font-style: ' + o['font_style'].getVal() + '; ';
                this.fontStyles += 'font-style: ' + o['font_style'].getVal() + '; ';
            }

            // Line Height
            if (isNumeric(o['line_height'].getVal())) {
                css += 'line-height: ' + o['line_height'].getVal() + 'px; ';
                this.fontStyles += 'line-height: ' + o['line_height'].getVal() + 'px; ';
            }

            // Text Color
            if (o['text_color'].getVal() !== '') {
                css += 'color: ' + o['text_color'].getVal() + '; ';
                this.fontStyles += 'color: ' + o['text_color'].getVal() + '; ';
            }

            // Text Align
            if (o['text_align'].getVal() !== '') {
                css += 'text-align: ' + o['text_align'].getVal() + '; ';
                this.fontStyles += 'text-align: ' + o['text_align'].getVal() + '; ';
            }

            // Text Decoration
            if (o['text_decoration'].getVal() !== '') {
                css += 'text-decoration: ' + o['text_decoration'].getVal() + '; ';
                this.fontStyles += 'text-decoration: ' + o['text_decoration'].getVal() + '; ';
            }

            // Text Transform
            if (o['text_transform'].getVal() !== '') {
                css += 'text-transform: ' + o['text_transform'].getVal() + '; ';
                this.fontStyles += 'text-transform: ' + o['text_transform'].getVal() + '; ';
            }

            // Text Shadow
            if (o['text_shadow'].getVal() !== '') {
                css += 'text-shadow: ' + o['text_shadow'].getVal() + '; ';
                this.fontStyles += 'text-shadow: ' + o['text_shadow'].getVal() + '; ';
            }
        }

        // =====================================================================
        // Style
        // =====================================================================
        var o = this.controls['style'];

        if (o) {
            // Background Color
            var c_bg = hexToRgb(o['background_color'].getVal());
            css += 'background-color: rgba('+ c_bg.r +', '+ c_bg.g +', '+ c_bg.b +', '+ o['background_opacity'].getVal() +'); ';

            // Opacity
            if (isNumeric(o['opacity'].getVal())) {
                css += 'opacity: ' + o['opacity'].getVal() + '; ';
            }

            // Box Shadow
            if (o['box_shadow'].getVal() !== '') {
                css += 'box-shadow: ' + o['box_shadow'].getVal() + '; ';
            }

            // Border Width
            if (isNumeric(o['border_width'].getVal())) {
                css += 'border-width: ' + o['border_width'].getVal() + 'px; ';
            }

            // Border Style
            if (o['border_style'].getVal() !== '') {
                css += 'border-style: ' + o['border_style'].getVal() + '; ';
            }

            // Border Color
            var c_bg = hexToRgb(o['border_color'].getVal());
            css += 'border-color: rgba('+ c_bg.r +', '+ c_bg.g +', '+ c_bg.b +', '+ o['border_opacity'].getVal() +'); ';

            // Border Radius
            if (isNumeric(o['border_radius'].getVal())) {
                css += 'border-radius: ' + o['border_radius'].getVal() + 'px; ';
            }
        }

        return css;
    }
    Element.prototype.generateLayoutClass = function() {
        var o = this.controls['layout'];

        if (parseInt(o['use_grid'].getVal(), 10) == 1) {
            var classes = '';
            var v = o['column_span'].getVal();

            if (parseInt(v.xs.use, 10) == 1) {
                classes += v.xs.class + ' ';

                if (parseInt(v.xs.visible, 10) == 0) {
                    classes += 'sq-hidden-xs ';
                }
            }
            if (parseInt(v.sm.use, 10) == 1) {
                classes += v.sm.class + ' ';

                if (parseInt(v.sm.visible, 10) == 0) {
                    classes += 'sq-hidden-sm ';
                }
            }
            if (parseInt(v.md.use, 10) == 1) {
                classes += v.md.class + ' ';

                if (parseInt(v.md.visible, 10) == 0) {
                    classes += 'sq-hidden-md ';
                }
            }
            if (parseInt(v.lg.use, 10) == 1) {
                classes += v.lg.class + ' ';

                if (parseInt(v.lg.visible, 10) == 0) {
                    classes += 'sq-hidden-lg ';
                }
            }
            return classes;
        } else {
            return '';
        }
    }
    Element.prototype.render = function() {
        // Preserve selection
        var selected = false;
        if ($('#' + this.id).hasClass('sq-element-selected')) {
            selected = true;
        }

        // Update the element's style
        $('#' + this.id).attr('style', this.generateStyles());

        // Add layout classes to the element
        $('#' + this.id).attr('class', 'sq-element ' + this.generateLayoutClass());

        // Update the element's user set content
        $('#' + this.id).html(this.content());

        if (selected) {
            $('#' + this.id).addClass('sq-element-selected');
        }

        // Send event that the content has changed
        $.squaresUpdatedContent($.squaresGetCurrentSettings(this.container.host));
    }
    Element.prototype.appendEditorControls = function() {
        var html = '';

        html += '     <div class="sq-element-controls">';
        html += '         <div class="sq-element-control-drag"></div>';
        html += '     </div>';

        $('#' + this.id).append(html);
    }
    Element.prototype.getCurrentOptions = function() {
        // Loop over all controls and put their values in an associative array

        var options = {};

        for (var controlGroupName in this.controls) {
            for (var controlName in this.controls[controlGroupName]) {
                var c = this.controls[controlGroupName][controlName];
                if (!options[controlGroupName]) {
                    options[controlGroupName] = {};
                }

                options[controlGroupName][controlName] = c.getVal();
            }
        }

        return options;
    }
    Element.prototype.generateHTML = function() {
        // function generating the HTML code that will be used in the end product

        var html = '';

        html += '<div id="'+ this.id +'" class="squares-element '+ this.generateLayoutClass() +'" style="'+ this.generateStyles() +'">';
        html += this.content();
        html += '</div>';

        return html;
    }

    function EditorWindow() {
        this.root = undefined;
        this.id = Math.floor(Math.random() * 10000) + 1;

        this.visible = false;

        // flags for dragging the window
        this.shouldStartDragging = false;
        this.didStartDragging = false;
        this.dragging = false;
        this.iex = 0; // initial event x
        this.iey = 0; // initial event y
        this.ix = 0; // initial window x
        this.iy = 0; // initial window y

        this.editorWindowContents = '';

        this.init();
        this.events();
        // this.show(600, 100);
    }
    EditorWindow.prototype.init = function() {
        // todo
        // Generate window contents HTML only and store it in a variable
        this.generateEditorWindowContents();
    }
    EditorWindow.prototype.generateEditorWindowContents = function() {
        var html = '';

        html += '     <div class="sq-window-header">';
        html += '         <div class="sq-window-main-nav">';
        html += '             <div id="sq-window-main-nav-button-elements" class="sq-window-main-nav-button active" data-tab-group="sq-window-main-tab-group" data-tab-index="0" data-tab-button><i class="fa fa-cube" aria-hidden="true"></i> Elements</div>';
        html += '             <div id="sq-window-main-nav-button-settings" class="sq-window-main-nav-button" data-tab-group="sq-window-main-tab-group" data-tab-index="1" data-tab-button><i class="fa fa-cog" aria-hidden="true"></i> Settings</div>';
        html += '         </div>';
        // html += '         <div class="sq-window-close"><i class="fa fa-times"></i></div>';
        html += '     </div>';
        html += '     <div class="sq-window-container">';

        // Elements tab
        html += '         <div class="sq-window-tab-content" data-tab-group="sq-window-main-tab-group" data-tab-index="0" data-tab-content id="sq-window-elements-tab-content">';
        html += '             <div class="sq-window-content">';
        html += '                 <div id="sq-no-elements-found">No elements found.</div>';
        html += '                 <div id="sq-drag-elements">Drag and drop elements from this window</div>';

        html += '                 <div id="sq-elements-wrap">';

        for (var i=0; i<registeredElements.length; i++) {
            var borderBottomClass = '';

            if (i >= registeredElements.length-3) {
                borderBottomClass = 'sq-element-thumb-no-border-bottom';
            }

            html += '             <div class="sq-element-thumb '+ borderBottomClass +'" data-index="' + i + '">';
            html += '                 <div class="sq-element-thumb-icon">';
            html += '                     <i class="' + registeredElements[i].iconClass + '"></i>';
            html += '                 </div>';
            html += '                 <div class="sq-element-thumb-title">';
            html += '                     <h2>'+ registeredElements[i].name +'</h2>';
            html += '                 </div>';
            html += '             </div>';
        }

        html += '                 </div>';

        html += '             </div>';
        html += '         </div>';

        // Settings tab
        html += '         <div class="sq-window-tab-content" style="display: none" data-tab-group="sq-window-main-tab-group" data-tab-index="1" data-tab-content id="sq-window-settings-tab-content">';
        html += '             <div id="sq-window-settings-tab-inner-content"><div id="sq-window-settings-tab-no-element">No element selected. To create an element, open the Elements tab and drag an element into a container.</div></div>';
        html += '         </div>';

        html += '     </div>';

        this.editorWindowContents = html;
    }
    EditorWindow.prototype.events = function() {
        var self = this;

        // Open the editor window's settings tab when user clicks on an element
        $(document).off('click', '.sq-element');
        $(document).on('click', '.sq-element', function() {

            var editor = $(this).closest('.sq-root-container').data.editor;
            var containerIndex = $(this).closest('.sq-container').data('index');
            var elementIndex = $(this).data('index');
            var el = editor.settings.containers[containerIndex].settings.elements[elementIndex];

            // Open the settings tab
            $('#sq-window-elements-tab-content').hide();
            $('#sq-window-settings-tab-content').show();

            // Highlight the settings tab
            $('.sq-window-main-nav-button').removeClass('active');
            $('#sq-window-main-nav-button-settings').addClass('active');

            // Load the element settings
            $('#sq-window-settings-tab-inner-content').html(el.getSettingsForm());
            el.loadOptions();

            // Go to the first tab of the settings
            $('[data-tab-content][data-tab-group="sq-element-settings-tab-group"]').hide();
            $('[data-tab-content][data-tab-group="sq-element-settings-tab-group"][data-tab-index="0"]').show();

            // Highlight the first tab button
            $('[data-tab-button][data-tab-group="sq-element-settings-tab-group"]').removeClass('active').first().addClass('active');

            // Select the element
            editor.selectElement(el.id);
        });

        // Open the window when clicked on the add elements button
        $(document).off('click', '.sq-add-elements');
        $(document).on('click', '.sq-add-elements', function() {
            if (!self.visible) {
                var x = $(this).closest('.sq-root-container').offset().left + $(this).closest('.sq-root-container').width() + 40;
                var y = $(this).closest('.sq-root-container').offset().top;
                self.show(x + 20, y + 20);
            }

            // Show the elements tab
            $('#sq-window-elements-tab-content').show();
            $('#sq-window-settings-tab-content').hide();

            // Tabs
            $('.sq-window-main-nav-button').removeClass('active');
            $('#sq-window-main-nav-button-elements').addClass('active');
        });

        // Generic Tab functionality
        $(document).off('click', '[data-tab-button]');
        $(document).on('click', '[data-tab-button]', function() {
            var index = $(this).data('tab-index');
            var tabGroup = $(this).data('tab-group');

            $('[data-tab-button][data-tab-group="'+ tabGroup +'"]').removeClass('active');
            $(this).addClass('active');

            $('[data-tab-content][data-tab-group="'+ tabGroup +'"]').hide();
            $('[data-tab-content][data-tab-group="'+ tabGroup +'"][data-tab-index="'+ index +'"]').show();
        });

        // =====================================================================
        // Needs tidying up
        // Drag elements from window to container functionality
        var shouldStartDraggingElementToContainer = false,
        didStartDraggingElementToContainer = false,
        virtualIndexOfDraggedElement = -1,
        draggedElementFromWindowCatalogIndex = -1,
        thumbElWhenDraggingFromWindow = undefined,
        dummyElementAtMouse = undefined,
        elementDragMap = undefined;
        var iex = 0, iey = 0, ix = 0, iy = 0;

        $(document).off('mousedown.squaresEditorWindow', '.sq-element-thumb');
        $(document).on('mousedown.squaresEditorWindow', '.sq-element-thumb', function(e) {
            shouldStartDraggingElementToContainer = true;

            iex = e.pageX;
            iey = e.pageY;

            thumbElWhenDraggingFromWindow = $(this);

            return false;
        });
        $(document).off('mousemove.elementFromWindow.squaresEditorWindow');
        $(document).on('mousemove.elementFromWindow.squaresEditorWindow', function(e) {
            if (shouldStartDraggingElementToContainer && !didStartDraggingElementToContainer) {
                if (Math.abs(e.pageX - iex) > 5 || Math.abs(e.pageY - iey) > 5) {
                    didStartDraggingElementToContainer = true;

                    // Get contents and position of the element thumb
                    draggedElementFromWindowCatalogIndex = thumbElWhenDraggingFromWindow.data('index');

                    var contents = thumbElWhenDraggingFromWindow.html();

                    ix = thumbElWhenDraggingFromWindow.offset().left;
                    iy = thumbElWhenDraggingFromWindow.offset().top;

                    // Create a copy of the thumb and place it at mouse location
                    $('body').prepend('<div id="sq-dragged-element-clone" class="sq-element-thumb">' + contents + '</div>');
                    dummyElementAtMouse = $('#sq-dragged-element-clone');
                    dummyElementAtMouse.css({
                        left: ix,
                        top: iy,
                        margin: 0
                    });

                    // Create a virtual map of all possible positions of the
                    // dragged element in all editors
                    elementDragMap = new Array();

                    for (var k=0; k<editors.length; k++) {
                        var editor = editors[k];

                        for (var i=0; i<editor.settings.containers.length; i++) {
                            var coords = { x: 0, y: 0 };
                            var c = editor.host.find('.sq-container[data-index='+ i +']');

                            // if the container has no elements, add one dummy element
                            // and move on to next container
                            if (editor.settings.containers[i].settings.elements.length == 0) {
                                c.append('<div id="sq-dummy-element-dragging-from-window-tmp"></div>');
                                var x = $('#sq-dummy-element-dragging-from-window-tmp').offset().left + $('#sq-dummy-element-dragging-from-window-tmp').outerWidth()/2;
                                var y = $('#sq-dummy-element-dragging-from-window-tmp').offset().top + $('#sq-dummy-element-dragging-from-window-tmp').outerHeight()/2;
                                elementDragMap.push({ x: x, y: y, elementIndex: 0, containerIndex: i, editorIndex: k });
                                $('#sq-dummy-element-dragging-from-window-tmp').remove();
                            }

                            for (var j=0; j<editor.settings.containers[i].settings.elements.length; j++) {
                                var el = c.find('.sq-element[data-index='+ j +']');

                                el.before('<div id="sq-dummy-element-dragging-from-window-tmp"></div>');

                                var x = $('#sq-dummy-element-dragging-from-window-tmp').offset().left + $('#sq-dummy-element-dragging-from-window-tmp').outerWidth()/2;
                                var y = $('#sq-dummy-element-dragging-from-window-tmp').offset().top + $('#sq-dummy-element-dragging-from-window-tmp').outerHeight()/2;
                                elementDragMap.push({ x: x, y: y, elementIndex: j, containerIndex: i, editorIndex: k });
                                $('#sq-dummy-element-dragging-from-window-tmp').remove();

                                // When we reach the end of the elements array, add a dummy element after the last element
                                if (j == editor.settings.containers[i].settings.elements.length - 1) {
                                    el.after('<div id="sq-dummy-element-dragging-from-window-tmp"></div>');
                                    var x = $('#sq-dummy-element-dragging-from-window-tmp').offset().left + $('#sq-dummy-element-dragging-from-window-tmp').outerWidth()/2;
                                    var y = $('#sq-dummy-element-dragging-from-window-tmp').offset().top + $('#sq-dummy-element-dragging-from-window-tmp').outerHeight()/2;
                                    elementDragMap.push({ x: x, y: y, elementIndex: j+1, containerIndex: i, editorIndex: k });
                                    $('#sq-dummy-element-dragging-from-window-tmp').remove();
                                }
                            }
                        }
                    }

                    if (elementDragMap.length == 0) {
                        // no valid containers found
                        dummyElementAtMouse.remove();
                        didStartDraggingElementToContainer = false;
                        shouldStartDraggingElementToContainer = false;
                        didStartDraggingElementToContainer = false;
                        virtualIndexOfDraggedElement = -1;
                    }
                }

                return false;
            }
            if (didStartDraggingElementToContainer) {
                // Update dummy element at mouse position
                dummyElementAtMouse.css({
                    left: ix + e.pageX - iex,
                    top: iy + e.pageY - iey
                });

                // Find the closest virtual position to the mouse position
                var closestIndex = 0;
                var closestDistance = 999999;

                for (var i=0; i<elementDragMap.length; i++) {
                    var d = Math.abs(e.pageX - elementDragMap[i].x) + Math.abs(e.pageY - elementDragMap[i].y);
                    if (d < closestDistance) {
                        closestDistance = d;
                        closestIndex = i;
                    }
                }

                // If the closest index is different than the current index,
                // remove the dummy element and insert a new one and the new index
                if (closestIndex != virtualIndexOfDraggedElement) {
                    virtualIndexOfDraggedElement = closestIndex;

                    // Remove the current dummy element
                    $('#sq-dummy-element-dragging-from-window').remove();

                    // Insert a new dummy element at the container/element index
                    var containerIndex = elementDragMap[virtualIndexOfDraggedElement].containerIndex;
                    var elementIndex = elementDragMap[virtualIndexOfDraggedElement].elementIndex;
                    var editorIndex = elementDragMap[virtualIndexOfDraggedElement].editorIndex;
                    var c = editors[editorIndex].host.find('.sq-container[data-index='+ containerIndex +']');

                    // If the index of the dummy element is bigger than the number
                    // of elements in that container, insert the dummy at the end
                    if (editors[editorIndex].settings.containers[containerIndex].settings.elements.length == 0) {
                        c.prepend('<div id="sq-dummy-element-dragging-from-window"><div id="sq-dummy-element-dragging-from-window-inner"></div></div>');
                    } else if (elementIndex == editors[editorIndex].settings.containers[containerIndex].settings.elements.length) {
                        var lastElementIndex = editors[editorIndex].settings.containers[containerIndex].settings.elements.length - 1;
                        var e = c.find('.sq-element[data-index='+ lastElementIndex +']');
                        e.after('<div id="sq-dummy-element-dragging-from-window"><div id="sq-dummy-element-dragging-from-window-inner"></div></div>');
                    } else {
                        var e = c.find('.sq-element[data-index='+ elementIndex +']');
                        e.before('<div id="sq-dummy-element-dragging-from-window"><div id="sq-dummy-element-dragging-from-window-inner"></div></div>');
                    }
                }

                return false;
            }
        });
        $(document).off('mouseup.elementFromWindow');
        $(document).on('mouseup.elementFromWindow', function() {
            if (didStartDraggingElementToContainer) {
                // Remove element clone (at mouse position)
                dummyElementAtMouse.remove();

                var containerIndex = elementDragMap[virtualIndexOfDraggedElement].containerIndex;
                var elementIndex = elementDragMap[virtualIndexOfDraggedElement].elementIndex;
                var editorIndex = elementDragMap[virtualIndexOfDraggedElement].editorIndex;

                editors[editorIndex].addElement(containerIndex, elementIndex, draggedElementFromWindowCatalogIndex);
            }

            shouldStartDraggingElementToContainer = false;
            didStartDraggingElementToContainer = false;
            virtualIndexOfDraggedElement = -1;

            return false;
        });

        // [end] Drag elements from window to container functionality
    }
    EditorWindow.prototype.createEditorWindow = function(x, y) {
        var html = '';

        html += ' <div class="sq-window" id="sq-window-'+ this.id +'">';
        html += this.editorWindowContents;
        html += ' </div>';

        if ($('.sq-windows-root').length == 0) {
            $('body').prepend('<div class="sq-windows-root"></div>');
        }

        $('.sq-windows-root').html(html);

        this.root = $('#sq-window-' + this.id);

        // this.openFirstTab();
        this.removeElementSettings();
    }
    EditorWindow.prototype.show = function(x, y) {
        // todo
        // Check if window exists
        if (!this.root) {
            // If not, create a new window
            this.createEditorWindow();
        }

        // Show the window
        this.visible = true;
        this.root.show();

        if (x !== undefined && y !== undefined) {
            this.root.css({
                left: x,
                top: y
            });
        }
    }
    EditorWindow.prototype.hide = function() {
        this.visible = false;
        this.root.hide();
    }
    EditorWindow.prototype.getContentHTML = function() {
        return this.editorWindowContents;
    }
    EditorWindow.prototype.openFirstTab = function() {
        // Open the first tab
        $('.sq-window-main-nav-button').removeClass('active');
        $('#sq-window-main-nav-button-elements').addClass('active');
        $('[data-tab-content][data-tab-group="sq-window-main-tab-group"]').hide();
        $('[data-tab-content][data-tab-group="sq-window-main-tab-group"][data-tab-index="0"]').show();
    }
    EditorWindow.prototype.removeElementSettings = function() {
        $('#sq-window-settings-tab-inner-content').html('<div id="sq-window-settings-tab-no-element">No element selected. To create an element, open the Elements tab and drag an element into a container.</div>');
    }

    function SquaresControl(s, name, group, tabGroup, options, valueUpdated) {
        // The 's' argument is the array coming from the registeredControls array
        // Automatically generated at the time of object creation
        this.id = Math.floor(Math.random() * 9999) + 1;
        this.elementID = 'sq-control-' + this.id;
        this.elementClass = 'sq-element-option-group';

        // Settings coming from the registered controls catalog
        // referenced in the 'this' variable, so 'this' can be accessed within
        // those functions (in case of validate(), HTML(), events(), etc)
        // These settings are also common in all controls
        this.type = s.type;
        this.getValue = s.getValue;
        this.setValue = s.setValue;
        this.HTML = s.HTML;

        // These variables are specific for each individual control
        this.name = name;
        this.options = options;
        this.group = group;
        this.tabGroup = tabGroup;

        // Private property, must be accessed only via setter and getter
        this._value = undefined;

        // Update this.elementClass
        if (this.group !== undefined) {
            this.elementClass = 'sq-element-option-group-' + this.group.toLowerCase().replace(/\s/g, '-');
        }

        // Launch the events provided from the settings
        this.init = s.init;
        this.init();

        // Create a callback function for when the control updates its value
        this.valueUpdated = valueUpdated;

        // Inline label flag
        this.customLabel = s.customLabel;
    }
    SquaresControl.prototype.getVal = function() {
        return this._value;
    }
    SquaresControl.prototype.setVal = function(v) {
        this._value = v;

        try {
            this.setValue(v);
        } catch (err) {

        }
    }
    SquaresControl.prototype.loadVal = function(v) {
        this.setValue(this._value);
    }
    SquaresControl.prototype.valueChanged = function() {
        // Re-sets the control to its stored value
        this._value = this.getValue();
        this.valueUpdated();
    }

    // Utility
    function hexToRgb(hex) {
        var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
        return result ? {
            r: parseInt(result[1], 16),
            g: parseInt(result[2], 16),
            b: parseInt(result[3], 16)
        } : null;
    }
    function subtract(a, b) {
        var r = {};

        // For each property of 'b'
        // if it's different than the corresponding property of 'a'
        // place it in 'r'
        for (var key in b) {
            if (typeof(b[key]) == 'object') {
                if (!a[key]) a[key] = {};
                r[key] = subtract(a[key], b[key]);
            } else {
                if (b[key] != a[key]) {
                    r[key] = a[key];
                }
            }
        }

        return r;
    }
    function clean(a) {
        var r = undefined;

        // Check if 'a' is an object
        if (typeof(a) == 'object') {
            // If 'a' is an object, check if it's empty and set to undefined if true
            if (isEmpty(a)) {
                r = undefined;
            } else {
                // If 'a' is NOT empty, iterate over each of its properties
                // and recursively clean
                for (var key in a) {
                    var cleaned = clean(a[key]);

                    if (cleaned !== undefined) {
                        if (r === undefined) r = {};

                        r[key] = cleaned;
                    }
                }
            }
        } else {
            r = a;
        }

        return r;
    }
    function isEmpty(obj) {
        for(var prop in obj) {
            if(obj.hasOwnProperty(prop))
            return false;
        }

        return true && JSON.stringify(obj) === JSON.stringify({});
    }
    function getWidthOfElementInGrid(span) {
        var columnWidth = 8.33333333;
        var elementWidth = columnWidth * span;

        return elementWidth + '%';
    }
    function isNumeric(n) {
        return !isNaN(parseFloat(n)) && isFinite(n);
    }

})(jQuery, window, document);
