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/komma.pro/resources/views/kms/attributes/documents.blade.php
<div class="entity-attribute entity-attribute-documents @if($errors->has((string) $attribute->getKey())) error @endif {!!$attribute->getStyleClass()!!}"
     data-uk-tooltip="{pos:'bottom-right'}" title="{!! $attribute->getErrors[0] or '' !!}">

    <label for="files">{{ $attribute->getLabelText() }}</label>

    <ul class="files"></ul>

    <div class="drag-and-drop-area">
        <span class="icon"></span>
        <p>@lang('kms/documents.drag-and-drop.area')</p>
        <span class="or">@lang('kms/documents.drag-and-drop.or')</span>
        <span class="button">@lang('kms/documents.drag-and-drop.button')</span>
    </div>

    <div class="hidden-input">
        <input type="hidden" name="{{(string) $attribute->getKey()}}-data">
    </div>

</div>

<script>
    let accept = "{{ $attribute->getAccept() }}";
    let extensionThumbsFolder = "{{ $attribute->getExtensionThumbsFolder() }}";
    let key = "{{ $attribute->getKey() }}";
    let enablePreviewsIfPossible = "{{ $attribute->getEnablePreviewsIfPossible() }}";
    let isSortable = "{{ $attribute->getIsSortable() }}";
    let jsonString = '{!! $attribute->getValue() !!}'.replace(/\\/g, '\\\\'); //Escape \ because json.parse does not work if you don't escape it
    let uploadedDocuments = JSON.parse(jsonString);

    let form = document.getElementById('entity-form');
    let wrapper = document.querySelector("div.entity-attribute.entity-attribute-documents");
    let maxUploadSizeInBytes = form.dataset.maxUploadSize;

    /**
     * Manages file inputs in a wrapper so that files
     */
    class DocumentManager {
        /**
         * The constructor
         *
         * @param wrapper HtmlElement in which an ul resides with class files which in turn holds documents
         * @param key The attributes key as a string
         * @param accept string A comma seperated string containing mime types that are allowed to be uploaded
         * @param extensionThumbsFolder
         * @param availableExtensionThumbs
         * @param maxUploadSizeInBytes
         * @param enablePreviewsIfPossible
         * @param isSortable
         * @param uploadSizeExceededMessage
         */
        constructor(
            wrapper,
            key,
            accept = undefined,
            maxUploadSizeInBytes = undefined,
            enablePreviewsIfPossible = true,
            isSortable = true,
            extensionThumbsFolder = '/img/kms/extension_thumbs/',
            availableExtensionThumbs = ['svg', 'pdf', 'zip', 'rar', 'csv', 'xlsx', 'mp3', 'mp4', 'docx', 'doc', 'png', 'jpg', 'jpeg', 'gif'],
            uploadSizeExceededMessage = 'The last document won\'t be uploaded because it exceeded the total form limit of') {
            //Initialize variables
            this.wrapper = wrapper;
            this.accept = accept;
            this.key = key;
            this.isSortable = isSortable;
            this.extensionThumbsFolder = extensionThumbsFolder;
            this.availableExtensionThumbs = availableExtensionThumbs;
            this.enablePreviewsIfPossible = enablePreviewsIfPossible;

            this.oldInputValue = undefined;
            this.maxUploadSizeInBytes = maxUploadSizeInBytes;
            this.uploadSizeExceededMessage = uploadSizeExceededMessage;

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

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


            if (dataInput === undefined) {
                console.error('The document uploader needs an input element with name "' + key + '-data" in the given wrapper');
                return;
            }
            this.dataInput = dataInput;
        }

        /**
         * Initializes the setup of the document uploader.
         * @param documents string Example structure:
         *  '[{
         *          "id": 3,
         *           "path": "uploads/products/Before 2018-01-25 at 14,48,23_1522928384.png",
         *          "documentable_id": 1,
         *          "documentable_type": "App\\KommaApp\\Shop\\Products\\Product\\Product",
         *          "created_at": "2018-04-05 11:39:44",
         *          "updated_at": "2018-04-05 11:39:44"
         *  }]'
         *
         */
        initialize(documents) {
            if (documents) {
                for (let initDocument of documents) {
                    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(document);
                    }
                }
                this.sortDocuments();
            }

            this.updateDataInput();
            this.addNewDocumentButton(undefined);
        }

        /**
         * Occurs when the user clicked the delete document button
         *
         * @param mouseEvent A javascript mouse event
         */
        deleteDocumentButtonClicked(mouseEvent) {
            let wrapper = mouseEvent.target.parentElement;
            let document = JSON.parse(wrapper.dataset.json);
            if (document.state === DOCUMENT_STATE_DELETED) return;

            if (document.state !== DOCUMENT_STATE_NEW) {
                document.state = DOCUMENT_STATE_DELETED;
                wrapper.classList.add(DOCUMENT_STATE_DELETED);
                wrapper.dataset.json = JSON.stringify(document);
            } else {
                this.documentList.removeChild(wrapper);
            }

            this.updateDataInput();
            this.updateSortOrder();
            this.addNewDocumentButtonIfNeeded();
        }

        /**
         * Occurs when the user modified the document
         *
         * @param event A javascript event
         */
        modifiedDocument(event) {
            let wrapper = event.target.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();
        }

        /**
         * Occurs when the user clicked the add document button
         */
        addNewDocumentButton() {
            this.addDocumentElement();
            this.updateDataInput();
        }

        /**
         * If there are no "new document" button, this method wil add one
         */
        addNewDocumentButtonIfNeeded() {
            let documentElements = this.documentList.children;
            let foundAddDocumentButton = false;
            for (let documentElement of documentElements) {
                let document = JSON.parse(documentElement.dataset.json);
                if (document.state === DOCUMENT_STATE_NEW) {
                    foundAddDocumentButton = true;
                    break;
                }
            }
            console.log(foundAddDocumentButton);

            if (foundAddDocumentButton === false) this.addNewDocumentButton();
        }

        /**
         * Adds a new document element
         *
         * @param documentModel DocumentModel. You only need to specify it when it is an existing document.
         */
        addDocumentElement(documentModel = undefined) {
            //Create an fileInput wrapper in which we put the fileInput and delete / remove button
            let inputWrapper = document.createElement('li');
            inputWrapper.className = 'document';

            let fileInput = undefined;
            let thumbUrl = this.getThumbUrlUsingDocumentModel(documentModel);

            if (documentModel === undefined) {
                //Create a basic file input HtmlElement and add a listener to detect changes
                fileInput = document.createElement('input');
                fileInput.setAttribute('type', 'file');
                fileInput.setAttribute('name', this.key + '-' + (this.getCurrentDocumentsCount() + 1));
                fileInput.addEventListener('click', this.setOldInputvalue.bind(this));
                fileInput.addEventListener('change', this.selectedDocumentOnDevice.bind(this));
            } else {
                //Omit the file input since it is an existing file and rather store the json version of the document in the element.
                thumbUrl = this.getThumbUrlUsingDocumentModel(documentModel);
            }

            if (documentModel !== undefined) {
                inputWrapper.dataset.json = JSON.stringify(documentModel);
            } else {
                documentModel = new DocumentModel();
                documentModel.sort_order = this.documentList.childNodes.length + 1;
                inputWrapper.dataset.json = JSON.stringify(documentModel);
            }

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

            let pathText = document.createElement('p');
            pathText.setAttribute('class', 'path');
            pathText.innerText = documentModel.path;

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

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

            let dragIcon = document.createElement('span');
            dragIcon.className = 'drag-icon';
            // dragIcon.addEventListener('click', this.deleteDocumentButtonClicked.bind(this)); // TODO initalize function for triggering drag and drop

            let thumbImage = document.createElement('div');
            thumbImage.className = 'thumb';
            thumbImage.setAttribute('draggable', 'false'); //Prevent image from being dragged and interfering with what the makeElementDraggableIfNeeded method does
            if (fileInput) {
                thumbImage.addEventListener('click', (function (fileInput) {
                    return function (mouseEvent) {
                        fileInput.click(mouseEvent);
                    }
                })(fileInput));
            }

            let documentExtension = this.getExtensionFromFileName(documentModel.path);

            thumbImage.setAttribute('data-filetype', documentExtension);
            if (thumbUrl){
                thumbImage.style.backgroundImage = "url('" + thumbUrl + "')";
                thumbImage.classList.add('has-image');
            }
            else if(this.availableExtensionThumbs.indexOf(documentExtension) !== -1) {
                thumbImage.style.backgroundImage = "url('" + this.extensionThumbsFolder + documentExtension + ".svg')";
                thumbImage.classList.add('has-icon');
            }

            //Set the accept and placeholder attributes if needed
            if (fileInput) {
                if (this.accept) fileInput.setAttribute('accept', this.accept);
            }

            //Give the input wrapper its input and delete button
            if (fileInput) inputWrapper.appendChild(fileInput);
            inputWrapper.appendChild(dragIcon);
            inputWrapper.appendChild(thumbImage);
            inputWrapper.appendChild(contentWrapper);
            inputWrapper.appendChild(deleteButton);

            //Make it dragable if needed. With this it can be sorted
            if (!fileInput) {
                this.makeElementRespondToDragging(inputWrapper, this.isSortable);
                this.makeElementRespondToDragOverAndLeave(inputWrapper, this.isSortable);
                this.makeElementRespondToDrop(inputWrapper, this.isSortable);
            }

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

        /**
         * Returns the amount of documents
         */
        getCurrentDocumentsCount() {
            return this.documentList.getElementsByClassName('document').length;
        }

        /**
         * Triggered when the user did choose a file on the device
         */
        selectedDocumentOnDevice(changeEvent) {
            let input = changeEvent.target;

            if (this.formSizeExceeded()) {
                input.parentElement.removeChild(input);
            } else {
                let file = input.files[0];
                let localFileUrl = input.value;
                let filename = input.value.split(/(\\|\/)/g).pop();

                console.log(input);
                console.log(input.files);

                let extension = this.getExtensionFromFileName(filename);
                let thumbUrl = this.previewThumbUrlIsImage(filename);

                wrapper = input.parentNode;
                let nameInputElement = wrapper.getElementsByClassName('name')[0];

                let thumbImageElement = wrapper.getElementsByClassName('thumb')[0];
                thumbImageElement.setAttribute('data-filetype', extension);

                if(this.availableExtensionThumbs.indexOf(extension) !== -1) {
                    thumbImageElement.style.backgroundImage = "url('" + this.extensionThumbsFolder + extension + ".svg')";
                    thumbImageElement.classList.add('has-icon');
                }

                nameInputElement.value = filename;

                if (input.value !== "" && input.value !== this.oldInputValue) {
                    this.addNewDocumentButton();
                }
            }
        }

        /**
         * Triggered when the user clicked a file input
         */
        setOldInputvalue(mouseEvent) {
            this.oldInputValue = mouseEvent.target.value;
        }

        /**
         * Returns the thumbnail for the document or false if it isn't available
         */
        getThumbUrlUsingDocumentModel(documentModel) {
            if (documentModel) {
                let filename = documentModel.path;
                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
         */
        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.
         */
        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.
         */
        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: ' + maxUploadSizeInBytes + ' current upload size in bytes: ' + currentTotalInBytes);


                if (currentTotalInBytes > maxUploadSizeInBytes) {
                    let maxUploadSizeInMegaBytes = 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 attribues of each "document" li.
         * This is used for the backend to determine what has changed and what did not.
         */
        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;
            for (let documentElement of documents) {
                if ("json" in documentElement.dataset) {
                    documentsDataArray.push(JSON.parse(documentElement.dataset.json));
                }
            }

            this.dataInput.value = JSON.stringify(documentsDataArray);
        }

        //Drag n drop sortable code
        /**
         * @param element HTMLElement that needs to be draggable or not
         * @param respondOrNotBoolean
         */
        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);
            }
        }

        /**
         * @param element HTMLElement that needs to be draggable or not
         * @param respondOrNotBoolean
         */
        makeElementRespondToDrop(element, respondOrNotBoolean = true) {
            if (respondOrNotBoolean) {
                element.removeEventListener('drop', this.drop.bind(this));
                element.addEventListener('drop', this.drop.bind(this));
            } else {
                element.removeEventListener('drop', this.drop.bind(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
         */
        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
         */
        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.
        }

        /**
         * 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
         */
        dragOver(dragEvent) {
            dragEvent.preventDefault(); //Sets target HTMLElement to allow a drop
            dragEvent.stopPropagation();
            if (!dragEvent.target.id) return;

            documentManager.enableOrDisablePointerEventsOnChildrenOfElement(dragEvent.target, false);
            dragEvent.target.classList.add('isDropTarget');
        }

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

            documentManager.enableOrDisablePointerEventsOnChildrenOfElement(dragEvent.target, true);
            dragEvent.target.classList.remove('isDropTarget');
        }

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

            if(targetElement.nextSibling.nextSibling == null || draggedElement.nextSibling === targetElement)
            {
                targetElement.parentNode.insertBefore(draggedElement, targetElement.nextSibling); //newnode, reference node
            } else {
                targetElement.parentNode.insertBefore(draggedElement, targetElement); //newnode, reference node
            }

            this.updateSortOrder();

            documentManager.enableOrDisablePointerEventsOnChildrenOfElement(targetElement, true);
        }

        /**
         * Updates the sort order of each document when needed and then updates the data input field
         */
        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;
            let currentSortNumber = 1;

            for (let documentElement of documents) {
                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();
        }

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

        /**
         * Does have a look at the documentElements and sorts them by using their sort order values
         */
        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++)
                // for(let index = sortedDocumentsLength-1; index > 0; 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]);
            }


        }
    }

    /**
     * Represents a Document. It the equivalent of the laravel php version
     */
    class DocumentModel {
        constructor() {
            this.id = -1;
            this.path = '';
            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('path') &&
                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)) return false;

            let documentModel = new DocumentModel();
            documentModel.id = jsonData.id;
            documentModel.path = jsonData.path;
            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';

    let documentManager = new DocumentManager(wrapper, key, accept, maxUploadSizeInBytes, enablePreviewsIfPossible, isSortable, extensionThumbsFolder);
    documentManager.initialize(uploadedDocuments);
</script>