0
0
Fork 0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-05-05 00:30:26 +00:00

Finished breakdown of attachment vue into components

This commit is contained in:
Dan Brown 2020-07-04 16:53:02 +01:00
parent 14b6cd1091
commit d41452f39c
No known key found for this signature in database
GPG key ID: 46D9F943C24A2EF9
24 changed files with 371 additions and 321 deletions

View file

@ -0,0 +1,58 @@
import {onEnterPress, onSelect} from "../services/dom";
/**
* Ajax Form
* Will handle button clicks or input enter press events and submit
* the data over ajax. Will always expect a partial HTML view to be returned.
* Fires an 'ajax-form-success' event when submitted successfully.
* @extends {Component}
*/
class AjaxForm {
setup() {
this.container = this.$el;
this.url = this.$opts.url;
this.method = this.$opts.method || 'post';
this.successMessage = this.$opts.successMessage;
this.submitButtons = this.$manyRefs.submit || [];
this.setupListeners();
}
setupListeners() {
onEnterPress(this.container, event => {
this.submit();
event.preventDefault();
});
this.submitButtons.forEach(button => onSelect(button, this.submit.bind(this)));
}
async submit() {
const fd = new FormData();
const inputs = this.container.querySelectorAll(`[name]`);
console.log(inputs);
for (const input of inputs) {
fd.append(input.getAttribute('name'), input.value);
}
this.container.style.opacity = '0.7';
this.container.style.pointerEvents = 'none';
try {
const resp = await window.$http[this.method.toLowerCase()](this.url, fd);
this.container.innerHTML = resp.data;
this.$emit('success', {formData: fd});
if (this.successMessage) {
window.$events.emit('success', this.successMessage);
}
} catch (err) {
this.container.innerHTML = err.data;
}
window.components.init(this.container);
this.container.style.opacity = null;
this.container.style.pointerEvents = null;
}
}
export default AjaxForm;

View file

@ -1,14 +1,16 @@
/**
* Attachments
* @extends {Component}
*/
import {showLoading} from "../services/dom";
class Attachments {
setup() {
this.container = this.$el;
this.pageId = this.$opts.pageId;
this.editContainer = this.$refs.editContainer;
this.listContainer = this.$refs.listContainer;
this.mainTabs = this.$refs.mainTabs;
this.list = this.$refs.list;
@ -16,23 +18,30 @@ class Attachments {
}
setupListeners() {
this.container.addEventListener('dropzone-success', event => {
this.mainTabs.components.tabs.show('items');
window.$http.get(`/attachments/get/page/${this.pageId}`).then(resp => {
this.list.innerHTML = resp.data;
window.components.init(this.list);
})
});
const reloadListBound = this.reloadList.bind(this);
this.container.addEventListener('dropzone-success', reloadListBound);
this.container.addEventListener('ajax-form-success', reloadListBound);
this.container.addEventListener('sortable-list-sort', event => {
this.updateOrder(event.detail.ids);
});
this.editContainer.addEventListener('keypress', event => {
if (event.key === 'Enter') {
// TODO - Update editing file
}
})
this.container.addEventListener('event-emit-select-edit', event => {
this.startEdit(event.detail.id);
});
this.container.addEventListener('event-emit-select-edit-back', event => {
this.stopEdit();
});
}
reloadList() {
this.stopEdit();
this.mainTabs.components.tabs.show('items');
window.$http.get(`/attachments/get/page/${this.pageId}`).then(resp => {
this.list.innerHTML = resp.data;
window.components.init(this.list);
});
}
updateOrder(idOrder) {
@ -41,6 +50,21 @@ class Attachments {
});
}
async startEdit(id) {
this.editContainer.classList.remove('hidden');
this.listContainer.classList.add('hidden');
showLoading(this.editContainer);
const resp = await window.$http.get(`/attachments/edit/${id}`);
this.editContainer.innerHTML = resp.data;
window.components.init(this.editContainer);
}
stopEdit() {
this.editContainer.classList.add('hidden');
this.listContainer.classList.remove('hidden');
}
}
export default Attachments;

View file

@ -9,11 +9,15 @@ class Dropzone {
setup() {
this.container = this.$el;
this.url = this.$opts.url;
this.successMessage = this.$opts.successMessage;
this.removeMessage = this.$opts.removeMessage;
this.uploadLimitMessage = this.$opts.uploadLimitMessage;
this.timeoutMessage = this.$opts.timeoutMessage;
const _this = this;
this.dz = new DropZoneLib(this.container, {
addRemoveLinks: true,
dictRemoveFile: window.trans('components.image_upload_remove'),
dictRemoveFile: this.removeMessage,
timeout: Number(window.uploadTimeout) || 60000,
maxFilesize: Number(window.uploadLimit) || 256,
url: this.url,
@ -32,15 +36,20 @@ class Dropzone {
const token = window.document.querySelector('meta[name=token]').getAttribute('content');
data.append('_token', token);
xhr.ontimeout = function (e) {
xhr.ontimeout = (e) => {
this.dz.emit('complete', file);
this.dz.emit('error', file, window.trans('errors.file_upload_timeout'));
this.dz.emit('error', file, this.timeoutMessage);
}
}
onSuccess(file, data) {
this.container.dispatchEvent(new Event('dropzone'))
this.$emit('success', {file, data});
if (this.successMessage) {
window.$events.emit('success', this.successMessage);
}
fadeOut(file.previewElement, 800, () => {
this.dz.removeFile(file);
});
@ -55,7 +64,7 @@ class Dropzone {
}
if (xhr && xhr.status === 413) {
setMessage(window.trans('errors.server_upload_limit'))
setMessage(this.uploadLimitMessage);
} else if (errorMessage.file) {
setMessage(errorMessage.file);
}

View file

@ -0,0 +1,29 @@
import {onSelect} from "../services/dom";
/**
* EventEmitSelect
* Component will simply emit an event when selected.
*
* Has one required option: "name".
* A name of "hello" will emit a component DOM event of
* "event-emit-select-name"
*
* All options will be set as the "detail" of the event with
* their values included.
*
* @extends {Component}
*/
class EventEmitSelect {
setup() {
this.container = this.$el;
this.name = this.$opts.name;
onSelect(this.$el, () => {
this.$emit(this.name, this.$opts);
});
}
}
export default EventEmitSelect;

View file

@ -53,6 +53,14 @@ export function onEnterPress(elements, callback) {
if (!Array.isArray(elements)) {
elements = [elements];
}
const listener = event => {
if (event.key === 'Enter') {
callback(event);
}
}
elements.forEach(e => e.addEventListener('keypress', listener));
}
/**
@ -89,4 +97,13 @@ export function findText(selector, text) {
}
}
return null;
}
/**
* Show a loading indicator in the given element.
* This will effectively clear the element.
* @param {Element} element
*/
export function showLoading(element) {
element.innerHTML = `<div class="loading-container"><div></div><div></div><div></div></div>`;
}

View file

@ -67,11 +67,20 @@ async function dataRequest(method, url, data = null) {
body: data,
};
// Send data as JSON if a plain object
if (typeof data === 'object' && !(data instanceof FormData)) {
options.headers = {'Content-Type': 'application/json'};
options.body = JSON.stringify(data);
}
// Ensure FormData instances are sent over POST
// Since Laravel does not read multipart/form-data from other types
// of request. Hence the addition of the magic _method value.
if (data instanceof FormData && method !== 'post') {
data.append('_method', method);
options.method = 'post';
}
return request(url, options)
}
@ -109,7 +118,7 @@ async function request(url, options = {}) {
const response = await fetch(url, options);
const content = await getResponseContent(response);
return {
const returnData = {
data: content,
headers: response.headers,
redirected: response.redirected,
@ -117,7 +126,13 @@ async function request(url, options = {}) {
statusText: response.statusText,
url: response.url,
original: response,
};
if (!response.ok) {
throw returnData;
}
return returnData;
}
/**

View file

@ -1,142 +0,0 @@
import draggable from "vuedraggable";
import dropzone from "./components/dropzone";
function mounted() {
this.pageId = this.$el.getAttribute('page-id');
this.file = this.newFile();
this.$http.get(window.baseUrl(`/attachments/get/page/${this.pageId}`)).then(resp => {
this.files = resp.data;
}).catch(err => {
this.checkValidationErrors('get', err);
});
}
let data = {
pageId: null,
files: [],
fileToEdit: null,
file: {},
tab: 'list',
editTab: 'file',
errors: {link: {}, edit: {}, delete: {}}
};
const components = {dropzone, draggable};
let methods = {
newFile() {
return {page_id: this.pageId};
},
getFileUrl(file) {
if (file.external && file.path.indexOf('http') !== 0) {
return file.path;
}
return window.baseUrl(`/attachments/${file.id}`);
},
fileSortUpdate() {
this.$http.put(window.baseUrl(`/attachments/sort/page/${this.pageId}`), {files: this.files}).then(resp => {
this.$events.emit('success', resp.data.message);
}).catch(err => {
this.checkValidationErrors('sort', err);
});
},
startEdit(file) {
this.fileToEdit = Object.assign({}, file);
this.fileToEdit.link = file.external ? file.path : '';
this.editTab = file.external ? 'link' : 'file';
},
deleteFile(file) {
if (!file.deleting) {
return this.$set(file, 'deleting', true);
}
this.$http.delete(window.baseUrl(`/attachments/${file.id}`)).then(resp => {
this.$events.emit('success', resp.data.message);
this.files.splice(this.files.indexOf(file), 1);
}).catch(err => {
this.checkValidationErrors('delete', err)
});
},
uploadSuccess(upload) {
this.files.push(upload.data);
this.$events.emit('success', trans('entities.attachments_file_uploaded'));
},
uploadSuccessUpdate(upload) {
let fileIndex = this.filesIndex(upload.data);
if (fileIndex === -1) {
this.files.push(upload.data)
} else {
this.files.splice(fileIndex, 1, upload.data);
}
if (this.fileToEdit && this.fileToEdit.id === upload.data.id) {
this.fileToEdit = Object.assign({}, upload.data);
}
this.$events.emit('success', trans('entities.attachments_file_updated'));
},
checkValidationErrors(groupName, err) {
if (typeof err.response.data === "undefined" && typeof err.response.data === "undefined") return;
this.errors[groupName] = err.response.data;
},
getUploadUrl(file) {
let url = window.baseUrl(`/attachments/upload`);
if (typeof file !== 'undefined') url += `/${file.id}`;
return url;
},
cancelEdit() {
this.fileToEdit = null;
},
attachNewLink(file) {
file.uploaded_to = this.pageId;
this.errors.link = {};
this.$http.post(window.baseUrl('/attachments/link'), file).then(resp => {
this.files.push(resp.data);
this.file = this.newFile();
this.$events.emit('success', trans('entities.attachments_link_attached'));
}).catch(err => {
this.checkValidationErrors('link', err);
});
},
updateFile(file) {
$http.put(window.baseUrl(`/attachments/${file.id}`), file).then(resp => {
let search = this.filesIndex(resp.data);
if (search === -1) {
this.files.push(resp.data);
} else {
this.files.splice(search, 1, resp.data);
}
if (this.fileToEdit && !file.external) this.fileToEdit.link = '';
this.fileToEdit = false;
this.$events.emit('success', trans('entities.attachments_updated_success'));
}).catch(err => {
this.checkValidationErrors('edit', err);
});
},
filesIndex(file) {
for (let i = 0, len = this.files.length; i < len; i++) {
if (this.files[i].id === file.id) return i;
}
return -1;
}
};
export default {
data, methods, mounted, components,
};

View file

@ -5,12 +5,10 @@ function exists(id) {
}
import imageManager from "./image-manager";
import attachmentManager from "./attachment-manager";
import pageEditor from "./page-editor";
let vueMapping = {
'image-manager': imageManager,
'attachment-manager': attachmentManager,
'page-editor': pageEditor,
};