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:
parent
14b6cd1091
commit
d41452f39c
24 changed files with 371 additions and 321 deletions
resources/js
58
resources/js/components/ajax-form.js
Normal file
58
resources/js/components/ajax-form.js
Normal 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;
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
|
|
29
resources/js/components/event-emit-select.js
Normal file
29
resources/js/components/event-emit-select.js
Normal 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;
|
|
@ -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>`;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue