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/carrot.komma.pro/resources/js/kms/attributes/documents.js
/**
 * Manages file inputs in a wrapper so that files
 *
 * @param {HTMLElement} wrapper
 * @param {HTML5Uploader} HTML5Uploader
 */
class DocumentManager {
    constructor(wrapper, HTML5Uploader) {
        //Initialize variables
        this.constructedSuccessFully = false;
        this.wrapper = wrapper;

        this.HTML5Uploader = HTML5Uploader;

        this.illegalMoveCount = 0;

        this.accept = ("documentsAccept" in wrapper.dataset) ? wrapper.dataset.documentsAccept : undefined;

        if("documentsKey" in wrapper.dataset) {
            this.key = wrapper.dataset.documentsKey;
        } else {
            console.error('DocumentManager: Make sure that the wrapper contains the attributes key');
            return;
        }

        this.isSortable = ("documentsIsSortable" in wrapper.dataset) ? (wrapper.dataset.documentsIsSortable === "1")  : false;

        this.extensionThumbsFolder = ("documentsExtensionThumbsFolder" in wrapper.dataset) ? wrapper.dataset.documentsExtensionThumbsFolder : '/img/kms/extension_thumbs/';
        this.availableExtensionThumbs = ("documentsAvailableExtensionThumbs" in wrapper.dataset) ? wrapper.dataset.documentsAvailableExtensionThumbs : ['svg', 'pdf', 'zip', 'rar', 'csv', 'xlsx', 'mp3', 'mp4', 'docx', 'doc', 'png', 'jpg', 'jpeg', 'gif'],
        this.enablePreviewsIfPossible = ("documentsEnablePreviewsIfPossible" in wrapper.dataset) ? (wrapper.dataset.documentsEnablePreviewsIfPossible === "1") : false;
        this.maxDocuments = ("documentsMaxDocuments" in wrapper.dataset) ? wrapper.dataset.documentsMaxDocuments : undefined;
        this.imageProperties = ("imageProperties" in wrapper.dataset) ? wrapper.dataset.imageProperties : null;
        this.subFolder = ("subFolder" in wrapper.dataset) ? wrapper.dataset.subFolder : 'documents';

        this.eventMap = {};

        let form = document.getElementById('entity-form');
        if(form) {
            let maxUploadSizeInBytes = form.dataset.maxUploadSize;
            if(maxUploadSizeInBytes) this.maxUploadSizeInBytes = maxUploadSizeInBytes;
        }

        this.uploadSizeExceededMessage = 'You cannot upload more files right now. Please save these first before continuing. The limit is:';

        if(wrapper == null) {
            console.error('DocumentManager: The wrapper was not a valid html element. Stopping DocumentManager construction');
            return;
        }

        let documentList = wrapper.getElementsByClassName('files')[0];
        let dataInput = wrapper.querySelector('input[name="' + this.key + '"]');

        if (!documentList) {
            console.error('DocumentManager: The document uploader needs an ul element with class "files" in the given wrapper');
            return
        }
        this.documentList = documentList;


        if (!dataInput) {
            console.error('DocumentManager: The document uploader needs an input element with name "' + this.key + '" in the given wrapper. It is used to keep track of the states of all documents. Stopping DocumentManager construction');
            return;
        }
        this.dataInput = dataInput;

        if(!"uploadedDocuments" in wrapper.dataset) {
            console.error('DocumentManager: The wrapper must have a uploadedDocuments dataset attribute containing a json string representing the uploaded documents as an array of documents. Stopping DocumentManager construction')

        }

        let documents = JSON.parse(this.dataInput.value);
        this.constructedSuccessFully = true;


        //Make sure the "this" inside event callback functions are referencing to the documentManager
        this._deleteDocumentButtonClicked = this._deleteDocumentButtonClicked.bind(this);
        this._modifiedDocument = this._modifiedDocument.bind(this);

        this._drag = this._drag.bind(this);
        this._drop = this._drop.bind(this);
        this._dragOver = this._dragOver.bind(this);
        this._dragLeave = this._dragLeave.bind(this);

        this._HTML5UploadStarted = this._HTML5UploadStarted.bind(this);
        this._HTML5UploadProgress = this._HTML5UploadProgress.bind(this);
        this._HTML5UploadedFile = this._HTML5UploadedFile.bind(this);
        this._HTML5UploadFailedOrCanceled = this._HTML5UploadFailedOrCanceled.bind(this);
        this._HTML5UploadFailedOrCanceled = this._HTML5UploadFailedOrCanceled.bind(this);

        //Initialize the document manager with the existing components
        this._initialize(documents);
    }

    /**
     * Initializes the setup of the document uploader.
     *
     * @private
     * @param documents string Example structure:
     *  '[{
     *          "id": 3,
     *          "file_url": "uploads/products/Before 2018-01-25 at 14,48,23_1522928384.png",
     *          "documentable_id": 1,
     *          "documentable_type": "App\\Komma\\Shop\\Products\\Product\\Product",
     *          "created_at": "2018-04-05 11:39:44",
     *          "updated_at": "2018-04-05 11:39:44"
     *  }]'
     *
     */
    _initialize(documents) {
        if(!this.constructedSuccessFully) return;

        if (documents) {
            let length = documents.length;
            for(let index = 0; index < length; index++) {
                let initDocument = documents[index];
                let documentModel = DocumentModel.fromJson(initDocument);
                if (documentModel) {
                    this._addDocumentElement(documentModel);
                } else {
                    console.error("DocumentManager stumbled upon a document that is not valid upon initializing: ");
                    console.error(initDocument);
                }
            }
            this._sortDocuments();
        }

        this._updateDataInput();
        this._setupHtml5Uploader();
    }

    /**
     * Set up the html 5 uploader. Hook to its events
     *
     * @private
     */
    _setupHtml5Uploader()
    {
        let self = this;

        //Uploading event binding
        this.HTML5Uploader.on('uploadStart', this._HTML5UploadStarted);
        this.HTML5Uploader.on('updateProgress', this._HTML5UploadProgress);
        this.HTML5Uploader.on('uploadComplete', this._HTML5UploadedFile);
        this.HTML5Uploader.on('uploadFailed', this._HTML5UploadFailedOrCanceled);
        this.HTML5Uploader.on('uploadCanceled', this._HTML5UploadFailedOrCanceled);
    }

    /**
     * Triggered when a HTML file upload failed to upload or was canceled.
     *
     * @constructor
     * @private
     */
    _HTML5UploadFailedOrCanceled(file, responseText, extraData)
    {
        let documentElement = file.documentElement;
        if(!documentElement) return;

        this.documentList.removeChild(documentElement);
        this._updateDataInput();
    }

    /**
     * Triggered when a file was uploaded successfully
     *
     * @param file
     * @param responseText
     * @private
     */
    _HTML5UploadedFile(file, responseText) {
        let documentElement = file.documentElement;
        if(!documentElement) return;

        let backendDocumentModel = JSON.parse(responseText);
        let documentModel = DocumentModel.fromJson(backendDocumentModel);


        this._attachDocumentModelToDocumentElement(documentElement, documentModel);
        this._updateDocumentElementWithDocumentData(documentElement, documentModel);
        this._updateSortOrder(); //Does also update the data input

        //documentElement.querySelector('.drag-icon').classList.remove('is-hidden');
        documentElement.querySelector('.thumb').classList.remove('is-uploading');
        documentElement.querySelector('.percentage').remove();
    }

    /**
     * Triggered when HTML5 Uploader made progress in uploading a file
     *
     * @param file
     * @param percentCompleteOrNull
     * @private
     */
    _HTML5UploadProgress(file, percentCompleteOrNull)
    {
        const documentElement = file.documentElement;
        if(!documentElement) return;

        const progress = documentElement.querySelector('.percentage');

        //There is still some backend processing time needed. So we don't show 100%. Only when the backend responds with a complete
        if(percentCompleteOrNull && percentCompleteOrNull === 100) percentCompleteOrNull = 99;
        else if(!percentCompleteOrNull) percentCompleteOrNull = 1;

        // Update the progress value
        progress.setAttribute("aria-valuenow", percentCompleteOrNull);

    }

    /**
     * Triggered when a HTML 5 upload was started.
     *
     * @param file
     * @private
     */
    _HTML5UploadStarted(file) {
        let documentElement = this._createDocumentElement();

        this._attachDocumentModelToDocumentElement(documentElement);
        let documentModel = DocumentModel.fromJson(JSON.parse(documentElement.dataset.json));
        documentModel.state = DOCUMENT_STATE_NEW;
        documentModel.name = file.name;
        documentModel.file_url = file.name;
        documentElement = this._updateDocumentElementWithDocumentData(documentElement, documentModel);
        this._attachDocumentModelToDocumentElement(documentElement, documentModel);

        // Add the progress element to the thumb
        let progressPercentageString = '<span class="percentage" role="progressbar" aria-valuemin="0" aria-valuemax="100"></span>';

        // let progressPercentage = document.createElement('span');
        // progressPercentage.setAttribute('class', 'percentage');
        // progressPercentage.setAttribute("role", "progressbar");
        // progressPercentage.setAttribute("aria-valuemin", 0);
        // progressPercentage.setAttribute("aria-valuemax", 100);

        documentElement.querySelector('.thumb').insertAdjacentHTML('afterbegin', progressPercentageString);

        this.documentList.appendChild(documentElement);

        documentElement.querySelector('.drag-icon').classList.add('is-hidden');
        documentElement.querySelector('.thumb').classList.add('is-uploading');
        // documentElement.querySelector('.thumb').classList.add('has-image');

        file.documentElement = documentElement;
    }

    /**
     * Occurs when the user clicked the delete document button
     *
     * @param mouseEvent A javascript mouse event
     * @private
     */
    _deleteDocumentButtonClicked(mouseEvent) {
        let documentElement = mouseEvent.target.parentElement;

        let document = JSON.parse(documentElement.dataset.json);
        if (document.state === DOCUMENT_STATE_DELETED) return;

        if (document.state !== DOCUMENT_STATE_NEW) {
            document.state = DOCUMENT_STATE_DELETED;
            documentElement.classList.add(DOCUMENT_STATE_DELETED);
            documentElement.dataset.json = JSON.stringify(document);
        } else {
            this.documentList.removeChild(documentElement); //A housekeeping functionality should delete it in the near future.
        }

        this._updateDataInput();
        this._updateSortOrder();
    }

    /**
     * Occurs when the user modified the document
     *
     * @param event A javascript event
     * @private
     */
    _modifiedDocument(event) {
        let wrapper = event.target.parentElement.parentElement;

        //Document exists. Updates its json state and hide it.
        let document = JSON.parse(wrapper.dataset.json);
        if (document.state === DOCUMENT_STATE_DELETED) return;

        if (document.state !== DOCUMENT_STATE_NEW) document.state = DOCUMENT_STATE_MODIFIED;

        //Check what is modified and update the document accordingly
        let nameInput = wrapper.getElementsByClassName('name')[0];
        document.name = nameInput.value;

        wrapper.dataset.json = JSON.stringify(document);
        wrapper.classList.add(DOCUMENT_STATE_MODIFIED);

        this._updateDataInput();
    }

    /**
     * Adds a new document element. When documentModel isn't specified, the document element wil give the user the
     * ability to upload a new document by transforming that document element to a document uploader (fancy fileinput)
     *
     * @param documentModel DocumentModel. You only need to specify it when it is an existing document.
     * @private
     */
    _addDocumentElement(documentModel = undefined) {
        //Create an fileInput wrapper in which we put the fileInput and delete / remove button

        let documentElement = this._createDocumentElement();
        if(documentModel !== undefined) this._updateDocumentElementWithDocumentData(documentElement, documentModel);
        this._attachDocumentModelToDocumentElement(documentElement, documentModel);
        // if(documentModel === undefined) this.transformDocumentElementIntoADocumentUpload(documentElement);

        //Make it dragable if needed. With this it can be sorted
        if (documentModel) {
            this._makeElementRespondToDragging(documentElement, this.isSortable);
            this._makeElementRespondToDragOverAndLeave(documentElement, this.isSortable);
            this._makeElementRespondToDrop(documentElement, this.isSortable);
        }

        //Add the newly created HTMLElement to the document list
        this.documentList.appendChild(documentElement);
    }

    /**
     * @param documentElement {HTMLElement}
     * @param documentModel {DocumentModel|undefined}
     * @returns {HTMLElement} The document element
     * @private
     */
    _attachDocumentModelToDocumentElement(documentElement, documentModel = undefined)
    {
        if (documentModel !== undefined) {
            documentElement.dataset.json = JSON.stringify(documentModel);
        } else {
            documentModel = new DocumentModel();
            documentModel.sort_order = this.getCurrentDocumentsCount() + 1;
            documentElement.dataset.json = JSON.stringify(documentModel);
        }

        return documentElement;
    }

    /**
     *
     * @param documentElement {HTMLElement}
     * @param documentModel {DocumentModel}
     * @return {HTMLElement}
     * @private
     */
    _updateDocumentElementWithDocumentData(documentElement, documentModel)
    {
        let nameInput = documentElement.querySelector('.name');
        let fileUrl = documentElement.querySelector('.path');
        let thumbImageElement = documentElement.querySelector('.thumb');

        //Set the path of the document
        if(fileUrl)
        {
            fileUrl.innerText = documentModel.file_url;
        }

        //Set the name of the document
        if(nameInput) {
            nameInput.setAttribute('value', documentModel.name);
        }

        //Set the thumb image
        if(thumbImageElement && documentModel.file_url) {
            let thumbUrl = this._getThumbUrlUsingDocumentModel(documentModel);
            let documentExtension = this._getExtensionFromFileName(documentModel.file_url);

            thumbImageElement.setAttribute('data-filetype', documentExtension);
            const thumbImage = thumbImageElement.querySelector('.thumb__image');

            if (thumbImageElement) {
                if (thumbUrl && documentModel.state !== 'new') {
                    thumbImage.style.backgroundImage = "url('" + thumbUrl + "')";
                    thumbImageElement.classList.add('has-image');
                }
                else if (this.availableExtensionThumbs.indexOf(documentExtension) !== -1) {
                    thumbImage.style.backgroundImage = "url('" + this.extensionThumbsFolder + documentExtension + ".svg')";
                    thumbImageElement.classList.add('has-icon');
                }
            }
        }

        return documentElement;
    }

    /**
     * @private
     */
    _incrementIllegalMoveCount()
    {
        this.illegalMoveCount++;
        if(this.illegalMoveCount >= 3) {
            this.illegalMoveCount = 0;
            this._say('Je kunt niet tussen 2 lijsten slepen.'); //Tell the user that he....well could not do what he was doing
        }
    }

    /**
     * Says something
     *
     * @param message
     * @private
     */
    _say(message) {
        if ( SpeechSynthesisUtterance !== undefined ) {
            let msg = new SpeechSynthesisUtterance();
            let voices = window.speechSynthesis.getVoices();
            msg.voice = voices[10];
            msg.voiceURI = "native";
            msg.volume = 1;
            msg.rate = 1;
            msg.pitch = 0.8;
            msg.text = message;
            msg.lang = 'nl-NL';
            speechSynthesis.speak(msg);
        }
    }


    /**
     * Creates a li item with a name input, file_url text paragraph, delete button and a thumb image.
     *
     * @returns {HTMLElement}
     * @private
     */
    _createDocumentElement()
    {
        let documentElement = document.createElement('li');
        documentElement.className = 'document';

        let nameInput = document.createElement('input');
        nameInput.setAttribute('class', 'name');
        nameInput.setAttribute('type', 'text');
        nameInput.addEventListener('change', this._modifiedDocument);

        let fileUrl = document.createElement('p');
        fileUrl.setAttribute('class', 'path');

        let contentWrapper = document.createElement('div');
        contentWrapper.setAttribute('class', 'content-wrapper');
        contentWrapper.appendChild(nameInput);
        contentWrapper.appendChild(fileUrl);

        let deleteButton = document.createElement('span');
        deleteButton.className = 'delete';
        // deleteButton.innerText = 'x';
        deleteButton.addEventListener('click', this._deleteDocumentButtonClicked);

        let dragIcon = document.createElement('span');
        dragIcon.className = 'drag-icon';

        let thumbContainer = document.createElement('div');
        thumbContainer.className = 'thumb';
        thumbContainer.setAttribute('draggable', 'false'); //Prevent image from being dragged and interfering with what the makeElementDraggableIfNeeded method does

        let thumbImage = document.createElement('div');
        thumbImage.className = 'thumb__image';
        thumbContainer.appendChild(thumbImage);

        let fileInput = document.createElement('input');
        fileInput.setAttribute('type', 'file');
        fileInput.setAttribute('name', this.key + '-' + (this.getCurrentDocumentsCount() + 1));
        if(this.accept) fileInput.setAttribute('accept', this.accept);
        documentElement.appendChild(fileInput);
        fileInput.style.display = "none";

        documentElement.appendChild(dragIcon);
        documentElement.appendChild(thumbContainer);
        documentElement.appendChild(contentWrapper);
        documentElement.appendChild(deleteButton);

        return documentElement;
    }

    /**
     * Retrieves a javascript file instance and creates a documentElement for it.
     * Usually used by outside classes to inject a document from a file to upload.
     *
     * @param fileInstance {File}
     */
    receiveFile(fileInstance)
    {
        if(!fileInstance) {
            console.warn('DocumentManager:receiveFile: Expected to get a file but did not get one');
            return;
        }
        // console.log('this.getCurrentDocumentsCount() < this.maxDocuments: ', this.getCurrentDocumentsCount(), this.maxDocuments, 'File', fileInstance);
        if(this.getCurrentDocumentsCount() < this.maxDocuments) {
            let documentElement = this._createDocumentElement();
            this._attachDocumentModelToDocumentElement(documentElement);

            if(this.HTML5Uploader.isSupported() === false) {
                documentElement = this._giveDocumentElementAFile(documentElement, fileInstance);

                //Add the newly created HTMLElement to the document list
                this.documentList.appendChild(documentElement);

                if (this._formSizeExceeded()) {
                    documentElement.parentElement.removeChild(documentElement); //Remove the documentElement
                }

                this._updateSortOrder();
            } else {
                // console.log('Uploading via HTML5');
                let extraData = {
                    'imageProperties': this.imageProperties,
                    'subFolder': this.subFolder
                };

                this.HTML5Uploader.upload(fileInstance, JSON.stringify(extraData));
            }
        }
    }

    /**
     * Returns the amount of documents that are not deleted.
     * If you set the boolean parameter to true it will include the deleted ones.
     */
    getCurrentDocumentsCount(includingDeleted = false) {
        let documents = this.documentList.getElementsByClassName('document');
        let realDocumentCount = documents.length;
        let documentCount = realDocumentCount;

        //Subtract the ones that are about to be deleted from the real document count
        if (!includingDeleted) {
            for (let documentsIndex = 0; documentsIndex < realDocumentCount; documentsIndex++) {
                let documentElement = documents[documentsIndex];
                if (("json" in documentElement.dataset) === false) continue; //used to skip the add new document button

                /** @var {DocumentModel} document **/
                let document = JSON.parse(documentElement.dataset.json);
                if (document.state === DOCUMENT_STATE_DELETED) documentCount--;
            }
        }

        return documentCount;
    }


    /**
     * Sets the file of an document element to the given one.
     * Makes the drag icon invisible since it was not uploaded yet
     * and cannot be positioned. Sets the file input to the files name and
     * loads the thumb with the appropiate extension
     *
     * @param {HTMLElement} documentElement
     * @param {File} file
     * @return {HTMLElement} the documentElement
     * @private
     */
    _giveDocumentElementAFile(documentElement, file)
    {
        let filename = file.name;

        let extension = this._getExtensionFromFileName(filename);
        let thumbUrl = this._previewThumbUrlIsImage(filename);

        //Get the document element and some parts in it
        let nameInputElement = documentElement.querySelector('.name');
        let thumbImageElement = documentElement.querySelector('.thumb');
        let contentWrapper = documentElement.querySelector('.content-wrapper');
        let deleteButton = documentElement.querySelector('.delete');
        let dragIcon = documentElement.querySelector('.drag-icon');
        let fileInputElement = documentElement.querySelector('input[type="file"]');

        //Hide the drag icon. Dragging is only supported after file upload
        dragIcon.style.display = 'none';

        //Update file input with a file if the input does not have a file.
        if(fileInputElement && !fileInputElement.files[0]) {
            fileInputElement.files = new FileList(file);
        }

        //Make thumb visible and load extension image
        thumbImageElement.setAttribute('data-filetype', extension);
        if(this.availableExtensionThumbs.indexOf(extension) !== -1) {
            const thumbImage = thumbImageElement.querySelector('thumb__image');
            thumbImage.style.backgroundImage = "url('" + this.extensionThumbsFolder + extension + ".svg')";
            thumbImage.classList.add('has-icon');
        }

        nameInputElement.value = filename;

        return documentElement;
    }

    /**
     * Returns the thumbnail for the document or false if it isn't available.
     * @private
     */
    _getThumbUrlUsingDocumentModel(documentModel) {
        if (documentModel) {
            let filename = documentModel.file_url;
            if (documentModel.thumb_image_url !== '') filename = documentModel.thumb_image_url;

            if (this._previewThumbUrlIsImage(filename)) return filename;
        }

        return false;
    }

    /**
     * Returns the given preview thumb url for an extension if that extension is an image or false if it is not an image
     * @private
     */
    _previewThumbUrlIsImage(url) {
        if (!this.enablePreviewsIfPossible) return false;

        let extension = this._getExtensionFromFileName(url);
        switch (extension) {
            case 'png':
            case 'jpg':
            case 'jpeg':
            case 'gif':
                return url;
            case 'pdf':
            default:
                return false;
        }
    }

    /**
     * Returns the extension without leading dot from a file name or '' when it did not have an extension.
     *
     * @private
     */
    _getExtensionFromFileName(filename) {
        let extension = filename.split(/[.]+/).pop();
        if (extension === filename) return '';
        return extension.toLowerCase();
    }

    /**
     * Check if the total form size isn't bigger then the maximum allowed upload size.
     * Warning: when maxUploadSizeInBytes isn't set this function wil always return false.
     *
     * @private
     */
    _formSizeExceeded() {
        if (this.maxUploadSizeInBytes) {
            let exceeded = false;

            let currentTotalInBytes = 0;

            //validate total
            this.documentList.querySelectorAll('input[type="file"]').forEach(
                function (fileInput) {
                    if (fileInput.files.length !== 1) return;

                    let document = fileInput.files[0];

                    currentTotalInBytes += document.size;
                }
            );
            console.log('max upload size in bytes: ' + this.maxUploadSizeInBytes + ' current upload size in bytes: ' + currentTotalInBytes);


            if (currentTotalInBytes > this.maxUploadSizeInBytes) {
                let maxUploadSizeInMegaBytes = this.maxUploadSizeInBytes / 1048576;
                exceeded = true;
                alert(this.uploadSizeExceededMessage + ' ' + maxUploadSizeInMegaBytes + ' MegaBytes')
            }

            return exceeded;
        } else {
            return false;
        }
    }

    /**
     * Update the data input with an array of json objects that are the data-json attributes of each "document" li.
     * This is used for the backend to determine what has changed and what did not.
     *
     * @private
     */
    _updateDataInput() {
        let documentsDataArray = [];

        //Please notice that the first document isnt a document but is the document add button. That's why we check for the json dataset
        let documents = this.documentList.children;
        const nDocuments = documents.length;
        for (let documentsIndex = 0; documentsIndex < nDocuments; documentsIndex++) {
            let documentElement = documents[documentsIndex];
            if ("json" in documentElement.dataset) {
                documentsDataArray.push(JSON.parse(documentElement.dataset.json));
            }
        }

        if(this.getCurrentDocumentsCount() >= this.maxDocuments){
            this.wrapper.querySelector('.drag-and-drop-area').classList.add('is-hidden');
        }
        else{
            this.wrapper.querySelector('.drag-and-drop-area').classList.remove('is-hidden');
        }

        this.dataInput.value = JSON.stringify(documentsDataArray);
        let changeEvent = createNewEvent('change');
        dispatchEventForElement(this.dataInput, changeEvent);
    }

    /**
     * Updates the sort order of each document when needed and then updates the data input field
     *
     * @private
     */
    _updateSortOrder() {
        //Please notice that the first document isnt a document but is the document add button. That's why we check for the json dataset
        let documents = this.documentList.children;
        const nDocuments = documents.length;
        let currentSortNumber = 1;

        for (let documentsIndex = 0; documentsIndex < nDocuments; documentsIndex++) {
            let documentElement = documents[documentsIndex];
            if (("json" in documentElement.dataset) === false) continue; //used to skip the add new document button

            let document = JSON.parse(documentElement.dataset.json);

            if (document.state === DOCUMENT_STATE_DELETED) continue;

            document.sort_order = currentSortNumber;
            if (document.state !== DOCUMENT_STATE_NEW) document.state = DOCUMENT_STATE_MODIFIED;

            documentElement.dataset.json = JSON.stringify(document);
            documentElement.classList.add(DOCUMENT_STATE_MODIFIED);

            currentSortNumber++;
        }

        this._updateDataInput();
    }

    /**
     * Does have a look at the documentElements and sorts them by using their sort order values
     *
     * @private
     */
    _sortDocuments() {
        let documentElements = this.documentList.children;
        documentElements = Array.prototype.slice.call(documentElements); //Converts the htmlCollection of HTMLElement documentElements to an Array of documentElements

        documentElements.sort(function(itemA, itemB) {
            let itemADocument = JSON.parse(itemA.dataset.json);
            let itemBDocument = JSON.parse(itemB.dataset.json);
            // console.log(itemADocument.sort_order, itemBDocument.sort_order);
            if(itemADocument.sort_order < itemBDocument.sort_order) return -1;
            return 1;
        });

        let sortedDocumentsLength = documentElements.length;

        for (let index = 1; index < sortedDocumentsLength; index++)
        {
            this.documentList.removeChild(documentElements[index]);
            if (this.documentList.length > 1)
                this.documentList.insertBefore(documentElements[index], this.documentList.firstChild);
            else
                this.documentList.appendChild(documentElements[index]);
        }
    }

    /**
     * @param element HTMLElement that needs to be draggable or not
     * @param respondOrNotBoolean
     * @return {DocumentManager}
     * @private
     */
    _makeElementRespondToDragging(element, respondOrNotBoolean = true) {
        element.setAttribute('draggable', (respondOrNotBoolean) ? 'true' : 'false');
        element.id = this.key + '_' + this.getCurrentDocumentsCount();
        if (respondOrNotBoolean) {
            element.removeEventListener('dragstart', this._drag);
            element.addEventListener('dragstart', this._drag);
        } else {
            element.removeEventListener('dragstart', this._drag);
        }

        return this;
    }

    /**
     * @param element HTMLElement that needs to be draggable or not
     * @param respondOrNotBoolean
     * @return {DocumentManager}
     * @private
     */
    _makeElementRespondToDrop(element, respondOrNotBoolean = true) {
        if (respondOrNotBoolean) {
            element.removeEventListener('drop', this._drop);
            element.addEventListener('drop', this._drop);
        } else {
            element.removeEventListener('drop', this._drop);
        }

        return this
    }

    /**
     * Prepares the element so that it can receive items that are dropped onto it
     *
     * @param element HTMLElement that needs can be a drop target or not
     * @param respondOrNotBoolean wheter or not it should be a drop target
     * @private
     */
    _makeElementRespondToDragOverAndLeave(element, respondOrNotBoolean = true) {
        if (respondOrNotBoolean) {
            element.removeEventListener('dragover', this._dragOver);
            element.removeEventListener('dragleave', this._dragLeave);
            element.addEventListener('dragover', this._dragOver);
            element.addEventListener('dragleave', this._dragLeave);
        } else {
            element.removeEventListener('dragover', this._dragOver);
            element.removeEventListener('dragleave', this._dragLeave);
        }
    }

    /**
     * Occurs when a document (li) HTMLElement is being dragged
     *
     * @param dragEvent
     * @private
     */
    _drag(dragEvent) {
        if (!dragEvent.target.id) return;
        dragEvent.stopPropagation();
        dragEvent.dataTransfer.setData("text", dragEvent.target.id); //Set the id of the thing that is being dragged in the event.

        let draggedElement = document.getElementById(dragEvent.target.id);
        this._triggerEvent('drag', draggedElement)
    }

    /**
     * Occurs when a document (li) HTMLElement is being dragged over the target element.
     * So the dragEvent target is not the element that you drag, but the place / HTMLElement where it may be dropped.
     *
     * @param dragEvent
     * @private
     */
    _dragOver(dragEvent) {
        dragEvent.preventDefault(); //Sets target HTMLElement to allow a drop
        dragEvent.stopPropagation();
        if (!dragEvent.target.id) return;

        let draggedElementId = dragEvent.dataTransfer.getData("text");
        let draggedElement = document.getElementById(draggedElementId);

        this._enableOrDisablePointerEventsOnChildrenOfElement(dragEvent.target, false);
        dragEvent.target.classList.add('isDropTarget');

        this._triggerEvent('dragLeave', [draggedElement, dragEvent.target])
    }

    /**
     * Occurs when a document (li) HTMLElement is NOT being dragged anymore over the target element.
     *
     * @param dragEvent
     * @private
     */
    _dragLeave(dragEvent) {
        if (!dragEvent.target.id) return;
        dragEvent.stopPropagation();

        let draggedElementId = dragEvent.dataTransfer.getData("text");
        let draggedElement = document.getElementById(draggedElementId);

        this._enableOrDisablePointerEventsOnChildrenOfElement(dragEvent.target, true);
        dragEvent.target.classList.remove('isDropTarget');

        this._triggerEvent('dragLeave', [draggedElement, dragEvent.target])
    }

    /**
     * Occurs when a document is being dropped
     *
     * @param dragEvent
     */
    _drop(dragEvent) {
        dragEvent.preventDefault(); //Prevent browser from activating links and buttons
        if (!dragEvent.target.id) return;

        let draggedElementId = dragEvent.dataTransfer.getData("text");
        let draggedElement = document.getElementById(draggedElementId);
        let targetElement = dragEvent.target;

        targetElement.classList.remove('isDropTarget');

        //Only "move" the dragged element to the new location when it has the same parent. E.g. when they both are in the same list
        //If they don't have the same parent. We are going to ignore the action
        if(targetElement.parentElement !== draggedElement.parentElement) {
            this._incrementIllegalMoveCount();
            return;
        }

        //Create a shim element that keeps track of where the dragged element was and where the target element must be inserted
        let shimElement = document.createElement('div');
        draggedElement.parentElement.insertBefore(shimElement, draggedElement);

        //Move the dragged element in just before the target element
        draggedElement.parentElement.insertBefore(draggedElement, targetElement);

        //Move the target element before the shim and then remove the shim since we only needed that to mark the dragged elements original position
        draggedElement.parentElement.insertBefore(targetElement, shimElement);
        shimElement.parentElement.removeChild(shimElement);


        this._triggerEvent('drop', [draggedElement, targetElement]);

        this._enableOrDisablePointerEventsOnChildrenOfElement(targetElement, true);
        this._updateSortOrder();
    }

    /**
     * Enables / disables the listening to pointer (example: mouse) events on an element
     * and all of it's children. This prevents for example the dragover event from beeing canceled
     * when dragging over an input that resides in an element which listens to the dragover event
     * because the input captures the mouse.
     */
    _enableOrDisablePointerEventsOnChildrenOfElement(element, enable)
    {
        let length = element.children.length;
        for(let index = 1; index < length; index++)
        {
            if(enable === false) {
                element.children[index].style.pointerEvents = 'none';
            } else {
                element.children[index].style.pointerEvents = null;
            }

            let childrenLength = element.children.children;
            for(let childrenIndex = 1; childrenIndex < childrenLength; childrenIndex++)
            {
                this._enableOrDisablePointerEventsOnChildrenOfElement(element.children.children[index], enable);
            }
        }
    }

    /**
     * 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
     * @private
     */
    _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);
            }
        }
    }
}

/**
 * Represents a Document. It the equivalent of the laravel php version
 */
class DocumentModel {
    constructor() {
        this.id = -1;
        this.file_url = '';
        this.state = DOCUMENT_STATE_NEW;
        this.name = '';
        this.sort_order = 1;
        this.thumb_image_url = '';
        this.small_image_url = '';
        this.medium_image_url = '';
        this.large_image_url = '';
        this.documentable_id = -1;
        this.documentable_type = '';
        this.created_at = '';
        this.updated_at = '';
    }



    static isValidDocumentJson(jsonData) {
        return jsonData.hasOwnProperty('id') &&
            jsonData.hasOwnProperty('file_url') &&
            jsonData.hasOwnProperty('state') &&
            jsonData.hasOwnProperty('name') &&
            jsonData.hasOwnProperty('sort_order') &&
            jsonData.hasOwnProperty('thumb_image_url') &&
            jsonData.hasOwnProperty('small_image_url') &&
            jsonData.hasOwnProperty('medium_image_url') &&
            jsonData.hasOwnProperty('large_image_url') &&
            jsonData.hasOwnProperty('documentable_id') &&
            jsonData.hasOwnProperty('documentable_type') &&
            jsonData.hasOwnProperty('created_at') &&
            jsonData.hasOwnProperty('updated_at');
    }

    static fromJson(jsonData) {
        if (!DocumentModel.isValidDocumentJson(jsonData)) {
            console.error('DocumentModel: Tried to create an instance of invalid json data: ');
            console.error(jsonData);
            return false;
        }

        let documentModel = new DocumentModel();
        documentModel.id = jsonData.id;
        documentModel.file_url = jsonData.file_url;
        documentModel.state = jsonData.state;
        documentModel.name = jsonData.name;
        documentModel.sort_order = jsonData.sort_order;
        documentModel.thumb_image_url = jsonData.thumb_image_url;
        documentModel.small_image_url = jsonData.small_image_url;
        documentModel.medium_image_url = jsonData.medium_image_url;
        documentModel.large_image_url = jsonData.large_image_url;
        documentModel.documentable_id = jsonData.id;
        documentModel.documentable_type = jsonData.documentable_type;
        documentModel.created_at = jsonData.created_at;
        documentModel.updated_at = jsonData.updated_at;

        return documentModel;
    }
}

const DOCUMENT_STATE_NEW = 'new';
const DOCUMENT_STATE_PRISTINE = 'pristine';
const DOCUMENT_STATE_MODIFIED = 'modified';
const DOCUMENT_STATE_DELETED = 'deleted';

/**
 * Used for setting files
 */
class FileList {
    constructor(...items) {
        // flatten rest parameter
        items = [].concat(...items);
        // check if every element of array is an instance of `File`
        if (items.length && !items.every(file => file instanceof File)) {
            throw new TypeError("expected argument to FileList is File or array of File objects");
        }
        // use `ClipboardEvent("").clipboardData` for Firefox, which returns `null` at Chromium
        // we just need the `DataTransfer` instance referenced by `.clipboardData`
        const dt = new ClipboardEvent("").clipboardData || new DataTransfer();
        // add `File` objects to `DataTransfer` `.items`
        for (let file of items) {
            dt.items.add(file)
        }
        return dt.files;
    }
}