File: D:/HostingSpaces/ReturnIndustries/return-industries.nl/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, translations) {
//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.translations = JSON.parse(translations);
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();
alert(this.translations.upload_failed);
}
/**
* 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;
}
}