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/SBogers47/leden.ehbocranendonck.nl/resources/assets/js/kms/attributes/documents.js
/**
 * 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.constructedSuccessFully = false;
        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;

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

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

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


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

        this.constructedSuccessFully = true;
    }

    /**
     * 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(!this.constructedSuccessFully) return;

        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: ' + 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 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 documentsIndex = 0; documentsIndex < documents.length; documentsIndex++) {
            let documentElement = documents[documentsIndex];
            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.bind(this));
            element.removeEventListener('dragleave', this.dragLeave.bind(this));
            element.addEventListener('dragover', this.dragOver.bind(this));
            element.addEventListener('dragleave', this.dragLeave.bind(this));
        } else {
            element.removeEventListener('dragover', this.dragOver.bind(this));
            element.removeEventListener('dragleave', this.dragLeave.bind(this));
        }
    }

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

        this.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();

        this.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();

        this.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 documentsIndex = 0; documentsIndex < documents.length; 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();
    }

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