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.

+
+ +
+