diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js
index 132580f68..8b37379fa 100644
--- a/resources/assets/js/controllers.js
+++ b/resources/assets/js/controllers.js
@@ -145,202 +145,6 @@ module.exports = function (ngApp, events) {
}]);
- ngApp.controller('PageAttachmentController', ['$scope', '$http', '$attrs',
- function ($scope, $http, $attrs) {
-
- const pageId = $scope.uploadedTo = $attrs.pageId;
- let currentOrder = '';
- $scope.files = [];
- $scope.editFile = false;
- $scope.file = getCleanFile();
- $scope.errors = {
- link: {},
- edit: {}
- };
-
- function getCleanFile() {
- return {
- page_id: pageId
- };
- }
-
- // Angular-UI-Sort options
- $scope.sortOptions = {
- handle: '.handle',
- items: '> tr',
- containment: "parent",
- axis: "y",
- stop: sortUpdate,
- };
-
- /**
- * Event listener for sort changes.
- * Updates the file ordering on the server.
- * @param event
- * @param ui
- */
- function sortUpdate(event, ui) {
- let newOrder = $scope.files.map(file => {return file.id}).join(':');
- if (newOrder === currentOrder) return;
-
- currentOrder = newOrder;
- $http.put(window.baseUrl(`/attachments/sort/page/${pageId}`), {files: $scope.files}).then(resp => {
- events.emit('success', resp.data.message);
- }, checkError('sort'));
- }
-
- /**
- * Used by dropzone to get the endpoint to upload to.
- * @returns {string}
- */
- $scope.getUploadUrl = function (file) {
- let suffix = (typeof file !== 'undefined') ? `/${file.id}` : '';
- return window.baseUrl(`/attachments/upload${suffix}`);
- };
-
- /**
- * Get files for the current page from the server.
- */
- function getFiles() {
- let url = window.baseUrl(`/attachments/get/page/${pageId}`);
- $http.get(url).then(resp => {
- $scope.files = resp.data;
- currentOrder = resp.data.map(file => {return file.id}).join(':');
- }, checkError('get'));
- }
- getFiles();
-
- /**
- * Runs on file upload, Adds an file to local file list
- * and shows a success message to the user.
- * @param file
- * @param data
- */
- $scope.uploadSuccess = function (file, data) {
- $scope.$apply(() => {
- $scope.files.push(data);
- });
- events.emit('success', trans('entities.attachments_file_uploaded'));
- };
-
- /**
- * Upload and overwrite an existing file.
- * @param file
- * @param data
- */
- $scope.uploadSuccessUpdate = function (file, data) {
- $scope.$apply(() => {
- let search = filesIndexOf(data);
- if (search !== -1) $scope.files[search] = data;
-
- if ($scope.editFile) {
- $scope.editFile = angular.copy(data);
- data.link = '';
- }
- });
- events.emit('success', trans('entities.attachments_file_updated'));
- };
-
- /**
- * Delete a file from the server and, on success, the local listing.
- * @param file
- */
- $scope.deleteFile = function(file) {
- if (!file.deleting) {
- file.deleting = true;
- return;
- }
- $http.delete(window.baseUrl(`/attachments/${file.id}`)).then(resp => {
- events.emit('success', resp.data.message);
- $scope.files.splice($scope.files.indexOf(file), 1);
- }, checkError('delete'));
- };
-
- /**
- * Attach a link to a page.
- * @param file
- */
- $scope.attachLinkSubmit = function(file) {
- file.uploaded_to = pageId;
- $http.post(window.baseUrl('/attachments/link'), file).then(resp => {
- $scope.files.push(resp.data);
- events.emit('success', trans('entities.attachments_link_attached'));
- $scope.file = getCleanFile();
- }, checkError('link'));
- };
-
- /**
- * Start the edit mode for a file.
- * @param file
- */
- $scope.startEdit = function(file) {
- $scope.editFile = angular.copy(file);
- $scope.editFile.link = (file.external) ? file.path : '';
- };
-
- /**
- * Cancel edit mode
- */
- $scope.cancelEdit = function() {
- $scope.editFile = false;
- };
-
- /**
- * Update the name and link of a file.
- * @param file
- */
- $scope.updateFile = function(file) {
- $http.put(window.baseUrl(`/attachments/${file.id}`), file).then(resp => {
- let search = filesIndexOf(resp.data);
- if (search !== -1) $scope.files[search] = resp.data;
-
- if ($scope.editFile && !file.external) {
- $scope.editFile.link = '';
- }
- $scope.editFile = false;
- events.emit('success', trans('entities.attachments_updated_success'));
- }, checkError('edit'));
- };
-
- /**
- * Get the url of a file.
- */
- $scope.getFileUrl = function(file) {
- return window.baseUrl('/attachments/' + file.id);
- };
-
- /**
- * Search the local files via another file object.
- * Used to search via object copies.
- * @param file
- * @returns int
- */
- function filesIndexOf(file) {
- for (let i = 0; i < $scope.files.length; i++) {
- if ($scope.files[i].id == file.id) return i;
- }
- return -1;
- }
-
- /**
- * Check for an error response in a ajax request.
- * @param errorGroupName
- */
- function checkError(errorGroupName) {
- $scope.errors[errorGroupName] = {};
- return function(response) {
- if (typeof response.data !== 'undefined' && typeof response.data.error !== 'undefined') {
- events.emit('error', response.data.error);
- }
- if (typeof response.data !== 'undefined' && typeof response.data.validation !== 'undefined') {
- $scope.errors[errorGroupName] = response.data.validation;
- console.log($scope.errors[errorGroupName])
- }
- }
- }
-
- }]);
-
// Controller used to reply to and add new comments
ngApp.controller('CommentReplyController', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) {
const MarkdownIt = require("markdown-it");
diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js
index 2a0547c97..fc92121ff 100644
--- a/resources/assets/js/directives.js
+++ b/resources/assets/js/directives.js
@@ -1,119 +1,10 @@
"use strict";
-const DropZone = require("dropzone");
const MarkdownIt = require("markdown-it");
const mdTasksLists = require('markdown-it-task-lists');
const code = require('./code');
module.exports = function (ngApp, events) {
- /**
- * Common tab controls using simple jQuery functions.
- */
- ngApp.directive('tabContainer', function() {
- return {
- restrict: 'A',
- link: function (scope, element, attrs) {
- const $content = element.find('[tab-content]');
- const $buttons = element.find('[tab-button]');
-
- if (attrs.tabContainer) {
- let initial = attrs.tabContainer;
- $buttons.filter(`[tab-button="${initial}"]`).addClass('selected');
- $content.hide().filter(`[tab-content="${initial}"]`).show();
- } else {
- $content.hide().first().show();
- $buttons.first().addClass('selected');
- }
-
- $buttons.click(function() {
- let clickedTab = $(this);
- $buttons.removeClass('selected');
- $content.hide();
- let name = clickedTab.addClass('selected').attr('tab-button');
- $content.filter(`[tab-content="${name}"]`).show();
- });
- }
- };
- });
-
- /**
- * Sub form component to allow inner-form sections to act like their own forms.
- */
- ngApp.directive('subForm', function() {
- return {
- restrict: 'A',
- link: function (scope, element, attrs) {
- element.on('keypress', e => {
- if (e.keyCode === 13) {
- submitEvent(e);
- }
- });
-
- element.find('button[type="submit"]').click(submitEvent);
-
- function submitEvent(e) {
- e.preventDefault();
- if (attrs.subForm) scope.$eval(attrs.subForm);
- }
- }
- };
- });
-
- /**
- * DropZone
- * Used for uploading images
- */
- ngApp.directive('dropZone', [function () {
- return {
- restrict: 'E',
- template: `
-
- `,
- scope: {
- uploadUrl: '@',
- eventSuccess: '=',
- eventError: '=',
- uploadedTo: '@',
- },
- link: function (scope, element, attrs) {
- scope.message = attrs.message;
- if (attrs.placeholder) element[0].querySelector('.dz-message').textContent = attrs.placeholder;
- let dropZone = new DropZone(element[0].querySelector('.dropzone-container'), {
- url: scope.uploadUrl,
- init: function () {
- let dz = this;
- dz.on('sending', function (file, xhr, data) {
- let token = window.document.querySelector('meta[name=token]').getAttribute('content');
- data.append('_token', token);
- let uploadedTo = typeof scope.uploadedTo === 'undefined' ? 0 : scope.uploadedTo;
- data.append('uploaded_to', uploadedTo);
- });
- if (typeof scope.eventSuccess !== 'undefined') dz.on('success', scope.eventSuccess);
- dz.on('success', function (file, data) {
- $(file.previewElement).fadeOut(400, function () {
- dz.removeFile(file);
- });
- });
- if (typeof scope.eventError !== 'undefined') dz.on('error', scope.eventError);
- dz.on('error', function (file, errorMessage, xhr) {
- console.log(errorMessage);
- console.log(xhr);
- function setMessage(message) {
- $(file.previewElement).find('[data-dz-errormessage]').text(message);
- }
-
- if (xhr.status === 413) setMessage(trans('errors.server_upload_limit'));
- if (errorMessage.file) setMessage(errorMessage.file[0]);
-
- });
- }
- });
- }
- };
- }]);
-
/**
* TinyMCE
* An angular wrapper around the tinyMCE editor.
diff --git a/resources/assets/js/global.js b/resources/assets/js/global.js
index 28d1e3b0c..ee7cf3cc1 100644
--- a/resources/assets/js/global.js
+++ b/resources/assets/js/global.js
@@ -9,34 +9,6 @@ window.baseUrl = function(path) {
return basePath + '/' + path;
};
-const Vue = require("vue");
-const axios = require("axios");
-
-let axiosInstance = axios.create({
- headers: {
- 'X-CSRF-TOKEN': document.querySelector('meta[name=token]').getAttribute('content'),
- 'baseURL': window.baseUrl('')
- }
-});
-window.$http = axiosInstance;
-Vue.prototype.$http = axiosInstance;
-
-
-// AngularJS - Create application and load components
-const angular = require("angular");
-require("angular-resource");
-require("angular-animate");
-require("angular-sanitize");
-require("angular-ui-sortable");
-
-let ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']);
-
-// Translation setup
-// Creates a global function with name 'trans' to be used in the same way as Laravel's translation system
-const Translations = require("./translations");
-let translator = new Translations(window.translations);
-window.trans = translator.get.bind(translator);
-
// Global Event System
class EventManager {
constructor() {
@@ -61,8 +33,45 @@ class EventManager {
}
window.Events = new EventManager();
+
+const Vue = require("vue");
+const axios = require("axios");
+
+let axiosInstance = axios.create({
+ headers: {
+ 'X-CSRF-TOKEN': document.querySelector('meta[name=token]').getAttribute('content'),
+ 'baseURL': window.baseUrl('')
+ }
+});
+axiosInstance.interceptors.request.use(resp => {
+ return resp;
+}, err => {
+ if (typeof err.response === "undefined" || typeof err.response.data === "undefined") return Promise.reject(err);
+ if (typeof err.response.data.error !== "undefined") window.Events.emit('error', err.response.data.error);
+ if (typeof err.response.data.message !== "undefined") window.Events.emit('error', err.response.data.message);
+});
+window.$http = axiosInstance;
+
+Vue.prototype.$http = axiosInstance;
Vue.prototype.$events = window.Events;
+
+// AngularJS - Create application and load components
+const angular = require("angular");
+require("angular-resource");
+require("angular-animate");
+require("angular-sanitize");
+require("angular-ui-sortable");
+
+let ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']);
+
+// Translation setup
+// Creates a global function with name 'trans' to be used in the same way as Laravel's translation system
+const Translations = require("./translations");
+let translator = new Translations(window.translations);
+window.trans = translator.get.bind(translator);
+
+
require("./vues/vues");
require("./components");
diff --git a/resources/assets/js/vues/attachment-manager.js b/resources/assets/js/vues/attachment-manager.js
new file mode 100644
index 000000000..635622b93
--- /dev/null
+++ b/resources/assets/js/vues/attachment-manager.js
@@ -0,0 +1,138 @@
+const draggable = require('vuedraggable');
+const dropzone = require('./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) {
+ 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 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) {
+ console.error(err);
+ if (typeof err.response.data === "undefined" && typeof err.response.data.validation === "undefined") return;
+ this.errors[groupName] = err.response.data.validation;
+ console.log(this.errors[groupName]);
+ },
+
+ 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.$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;
+ }
+
+};
+
+module.exports = {
+ data, methods, mounted, components,
+};
\ No newline at end of file
diff --git a/resources/assets/js/vues/image-manager.js b/resources/assets/js/vues/image-manager.js
index 9e3fa013e..12ccc970d 100644
--- a/resources/assets/js/vues/image-manager.js
+++ b/resources/assets/js/vues/image-manager.js
@@ -127,8 +127,6 @@ const methods = {
message += errors[key].join('\n');
});
this.$events.emit('error', message);
- } else if (error.response.status === 403) {
- this.$events.emit('error', error.response.data.error);
}
});
},
@@ -144,8 +142,6 @@ const methods = {
}).catch(error=> {
if (error.response.status === 400) {
this.dependantPages = error.response.data;
- } else if (error.response.status === 403) {
- this.$events.emit('error', error.response.data.error);
}
});
},
diff --git a/resources/assets/js/vues/vues.js b/resources/assets/js/vues/vues.js
index a3f6ec8e5..5f6f7d7a7 100644
--- a/resources/assets/js/vues/vues.js
+++ b/resources/assets/js/vues/vues.js
@@ -10,14 +10,15 @@ let vueMapping = {
'code-editor': require('./code-editor'),
'image-manager': require('./image-manager'),
'tag-manager': require('./tag-manager'),
+ 'attachment-manager': require('./attachment-manager'),
};
window.vues = {};
-Object.keys(vueMapping).forEach(id => {
- if (exists(id)) {
- let config = vueMapping[id];
- config.el = '#' + id;
- window.vues[id] = new Vue(config);
- }
-});
\ No newline at end of file
+let ids = Object.keys(vueMapping);
+for (let i = 0, len = ids.length; i < len; i++) {
+ if (!exists(ids[i])) continue;
+ let config = vueMapping[ids[i]];
+ config.el = '#' + ids[i];
+ window.vues[ids[i]] = new Vue(config);
+}
\ No newline at end of file
diff --git a/resources/assets/sass/_components.scss b/resources/assets/sass/_components.scss
index 8092caa07..525b4f8f1 100644
--- a/resources/assets/sass/_components.scss
+++ b/resources/assets/sass/_components.scss
@@ -512,7 +512,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
}
-[tab-container] .nav-tabs {
+.tab-container .nav-tabs {
text-align: left;
border-bottom: 1px solid #DDD;
margin-bottom: $-m;
diff --git a/resources/assets/sass/_tables.scss b/resources/assets/sass/_tables.scss
index ea517fee3..31ac92f60 100644
--- a/resources/assets/sass/_tables.scss
+++ b/resources/assets/sass/_tables.scss
@@ -59,18 +59,9 @@ table.list-table {
}
}
-table.file-table {
- @extend .no-style;
- td {
- padding: $-xs;
- }
- .ui-sortable-helper {
- display: table;
- }
-}
-
.fake-table {
display: table;
+ width: 100%;
> div {
display: table-row-group;
}
diff --git a/resources/views/pages/form-toolbox.blade.php b/resources/views/pages/form-toolbox.blade.php
index 3bc03a17f..bd60af89a 100644
--- a/resources/views/pages/form-toolbox.blade.php
+++ b/resources/views/pages/form-toolbox.blade.php
@@ -14,8 +14,8 @@