File: D:/HostingSpaces/SBogers10/edwingovers.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>