diff --git a/resources/assets/js/components/index.js b/resources/assets/js/components/index.js index c38e20aa2..43466a0d9 100644 --- a/resources/assets/js/components/index.js +++ b/resources/assets/js/components/index.js @@ -21,6 +21,8 @@ for (let i = 0, len = componentNames.length; i < len; i++) { if (typeof window.components[name] === "undefined") window.components[name] = []; for (let j = 0, jLen = elems.length; j < jLen; j++) { let instance = new component(elems[j]); + if (typeof elems[j].components === 'undefined') elems[j].components = {}; + elems[j].components[name] = instance; window.components[name].push(instance); } } \ No newline at end of file diff --git a/resources/assets/js/components/notification.js b/resources/assets/js/components/notification.js index 4b809c935..1a9819702 100644 --- a/resources/assets/js/components/notification.js +++ b/resources/assets/js/components/notification.js @@ -7,14 +7,16 @@ class Notification { this.textElem = elem.querySelector('span'); this.autohide = this.elem.hasAttribute('data-autohide'); window.Events.listen(this.type, text => { - console.log('show', text); this.show(text); }); elem.addEventListener('click', this.hide.bind(this)); if (elem.hasAttribute('data-show')) this.show(this.textElem.textContent); + + this.hideCleanup = this.hideCleanup.bind(this); } show(textToShow = '') { + this.elem.removeEventListener('transitionend', this.hideCleanup); this.textElem.textContent = textToShow; this.elem.style.display = 'block'; setTimeout(() => { @@ -26,13 +28,12 @@ class Notification { hide() { this.elem.classList.remove('showing'); + this.elem.addEventListener('transitionend', this.hideCleanup); + } - function transitionEnd() { - this.elem.style.display = 'none'; - this.elem.removeEventListener('transitionend', transitionEnd); - } - - this.elem.addEventListener('transitionend', transitionEnd.bind(this)); + hideCleanup() { + this.elem.style.display = 'none'; + this.elem.removeEventListener('transitionend', this.hideCleanup); } } diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js index 4acb40b35..de3ce81c6 100644 --- a/resources/assets/js/controllers.js +++ b/resources/assets/js/controllers.js @@ -8,256 +8,6 @@ moment.locale('en-gb'); module.exports = function (ngApp, events) { - ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService', - function ($scope, $attrs, $http, $timeout, imageManagerService) { - - $scope.images = []; - $scope.imageType = $attrs.imageType; - $scope.selectedImage = false; - $scope.dependantPages = false; - $scope.showing = false; - $scope.hasMore = false; - $scope.imageUpdateSuccess = false; - $scope.imageDeleteSuccess = false; - $scope.uploadedTo = $attrs.uploadedTo; - $scope.view = 'all'; - - $scope.searching = false; - $scope.searchTerm = ''; - - let page = 0; - let previousClickTime = 0; - let previousClickImage = 0; - let dataLoaded = false; - let callback = false; - - let preSearchImages = []; - let preSearchHasMore = false; - - /** - * Used by dropzone to get the endpoint to upload to. - * @returns {string} - */ - $scope.getUploadUrl = function () { - return window.baseUrl('/images/' + $scope.imageType + '/upload'); - }; - - /** - * Cancel the current search operation. - */ - function cancelSearch() { - $scope.searching = false; - $scope.searchTerm = ''; - $scope.images = preSearchImages; - $scope.hasMore = preSearchHasMore; - } - $scope.cancelSearch = cancelSearch; - - - /** - * Runs on image upload, Adds an image to local list of images - * and shows a success message to the user. - * @param file - * @param data - */ - $scope.uploadSuccess = function (file, data) { - $scope.$apply(() => { - $scope.images.unshift(data); - }); - events.emit('success', trans('components.image_upload_success')); - }; - - /** - * Runs the callback and hides the image manager. - * @param returnData - */ - function callbackAndHide(returnData) { - if (callback) callback(returnData); - $scope.hide(); - } - - /** - * Image select action. Checks if a double-click was fired. - * @param image - */ - $scope.imageSelect = function (image) { - let dblClickTime = 300; - let currentTime = Date.now(); - let timeDiff = currentTime - previousClickTime; - - if (timeDiff < dblClickTime && image.id === previousClickImage) { - // If double click - callbackAndHide(image); - } else { - // If single - $scope.selectedImage = image; - $scope.dependantPages = false; - } - previousClickTime = currentTime; - previousClickImage = image.id; - }; - - /** - * Action that runs when the 'Select image' button is clicked. - * Runs the callback and hides the image manager. - */ - $scope.selectButtonClick = function () { - callbackAndHide($scope.selectedImage); - }; - - /** - * Show the image manager. - * Takes a callback to execute later on. - * @param doneCallback - */ - function show(doneCallback) { - callback = doneCallback; - $scope.showing = true; - $('#image-manager').find('[overlay]').css('display', 'flex').hide().fadeIn(240); - // Get initial images if they have not yet been loaded in. - if (!dataLoaded) { - fetchData(); - dataLoaded = true; - } - } - - // Connects up the image manger so it can be used externally - // such as from TinyMCE. - imageManagerService.show = show; - imageManagerService.showExternal = function (doneCallback) { - $scope.$apply(() => { - show(doneCallback); - }); - }; - window.ImageManager = imageManagerService; - - /** - * Hide the image manager - */ - $scope.hide = function () { - $scope.showing = false; - $('#image-manager').find('[overlay]').fadeOut(240); - }; - - let baseUrl = window.baseUrl('/images/' + $scope.imageType + '/all/'); - - /** - * Fetch the list image data from the server. - */ - function fetchData() { - let url = baseUrl + page + '?'; - let components = {}; - if ($scope.uploadedTo) components['page_id'] = $scope.uploadedTo; - if ($scope.searching) components['term'] = $scope.searchTerm; - - - url += Object.keys(components).map((key) => { - return key + '=' + encodeURIComponent(components[key]); - }).join('&'); - - $http.get(url).then((response) => { - $scope.images = $scope.images.concat(response.data.images); - $scope.hasMore = response.data.hasMore; - page++; - }); - } - $scope.fetchData = fetchData; - - /** - * Start a search operation - */ - $scope.searchImages = function() { - - if ($scope.searchTerm === '') { - cancelSearch(); - return; - } - - if (!$scope.searching) { - preSearchImages = $scope.images; - preSearchHasMore = $scope.hasMore; - } - - $scope.searching = true; - $scope.images = []; - $scope.hasMore = false; - page = 0; - baseUrl = window.baseUrl('/images/' + $scope.imageType + '/search/'); - fetchData(); - }; - - /** - * Set the current image listing view. - * @param viewName - */ - $scope.setView = function(viewName) { - cancelSearch(); - $scope.images = []; - $scope.hasMore = false; - page = 0; - $scope.view = viewName; - baseUrl = window.baseUrl('/images/' + $scope.imageType + '/' + viewName + '/'); - fetchData(); - }; - - /** - * Save the details of an image. - * @param event - */ - $scope.saveImageDetails = function (event) { - event.preventDefault(); - let url = window.baseUrl('/images/update/' + $scope.selectedImage.id); - $http.put(url, this.selectedImage).then(response => { - events.emit('success', trans('components.image_update_success')); - }, (response) => { - if (response.status === 422) { - let errors = response.data; - let message = ''; - Object.keys(errors).forEach((key) => { - message += errors[key].join('\n'); - }); - events.emit('error', message); - } else if (response.status === 403) { - events.emit('error', response.data.error); - } - }); - }; - - /** - * Delete an image from system and notify of success. - * Checks if it should force delete when an image - * has dependant pages. - * @param event - */ - $scope.deleteImage = function (event) { - event.preventDefault(); - let force = $scope.dependantPages !== false; - let url = window.baseUrl('/images/' + $scope.selectedImage.id); - if (force) url += '?force=true'; - $http.delete(url).then((response) => { - $scope.images.splice($scope.images.indexOf($scope.selectedImage), 1); - $scope.selectedImage = false; - events.emit('success', trans('components.image_delete_success')); - }, (response) => { - // Pages failure - if (response.status === 400) { - $scope.dependantPages = response.data; - } else if (response.status === 403) { - events.emit('error', response.data.error); - } - }); - }; - - /** - * Simple date creator used to properly format dates. - * @param stringDate - * @returns {Date} - */ - $scope.getDate = function (stringDate) { - return new Date(stringDate); - }; - - }]); ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', '$sce', function ($scope, $http, $attrs, $interval, $timeout, $sce) { diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index 522dcacae..a156c961c 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -383,7 +383,7 @@ module.exports = function (ngApp, events) { // Show the image manager and handle image insertion function showImageManager() { let cursorPos = cm.getCursor('from'); - window.ImageManager.showExternal(image => { + window.ImageManager.show(image => { let selectedText = cm.getSelection(); let newText = ""; cm.focus(); diff --git a/resources/assets/js/global.js b/resources/assets/js/global.js index 3879a4d4f..28d1e3b0c 100644 --- a/resources/assets/js/global.js +++ b/resources/assets/js/global.js @@ -67,10 +67,8 @@ require("./vues/vues"); require("./components"); // Load in angular specific items -const Services = require('./services'); const Directives = require('./directives'); const Controllers = require('./controllers'); -Services(ngApp, window.Events); Directives(ngApp, window.Events); Controllers(ngApp, window.Events); diff --git a/resources/assets/js/pages/page-form.js b/resources/assets/js/pages/page-form.js index 4f4c1fbe0..08e4c0c34 100644 --- a/resources/assets/js/pages/page-form.js +++ b/resources/assets/js/pages/page-form.js @@ -283,7 +283,7 @@ module.exports = function() { if (type === 'image') { // Show image manager - window.ImageManager.showExternal(function (image) { + window.ImageManager.show(function (image) { // Set popover link input to image url then fire change event // to ensure the new value sticks @@ -365,7 +365,7 @@ module.exports = function() { icon: 'image', tooltip: 'Insert an image', onclick: function () { - window.ImageManager.showExternal(function (image) { + window.ImageManager.show(function (image) { let html = `<a href="${image.url}" target="_blank">`; html += `<img src="${image.thumbs.display}" alt="${image.name}">`; html += '</a>'; diff --git a/resources/assets/js/services.js b/resources/assets/js/services.js deleted file mode 100644 index cd2759c54..000000000 --- a/resources/assets/js/services.js +++ /dev/null @@ -1,12 +0,0 @@ -"use strict"; - -module.exports = function(ngApp, events) { - - ngApp.factory('imageManagerService', function() { - return { - show: false, - showExternal: false - }; - }); - -}; \ No newline at end of file diff --git a/resources/assets/js/vues/components/dropzone.js b/resources/assets/js/vues/components/dropzone.js new file mode 100644 index 000000000..0f31bd579 --- /dev/null +++ b/resources/assets/js/vues/components/dropzone.js @@ -0,0 +1,60 @@ +const DropZone = require("dropzone"); + +const template = ` + <div class="dropzone-container"> + <div class="dz-message">{{placeholder}}</div> + </div> +`; + +const props = ['placeholder', 'uploadUrl', 'uploadedTo']; + +// TODO - Remove jQuery usage +function mounted() { + let container = this.$el; + let _this = this; + new DropZone(container, { + url: function() { + return _this.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 _this.uploadedTo === 'undefined' ? 0 : _this.uploadedTo; + data.append('uploaded_to', uploadedTo); + }); + + dz.on('success', function (file, data) { + _this.$emit('success', {file, data}); + $(file.previewElement).fadeOut(400, function () { + dz.removeFile(file); + }); + }); + + dz.on('error', function (file, errorMessage, xhr) { + _this.$emit('error', {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]); + }); + } + }); +} + +function data() { + return {} +} + +module.exports = { + template, + props, + mounted, + data, +}; \ No newline at end of file diff --git a/resources/assets/js/vues/image-manager.js b/resources/assets/js/vues/image-manager.js new file mode 100644 index 000000000..9e3fa013e --- /dev/null +++ b/resources/assets/js/vues/image-manager.js @@ -0,0 +1,182 @@ +const dropzone = require('./components/dropzone'); + +let page = 0; +let previousClickTime = 0; +let previousClickImage = 0; +let dataLoaded = false; +let callback = false; +let baseUrl = ''; + +let preSearchImages = []; +let preSearchHasMore = false; + +const data = { + images: [], + + imageType: false, + uploadedTo: false, + + selectedImage: false, + dependantPages: false, + showing: false, + view: 'all', + hasMore: false, + searching: false, + searchTerm: '', + + imageUpdateSuccess: false, + imageDeleteSuccess: false, +}; + +const methods = { + + show(providedCallback) { + callback = providedCallback; + this.showing = true; + this.$el.children[0].components.overlay.show(); + + // Get initial images if they have not yet been loaded in. + if (dataLoaded) return; + this.fetchData(); + dataLoaded = true; + }, + + hide() { + this.showing = false; + this.$el.children[0].components.overlay.hide(); + }, + + fetchData() { + let url = baseUrl + page; + let query = {}; + if (this.uploadedTo !== false) query.page_id = this.uploadedTo; + if (this.searching) query.term = this.searchTerm; + + this.$http.get(url, {params: query}).then(response => { + this.images = this.images.concat(response.data.images); + this.hasMore = response.data.hasMore; + page++; + }); + }, + + setView(viewName) { + this.cancelSearch(); + this.images = []; + this.hasMore = false; + page = 0; + this.view = viewName; + baseUrl = window.baseUrl(`/images/${this.imageType}/${viewName}/`); + this.fetchData(); + }, + + searchImages() { + if (this.searchTerm === '') return this.cancelSearch(); + + // Cache current settings for later + if (!this.searching) { + preSearchImages = this.images; + preSearchHasMore = this.hasMore; + } + + this.searching = true; + this.images = []; + this.hasMore = false; + page = 0; + baseUrl = window.baseUrl(`/images/${this.imageType}/search/`); + this.fetchData(); + }, + + cancelSearch() { + this.searching = false; + this.searchTerm = ''; + this.images = preSearchImages; + this.hasMore = preSearchHasMore; + }, + + imageSelect(image) { + let dblClickTime = 300; + let currentTime = Date.now(); + let timeDiff = currentTime - previousClickTime; + let isDblClick = timeDiff < dblClickTime && image.id === previousClickImage; + + if (isDblClick) { + this.callbackAndHide(image); + } else { + this.selectedImage = image; + this.dependantPages = false; + } + + previousClickTime = currentTime; + previousClickImage = image.id; + }, + + callbackAndHide(imageResult) { + if (callback) callback(imageResult); + this.hide(); + }, + + saveImageDetails() { + let url = window.baseUrl(`/images/update/${this.selectedImage.id}`); + this.$http.put(url, this.selectedImage).then(response => { + this.$events.emit('success', trans('components.image_update_success')); + }).catch(error => { + if (error.response.status === 422) { + let errors = error.response.data; + let message = ''; + Object.keys(errors).forEach((key) => { + message += errors[key].join('\n'); + }); + this.$events.emit('error', message); + } else if (error.response.status === 403) { + this.$events.emit('error', error.response.data.error); + } + }); + }, + + deleteImage() { + let force = this.dependantPages !== false; + let url = window.baseUrl('/images/' + this.selectedImage.id); + if (force) url += '?force=true'; + this.$http.delete(url).then(response => { + this.images.splice(this.images.indexOf(this.selectedImage), 1); + this.selectedImage = false; + this.$events.emit('success', trans('components.image_delete_success')); + }).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); + } + }); + }, + + getDate(stringDate) { + return new Date(stringDate); + }, + + uploadSuccess(event) { + this.images.unshift(event.data); + this.$events.emit('success', trans('components.image_upload_success')); + }, +}; + +const computed = { + uploadUrl() { + return window.baseUrl(`/images/${this.imageType}/upload`); + } +}; + +function mounted() { + window.ImageManager = this; + this.imageType = this.$el.getAttribute('image-type'); + this.uploadedTo = this.$el.getAttribute('uploaded-to'); + baseUrl = window.baseUrl('/images/' + this.imageType + '/all/') +} + +module.exports = { + mounted, + methods, + data, + computed, + components: {dropzone}, +}; \ No newline at end of file diff --git a/resources/assets/js/vues/vues.js b/resources/assets/js/vues/vues.js index 31d833bfb..e6d4b9f5b 100644 --- a/resources/assets/js/vues/vues.js +++ b/resources/assets/js/vues/vues.js @@ -7,7 +7,8 @@ function exists(id) { let vueMapping = { 'search-system': require('./search'), 'entity-dashboard': require('./entity-search'), - 'code-editor': require('./code-editor') + 'code-editor': require('./code-editor'), + 'image-manager': require('./image-manager'), }; window.vues = {}; diff --git a/resources/views/components/image-manager.blade.php b/resources/views/components/image-manager.blade.php index 05bf09799..a4612a4ac 100644 --- a/resources/views/components/image-manager.blade.php +++ b/resources/views/components/image-manager.blade.php @@ -1,6 +1,6 @@ -<div id="image-manager" image-type="{{ $imageType }}" ng-controller="ImageManagerController" uploaded-to="{{ $uploaded_to or 0 }}"> - <div overlay ng-cloak ng-click="hide()"> - <div class="popup-body" ng-click="$event.stopPropagation()"> +<div id="image-manager" image-type="{{ $imageType }}" uploaded-to="{{ $uploaded_to or 0 }}"> + <div overlay v-cloak> + <div class="popup-body" @click.stop=""> <div class="popup-header primary-background"> <div class="popup-title">{{ trans('components.image_select') }}</div> @@ -10,82 +10,80 @@ <div class="flex-fill image-manager-body"> <div class="image-manager-content"> - <div ng-if="imageType === 'gallery'" class="container"> + <div v-if="imageType === 'gallery'" class="container"> <div class="image-manager-header row faded-small nav-tabs"> - <div class="col-xs-4 tab-item" title="{{ trans('components.image_all_title') }}" ng-class="{selected: (view=='all')}" ng-click="setView('all')"><i class="zmdi zmdi-collection-image"></i> {{ trans('components.image_all') }}</div> - <div class="col-xs-4 tab-item" title="{{ trans('components.image_book_title') }}" ng-class="{selected: (view=='book')}" ng-click="setView('book')"><i class="zmdi zmdi-book text-book"></i> {{ trans('entities.book') }}</div> - <div class="col-xs-4 tab-item" title="{{ trans('components.image_page_title') }}" ng-class="{selected: (view=='page')}" ng-click="setView('page')"><i class="zmdi zmdi-file-text text-page"></i> {{ trans('entities.page') }}</div> + <div class="col-xs-4 tab-item" title="{{ trans('components.image_all_title') }}" :class="{selected: (view=='all')}" @click="setView('all')"><i class="zmdi zmdi-collection-image"></i> {{ trans('components.image_all') }}</div> + <div class="col-xs-4 tab-item" title="{{ trans('components.image_book_title') }}" :class="{selected: (view=='book')}" @click="setView('book')"><i class="zmdi zmdi-book text-book"></i> {{ trans('entities.book') }}</div> + <div class="col-xs-4 tab-item" title="{{ trans('components.image_page_title') }}" :class="{selected: (view=='page')}" @click="setView('page')"><i class="zmdi zmdi-file-text text-page"></i> {{ trans('entities.page') }}</div> </div> </div> - <div ng-show="view === 'all'" > - <form ng-submit="searchImages()" class="contained-search-box"> - <input type="text" placeholder="{{ trans('components.image_search_hint') }}" ng-model="searchTerm"> - <button ng-class="{active: searching}" title="{{ trans('common.search_clear') }}" type="button" ng-click="cancelSearch()" class="text-button cancel"><i class="zmdi zmdi-close-circle-o"></i></button> - <button title="{{ trans('common.search') }}" class="text-button" type="submit"><i class="zmdi zmdi-search"></i></button> + <div v-show="view === 'all'" > + <form @submit="searchImages" class="contained-search-box"> + <input placeholder="{{ trans('components.image_search_hint') }}" v-model="searchTerm"> + <button :class="{active: searching}" title="{{ trans('common.search_clear') }}" type="button" @click="cancelSearch()" class="text-button cancel"><i class="zmdi zmdi-close-circle-o"></i></button> + <button title="{{ trans('common.search') }}" class="text-button"><i class="zmdi zmdi-search"></i></button> </form> </div> <div class="image-manager-list"> - <div ng-repeat="image in images"> - <div class="image anim fadeIn" ng-style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}" - ng-class="{selected: (image==selectedImage)}" ng-click="imageSelect(image)"> - <img ng-src="@{{image.thumbs.gallery}}" ng-attr-alt="@{{image.title}}" ng-attr-title="@{{image.name}}"> + <div v-if="images.length > 0" v-for="(image, idx) in images"> + <div class="image anim fadeIn" :style="{animationDelay: (idx > 26) ? '160ms' : ((idx * 25) + 'ms')}" + :class="{selected: (image==selectedImage)}" @click="imageSelect(image)"> + <img :src="image.thumbs.gallery" :alt="image.title" :title="image.name"> <div class="image-meta"> - <span class="name" ng-bind="image.name"></span> + <span class="name" v-text="image.name"></span> <span class="date">{{ trans('components.image_uploaded', ['uploadedDate' => "{{ getDate(image.created_at) }" . "}"]) }}</span> </div> </div> </div> - <div class="load-more" ng-show="hasMore" ng-click="fetchData()">{{ trans('components.image_load_more') }}</div> + <div class="load-more" v-show="hasMore" @click="fetchData">{{ trans('components.image_load_more') }}</div> </div> </div> <div class="image-manager-sidebar"> <div class="inner"> - <div class="image-manager-details anim fadeIn" ng-show="selectedImage"> + <div class="image-manager-details anim fadeIn" v-if="selectedImage"> - <form ng-submit="saveImageDetails($event)"> + <form @submit.prevent="saveImageDetails"> <div> - <a ng-href="@{{selectedImage.url}}" target="_blank" style="display: block;"> - <img ng-src="@{{selectedImage.thumbs.gallery}}" ng-attr-alt="@{{selectedImage.title}}" ng-attr-title="@{{selectedImage.name}}"> + <a :href="selectedImage.url" target="_blank" style="display: block;"> + <img :src="selectedImage.thumbs.gallery" :alt="selectedImage.title" + :title="selectedImage.name"> </a> </div> <div class="form-group"> <label for="name">{{ trans('components.image_image_name') }}</label> - <input type="text" id="name" name="name" ng-model="selectedImage.name"> + <input id="name" name="name" v-model="selectedImage.name"> </div> </form> - <div ng-show="dependantPages"> + <div v-show="dependantPages"> <p class="text-neg text-small"> {{ trans('components.image_delete_confirm') }} </p> <ul class="text-neg"> - <li ng-repeat="page in dependantPages"> - <a ng-href="@{{ page.url }}" target="_blank" class="text-neg" ng-bind="page.name"></a> + <li v-for="page in dependantPages"> + <a :href="page.url" target="_blank" class="text-neg" v-text="page.name"></a> </li> </ul> </div> <div class="clearfix"> - <form class="float left" ng-submit="deleteImage($event)"> + <form class="float left" @submit.prevent="deleteImage"> <button class="button icon neg"><i class="zmdi zmdi-delete"></i></button> </form> - <button class="button pos anim fadeIn float right" ng-show="selectedImage" ng-click="selectButtonClick()"> + <button class="button pos anim fadeIn float right" v-show="selectedImage" @click="callbackAndHide(selectedImage)"> <i class="zmdi zmdi-square-right"></i>{{ trans('components.image_select_image') }} </button> </div> </div> - <drop-zone message="{{ trans('components.image_dropzone') }}" upload-url="@{{getUploadUrl()}}" uploaded-to="@{{uploadedTo}}" event-success="uploadSuccess"></drop-zone> - + <dropzone placeholder="{{ trans('components.image_dropzone') }}" :upload-url="uploadUrl" :uploaded-to="uploadedTo" @success="uploadSuccess"></dropzone> </div> </div> - - </div> </div>