HEX
Server: Microsoft-IIS/8.5
System: Windows NT YDAWBH120 6.3 build 9600 (Windows Server 2012 R2 Standard Edition) AMD64
User: tentjecom_web (0)
PHP: 7.4.14
Disabled: NONE
Upload Files
File: D:/HostingSpaces/SBogers10/farmfun.komma.pro/resources/js/kms/attributes/componentAreaManager.js
/**
 * The main class to manage components
 */
class ComponentAreaManager {
    /**
     * @param {HTMLDivElement} wrapper
     * @param {ComponentManagerApiController} componentManagerApiController
     */
    constructor(wrapper, componentManagerApiController) {
        this._componentAreaWrapperElement = wrapper;
        this._componentManagerApiController = componentManagerApiController;

        if (this._componentAreaWrapperElement === undefined || this._componentAreaWrapperElement.tagName !== "DIV") {
            console.error('ComponentAreaManager:constructor Did not get the expected div element that represents the components wrapper.');
            return;
        }

        if (this._componentManagerApiController === undefined) {
            console.error('ComponentAreaManager:constructor Did not get the expected _componentManagerApiController.');
            return;
        }

        this._componentAreaAttributeKey = this._componentAreaWrapperElement.id;

        //Get the input field for the components attribute that will hold all data for the components attribute values.
        this._masterInput = this._componentAreaWrapperElement.querySelector('input[name="' + this._componentAreaAttributeKey + '"]');
        if (!this._masterInput) {
            console.error('ComponentAreaManager:constructor Did not find an hidden input with name "' + this._componentAreaAttributeKey + '" inside of the wrapper.');
            return;
        }

        this._componentAreaContainer = this._componentAreaWrapperElement.querySelector('.js-components');
        if (!this._componentAreaContainer) {
            console.error('ComponentAreaManager:constructor the wrapper did not contain a div with class components.');
            return;
        }

        if (!'componentAttributeFieldsRegex' in this._componentAreaWrapperElement.dataset) {
            console.error('The wrapper element must, but did not have a componentAttributeFieldsRegex dataset value');
            return;
        }

        this._componentAttributeFieldsRegex = this._componentAreaWrapperElement.dataset.componentAttributeFieldsRegex;
        this._saveVersion = '0.9.1';
        this._addComponentButtonElements = [];
        this._eventMap = {};
        this._loaded = false;

        this._setAddComponentButtonElements();
        this._loadComponentsUsingSaveStateData()
    }

    /**
     * Sets up the add component buttons by finding them in the wrapper and adding listeners to them
     */
    _setAddComponentButtonElements() {
        this._addComponentButtonElements = this._componentAreaWrapperElement.querySelectorAll(".js-add-component");


        let addComponentButtonsElementsLength = this._addComponentButtonElements.length;
        for(let index = 0; index < addComponentButtonsElementsLength; index++)
        {
            let currentAddComponentButtonElement = this._addComponentButtonElements[index];
            currentAddComponentButtonElement.addEventListener('click', this._addComponentButtonClicked.bind(this))
        }
    }

    /**
     * Click handler for add component buttons
     *
     * @param {MouseEvent} mouseEvent
     */
    _addComponentButtonClicked(mouseEvent)
    {
        let self = this;
        mouseEvent.preventDefault();
        let button = mouseEvent.target;

        if(!'component' in button.dataset) {
            console.error('ComponentAreaManager: One of the add component buttons was missing the component dataset attribute. Please set it to the name of a valid component');
            return;
        }

        let componentSaveState = new ComponentSaveState();
        componentSaveState.id = parseInt(self._componentAreaWrapperElement.dataset.timesAddedComponent) * -1;
        componentSaveState.componentTypeId = parseInt(button.dataset.componentType); //The id of the component to resolve
        componentSaveState.sortOrder = 0;

        this._componentManagerApiController.getComponentHtmlElement(
            componentSaveState,
        this._componentAreaWrapperElement.id).then(function(data) {
            self.incrementTimesComponentAddedCounter();
            let componentHtmlElement = data.element;
            self._addComponent(componentHtmlElement);
            self._updateMasterInput();
        });
    }

    /**
     * Adds new components based on the component area savestate from another component area manager
     *
     * @param {ComponentAreaSaveState} originalComponentAreaSaveState
     */
    addNewComponentsBasedOnComponentAreaSaveState(originalComponentAreaSaveState) {
        if(!(originalComponentAreaSaveState instanceof ComponentAreaSaveState)) {
            console.error('ComponentAreaManager: The given "originalComponentAreaSaveState" parameter was not an instance of ComponentAreaSaveState. Not adding its components.');
        }

        let self = this;

        let componentsCount = originalComponentAreaSaveState.componentsCount;
        for(let index = 0; index < componentsCount; index++) {
            //Get the component save state and its type to make the copy
            let originalComponentSaveState = originalComponentAreaSaveState.getComponentSavestateAt(index);

            //Create a new component save state based on the original
            let componentSaveState = new ComponentSaveState();
            componentSaveState.id = parseInt(self._componentAreaWrapperElement.dataset.timesAddedComponent) * -1;
            componentSaveState.componentTypeId = originalComponentSaveState.componentTypeId; //The id of the component to resolve
            componentSaveState.sortOrder = 0;

            //Add the components after loading it from the api
            this._componentManagerApiController.getComponentHtmlElement(
                componentSaveState,
                this._componentAreaWrapperElement.id).
            then(function(data) {
                self.incrementTimesComponentAddedCounter();
                let componentHtmlElement = data.element;
                self._addComponent(componentHtmlElement);
                self._updateMasterInput();
            });
        }
    }

    /**
     * Increments the counter that keeps track of how often a component was added before saving.
     */
    incrementTimesComponentAddedCounter()
    {
        return this._componentAreaWrapperElement.dataset.timesAddedComponent = (parseInt(this._componentAreaWrapperElement.dataset.timesAddedComponent) + 1).toString();
    }

    /**
     * Adds a component to the component wrapper and add listeners to it to detect changes
     * {HTMLElement} componentHtmlElement
     * {Number} position
     */
    _addComponent(componentHtmlElement)
    {
        let self = this;

        //Add listeners to the attributes to detect changes and trigger a master input update
        let foundComponentInputsModel = this._findAttributeDataInputs(componentHtmlElement);
        let inputsCount = foundComponentInputsModel.inputElements.length;
        for(let index = 0; index < inputsCount; index++) {
            let input = foundComponentInputsModel.inputElements[index];

            let debouncedKeyFunction = self._debounce(self._attributeInputChanged.bind(self), 500);

            input.addEventListener('change', self._attributeInputChanged.bind(self));
            input.addEventListener('blur', self._attributeInputChanged.bind(self));
            input.addEventListener('keydown', debouncedKeyFunction.bind(self));
        }

        //Add listeners to componentable attributes to detect changes and trigger a master input update
        let componentableComponentInputs = this._findComponentableInputs(componentHtmlElement);
        let componentableInputsCount = foundComponentInputsModel.inputElements.length;
        for(let index = 0; index < componentableInputsCount; index++) {
            let input = componentableComponentInputs.inputElements[index];
            if(!input) continue;

            let debouncedKeyFunction = self._debounce(self._attributeInputChanged.bind(self), 500);

            input.addEventListener('change', self._attributeInputChanged.bind(self));
            input.addEventListener('blur', self._attributeInputChanged.bind(self));
            input.addEventListener('keydown', debouncedKeyFunction.bind(self));
        }

        //Make the delete button work
        let deleteButton = componentHtmlElement.querySelector('.js-component-delete');
        deleteButton.addEventListener('click', function(event) {
            event.preventDefault();
            componentHtmlElement.parentElement.removeChild(componentHtmlElement);
            self._updateMasterInput();
        });
        
        //Make the move down button work
        let downButton = componentHtmlElement.querySelector('.js-component-move-up');
        downButton.addEventListener('click', function(event) {
            event.preventDefault();
            if(componentHtmlElement.previousSibling) {
                componentHtmlElement.parentElement.insertBefore(componentHtmlElement, componentHtmlElement.previousSibling);
            }
            self._updateMasterInput();
        });

        //Make the move up button work
        let upButton = componentHtmlElement.querySelector('.js-component-move-down');
        upButton.addEventListener('click', function(event) {
            event.preventDefault();
            if(componentHtmlElement.nextSibling && componentHtmlElement.nextSibling.nextSibling) {
                componentHtmlElement.parentElement.insertBefore(componentHtmlElement, componentHtmlElement.nextSibling.nextSibling);
            } else {
                componentHtmlElement.parentElement.appendChild(componentHtmlElement);
            }
            self._updateMasterInput();
        });

        //Put some of the component's attributes in tabs when needed
        this._putComponentAttributesInTabs(componentHtmlElement);

        //Add the component
        this._componentAreaContainer.appendChild(componentHtmlElement);

        this.triggerEvent('componentAdded', [componentHtmlElement, this._loaded]);
    }

    /**
     * Triggered when an attribute input was changed.
     *
     * @param event
     */
    _attributeInputChanged(event)
    {
        this._updateMasterInput();
    }

    /**
     * updates the master input which holds all attribute values
     */
    _updateMasterInput()
    {
        let self = this;
        let componentAreaSaveState = this.getComponentAreaSaveState();
        self._masterInput.value = JSON.stringify(componentAreaSaveState);
    }

    _putComponentAttributesInTabs(componentElement) {
        //Validate that we did get a component element, or show error and return;
        let attributeWrapper = componentElement.querySelector('.attributes');
        if (!attributeWrapper) {
            console.error('Expected to get a "componentElement" but did not. It did not contain a div with class "attributes" in it.');
            return;
        }

        //Retrieve the attributes in the component and store the length
        let attributes = attributeWrapper.children;
        let attributesCount = attributes.length;

        //Create tab configuration that we use to build the tabs later on. Each property represents a tab name.
        //each property value is an array of attribute elements that belong to the tab name.
        let tabsConfiguration = {};
        for (let i = 0; i < attributesCount; i++) {
            let attributeElement = attributes[i];
            if ("tab" in attributeElement.dataset) {
                if (!tabsConfiguration.hasOwnProperty(attributeElement.dataset.tab)) tabsConfiguration[attributeElement.dataset.tab] = [];
                tabsConfiguration[attributeElement.dataset.tab].push(attributeElement);
            }
        }

        //Now build tabs if the tabsConfiguration object has children
        let tabsCount = Object.keys(tabsConfiguration).length;
        if (tabsCount > 0) {
            //First build the tab "framework" html and add it to the attributes container
            let domParser = new DOMParser();
            let document = domParser.parseFromString(
                '<div class="component-tab">' +                 //Note check kms.js. It uses the class name to make the tabs work
                '    <ul class="component-tab__list">' +
                '    </ul>' +
                '    <div class="component-tab__container">' +
                '    </div>' +
                '</div>'
                , 'text/html');
            let componentTab = document.body.firstChild;
            attributeWrapper.appendChild(componentTab);

            //Build and add tabs. And put the attributes inside the correct ones
            let index = 0;
            for (let tabName in tabsConfiguration) {
                if (!tabsConfiguration.hasOwnProperty(tabName)) continue; //Skip "tabName" if it is a property on the objects prototype

                //Create the tab switch button to switch to the first tab
                document = domParser.parseFromString(
                    '<li class="component-tab__list-item">' +
                    '    <a class="component-tab__button">' + tabName + '</a>' +
                    '</li>'
                    , 'text/html');
                let tabSwitchButton = document.body.firstChild;

                //Add the tabSwitchButton to the tab list
                let tabList = componentTab.getElementsByClassName('component-tab__list')[0];
                tabList.appendChild(tabSwitchButton);

                //Create the content holder
                document = domParser.parseFromString('<div class="component-tab__content"></div>', 'text/html');
                let tabContent = document.body.firstChild;

                //Add the content holder in the componentTab container
                let container = componentTab.getElementsByClassName('component-tab__container')[0];
                container.appendChild(tabContent);

                //Put the attributes in the tab content holder
                let attributesCount = tabsConfiguration[tabName].length;
                for (let i = 0; i < attributesCount; i++) {
                    tabContent.appendChild(tabsConfiguration[tabName][i]);
                }

                //Make the first tab active
                if (index === 0) {
                    tabSwitchButton.classList.add('active');
                    tabContent.classList.add('active')
                }

                //Make the button switch to the correct tab by using the is-active classes
                tabSwitchButton.addEventListener('click', function (clickEvent) {
                    clickEvent.preventDefault();
                    let activeTabIndex = 0;
                    for (let tabSwitchButtonIndex = 0; tabSwitchButtonIndex < tabsCount; tabSwitchButtonIndex++) {

                        let currentTabSwitchButton = tabList.children[tabSwitchButtonIndex];
                        currentTabSwitchButton.classList.remove('active');

                        if (currentTabSwitchButton === tabSwitchButton) {
                            currentTabSwitchButton.classList.add('active');
                            activeTabIndex = tabSwitchButtonIndex;
                        }
                    }

                    for (let tabContentIndex = 0; tabContentIndex < tabsCount; tabContentIndex++) {
                        let currentTabContent = container.children[tabContentIndex];
                        currentTabContent.classList.remove('active');
                        if (tabContentIndex === activeTabIndex) currentTabContent.classList.add('active');
                    }
                });

                index++;
            }
        }
    }

    /**
     * Does search inputs, textarea's and select's in the given component div element or else inside of
     * the componentsWrapper and checks if they match against the regex that identifies them as attribute data fields
     *
     * @param {HTMLDivElement|null} componentElement
     * @param {ComponentSaveState} componentSavestate to also pass when resolved
     * @return {FoundComponentInputsModel}
     */
    _findAttributeDataInputs(componentElement, componentSavestate = undefined)
    {
        let dataInputs = [];

        let attributesContainer = componentElement.getElementsByClassName('attributes')[0];
        let possibleDataInputs = attributesContainer.querySelectorAll('input, textarea, select');
        let inputsQuantity = possibleDataInputs.length;
        for (let index = 0; index < inputsQuantity; index++) {
            let input = possibleDataInputs[index];
            if(input.name !== undefined && input.name.match(new RegExp(this._componentAttributeFieldsRegex))) {
                dataInputs.push(input)
            }
        }

        let model = new FoundComponentInputsModel();
        model.inputElements = dataInputs;
        model.componentElement = componentElement;
        model.componentSaveState = componentSavestate;

        return model;
    }

    /**
     * Gets the value of the select / autocomplete input for selecting a model you want to link the component to.
     * Notice that the code is pretty much the same as _findAttributeDataInputs.
     *
     * @param {HTMLDivElement|null} componentElement
     * @param {ComponentSaveState} componentSavestate to also pass when resolved
     * @return {FoundComponentInputsModel}
     *
     * @private
     */
    _findComponentableInputs(componentElement, componentSavestate = undefined) {
        let dataInputs = [];

        let componentableSelectorsContainer = componentElement.querySelector('.componentableSelectors');
        if(!componentableSelectorsContainer) return new FoundComponentInputsModel();

        let possibleDataInputs = componentableSelectorsContainer.querySelectorAll('input, textarea, select');
        let inputsQuantity = possibleDataInputs.length;
        for (let index = 0; index < inputsQuantity; index++) {
            let input = possibleDataInputs[index];
            if(input.name !== undefined && input.name.match(new RegExp(this._componentAttributeFieldsRegex))) {
                dataInputs.push(input);
            }
        }

        let model = new FoundComponentInputsModel();
        model.inputElements = dataInputs;
        model.componentElement = componentElement;
        model.componentSaveState = componentSavestate;

        return model;
    }

    /**
     * @return {NodeListOf<HTMLElementTagNameMap[HTMLDivElement]>}
     */
    getComponentElements()
    {
        return this._componentAreaContainer.querySelectorAll('.c-component');
    }

    /**
     * Returns the component area wrapper div
     *
     * @return {HTMLElement}
     */
    getWrapperElement()
    {
        return this._componentAreaWrapperElement;
    }

    /**
     * Returns a ComponentAreaSaveState
     * And contains a save version to allow non breaking updates in the future.
     *
     * @return ComponentAreaSaveState
     */
    getComponentAreaSaveState()
    {
       let saveState = new ComponentAreaSaveState();

        //Loop over all components.
        let components = this.getComponentElements();
        let componentsCount = components.length;

        for(let componentNumber = 0; componentNumber < componentsCount; componentNumber++)
        {
            //Loop over all attributes in the components...
            let currentComponent = components[componentNumber];
            // findAttributeDataInputsPromises.push(this._findAttributeDataInputs(currentComponent));

            let foundComponentInputsModel = this._findAttributeDataInputs(currentComponent);
            let foundComponentsInputModelForLinkable = this._findComponentableInputs(currentComponent);

            //Create an array of objects. The objects properties are field names and the objects values are the values of those field names
            let attributeInputData = {};
            let inputsCounts = foundComponentInputsModel.inputElements.length;
            for(let currentInputNumber = 0; currentInputNumber < inputsCounts; currentInputNumber++) {
                let currentInput = foundComponentInputsModel.inputElements[currentInputNumber];
                if(currentInput.tagName === "INPUT" && currentInput.type === "file") {
                    //Ignore file input
                } else {
                    attributeInputData[currentInput.name] = currentInput.value;
                }
            }

            //Create an array containing one object that has a property name that corresponds to an inputs field name and the value of that property is the value of that input.
            let linkableInputObject = {};
            let linkableInputsCount = foundComponentsInputModelForLinkable.inputElements.length;
            for(let currentInputNumber = 0; currentInputNumber < linkableInputsCount; currentInputNumber++) {
                let currentInput = foundComponentsInputModelForLinkable.inputElements[currentInputNumber];
                if(currentInput.tagName === "INPUT" && currentInput.type === "file") {
                    //Ignore file input
                } else {
                    linkableInputObject[currentInput.name] = currentInput.value;
                }
            }

            let componentSaveState = new ComponentSaveState();
            componentSaveState.id = Number(foundComponentInputsModel.componentElement.dataset.id);
            componentSaveState.componentTypeId = Number(foundComponentInputsModel.componentElement.dataset.componentTypeId);
            componentSaveState.data = attributeInputData;
            componentSaveState.version = this._saveVersion;
            componentSaveState.sortOrder = Number(componentNumber);
            componentSaveState.componentableData = linkableInputObject;

            saveState.addComponentSaveState(componentSaveState);
        }

        return saveState;
    }


    /**
     * Uses the master input to rebuild the saved componentarea with components with their data
     */
    _loadComponentsUsingSaveStateData() {
        let self = this;

        //Retrieve and decode the save state data
        let savedData = this._masterInput.value;

        if(!savedData) return false;
        let saveState = ComponentAreaSaveState.fromJsonString(savedData);
        if(!saveState) return false;

        let getComponentHtmlElementPromises = [];

        let savedComponentsCount = saveState.componentsCount;
        for(let currentComponentNumber = 0; currentComponentNumber < savedComponentsCount; currentComponentNumber++) {
            let currentComponentSaveState = saveState.getComponentSavestateAt(currentComponentNumber);
            getComponentHtmlElementPromises.push(this._componentManagerApiController.getComponentHtmlElement(currentComponentSaveState, this._componentAreaWrapperElement.id));
        }
        Promise.all(getComponentHtmlElementPromises).then(function(componentDatas) {
            let componentDatasCount = componentDatas.length;

            //Rebuild structure
            for (let componentNumber = 0; componentNumber < componentDatasCount; componentNumber++) {
                let componentData = componentDatas[componentNumber];

                let componentHTMLElement = componentData.element;
                self._addComponent(componentHTMLElement);
            }
    
            //Loop over the same componentHTMLElements and fill their attribute inputs
            for (let currentComponentNumber = 0; currentComponentNumber < componentDatasCount; currentComponentNumber++) {
                let currentComponentData = componentDatas[currentComponentNumber];
                let currentComponentSavestate = currentComponentData.componentSavestate;

                let attributeData = currentComponentSavestate.data;
                for (let attributeKeyName in attributeData) {
                    if(!attributeData.hasOwnProperty(attributeKeyName)) continue;
                    if (currentComponentSavestate.data.hasOwnProperty(attributeKeyName)) {
                        let attributeFields = self._componentAreaWrapperElement.querySelectorAll('[name="'+attributeKeyName+'"]');
                        let nAttributeFields = attributeFields.length;
                        if(nAttributeFields > 0) {
                            for (let index = 0; index < nAttributeFields; index++) {
                                attributeFields[index].value = attributeData[attributeKeyName]
                            }
                        } else {
                            console.error('Could not load data for field: '+attributeKeyName+' since it could not be found. Skipping it');
                        }
                    }
                }

                let componentableData = currentComponentSavestate.componentableData;
                for (let attributeKeyName in componentableData) {
                    if(!componentableData.hasOwnProperty(attributeKeyName)) continue;
                    if (currentComponentSavestate.componentableData.hasOwnProperty(attributeKeyName)) {
                        let attributeFields = self._componentAreaWrapperElement.querySelectorAll('[name="'+attributeKeyName+'"]');
                        let nAttributeFields = attributeFields.length;
                        if(nAttributeFields > 0) {
                            for (let index = 0; index < nAttributeFields; index++) {
                                attributeFields[index].value = componentableData[attributeKeyName]
                            }
                        } else {
                            console.error('Could not load data for field: '+attributeKeyName+' since it could not be found. Skipping it');
                        }
                    }
                }
            }

            //sessionStorage.removeItem("componentScrollPosition");

            if(isset(sessionStorage.getItem("componentScrollPosition"))) {
                self._componentAreaWrapperElement.parentElement.parentElement.parentElement.scrollTop = sessionStorage.getItem("componentScrollPosition");
            }

            self.loaded = true;
        });
    }

    /**
     * Registers a event to a callback and returns the a reference to the event handler for that specific event
     *
     * @param event (string)
     * @param callback (callable)
     */
    on(event, callback)
    {
        if(!this._eventMap.hasOwnProperty(event)) this._eventMap[event] = [];
        return this._eventMap[event].push(callback);
    }

    /**
     * Call event callbacks
     *
     * @param event (string)
     * @param eventArgs (array) an array of arguments to pass to the callback
     */
    triggerEvent(event, eventArgs) {
        if(!this._eventMap.hasOwnProperty(event)) return;

        let nEvents = this._eventMap[event].length;
        for(let index = 0; index < nEvents; index++)
        {
            let callback = this._eventMap[event][index];
            if(eventArgs && eventArgs.length > 0) {
                callback.apply(this, eventArgs)
            } else {
                callback.call(this);
            }
        }
    }

    /**
     * Returns a function, that, as long as it continues to be invoked, will not
     * be triggered. The function will be called after it stops being called for
     * N milliseconds. If `immediate` is passed, trigger the function on the
     * leading edge, instead of the trailing.
     *
     * @param func
     * @param wait
     * @param immediate
     * @return {Function}
     */
    _debounce(func, wait, immediate) {
        var timeout;
        return function() {
            var context = this, args = arguments;
            var later = function() {
                timeout = null;
                if (!immediate) func.apply(context, args);
            };
            var callNow = immediate && !timeout;
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
            if (callNow) func.apply(context, args);
        };
    };
}

/**
 * Used by the ComponentAreaManager to talk to the backend
 */
class ComponentManagerApiController {
    constructor()
    {
        this.domParser = new DOMParser();
    }

    /**
     * @param {ComponentSaveState} componentSavestate
     * @param {string} componentAreaAttributeKey
     * @return {Promise} that resolves with a HTMLDivElement that represents the newly requested component with it's attributes in it
     */
    getComponentHtmlElement(componentSavestate, componentAreaAttributeKey)
    {
        let self = this;

        return new Promise(function(resolve, reject) {
            let postData = {
                componentAreaAttributeKey: componentAreaAttributeKey,
                componentSaveState: JSON.stringify(componentSavestate)
            };

            axios.post('/api/dynamic/component/resolve', postData).then(function(response) {
                let toResolve = {
                    element: self._stringToHtmlElement(response.data),
                    componentSavestate: componentSavestate
                };

                resolve(toResolve);
            }).catch(function(error) {
                console.error('ComponentManagerApiController:getComponentHtmlElement Could not get HTML of a component because of the following error:',error);
                reject(error);
            })
        });
    }

    getComponentAreaRegexForComponent(componentKey)
    {
        if(componentKey === undefined) {
            console.error('ComponentManagerApiController:getComponentAreaRegexForComponent Could not get the regex for an undefined componentKey. Please specify it.');
            return;
        }

        return new Promise(function(resolve, reject) {
            axios.post('/api/dynamic/component/attribute_regex', {
                componentKey: componentKey,
            }).then(function(response) {
                resolve(response.data);
            }).catch(function(error) {
                console.error('ComponentManagerApiController:getComponentAreaRegexForComponent Could not get the regex to find inputs for a component because of the following error:',error);
                reject(error);
            })
        });
    }

    /**
     * @return {Promise} that resolves with an array of component type names. Or rejects with the already console logged error.
     */
    getAllComponentTypeNames()
    {
        return new Promise(function(resolve, reject) {
            axios.get('/api/dynamic/component').then(function(response) {
                return response.data; //Must be an array full of component type names
            }).catch(function(error) {
                console.error('ComponentManagerApiController:getAllComponentTypeNames Could not the names of all available componentTypes because of an error:', error);
                reject(error);
            })
        });
    }

    /**
     * Converts a string to an HTMLElement
     *
     * @param {string} data
     * @return {HTMLElement}
     */
    _stringToHtmlElement(data)
    {
        let document = this.domParser.parseFromString(data, "text/html");
        return document.body.firstElementChild;
    }
}


/**
 * Holds information about a component HtmlElement, its input elements
 * and the save state for that component.
 */
class FoundComponentInputsModel {
    constructor() {
        this._inputElements = [];
        this._componentElement = null;
        this._componentSaveState = null;
    }

    /**
     * @return {null|Array}
     */
    get inputElements() {
        return this._inputElements;
    }

    /**
     * @param {NodeList} value
     */
    set inputElements(value) {
        if(typeof value !== 'object') { //Pssst...array primitives don't exist. Don't tell anyone :)
            console.error('ComponentSaveState: Did not set inputElements since the parameter was not an array. Actual: ', value)
            return;
        }
        this._inputElements = value;
    }

    /**
     * @return {null|HTMLDivElement}
     */
    get componentElement() {
        return this._componentElement;
    }

    /**
     * @param {HTMLDivElement} value
     */
    set componentElement(value) {
        if(!value instanceof HTMLDivElement) {
            console.error('ComponentSaveState: Did not set componentElement since the parameter was not an HTMLDivElement.')
            return;
        }
        this._componentElement = value;
    }

    /**
     * @return {null|ComponentSaveState}
     */
    get componentSaveState() {
        return this._componentSaveState;
    }

    /**
     * @param {null|ComponentSaveState} value
     */
    set componentSaveState(value) {
        if(!value instanceof ComponentSaveState) {
            console.error('ComponentSaveState: Did not set componentSaveState since the parameter was not ComponentSaveState.')
            return;
        }
        this._componentSaveState = value;
    }
}

/**
 * Represents data that can be saved and loaded for a Component.
 * Always used in combination with ComponentAreaSaveState
 */
class ComponentSaveState
{
    constructor()
    {
        this._version = '0.9.1';
        this._id = null;
        this._componentTypeId = null;
        this._data = {};
        this._componentableData = {};
        this._sortOrder = null;

        this.toJSON = this._toJson.bind(this);
    }

    _toJson()
    {
        return {
            id: this._id,
            version: this._version,
            componentTypeId: this._componentTypeId,
            data: this._data,
            componentableData: this._componentableData,
            sortOrder: this._sortOrder,
        }
    }

    static fromObject(jsonObject)
    {
        //Validate the object
        if(
            (!jsonObject.hasOwnProperty('id') || typeof jsonObject.id !== 'number') ||
            (!jsonObject.hasOwnProperty('componentTypeId') || typeof jsonObject.componentTypeId !== 'number') ||
            (!jsonObject.hasOwnProperty('version') || typeof jsonObject.version !== 'string') ||
            (!jsonObject.hasOwnProperty('sortOrder') || typeof jsonObject.sortOrder !== 'number') ||
            (!jsonObject.hasOwnProperty('data') || typeof jsonObject.data !== 'object') ||
            (!jsonObject.hasOwnProperty('componentableData') || typeof jsonObject.data !== 'object')
        ) {
            console.error('Could not create a ComponentSaveState instance from an object since that object did not contain all of the following properties of the correct types: id (number), componentTypeId (number), version (string), sortOrder (number), data (array object). Actual: ', jsonObject)
            return;
        }


        //Create a new instance of the current class that we return later on.
        let componentSaveState = new this;

        componentSaveState.id = jsonObject.id;
        componentSaveState.componentTypeId = jsonObject.componentTypeId;
        componentSaveState.version = jsonObject.version;
        componentSaveState.sortOrder = jsonObject.sortOrder;
        componentSaveState.data = jsonObject.data;
        componentSaveState.componentableData = jsonObject.componentableData;

        return componentSaveState;
    }


    get version() {
        return this._version;
    }

    set version(value) {
        if(typeof value !== 'string') {
            console.error('ComponentSaveState: Did not set version since the parameter was not a string. Actual: ', value);
            return;
        }
        this._version = value;
    }

    get id() {
        return this._id;
    }

    set id(value) {
        if(value === "" || typeof value !== 'number') {
            console.error('ComponentSaveState: Did not set the id since the parameter was not a number. Actual: ', typeof value, 1);
            return;
        }
        this._id = value;
    }

    get componentTypeId() {
        return this._componentTypeId;
    }

    set componentTypeId(value) {
        if(value === "" || typeof value !== 'number') {
            console.error('ComponentSaveState: Did not set componentTypeId since the parameter was not a number. Actual: ', typeof value, 1);
            return;
        }
        this._componentTypeId = value;
    }

    get data() {
        return this._data;
    }

    set data(value) {
        if(typeof value !== 'object') {
            console.error('ComponentSaveState: Did not set data since the parameter was not an object. Actual: ', value);
            return;
        }
        this._data = value;
    }

    get sortOrder() {
        return this._sortOrder;
    }

    set sortOrder(value) {
        if(value === "" || typeof value !== 'number') {
            console.error('ComponentSaveState: Did not set sortOrder since the parameter was not a number. Actual: ', value);
        }
        this._sortOrder = value;
    }

    get componentableData() {
        return this._componentableData;
    }

    set componentableData(value) {
        if(typeof value !== 'object') {
            console.error('ComponentSaveState: Did not set componentableData since the parameter was not an object. Actual: ', value);
            return;
        }
        this._componentableData = value;
    }
}










/**
 * Represents data that can be saved and loaded for a ComponentArea.
 * This is the actual data container for the backend.
 */
class ComponentAreaSaveState {
    constructor()
    {
        this.toJSON = this._toJson.bind(this);
        this._componentSaveStates = [];
    }

    /**
     * @param {ComponentSaveState} value
     */
    addComponentSaveState(value) {
        if(!value instanceof ComponentSaveState) {
            console.error('ComponentAreaSaveState: The passed "componentSaveState" was not an instance of ComponentSaveState. Actual: ', value);
            return;
        }

        this._componentSaveStates.push(value);
    }

    /**
     *
     * @param index
     * @return {ComponentSaveState}
     */
    getComponentSavestateAt(index)
    {
        return this._componentSaveStates[index];
    }

    /**
     *
     * @return {Array}
     * @private
     */
    _toJson()
    {
        return this._componentSaveStates;
    }

    /**
     *
     * @param {string} json
     * @return {ComponentAreaSaveState}
     */
    static fromJsonString(json) {
        let jsonObject = null;
        try {
            jsonObject = JSON.parse(json);
        } catch (e) {
            console.error('ComponentAreaSaveState: The given jsonString does not represent ComponentAreaSaveState since the json string was not a valid json')
            return;
        }

        //Create a new instance of the current class that we return later on.
        let componentAreaSaveState = new this;

        //Create components from the saved data and put it in the new ComponentAreaSaveState instance
        let componentsCount = jsonObject.length;
        for(let index = 0; index < componentsCount; index++)
        {
            let currentComponentSaveStateObject = jsonObject[index];
            let currentComponentSaveState = ComponentSaveState.fromObject(currentComponentSaveStateObject);
            componentAreaSaveState.addComponentSaveState(currentComponentSaveState);
        }

        return componentAreaSaveState;
    }

    /**
     * Returns the amount of components
     *
     * @return {number}
     */
    get componentsCount() {
        return this._componentSaveStates.length;
    }
}