]> BookStack Code Mirror - bookstack/commitdiff
Dropzone: started on design/ui of uploading
authorDan Brown <redacted>
Mon, 24 Apr 2023 22:24:58 +0000 (23:24 +0100)
committerDan Brown <redacted>
Mon, 24 Apr 2023 22:24:58 +0000 (23:24 +0100)
- 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
resources/js/components/dropzone.js
resources/js/services/dom.js
resources/sass/_components.scss
resources/views/form/dropzone.blade.php
resources/views/pages/parts/image-manager.blade.php

index 48a0a32faa38c4821a9d71dda9a5fb4f97d35232..07a3ba3a70aad2673d24d2cf9a38c937b3b0075d 100644 (file)
@@ -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',
index 87c6b4d4fe8ce98f17c46994a90750c32679749b..b94d5d1f40a077749350a7a6e7d1a6bac643e399 100644 (file)
@@ -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');
-
-        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 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]);
+
+        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,
         };
     }
 
index 17f5a803aad7a03e80f8b2a45936eba1205a1aaa..78685574850643946fa8b305e479d3f3bc69b9df 100644 (file)
@@ -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<String, String>} 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 = '<div class="loading-container"><div></div><div></div><div></div></div>';
 }
 
+/**
+ * Get a loading element indicator element.
+ * @returns {Element}
+ */
+export function getLoading() {
+    const wrap = document.createElement('div');
+    wrap.classList.add('loading-container');
+    wrap.innerHTML = '<div></div><div></div><div></div>';
+    return wrap;
+}
+
 /**
  * Remove any loading indicators within the given element.
  * @param {Element} element
index 4e6a8d731f7aa2058456f973e974c434b6d4e9ad..0f66bd74ab6920a5041b40b9472794703622c0f7 100644 (file)
   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);
index 118761d4c1181b4cd38e9eebb89472a76c8344b8..22378ff74c741f30097dc322d9dd20003c70c264 100644 (file)
@@ -3,14 +3,6 @@
 @placeholder - Placeholder text
 @successMessage
 --}}
-<div component="dropzone"
-     option:dropzone:url="{{ $url }}"
-     option:dropzone:success-message="{{ $successMessage ?? '' }}"
-     option:dropzone:remove-message="{{ trans('components.image_upload_remove') }}"
-     option:dropzone:upload-limit="{{ config('app.upload_limit') }}"
-     option:dropzone:upload-limit-message="{{ trans('errors.server_upload_limit') }}"
-     option:dropzone:timeout-message="{{ trans('errors.file_upload_timeout') }}"
-
-     class="dropzone-container text-center">
+<div class="dropzone-container text-center">
     <button type="button" class="dz-message">{{ $placeholder }}</button>
 </div>
\ No newline at end of file
index 5832c0954fc9374a79fc99e81cbd4f400ac24e78..d546eb787e41088943e2b560a821f96e96c35bb2 100644 (file)
                 <button refs="popup@hide" type="button" class="popup-header-close">@icon('close')</button>
             </div>
 
-            <div class="flex-fill image-manager-body">
+            <div component="dropzone"
+                 option:dropzone:url="{{ url('/images/gallery?' . http_build_query(['uploaded_to' => $uploaded_to ?? 0])) }}"
+                 option:dropzone:success-message="{{ trans('components.image_upload_success') }}"
+                 option:dropzone:remove-message="{{ trans('components.image_upload_remove') }}"
+                 option:dropzone:upload-limit="{{ config('app.upload_limit') }}"
+                 option:dropzone:upload-limit-message="{{ trans('errors.server_upload_limit') }}"
+                 option:dropzone:timeout-message="{{ trans('errors.file_upload_timeout') }}"
+                 option:dropzone:zone-text="{{ trans('components.image_dropzone_drop') }}"
+                 class="flex-fill image-manager-body">
 
                 <div class="image-manager-content">
                     <div role="tablist" class="image-manager-header primary-background-light grid third no-gap">
 
                 <div class="image-manager-sidebar flex-container-column">
 
-                    <div refs="image-manager@dropzoneContainer">
+                    <div refs="image-manager@dropzoneContainer dropzone@statusArea">
                         @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]))
                         ])
                     </div>