From 23915c3b1a23837f30ca5a0def807869a224b1db Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 24 Apr 2023 16:19:20 +0100 Subject: [PATCH 01/11] Started custom dropzone implementation --- .../Images/GalleryImageController.php | 12 +- resources/js/components/dropzone.js | 164 ++++++++++++------ resources/js/components/image-manager.js | 5 + resources/js/services/clipboard.js | 17 +- 4 files changed, 133 insertions(+), 65 deletions(-) diff --git a/app/Http/Controllers/Images/GalleryImageController.php b/app/Http/Controllers/Images/GalleryImageController.php index 3f2f56265..99226e5e9 100644 --- a/app/Http/Controllers/Images/GalleryImageController.php +++ b/app/Http/Controllers/Images/GalleryImageController.php @@ -42,9 +42,15 @@ class GalleryImageController extends Controller public function create(Request $request) { $this->checkPermission('image-create-all'); - $this->validate($request, [ - 'file' => $this->getImageValidationRules(), - ]); + + try { + $this->validate($request, [ + 'file' => $this->getImageValidationRules(), + ]); + } catch (ValidationException $exception) { + // TODO - Check potential other upload locations? + return $this->jsonError(implode("\n", $exception->errors()['file'])); + } try { $imageUpload = $request->file('file'); diff --git a/resources/js/components/dropzone.js b/resources/js/components/dropzone.js index e7aae769e..7faa5489f 100644 --- a/resources/js/components/dropzone.js +++ b/resources/js/components/dropzone.js @@ -1,6 +1,5 @@ -import DropZoneLib from 'dropzone'; -import {fadeOut} from '../services/animations'; import {Component} from './component'; +import {Clipboard} from '../services/clipboard'; export class Dropzone extends Component { @@ -9,66 +8,119 @@ export class Dropzone extends Component { this.url = this.$opts.url; this.successMessage = this.$opts.successMessage; this.removeMessage = this.$opts.removeMessage; - this.uploadLimit = Number(this.$opts.uploadLimit); - this.uploadLimitMessage = this.$opts.uploadLimitMessage; - this.timeoutMessage = this.$opts.timeoutMessage; + this.uploadLimit = Number(this.$opts.uploadLimit); // TODO - Use + this.uploadLimitMessage = this.$opts.uploadLimitMessage; // TODO - Use + this.timeoutMessage = this.$opts.timeoutMessage; // TODO - Use + // window.uploadTimeout // TODO - Use + // TODO - Click-to-upload buttons/areas + // TODO - Drop zone highlighting of existing element + // (Add overlay via additional temp element). - const component = this; - this.dz = new DropZoneLib(this.container, { - addRemoveLinks: true, - dictRemoveFile: this.removeMessage, - timeout: Number(window.uploadTimeout) || 60000, - maxFilesize: this.uploadLimit, - url: this.url, - withCredentials: true, - init() { - this.dz = this; - this.dz.on('sending', component.onSending.bind(component)); - this.dz.on('success', component.onSuccess.bind(component)); - this.dz.on('error', component.onError.bind(component)); + this.setupListeners(); + } + + setupListeners() { + this.container.addEventListener('dragenter', event => { + this.container.style.border = '1px dotted tomato'; + event.preventDefault(); + }); + + this.container.addEventListener('dragover', event => { + event.preventDefault(); + }); + + const reset = () => { + this.container.style.border = null; + }; + + this.container.addEventListener('dragend', event => { + reset(); + }); + + this.container.addEventListener('dragleave', event => { + reset(); + }); + + this.container.addEventListener('drop', event => { + event.preventDefault(); + const clipboard = new Clipboard(event.dataTransfer); + const files = clipboard.getFiles(); + for (const file of files) { + this.createUploadFromFile(file); + } + }); + } + + /** + * @param {File} file + * @return {Upload} + */ + createUploadFromFile(file) { + const {dom, status} = this.createDomForFile(file); + this.container.append(dom); + + const formData = new FormData(); + formData.append('file', file, file.name); + + // TODO - Change to XMLHTTPRequest so we can track progress. + const uploadPromise = window.$http.post(this.url, formData); + + const upload = { + file, + dom, + markError(message) { + status.setAttribute('data-status', 'error'); + status.textContent = message; + }, + markSuccess(message) { + status.setAttribute('data-status', 'success'); + status.textContent = message; }, - }); - } - - onSending(file, xhr, data) { - const token = window.document.querySelector('meta[name=token]').getAttribute('content'); - data.append('_token', token); - - xhr.ontimeout = () => { - this.dz.emit('complete', file); - this.dz.emit('error', file, this.timeoutMessage); - }; - } - - onSuccess(file, data) { - this.$emit('success', {file, data}); - - if (this.successMessage) { - window.$events.emit('success', this.successMessage); - } - - fadeOut(file.previewElement, 800, () => { - this.dz.removeFile(file); - }); - } - - onError(file, errorMessage, xhr) { - this.$emit('error', {file, errorMessage, xhr}); - - const setMessage = message => { - const messsageEl = file.previewElement.querySelector('[data-dz-errormessage]'); - messsageEl.textContent = message; }; - if (xhr && xhr.status === 413) { - setMessage(this.uploadLimitMessage); - } else if (errorMessage.file) { - setMessage(errorMessage.file); - } + uploadPromise.then(returnData => { + upload.markSuccess(returnData.statusText); + }).catch(err => { + upload.markError(err?.data?.message || err.message); + }); + + return upload; } - removeAll() { - this.dz.removeAllFiles(true); + /** + * @param {File} file + * @return {{image: Element, dom: Element, progress: Element, label: Element, status: Element}} + */ + createDomForFile(file) { + const dom = document.createElement('div'); + const label = document.createElement('div'); + const status = document.createElement('div'); + const progress = document.createElement('div'); + const image = document.createElement('img'); + + dom.classList.add('dropzone-file-item'); + status.classList.add('dropzone-file-item-status'); + progress.classList.add('dropzone-file-item-progress'); + + image.src = ''; // TODO - file icon + label.innerText = file.name; + + if (file.type.startsWith('image/')) { + image.src = URL.createObjectURL(file); + } + + dom.append(image, label, progress, status); + return { + dom, label, image, progress, status, + }; } } + +/** + * @typedef Upload + * @property {File} file + * @property {Element} dom + * @property {function(String)} markError + * @property {function(String)} markSuccess + */ diff --git a/resources/js/components/image-manager.js b/resources/js/components/image-manager.js index 3cb99f0e2..bb21bd5ec 100644 --- a/resources/js/components/image-manager.js +++ b/resources/js/components/image-manager.js @@ -34,6 +34,11 @@ export class ImageManager extends Component { this.resetState(); this.setupListeners(); + + window.setTimeout(() => { + this.show(() => { + }, 'gallery'); + }, 500); } setupListeners() { diff --git a/resources/js/services/clipboard.js b/resources/js/services/clipboard.js index 02db29be0..5f73c3020 100644 --- a/resources/js/services/clipboard.js +++ b/resources/js/services/clipboard.js @@ -30,7 +30,6 @@ export class Clipboard { */ getImages() { const {types} = this.data; - const {files} = this.data; const images = []; for (const type of types) { @@ -40,15 +39,21 @@ export class Clipboard { } } - for (const file of files) { - if (file.type.includes('image')) { - images.push(file); - } - } + const imageFiles = this.getFiles().filter(f => f.type.includes('image')); + images.push(...imageFiles); return images; } + /** + * Get the files included in the clipboard data. + * @return {File[]} + */ + getFiles() { + const {files} = this.data; + return [...files]; + } + } export async function copyTextToClipboard(text) { From 36116a45d4047b68c5a6ecf2ab3bb19e3e40782a Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 24 Apr 2023 18:18:08 +0100 Subject: [PATCH 02/11] Dropzone: Swapped fetch for XHR for progress tracking --- resources/js/components/dropzone.js | 52 ++++++++++++++++++++++------- resources/js/services/http.js | 21 ++++++++++++ 2 files changed, 61 insertions(+), 12 deletions(-) diff --git a/resources/js/components/dropzone.js b/resources/js/components/dropzone.js index 7faa5489f..87c6b4d4f 100644 --- a/resources/js/components/dropzone.js +++ b/resources/js/components/dropzone.js @@ -56,18 +56,17 @@ export class Dropzone extends Component { * @return {Upload} */ createUploadFromFile(file) { - const {dom, status} = this.createDomForFile(file); + const {dom, status, progress} = this.createDomForFile(file); this.container.append(dom); - const formData = new FormData(); - formData.append('file', file, file.name); - - // TODO - Change to XMLHTTPRequest so we can track progress. - const uploadPromise = window.$http.post(this.url, formData); - const upload = { file, dom, + updateProgress(percentComplete) { + console.log(`progress: ${percentComplete}%`); + progress.textContent = `${percentComplete}%`; + progress.style.width = `${percentComplete}%`; + }, markError(message) { status.setAttribute('data-status', 'error'); status.textContent = message; @@ -78,15 +77,43 @@ export class Dropzone extends Component { }, }; - uploadPromise.then(returnData => { - upload.markSuccess(returnData.statusText); - }).catch(err => { - upload.markError(err?.data?.message || err.message); - }); + this.startXhrForUpload(upload); return upload; } + /** + * @param {Upload} upload + */ + startXhrForUpload(upload) { + const formData = new FormData(); + formData.append('file', upload.file, upload.file.name); + + const req = window.$http.createXMLHttpRequest('POST', this.url, { + error() { + upload.markError('Upload failed'); // TODO - Update text + }, + readystatechange() { + if (this.readyState === XMLHttpRequest.DONE && this.status === 200) { + upload.markSuccess('Finished upload!'); + } else if (this.readyState === XMLHttpRequest.DONE && this.status >= 400) { + const content = this.responseText; + const data = content.startsWith('{') ? JSON.parse(content) : {message: content}; + const message = data?.message || content; + upload.markError(message); + } + }, + }); + + req.upload.addEventListener('progress', evt => { + const percent = Math.min(Math.ceil((evt.loaded / evt.total) * 100), 100); + upload.updateProgress(percent); + }); + + req.setRequestHeader('Accept', 'application/json'); + req.send(formData); + } + /** * @param {File} file * @return {{image: Element, dom: Element, progress: Element, label: Element, status: Element}} @@ -121,6 +148,7 @@ export class Dropzone extends Component { * @typedef Upload * @property {File} file * @property {Element} dom + * @property {function(Number)} updateProgress * @property {function(String)} markError * @property {function(String)} markSuccess */ diff --git a/resources/js/services/http.js b/resources/js/services/http.js index d0d33e317..49d5b6df4 100644 --- a/resources/js/services/http.js +++ b/resources/js/services/http.js @@ -45,6 +45,27 @@ export class HttpError extends Error { } +/** + * @param {String} method + * @param {String} url + * @param {Object} events + * @return {XMLHttpRequest} + */ +export function createXMLHttpRequest(method, url, events = {}) { + const csrfToken = document.querySelector('meta[name=token]').getAttribute('content'); + const req = new XMLHttpRequest(); + + for (const [eventName, callback] of Object.entries(events)) { + req.addEventListener(eventName, callback.bind(req)); + } + + req.open(method, url); + req.withCredentials = true; + req.setRequestHeader('X-CSRF-TOKEN', csrfToken); + + return req; +} + /** * Create a new HTTP request, setting the required CSRF information * to communicate with the back-end. Parses & formats the response. From a8fc29a31ef9f657fca53308c9e95186a227d8d7 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 24 Apr 2023 23:24:58 +0100 Subject: [PATCH 03/11] Dropzone: started on design/ui of uploading - Added new wider target handling. - Updated upload item dom with design and seperate "landing" zone. - Added new helper for simple dom element creation. --- lang/en/components.php | 1 + resources/js/components/dropzone.js | 67 ++++++++--- resources/js/services/dom.js | 37 ++++++ resources/sass/_components.scss | 112 ++++++++++++++++++ resources/views/form/dropzone.blade.php | 10 +- .../views/pages/parts/image-manager.blade.php | 14 ++- 6 files changed, 212 insertions(+), 29 deletions(-) diff --git a/lang/en/components.php b/lang/en/components.php index 48a0a32fa..07a3ba3a7 100644 --- a/lang/en/components.php +++ b/lang/en/components.php @@ -18,6 +18,7 @@ return [ 'image_delete_confirm_text' => 'Are you sure you want to delete this image?', 'image_select_image' => 'Select Image', 'image_dropzone' => 'Drop images or click here to upload', + 'image_dropzone_drop' => 'Drop images here to upload', 'images_deleted' => 'Images Deleted', 'image_preview' => 'Image Preview', 'image_upload_success' => 'Image uploaded successfully', diff --git a/resources/js/components/dropzone.js b/resources/js/components/dropzone.js index 87c6b4d4f..b94d5d1f4 100644 --- a/resources/js/components/dropzone.js +++ b/resources/js/components/dropzone.js @@ -1,16 +1,22 @@ import {Component} from './component'; import {Clipboard} from '../services/clipboard'; +import { + elem, getLoading, removeLoading, +} from '../services/dom'; export class Dropzone extends Component { setup() { this.container = this.$el; + this.statusArea = this.$refs.statusArea; + this.url = this.$opts.url; this.successMessage = this.$opts.successMessage; this.removeMessage = this.$opts.removeMessage; this.uploadLimit = Number(this.$opts.uploadLimit); // TODO - Use this.uploadLimitMessage = this.$opts.uploadLimitMessage; // TODO - Use this.timeoutMessage = this.$opts.timeoutMessage; // TODO - Use + this.zoneText = this.$opts.zoneText; // window.uploadTimeout // TODO - Use // TODO - Click-to-upload buttons/areas // TODO - Drop zone highlighting of existing element @@ -20,9 +26,15 @@ export class Dropzone extends Component { } setupListeners() { + let depth = 0; + this.container.addEventListener('dragenter', event => { - this.container.style.border = '1px dotted tomato'; event.preventDefault(); + depth += 1; + + if (depth === 1) { + this.showOverlay(); + } }); this.container.addEventListener('dragover', event => { @@ -30,7 +42,8 @@ export class Dropzone extends Component { }); const reset = () => { - this.container.style.border = null; + this.hideOverlay(); + depth = 0; }; this.container.addEventListener('dragend', event => { @@ -38,11 +51,15 @@ export class Dropzone extends Component { }); this.container.addEventListener('dragleave', event => { - reset(); + depth -= 1; + if (depth === 0) { + reset(); + } }); this.container.addEventListener('drop', event => { event.preventDefault(); + reset(); const clipboard = new Clipboard(event.dataTransfer); const files = clipboard.getFiles(); for (const file of files) { @@ -51,6 +68,21 @@ export class Dropzone extends Component { }); } + showOverlay() { + const overlay = this.container.querySelector('.dropzone-overlay'); + if (!overlay) { + const zoneElem = elem('div', {class: 'dropzone-overlay'}, [this.zoneText]); + this.container.append(zoneElem); + } + } + + hideOverlay() { + const overlay = this.container.querySelector('.dropzone-overlay'); + if (overlay) { + overlay.remove(); + } + } + /** * @param {File} file * @return {Upload} @@ -70,10 +102,12 @@ export class Dropzone extends Component { markError(message) { status.setAttribute('data-status', 'error'); status.textContent = message; + removeLoading(dom); }, markSuccess(message) { status.setAttribute('data-status', 'success'); status.textContent = message; + removeLoading(dom); }, }; @@ -119,26 +153,27 @@ export class Dropzone extends Component { * @return {{image: Element, dom: Element, progress: Element, label: Element, status: Element}} */ createDomForFile(file) { - const dom = document.createElement('div'); - const label = document.createElement('div'); - const status = document.createElement('div'); - const progress = document.createElement('div'); - const image = document.createElement('img'); + const image = elem('img', {src: ''}); + const status = elem('div', {class: 'dropzone-file-item-status'}, []); + const progress = elem('div', {class: 'dropzone-file-item-progress'}); + const imageWrap = elem('div', {class: 'dropzone-file-item-image-wrap'}, [image]); - dom.classList.add('dropzone-file-item'); - status.classList.add('dropzone-file-item-status'); - progress.classList.add('dropzone-file-item-progress'); - - image.src = ''; // TODO - file icon - label.innerText = file.name; + const dom = elem('div', {class: 'dropzone-file-item'}, [ + imageWrap, + elem('div', {class: 'dropzone-file-item-text-wrap'}, [ + elem('div', {class: 'dropzone-file-item-label'}, [file.name]), + getLoading(), + status, + ]), + progress, + ]); if (file.type.startsWith('image/')) { image.src = URL.createObjectURL(file); } - dom.append(image, label, progress, status); return { - dom, label, image, progress, status, + dom, progress, status, }; } diff --git a/resources/js/services/dom.js b/resources/js/services/dom.js index 17f5a803a..786855748 100644 --- a/resources/js/services/dom.js +++ b/resources/js/services/dom.js @@ -1,3 +1,29 @@ +/** + * Create a new element with the given attrs and children. + * Children can be a string for text nodes or other elements. + * @param {String} tagName + * @param {Object} attrs + * @param {Element[]|String[]}children + * @return {*} + */ +export function elem(tagName, attrs = {}, children = []) { + const el = document.createElement(tagName); + + for (const [key, val] of Object.entries(attrs)) { + el.setAttribute(key, val); + } + + for (const child of children) { + if (typeof child === 'string') { + el.append(document.createTextNode(child)); + } else { + el.append(child); + } + } + + return el; +} + /** * Run the given callback against each element that matches the given selector. * @param {String} selector @@ -108,6 +134,17 @@ export function showLoading(element) { element.innerHTML = '
'; } +/** + * Get a loading element indicator element. + * @returns {Element} + */ +export function getLoading() { + const wrap = document.createElement('div'); + wrap.classList.add('loading-container'); + wrap.innerHTML = '
'; + return wrap; +} + /** * Remove any loading indicators within the given element. * @param {Element} element diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss index 4e6a8d731..0f66bd74a 100644 --- a/resources/sass/_components.scss +++ b/resources/sass/_components.scss @@ -116,6 +116,7 @@ z-index: 999; display: flex; flex-direction: column; + position: relative; &.small { margin: 2% auto; width: 800px; @@ -202,6 +203,117 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { min-height: 70vh; } +.dropzone-overlay { + position: absolute; + display: flex; + justify-content: center; + align-items: center; + font-size: 2rem; + width: 98%; + height: 98%; + left: 1%; + top: 1%; + background-color: var(--color-primary); + border: 4px dashed rgba(0, 0, 0, 0.5); + border-radius: 4px; + color: #FFF; + opacity: .8; + z-index: 9; + box-sizing: border-box; + pointer-events: none; + animation: dzAnimIn 240ms ease-in-out; +} + +@keyframes dzAnimIn { + 0% { + opacity: 0; + transform: scale(.7); + } + 60% { + transform: scale(1.1); + } + 100% { + transform: scale(1); + opacity: .8; + } +} + +@keyframes dzFileItemIn { + 0% { + opacity: .5; + transform: translateY(28px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + +.dropzone-file-item { + width: 260px; + height: 80px; + position: relative; + display: flex; + margin: 1rem; + flex-direction: row; + background-color: #FFF; + box-shadow: $bs-large; + border-radius: 4px; + overflow: hidden; + padding-bottom: 3px; + animation: dzFileItemIn ease-in-out 240ms; +} +.dropzone-file-item .loading-container { + text-align: start !important; + margin: 0; +} +.dropzone-file-item-image-wrap { + width: 80px; + position: relative; + img { + object-fit: cover; + width: 100%; + height: 100%; + opacity: .8; + } +} +.dropzone-file-item-text-wrap { + flex: 1; + display: block; + padding: 1rem; + overflow: auto; +} +.dropzone-file-item-progress { + position: absolute; + bottom: 0; + left: 0; + font-size: 0; + height: 3px; + background-color: var(--color-primary); + transition: width ease-in-out 240ms; +} +.dropzone-file-item-label, +.dropzone-file-item-status { + align-items: center; + font-size: .9rem; + font-weight: 700; +} +.dropzone-file-item-status[data-status] { + display: flex; + font-size: .8rem; + font-weight: 500; + line-height: 1.2; +} +.dropzone-file-item-status[data-status="success"] { + color: $positive; +} +.dropzone-file-item-status[data-status="error"] { + color: $negative; +} +.dropzone-file-item-status[data-status] + .dropzone-file-item-label { + display: none; +} + .dropzone-container { position: relative; @include lightDark(background-color, #eee, #222); diff --git a/resources/views/form/dropzone.blade.php b/resources/views/form/dropzone.blade.php index 118761d4c..22378ff74 100644 --- a/resources/views/form/dropzone.blade.php +++ b/resources/views/form/dropzone.blade.php @@ -3,14 +3,6 @@ @placeholder - Placeholder text @successMessage --}} -
+
\ No newline at end of file diff --git a/resources/views/pages/parts/image-manager.blade.php b/resources/views/pages/parts/image-manager.blade.php index 5832c0954..d546eb787 100644 --- a/resources/views/pages/parts/image-manager.blade.php +++ b/resources/views/pages/parts/image-manager.blade.php @@ -12,7 +12,15 @@
-
+
@@ -50,11 +58,9 @@
-
+
@include('form.dropzone', [ 'placeholder' => trans('components.image_dropzone'), - 'successMessage' => trans('components.image_upload_success'), - 'url' => url('/images/gallery?' . http_build_query(['uploaded_to' => $uploaded_to ?? 0])) ])
From b21a9007c5df61e7b03b9099e37296bb403b44f6 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 25 Apr 2023 13:10:25 +0100 Subject: [PATCH 04/11] Dropzone: Developed ux further - Added image manager button for uploads. - Added image manager placeholder sidebar text for guidance. - Improved dropzone layer styling. - Removed old dropzone styles. - Got success events and auto-hide working. - Updated upload items to animate out. --- lang/en/components.php | 1 + resources/icons/upload.svg | 1 + resources/js/components/dropzone.js | 60 +++- resources/js/components/image-manager.js | 10 +- resources/sass/_components.scss | 329 ++---------------- resources/views/form/dropzone.blade.php | 2 +- .../views/pages/parts/image-manager.blade.php | 38 +- 7 files changed, 103 insertions(+), 338 deletions(-) create mode 100644 resources/icons/upload.svg diff --git a/lang/en/components.php b/lang/en/components.php index 07a3ba3a7..843bd7e56 100644 --- a/lang/en/components.php +++ b/lang/en/components.php @@ -6,6 +6,7 @@ return [ // Image Manager 'image_select' => 'Image Select', + 'image_upload' => 'Upload Image', 'image_all' => 'All', 'image_all_title' => 'View all images', 'image_book_title' => 'View images uploaded to this book', diff --git a/resources/icons/upload.svg b/resources/icons/upload.svg new file mode 100644 index 000000000..c9b0346a1 --- /dev/null +++ b/resources/icons/upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/js/components/dropzone.js b/resources/js/components/dropzone.js index b94d5d1f4..c6dc4df99 100644 --- a/resources/js/components/dropzone.js +++ b/resources/js/components/dropzone.js @@ -1,7 +1,7 @@ import {Component} from './component'; import {Clipboard} from '../services/clipboard'; import { - elem, getLoading, removeLoading, + elem, getLoading, onSelect, removeLoading, } from '../services/dom'; export class Dropzone extends Component { @@ -9,6 +9,8 @@ export class Dropzone extends Component { setup() { this.container = this.$el; this.statusArea = this.$refs.statusArea; + this.dropTarget = this.$refs.dropTarget; + this.selectButtons = this.$manyRefs.selectButton || []; this.url = this.$opts.url; this.successMessage = this.$opts.successMessage; @@ -18,17 +20,16 @@ export class Dropzone extends Component { this.timeoutMessage = this.$opts.timeoutMessage; // TODO - Use this.zoneText = this.$opts.zoneText; // window.uploadTimeout // TODO - Use - // TODO - Click-to-upload buttons/areas - // TODO - Drop zone highlighting of existing element - // (Add overlay via additional temp element). this.setupListeners(); } setupListeners() { + onSelect(this.selectButtons, this.manualSelectHandler.bind(this)); + let depth = 0; - this.container.addEventListener('dragenter', event => { + this.dropTarget.addEventListener('dragenter', event => { event.preventDefault(); depth += 1; @@ -37,7 +38,7 @@ export class Dropzone extends Component { } }); - this.container.addEventListener('dragover', event => { + this.dropTarget.addEventListener('dragover', event => { event.preventDefault(); }); @@ -46,18 +47,18 @@ export class Dropzone extends Component { depth = 0; }; - this.container.addEventListener('dragend', event => { + this.dropTarget.addEventListener('dragend', event => { reset(); }); - this.container.addEventListener('dragleave', event => { + this.dropTarget.addEventListener('dragleave', event => { depth -= 1; if (depth === 0) { reset(); } }); - this.container.addEventListener('drop', event => { + this.dropTarget.addEventListener('drop', event => { event.preventDefault(); reset(); const clipboard = new Clipboard(event.dataTransfer); @@ -68,16 +69,28 @@ export class Dropzone extends Component { }); } + manualSelectHandler() { + const input = elem('input', {type: 'file', style: 'left: -400px; visibility: hidden; position: fixed;'}); + this.container.append(input); + input.click(); + input.addEventListener('change', event => { + for (const file of input.files) { + this.createUploadFromFile(file); + } + input.remove(); + }); + } + showOverlay() { - const overlay = this.container.querySelector('.dropzone-overlay'); + const overlay = this.dropTarget.querySelector('.dropzone-overlay'); if (!overlay) { const zoneElem = elem('div', {class: 'dropzone-overlay'}, [this.zoneText]); - this.container.append(zoneElem); + this.dropTarget.append(zoneElem); } } hideOverlay() { - const overlay = this.container.querySelector('.dropzone-overlay'); + const overlay = this.dropTarget.querySelector('.dropzone-overlay'); if (overlay) { overlay.remove(); } @@ -88,14 +101,14 @@ export class Dropzone extends Component { * @return {Upload} */ createUploadFromFile(file) { - const {dom, status, progress} = this.createDomForFile(file); - this.container.append(dom); + const {dom, status, progress, dismiss} = this.createDomForFile(file); + this.statusArea.append(dom); + const component = this; const upload = { file, dom, updateProgress(percentComplete) { - console.log(`progress: ${percentComplete}%`); progress.textContent = `${percentComplete}%`; progress.style.width = `${percentComplete}%`; }, @@ -108,6 +121,10 @@ export class Dropzone extends Component { status.setAttribute('data-status', 'success'); status.textContent = message; removeLoading(dom); + setTimeout(dismiss, 2400); + component.$emit('upload-success', { + name: file.name, + }); }, }; @@ -150,7 +167,7 @@ export class Dropzone extends Component { /** * @param {File} file - * @return {{image: Element, dom: Element, progress: Element, label: Element, status: Element}} + * @return {{image: Element, dom: Element, progress: Element, status: Element, dismiss: function}} */ createDomForFile(file) { const image = elem('img', {src: ''}); @@ -172,8 +189,17 @@ export class Dropzone extends Component { image.src = URL.createObjectURL(file); } + const dismiss = () => { + dom.classList.add('dismiss'); + dom.addEventListener('animationend', event => { + dom.remove(); + }); + }; + + dom.addEventListener('click', dismiss); + return { - dom, progress, status, + dom, progress, status, dismiss, }; } diff --git a/resources/js/components/image-manager.js b/resources/js/components/image-manager.js index bb21bd5ec..fe01d4595 100644 --- a/resources/js/components/image-manager.js +++ b/resources/js/components/image-manager.js @@ -19,6 +19,7 @@ export class ImageManager extends Component { this.filterTabs = this.$manyRefs.filterTabs; this.selectButton = this.$refs.selectButton; this.formContainer = this.$refs.formContainer; + this.formContainerPlaceholder = this.$refs.formContainerPlaceholder; this.dropzoneContainer = this.$refs.dropzoneContainer; // Instance data @@ -92,8 +93,11 @@ export class ImageManager extends Component { } }); - this.formContainer.addEventListener('ajax-form-success', this.refreshGallery.bind(this)); - this.container.addEventListener('dropzone-success', this.refreshGallery.bind(this)); + this.formContainer.addEventListener('ajax-form-success', () => { + this.refreshGallery(); + this.resetEditForm(); + }); + this.container.addEventListener('dropzone-upload-success', this.refreshGallery.bind(this)); } show(callback, type = 'gallery') { @@ -168,6 +172,7 @@ export class ImageManager extends Component { resetEditForm() { this.formContainer.innerHTML = ''; + this.formContainerPlaceholder.removeAttribute('hidden'); } resetListView() { @@ -214,6 +219,7 @@ export class ImageManager extends Component { const params = requestDelete ? {delete: true} : {}; const {data: formHtml} = await window.$http.get(`/images/edit/${imageId}`, params); this.formContainer.innerHTML = formHtml; + this.formContainerPlaceholder.setAttribute('hidden', ''); window.$components.init(this.formContainer); } diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss index 0f66bd74a..e889e1515 100644 --- a/resources/sass/_components.scss +++ b/resources/sass/_components.scss @@ -208,18 +208,21 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { display: flex; justify-content: center; align-items: center; - font-size: 2rem; + font-size: 1.333rem; width: 98%; height: 98%; left: 1%; top: 1%; - background-color: var(--color-primary); - border: 4px dashed rgba(0, 0, 0, 0.5); border-radius: 4px; + border: 1px dashed var(--color-primary); + font-style: italic; + box-sizing: content-box; + background-clip: padding-box; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3E%3Cpath fill='%23a9a9a9' fill-opacity='0.52' d='M1 3h1v1H1V3zm2-2h1v1H3V1z'%3E%3C/path%3E%3C/svg%3E"); + background-color: var(--color-primary); color: #FFF; opacity: .8; z-index: 9; - box-sizing: border-box; pointer-events: none; animation: dzAnimIn 240ms ease-in-out; } @@ -248,6 +251,16 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { transform: translateY(0); } } +@keyframes dzFileItemOut { + 0% { + opacity: 1; + transform: translateY(0); + } + 100% { + opacity: .5; + transform: translateY(28px); + } +} .dropzone-file-item { width: 260px; @@ -262,6 +275,15 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { overflow: hidden; padding-bottom: 3px; animation: dzFileItemIn ease-in-out 240ms; + transition: transform ease-in-out 120ms, box-shadow ease-in-out 120ms; + cursor: pointer; + &:hover { + transform: translateY(-3px); + box-shadow: 0 3px 8px 1px rgba(22, 22, 22, 0.2); + } +} +.dropzone-file-item.dismiss { + animation: dzFileItemOut ease-in-out 240ms; } .dropzone-file-item .loading-container { text-align: start !important; @@ -314,12 +336,6 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { display: none; } -.dropzone-container { - position: relative; - @include lightDark(background-color, #eee, #222); - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3E%3Cpath fill='%23a9a9a9' fill-opacity='0.52' d='M1 3h1v1H1V3zm2-2h1v1H3V1z'%3E%3C/path%3E%3C/svg%3E"); -} - .image-manager-list .image { display: block; position: relative; @@ -407,10 +423,6 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { display: inline-block; } } - .dropzone-container { - border-bottom: 1px solid #DDD; - @include lightDark(border-color, #ddd, #000); - } } .image-manager-list { @@ -430,295 +442,6 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { } } -// Dropzone -/* - * The MIT License - * Copyright (c) 2012 Matias Meno - */ -.dz-message { - font-size: 1em; - line-height: 2.85; - font-style: italic; - color: #888; - text-align: center; - cursor: pointer; - padding: $-l $-m; - transition: all ease-in-out 120ms; -} - -.dz-drag-hover .dz-message { - background-color: rgb(16, 126, 210); - color: #EEE; -} - -@keyframes passing-through { - 0% { - opacity: 0; - transform: translateY(40px); - } - 30%, 70% { - opacity: 1; - transform: translateY(0px); - } - 100% { - opacity: 0; - transform: translateY(-40px); - } -} - -@keyframes slide-in { - 0% { - opacity: 0; - transform: translateY(40px); - } - 30% { - opacity: 1; - transform: translateY(0px); - } -} - -@keyframes pulse { - 0% { - transform: scale(1); - } - 10% { - transform: scale(1.1); - } - 20% { - transform: scale(1); - } -} - -.dropzone, .dropzone * { - box-sizing: border-box; -} - -.dz-preview { - position: relative; - display: inline-block; - vertical-align: top; - margin: 12px; - min-height: 80px; -} - -.dz-preview:hover { - z-index: 1000; -} - -.dz-preview:hover .dz-details { - opacity: 1; -} - -.dz-preview.dz-file-preview .dz-image { - border-radius: 4px; - background: #e9e9e9; -} - -.dz-preview.dz-file-preview .dz-details { - opacity: 1; -} - -.dz-preview.dz-image-preview { - background: white; -} - -.dz-preview.dz-image-preview .dz-details { - transition: opacity 0.2s linear; -} - -.dz-preview .dz-remove { - font-size: 13px; - text-align: center; - display: block; - cursor: pointer; - border: none; - margin-top: 3px; -} - -.dz-preview .dz-remove:hover { - text-decoration: underline; -} - -.dz-preview:hover .dz-details { - opacity: 1; -} - -.dz-preview .dz-details { - z-index: 20; - position: absolute; - top: 0; - left: 0; - opacity: 0; - font-size: 10px; - min-width: 100%; - max-width: 100%; - padding: 6px 3px; - text-align: center; - color: rgba(0, 0, 0, 0.9); - line-height: 150%; -} - -.dz-preview .dz-details .dz-size { - margin-bottom: 0.5em; - font-size: 12px; -} - -.dz-preview .dz-details .dz-filename { - white-space: nowrap; -} - -.dz-preview .dz-details .dz-filename:hover span { - border: 1px solid rgba(200, 200, 200, 0.8); - background-color: rgba(255, 255, 255, 0.8); -} - -.dz-preview .dz-details .dz-filename:not(:hover) { - overflow: hidden; - text-overflow: ellipsis; -} - -.dz-preview .dz-details .dz-filename:not(:hover) span { - border: 1px solid transparent; -} - -.dz-preview .dz-details .dz-filename span { - background-color: rgba(255, 255, 255, 0.4); - padding: 0 0.4em; - border-radius: 3px; -} - -.dz-preview:hover .dz-image img { - filter: blur(8px); -} - -.dz-preview .dz-image { - border-radius: 4px; - overflow: hidden; - width: 80px; - height: 80px; - position: relative; - display: block; - z-index: 10; -} - -.dz-preview .dz-image img { - display: block; -} - -.dz-preview.dz-success .dz-success-mark { - animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); -} - -.dz-preview.dz-error .dz-error-mark { - opacity: 1; - animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); -} - -.dz-preview .dz-success-mark, .dz-preview .dz-error-mark { - pointer-events: none; - opacity: 0; - z-index: 1001; - position: absolute; - display: block; - top: 50%; - left: 50%; - margin-inline-start: -27px; - margin-top: -35px; -} - -.dz-preview .dz-success-mark svg, .dz-preview .dz-error-mark svg { - display: block; - width: 54px; - height: 54px; -} - -.dz-preview.dz-processing .dz-progress { - opacity: 1; - transition: all 0.2s linear; -} - -.dz-preview.dz-complete .dz-progress { - opacity: 0; - transition: opacity 0.4s ease-in; -} - -.dz-preview:not(.dz-processing) .dz-progress { - animation: pulse 6s ease infinite; -} - -.dz-preview .dz-progress { - opacity: 1; - z-index: 1000; - pointer-events: none; - position: absolute; - height: 16px; - left: 50%; - top: 50%; - margin-top: -8px; - width: 80px; - margin-inline-start: -40px; - background: rgba(255, 255, 255, 0.9); - transform: scale(1); - border-radius: 8px; - overflow: hidden; -} - -.dz-preview .dz-progress .dz-upload { - background: #333; - background: linear-gradient(to bottom, #666, #444); - position: absolute; - top: 0; - left: 0; - bottom: 0; - width: 0; - transition: width 300ms ease-in-out; -} - -.dz-preview.dz-error .dz-error-message { - display: block; -} - -.dz-preview.dz-error { - .dz-image, .dz-details { - &:hover ~ .dz-error-message { - opacity: 1; - pointer-events: auto; - } - } -} - -.dz-preview .dz-error-message { - pointer-events: none; - z-index: 1000; - position: absolute; - display: block; - display: none; - opacity: 0; - transition: opacity 0.3s ease; - border-radius: 4px; - font-size: 12px; - line-height: 1.2; - top: 88px; - left: -12px; - width: 160px; - background: $negative; - padding: $-xs; - color: white; -} - -.dz-preview .dz-error-message:after { - content: ''; - position: absolute; - top: -6px; - left: 44px; - width: 0; - height: 0; - border-inline-start: 6px solid transparent; - border-inline-end: 6px solid transparent; - border-bottom: 6px solid $negative; -} - - .tab-container [role="tablist"] { display: flex; align-items: end; diff --git a/resources/views/form/dropzone.blade.php b/resources/views/form/dropzone.blade.php index 22378ff74..2eeec7505 100644 --- a/resources/views/form/dropzone.blade.php +++ b/resources/views/form/dropzone.blade.php @@ -3,6 +3,6 @@ @placeholder - Placeholder text @successMessage --}} -
+
\ No newline at end of file diff --git a/resources/views/pages/parts/image-manager.blade.php b/resources/views/pages/parts/image-manager.blade.php index d546eb787..022ea1f1e 100644 --- a/resources/views/pages/parts/image-manager.blade.php +++ b/resources/views/pages/parts/image-manager.blade.php @@ -1,4 +1,11 @@ -
@@ -9,18 +16,14 @@ -
+
@@ -58,13 +61,18 @@
-
- @include('form.dropzone', [ - 'placeholder' => trans('components.image_dropzone'), - ]) +
+
-
+
+

Here you can manage or select images that have been previously uploaded to the system.

+

Upload a new image by dragging an image file into this window, + or by using the "Upload Image" button above.

+
+ +
+
From 61d2ea6ac794fc0dd970503a19252c3dae48c377 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 25 Apr 2023 16:41:39 +0100 Subject: [PATCH 05/11] Dropzone: Polished image manager elements - Added file placeholder for non-image uploads. - Added use of upload limits. - Removed upload timeout variable. - Added pass-through and usage of filetypes. - Extracted some view text to language files and made use of existing text. --- lang/en/components.php | 4 +- lang/en/errors.php | 1 - resources/icons/file.svg | 5 +- resources/js/components/dropzone.js | 54 +++++++++++-------- resources/sass/_components.scss | 6 ++- .../views/pages/parts/image-manager.blade.php | 9 ++-- 6 files changed, 43 insertions(+), 36 deletions(-) diff --git a/lang/en/components.php b/lang/en/components.php index 843bd7e56..426a427d2 100644 --- a/lang/en/components.php +++ b/lang/en/components.php @@ -7,6 +7,8 @@ return [ // Image Manager 'image_select' => 'Image Select', 'image_upload' => 'Upload Image', + 'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.', + 'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.', 'image_all' => 'All', 'image_all_title' => 'View all images', 'image_book_title' => 'View images uploaded to this book', @@ -23,9 +25,9 @@ return [ 'images_deleted' => 'Images Deleted', 'image_preview' => 'Image Preview', 'image_upload_success' => 'Image uploaded successfully', + 'image_upload_failure' => 'Image failed to upload', 'image_update_success' => 'Image details successfully updated', 'image_delete_success' => 'Image successfully deleted', - 'image_upload_remove' => 'Remove', // Code Editor 'code_editor' => 'Edit Code', diff --git a/lang/en/errors.php b/lang/en/errors.php index 703d0edbe..eca39c946 100644 --- a/lang/en/errors.php +++ b/lang/en/errors.php @@ -45,7 +45,6 @@ return [ 'cannot_create_thumbs' => 'The server cannot create thumbnails. Please check you have the GD PHP extension installed.', 'server_upload_limit' => 'The server does not allow uploads of this size. Please try a smaller file size.', 'uploaded' => 'The server does not allow uploads of this size. Please try a smaller file size.', - 'file_upload_timeout' => 'The file upload has timed out.', // Drawing & Images 'image_upload_error' => 'An error occurred uploading the image', diff --git a/resources/icons/file.svg b/resources/icons/file.svg index b6acb0988..2e87f2ca6 100644 --- a/resources/icons/file.svg +++ b/resources/icons/file.svg @@ -1,4 +1 @@ - - - - \ No newline at end of file + \ No newline at end of file diff --git a/resources/js/components/dropzone.js b/resources/js/components/dropzone.js index c6dc4df99..56c576f29 100644 --- a/resources/js/components/dropzone.js +++ b/resources/js/components/dropzone.js @@ -14,21 +14,28 @@ export class Dropzone extends Component { this.url = this.$opts.url; this.successMessage = this.$opts.successMessage; - this.removeMessage = this.$opts.removeMessage; - this.uploadLimit = Number(this.$opts.uploadLimit); // TODO - Use - this.uploadLimitMessage = this.$opts.uploadLimitMessage; // TODO - Use - this.timeoutMessage = this.$opts.timeoutMessage; // TODO - Use + this.errorMessage = this.$opts.errorMessage; + this.uploadLimitMb = Number(this.$opts.uploadLimit); + this.uploadLimitMessage = this.$opts.uploadLimitMessage; this.zoneText = this.$opts.zoneText; - // window.uploadTimeout // TODO - Use + this.fileAcceptTypes = this.$opts.fileAccept; this.setupListeners(); } setupListeners() { onSelect(this.selectButtons, this.manualSelectHandler.bind(this)); + this.setupDropTargetHandlers(); + } + setupDropTargetHandlers() { let depth = 0; + const reset = () => { + this.hideOverlay(); + depth = 0; + }; + this.dropTarget.addEventListener('dragenter', event => { event.preventDefault(); depth += 1; @@ -42,22 +49,13 @@ export class Dropzone extends Component { event.preventDefault(); }); - const reset = () => { - this.hideOverlay(); - depth = 0; - }; - - this.dropTarget.addEventListener('dragend', event => { - reset(); - }); - - this.dropTarget.addEventListener('dragleave', event => { + this.dropTarget.addEventListener('dragend', reset); + this.dropTarget.addEventListener('dragleave', () => { depth -= 1; if (depth === 0) { reset(); } }); - this.dropTarget.addEventListener('drop', event => { event.preventDefault(); reset(); @@ -70,10 +68,10 @@ export class Dropzone extends Component { } manualSelectHandler() { - const input = elem('input', {type: 'file', style: 'left: -400px; visibility: hidden; position: fixed;'}); + const input = elem('input', {type: 'file', style: 'left: -400px; visibility: hidden; position: fixed;', accept: this.fileAcceptTypes}); this.container.append(input); input.click(); - input.addEventListener('change', event => { + input.addEventListener('change', () => { for (const file of input.files) { this.createUploadFromFile(file); } @@ -101,7 +99,9 @@ export class Dropzone extends Component { * @return {Upload} */ createUploadFromFile(file) { - const {dom, status, progress, dismiss} = this.createDomForFile(file); + const { + dom, status, progress, dismiss, + } = this.createDomForFile(file); this.statusArea.append(dom); const component = this; @@ -116,6 +116,7 @@ export class Dropzone extends Component { status.setAttribute('data-status', 'error'); status.textContent = message; removeLoading(dom); + this.updateProgress(100); }, markSuccess(message) { status.setAttribute('data-status', 'success'); @@ -128,6 +129,12 @@ export class Dropzone extends Component { }, }; + // Enforce early upload filesize limit + if (file.size > (this.uploadLimitMb * 1000000)) { + upload.markError(this.uploadLimitMessage); + return upload; + } + this.startXhrForUpload(upload); return upload; @@ -139,14 +146,15 @@ export class Dropzone extends Component { startXhrForUpload(upload) { const formData = new FormData(); formData.append('file', upload.file, upload.file.name); + const component = this; const req = window.$http.createXMLHttpRequest('POST', this.url, { error() { - upload.markError('Upload failed'); // TODO - Update text + upload.markError(component.errorMessage); }, readystatechange() { if (this.readyState === XMLHttpRequest.DONE && this.status === 200) { - upload.markSuccess('Finished upload!'); + upload.markSuccess(component.successMessage); } else if (this.readyState === XMLHttpRequest.DONE && this.status >= 400) { const content = this.responseText; const data = content.startsWith('{') ? JSON.parse(content) : {message: content}; @@ -170,7 +178,7 @@ export class Dropzone extends Component { * @return {{image: Element, dom: Element, progress: Element, status: Element, dismiss: function}} */ createDomForFile(file) { - const image = elem('img', {src: ''}); + const image = elem('img', {src: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9.224 7.373a.924.924 0 0 0-.92.925l-.006 7.404c0 .509.412.925.921.925h5.557a.928.928 0 0 0 .926-.925v-5.553l-2.777-2.776Zm3.239 3.239V8.067l2.545 2.545z' style='fill:%23000;fill-opacity:.75'/%3E%3C/svg%3E"}); const status = elem('div', {class: 'dropzone-file-item-status'}, []); const progress = elem('div', {class: 'dropzone-file-item-progress'}); const imageWrap = elem('div', {class: 'dropzone-file-item-image-wrap'}, [image]); @@ -191,7 +199,7 @@ export class Dropzone extends Component { const dismiss = () => { dom.classList.add('dismiss'); - dom.addEventListener('animationend', event => { + dom.addEventListener('animationend', () => { dom.remove(); }); }; diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss index e889e1515..1c417b2ad 100644 --- a/resources/sass/_components.scss +++ b/resources/sass/_components.scss @@ -145,6 +145,7 @@ box-shadow: none; color: #FFF; padding: $-xs $-m; + cursor: pointer; } .popup-header button:not(.popup-header-close) { @@ -292,6 +293,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { .dropzone-file-item-image-wrap { width: 80px; position: relative; + background-color: var(--color-primary-light); img { object-fit: cover; width: 100%; @@ -322,7 +324,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { } .dropzone-file-item-status[data-status] { display: flex; - font-size: .8rem; + font-size: .6rem; font-weight: 500; line-height: 1.2; } @@ -407,7 +409,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { min-height: auto; padding: $-m; } - img { + .image-manager-viewer img { max-width: 100%; max-height: 180px; display: block; diff --git a/resources/views/pages/parts/image-manager.blade.php b/resources/views/pages/parts/image-manager.blade.php index 022ea1f1e..aee292f38 100644 --- a/resources/views/pages/parts/image-manager.blade.php +++ b/resources/views/pages/parts/image-manager.blade.php @@ -1,11 +1,11 @@
@@ -66,9 +66,8 @@
-

Here you can manage or select images that have been previously uploaded to the system.

-

Upload a new image by dragging an image file into this window, - or by using the "Upload Image" button above.

+

{{ trans('components.image_intro') }}

+

{{ trans('components.image_intro_upload') }}

From 722c38d576ecd919ce527710172bb5281c7f3d1c Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 26 Apr 2023 14:23:28 +0100 Subject: [PATCH 06/11] Image manager: fix upload control for drawing, updated styles - Tightened image manager styles to address things that looked akward. - Prevented visiblity/use of upload controls for drawings. - Updated dropzone to use error handling from validation messages. --- resources/js/components/dropzone.js | 19 ++++++++++-- resources/js/components/image-manager.js | 29 ++++++++++--------- resources/sass/_components.scss | 19 ++++++++---- resources/sass/styles.scss | 17 +++++++---- .../pages/parts/image-manager-list.blade.php | 4 ++- .../views/pages/parts/image-manager.blade.php | 6 ++-- 6 files changed, 64 insertions(+), 30 deletions(-) diff --git a/resources/js/components/dropzone.js b/resources/js/components/dropzone.js index 56c576f29..1fdf824ae 100644 --- a/resources/js/components/dropzone.js +++ b/resources/js/components/dropzone.js @@ -12,6 +12,8 @@ export class Dropzone extends Component { this.dropTarget = this.$refs.dropTarget; this.selectButtons = this.$manyRefs.selectButton || []; + this.isActive = true; + this.url = this.$opts.url; this.successMessage = this.$opts.successMessage; this.errorMessage = this.$opts.errorMessage; @@ -23,6 +25,14 @@ export class Dropzone extends Component { this.setupListeners(); } + /** + * Public method to allow external disabling/enabling of this drag+drop dropzone. + * @param {Boolean} active + */ + toggleActive(active) { + this.isActive = active; + } + setupListeners() { onSelect(this.selectButtons, this.manualSelectHandler.bind(this)); this.setupDropTargetHandlers(); @@ -40,7 +50,7 @@ export class Dropzone extends Component { event.preventDefault(); depth += 1; - if (depth === 1) { + if (depth === 1 && this.isActive) { this.showOverlay(); } }); @@ -59,6 +69,11 @@ export class Dropzone extends Component { this.dropTarget.addEventListener('drop', event => { event.preventDefault(); reset(); + + if (!this.isActive) { + return; + } + const clipboard = new Clipboard(event.dataTransfer); const files = clipboard.getFiles(); for (const file of files) { @@ -158,7 +173,7 @@ export class Dropzone extends Component { } else if (this.readyState === XMLHttpRequest.DONE && this.status >= 400) { const content = this.responseText; const data = content.startsWith('{') ? JSON.parse(content) : {message: content}; - const message = data?.message || content; + const message = data?.message || data?.error || content; upload.markError(message); } }, diff --git a/resources/js/components/image-manager.js b/resources/js/components/image-manager.js index fe01d4595..b81782364 100644 --- a/resources/js/components/image-manager.js +++ b/resources/js/components/image-manager.js @@ -18,6 +18,8 @@ export class ImageManager extends Component { this.listContainer = this.$refs.listContainer; this.filterTabs = this.$manyRefs.filterTabs; this.selectButton = this.$refs.selectButton; + this.uploadButton = this.$refs.uploadButton; + this.uploadHint = this.$refs.uploadHint; this.formContainer = this.$refs.formContainer; this.formContainerPlaceholder = this.$refs.formContainerPlaceholder; this.dropzoneContainer = this.$refs.dropzoneContainer; @@ -35,11 +37,6 @@ export class ImageManager extends Component { this.resetState(); this.setupListeners(); - - window.setTimeout(() => { - this.show(() => { - }, 'gallery'); - }, 500); } setupListeners() { @@ -60,18 +57,14 @@ export class ImageManager extends Component { this.resetListView(); this.resetSearchView(); this.loadGallery(); - this.cancelSearch.classList.remove('active'); }); - this.searchInput.addEventListener('input', () => { - this.cancelSearch.classList.toggle('active', this.searchInput.value.trim()); - }); - - onChildEvent(this.listContainer, '.load-more', 'click', async event => { - showLoading(event.target); + onChildEvent(this.listContainer, '.load-more button', 'click', async event => { + const wrapper = event.target.closest('.load-more'); + showLoading(wrapper); this.page += 1; await this.loadGallery(); - event.target.remove(); + wrapper.remove(); }); this.listContainer.addEventListener('event-emit-select-image', this.onImageSelectEvent.bind(this)); @@ -106,7 +99,15 @@ export class ImageManager extends Component { this.callback = callback; this.type = type; this.getPopup().show(); - this.dropzoneContainer.classList.toggle('hidden', type !== 'gallery'); + + const hideUploads = type !== 'gallery'; + this.dropzoneContainer.classList.toggle('hidden', hideUploads); + this.uploadButton.classList.toggle('hidden', hideUploads); + this.uploadHint.classList.toggle('hidden', hideUploads); + + /** @var {Dropzone} * */ + const dropzone = window.$components.firstOnElement(this.container, 'dropzone'); + dropzone.toggleActive(!hideUploads); if (!this.hasData) { this.loadGallery(); diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss index 1c417b2ad..d1a11a964 100644 --- a/resources/sass/_components.scss +++ b/resources/sass/_components.scss @@ -386,13 +386,11 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { .image-manager .load-more { display: block; text-align: center; - @include lightDark(background-color, #EEE, #444); padding: $-s $-m; - color: #AAA; clear: both; - font-size: 20px; - cursor: pointer; - font-style: italic; + .loading-container { + margin: 0; + } } .image-manager .loading-container { @@ -444,6 +442,17 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { } } +.image-manager [role="tablist"] button[role="tab"] { + border-right: 1px solid #DDD; + &:last-child { + border-right: none; + } +} + +.image-manager-header { + z-index: 4; +} + .tab-container [role="tablist"] { display: flex; align-items: end; diff --git a/resources/sass/styles.scss b/resources/sass/styles.scss index 0f4ec7041..2ed680646 100644 --- a/resources/sass/styles.scss +++ b/resources/sass/styles.scss @@ -120,29 +120,36 @@ $loadingSize: 10px; .contained-search-box { display: flex; height: 38px; + z-index: -1; input, button { + height: 100%; border-radius: 0; border: 1px solid #ddd; @include lightDark(border-color, #ddd, #000); margin-inline-start: -1px; + &:last-child { + border-inline-end: 0; + } } input { flex: 5; padding: $-xs $-s; &:focus, &:active { - outline: 0; + outline: 1px dotted var(--color-primary); + outline-offset: -2px; + border: 1px solid #ddd; + @include lightDark(border-color, #ddd, #000); } } button { width: 60px; } + button.primary-background { + border-color: var(--color-primary); + } button i { padding: 0; } - button.cancel.active { - background-color: $negative; - color: #EEE; - } svg { margin: 0; } diff --git a/resources/views/pages/parts/image-manager-list.blade.php b/resources/views/pages/parts/image-manager-list.blade.php index 22fe7addd..ccf79fb6d 100644 --- a/resources/views/pages/parts/image-manager-list.blade.php +++ b/resources/views/pages/parts/image-manager-list.blade.php @@ -19,5 +19,7 @@
@endforeach @if($hasMore) -
{{ trans('components.image_load_more') }}
+
+ +
@endif \ No newline at end of file diff --git a/resources/views/pages/parts/image-manager.blade.php b/resources/views/pages/parts/image-manager.blade.php index aee292f38..cab5daa64 100644 --- a/resources/views/pages/parts/image-manager.blade.php +++ b/resources/views/pages/parts/image-manager.blade.php @@ -16,7 +16,7 @@