Skip to content

Commit d1e2a24

Browse files
committed
feat: create dropzones for avatar and editor modes, handle drag and drop
1 parent 52653be commit d1e2a24

File tree

3 files changed

+122
-72
lines changed

3 files changed

+122
-72
lines changed

src/components/images/ImageUploader.vue

Lines changed: 109 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,66 @@
11
<template>
2-
<label v-if="purpose === 'avatar'" for="fileInput">Choose Image File</label>
3-
<input type="file" name="fileInput" id="fileInput" @change="uploadFile" ref="fileInput" :multiple="multiple" @drop="uploadFile" @dragenter.prevent @dragover.prevent><br>
4-
<progress v-if="!purpose" ref="progressBar" style="width: 100%" :value="imagesProgress" max="100"></progress>
2+
<div class="input-container" @dragend.prevent.self="hover=false;" @dragexit.prevent.self="hover=false" @dragleave.prevent.self="hover=false">
3+
<div>
4+
<label v-if="purpose !== 'editor'" for="fileInput" class="input-label" :class="{ 'hidden': hover }">Choose Image File or Drag and Drop</label>
5+
<input type="file" name="fileInput" id="fileInput" @change="uploadFile" ref="fileInput" :multiple="multiple" @dragenter.prevent.self.stop="hover=true" @dragover.prevent.self.stop="hover=true" :disabled="hover" :class="{ 'hidden': hover }" :hidden="purpose === 'editor' ? 'hidden' : null">
6+
<div class="progress-container" :class="{ 'hidden': hover }">
7+
<div class="progress" :class="{ 'progress-editor': purpose === 'editor'}">
8+
<span class="meter" :style="{ width: imagesProgress + '%' }"></span>
9+
</div>
10+
</div>
11+
<div v-if="purpose !== 'editor'" class="input-info">Images should not exceed 100kB</div>
12+
</div>
13+
<div :class="{'visible': hover, 'hidden': !hover, 'editorMode': showDropzone }" id="dropzone" @drop.stop.prevent="uploadFile" @dragenter.prevent.self="hover=true" @dragover.prevent.self="hover=true" @dragend.prevent.self="hover=false" @dragexit.prevent.self="hover=false" @dragleave.prevent.self="hover=false">
14+
Drop Image{{ purpose === 'editor' ? 's' : ''}} Here
15+
</div>
16+
</div>
17+
18+
<div v-if="purpose === 'editor'" class="upload-editor">
19+
<a href="#" data-balloon="Upload Images" @click.prevent="fileInput.click()"><i class="far fa-images" aria-hidden="true"></i></a>
20+
<span v-if="images.length > 0 && purpose === 'editor'">
21+
(<a href="#" @click.prevent="openImageModal()">
22+
<span v-if="images.length === 1">
23+
view <span v-html="images.length"></span> image
24+
</span>
25+
<span v-if="images.length > 1">
26+
view <span v-html="images.length"></span> images
27+
</span>
28+
</a>)
29+
</span>
30+
</div>
31+
32+
533
</template>
634

735
<script>
8-
import { reactive, toRefs } from 'vue'
36+
import { reactive, toRefs, watch } from 'vue'
937
import { policy, upload } from '@/composables/services/image-upload'
1038
1139
Promise.each = async (arr, fn) => { for(const item of arr) await fn(item) }
1240
1341
export default {
1442
name: 'image-uploader',
15-
props: ['onUpload-success', 'onUpload-error', 'purpose'],
43+
props: ['onUpload-success', 'onUpload-error', 'onHover-stop', 'purpose', 'showDropzone'],
1644
setup(props, { emit }) { //, { emit }) {
1745
/* View Methods */
1846
const uploadFile = (e) => {
47+
v.hover = false
1948
emit('upload-error', null) // clear previous errors
20-
2149
let files = e.target.files || e.dataTransfer.files
50+
v.fileInput.files = files
2251
if (!files.length) return
2352
let images = []
2453
for (let i = 0; i < files.length; i++) {
2554
let file = files[i]
2655
if (!file.type.match(/image.*/)) continue
2756
images.push(file)
2857
}
29-
if (props.purpose === 'avatar' || props.purpose === 'logo' || props.purpose === 'favicon') { images = [images[0]] }
58+
if (props.purpose === 'avatar' || props.purpose === 'logo' || props.purpose === 'favicon') {
59+
images = [images[0]]
60+
const dataTransfer = new DataTransfer()
61+
dataTransfer.items.add(images[0])
62+
v.fileInput.files = dataTransfer.files
63+
}
3064
3165
if (images.length > 10) return handleError('Error: Exceeded 10 images.')
3266
else if (images.length > 0) {
@@ -68,9 +102,9 @@ export default {
68102
v.uploadingImages = v.currentImages.length
69103
return policy(v.currentImages)
70104
// upload each image
71-
.then(images => {
105+
.then(imagesToUpload => {
72106
let index = 0
73-
return Promise.each(images, image => {
107+
return Promise.each(imagesToUpload, image => {
74108
v.currentImages[index].status = 'Starting'
75109
return Promise.resolve(upload(image)
76110
.progress(p => updateImagesUploading(index, p))
@@ -84,7 +118,6 @@ export default {
84118
})
85119
.success(url => {
86120
updateImagesUploading(index, 100, url)
87-
// TODO(akinsey): if ($scope.onDone) { $scope.onDone({data: url}); }
88121
if (props.purpose === 'avatar' || props.purpose === 'logo' || props.purpose === 'favicon') {
89122
v.model = url
90123
@@ -106,11 +139,17 @@ export default {
106139
.then(() => { if (errImages.length) handleError(v.warningMsg) })
107140
})
108141
.catch(() => handleError(v.warningMsg))
142+
.finally(() => {
143+
v.currentImages = []
144+
if (props.purpose === 'editor') setTimeout(() => v.imagesProgress = 0, 500)
145+
})
109146
}
110147
}
111148
112149
const handleError = msg => {
113150
v.currentImages = []
151+
v.image = []
152+
v.imagesProgress = 0
114153
emit('upload-error', msg)
115154
}
116155
@@ -147,61 +186,15 @@ export default {
147186
if (v.uploadingImages <= 0) v.imagesUploading = false
148187
}
149188
150-
// const progressHandler = e => {
151-
// v.amountUploaded.innerHTML = 'Uploaded ' + e.loaded + ' bytes of ' + e.total
152-
// var percent = (e.loaded / e.total) * 100
153-
// v.progressBar.value = Math.round(percent)
154-
// v.status.innerHTML = Math.round(percent) + '% uploaded... please wait'
155-
// }
156-
157-
// const completeHandler = e => {
158-
// v.status.innerHTML = e.target.responseText
159-
// emit('upload-success')
160-
// v.progressBar.value = 0
161-
// }
162-
163-
// const errorHandler = e => {
164-
// v.status.innerHTML = 'Upload Failed'
165-
// v.status.innerHTML = e.target.responseText
166-
// emit('upload-error')
167-
// }
168-
169-
// const abortHandler = () => {
170-
// v.status.innerHTML = 'Upload Aborted'
171-
// emit('upload-error')
172-
// }
173-
174-
// var cancelEvent = function(e) {
175-
// e.stopPropagation();
176-
// e.preventDefault();
177-
// };
178-
179-
// var removeDrag = function(e) {
180-
// e.stopPropagation();
181-
// e.preventDefault();
182-
// };
183-
184-
// var dropEvent = function(e) {
185-
// removeDrag(e);
186-
// uploadFile(e)
187-
// };
188-
189-
// $parent.on('dragenter', cancelEvent);
190-
// $parent.on('dragover', cancelEvent);
191-
// $dragZone.on('dragenter', cancelEvent);
192-
// $dragZone.on('dragover', cancelEvent);
193-
// $dragZone.on('dragend', removeDrag);
194-
// $dragZone.on('dragexit', removeDrag);
195-
// $dragZone.on('dragleave', removeDrag);
196-
197-
// $parent.on('drop', dropEvent);
198-
// $dragZone.on('drop', dropEvent);
189+
const openImageModal = () => console.log('open image modal')
199190
200191
const v = reactive({
192+
hover: false,
201193
fileInput: null,
202-
multiple: !props.purpose,
194+
multiple: props.purpose === 'editor',
203195
progressBar: null,
204196
amountUploaded: null,
197+
showDropzone: props.showDropzone,
205198
currentImages: [],
206199
images: [],
207200
imagesUploading: false,
@@ -214,13 +207,67 @@ export default {
214207
status: null
215208
})
216209
217-
return { ...toRefs(v), uploadFile }
210+
watch(() => v.hover, a => a ? null : emit('hover-stop'))
211+
212+
return { ...toRefs(v), uploadFile, openImageModal }
218213
}
219214
}
220215
</script>
221216

222217
<style lang="scss">
218+
.input-container { position: relative; height: 4.625rem; }
219+
.input-section label.hidden { opacity: .1; }
220+
223221
#fileInput {
224-
height: auto;
222+
height: 4.625rem;
223+
&.hidden {
224+
opacity: .1;
225+
border: 1px solid transparent;
226+
}
225227
}
228+
#dropzone {
229+
position: absolute;
230+
float: left;
231+
border: 3px dashed $color-primary;
232+
border-radius: 3px;
233+
box-shadow: none;
234+
margin-bottom: 0;
235+
padding-top: 2rem;
236+
padding-bottom: 1rem;
237+
height: 4.625rem;
238+
text-align: center;
239+
font-weight: bold;
240+
font-size: 1.5rem;
241+
line-height: .5rem;
242+
color: $base-background-color;
243+
width: 100%;
244+
&.visible {
245+
display: block;
246+
z-index: 99999;
247+
background-color: $color-primary-transparent;
248+
margin-top: -6.5625rem;
249+
}
250+
&.hidden { display: none; }
251+
&.editorMode {
252+
display: block;
253+
height: 150vh;
254+
bottom: 0;
255+
z-index: 99999;
256+
border: 0;
257+
background-color: transparent;
258+
}
259+
}
260+
261+
.progress-container {
262+
text-align: center;
263+
margin-bottom: 0.625rem;
264+
margin-top: -.3125rem;
265+
&.hidden { opacity: .1; }
266+
.progress {
267+
height: .25rem;
268+
border-radius: 3px;
269+
.meter { background: $color-primary; height: 100%; display: block; border-radius: 3px; }
270+
}
271+
}
272+
226273
</style>

src/components/layout/Editor.vue

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
22
<div class="messages-wrap">
3-
<div :class="editorPosition" v-if="showEditor">
3+
<div :class="fullscreen ? 'editor-full-screen' : 'editor-fixed-right'" v-if="showEditor">
44
<!-- Editor Container-->
55
<div class="editor-container" :class="{ 'new-message' : editorConvoMode, 'new-thread' : threadEditorMode, 'add-poll' : threadCopy?.addPoll }">
66
<!-- Draft Alert -->
@@ -155,10 +155,10 @@
155155
<a :class="{'selected': preview }" tabindex="-1" @click="preview = true">Preview</a>
156156
</div>
157157

158-
<div class="editor-body">
158+
<div class="editor-body" @dragenter.prevent="showDropzone = true" @dragover.prevent="showDropzone = true">
159159
<div class="editor-column-input" :class="{ 'hidden': preview }">
160160
<textarea class="editor-input" :class="{ 'rtl': rtl }" placeholder="Enter your reply here. (BTW, you can drag and drop images directly into the editor panel)" :maxlength="postMaxLength || 10000"></textarea>
161-
<div class="editor-drag-container">
161+
<div class="editor-drag-container" :class="{ 'visible': showDropzone}">
162162
<div class="editor-drag">
163163
<div class="editor-drag-text">Drag and Drop Images</div>
164164
</div>
@@ -170,17 +170,18 @@
170170
</div>
171171
</div>
172172

173-
<!-- <image-uploader class="editor-image-uploader" purpose="editor" reset="resetImages" on-done="insertImageUrl(data)"></image-uploader> -->
174173
<div class="editor-footer">
175-
<span>&nbsp;</span>
174+
<div class="editor-image-uploader">
175+
<image-uploader purpose="editor" @upload-success="() => {}" @upload-error="() => {}" :show-dropzone="showDropzone" @hover-stop="showDropzone = false" />
176+
</div>
176177
</div>
177178
<!-- END EDITOR -->
178179
</form>
179180

180181
<div class="editor-tools">
181182
<div class="tools">
182183
<a data-balloon="Formatting Help" @click="showFormatting = true"><i class="fa fa-code"></i></a>
183-
<a :data-balloon="isMinimized ? 'Expand Editor' : 'Minimize Editor'" @click="fullscreen()"><i class="fa expand" :class="{ 'fa-expand': isMinimized, 'fa-compress': !isMinimized }"></i></a>
184+
<a :data-balloon="isMinimized ? 'Expand Editor' : 'Minimize Editor'" @click="fullscreen = !fullscreen"><i class="fa expand" :class="{ 'fa-expand': isMinimized, 'fa-compress': !isMinimized }"></i></a>
184185
</div>
185186
</div>
186187

@@ -230,18 +231,19 @@
230231
<script>
231232
import { reactive, toRefs } from 'vue'
232233
// import { useRoute, useRouter } from 'vue-router'
234+
import ImageUploader from '@/components/images/ImageUploader.vue'
233235
234236
export default {
235237
props: ['editorConvoMode', 'threadEditorMode', 'postEditorMode', 'createAction', 'updateAction', 'showEditor', 'thread' ],
236238
emits: ['close'],
239+
components: { ImageUploader },
237240
setup(props, { emit }) {
238241
239242
const canCreate = () => true
240243
const canLock = () => true
241244
const canSticky = () => true
242245
const canModerate = () => true
243246
const canCreatePoll = () => true
244-
const fullscreen = () => console.log('fullscreen')
245247
const cancel = () => emit('close')
246248
const closeEditor = () => emit('close')
247249
/* Internal Data */
@@ -252,10 +254,11 @@ export default {
252254
const v = reactive({
253255
isMinimized: true,
254256
threadCopy: props.thread,
255-
editorPosition: 'editor-fixed-right',
257+
fullscreen: false,
256258
showFormatting: false,
257259
preview: false,
258260
showEditor: props.showEditor,
261+
showDropzone: false,
259262
draftStatus: null,
260263
receivers: [],
261264
postMaxLength: window.post_max_length,
@@ -265,7 +268,7 @@ export default {
265268
rtl: false
266269
})
267270
268-
return { ...toRefs(v), canLock, canCreate, canSticky, canModerate, canCreatePoll, fullscreen, cancel, closeEditor }
271+
return { ...toRefs(v), canLock, canCreate, canSticky, canModerate, canCreatePoll, cancel, closeEditor }
269272
}
270273
}
271274
</script>

src/views/Posts.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@
293293
<div class="sidebar-block">
294294
<div v-if="canPost()" class="sidebar-actions">
295295
<a class="button" @click.prevent="loadEditor()" v-if="canPost()">Post Reply</a>
296-
296+
<br />
297297
<!-- Post Tools -->
298298
<div class="post-tools">
299299
<!-- Watch Thread -->

0 commit comments

Comments
 (0)