Skip to content

Commit b992f37

Browse files
author
Jicheng Lu
committed
add dropzone and gallery
1 parent 014128e commit b992f37

File tree

11 files changed

+341
-68
lines changed

11 files changed

+341
-68
lines changed

package-lock.json

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/common/FileDropZone.svelte

Lines changed: 100 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
<script>
2+
// @ts-nocheck
23
import { fromEvent } from "file-selector";
34
import {
45
allFilesAccepted,
56
composeEventHandlers,
67
fileAccepted,
78
fileMatchSize,
9+
getBase64,
810
isEvtWithFiles,
911
isIeOrEdge,
1012
isPropagationStopped,
@@ -32,7 +34,31 @@
3234
/** @type {any} */
3335
export let inputElement = undefined;
3436
export let required = false;
35-
export let dropText = "Drag 'n' drop some files here, or click to select files";
37+
export let dropText = "Drag and drop some files here, or click to select files";
38+
39+
let innerDropText = dropText;
40+
let isLoading = false;
41+
let isError = false;
42+
let isSuccess = false;
43+
let reason = '';
44+
45+
$: {
46+
if (isLoading) {
47+
innerDropText = "Uploading...";
48+
disabled = true;
49+
} else if (isSuccess) {
50+
innerDropText = "Upload succeeded";
51+
disabled = true;
52+
} else if (isError) {
53+
innerDropText = reason;
54+
disabled = true;
55+
} else {
56+
innerDropText = dropText;
57+
disabled = false;
58+
}
59+
}
60+
61+
const duration = 3000;
3662
const dispatch = createEventDispatcher();
3763
3864
//state
@@ -110,7 +136,6 @@
110136
111137
/** @param {any} event */
112138
function onDragEnterCb(event) {
113-
console.log('inside drag enter: ', event);
114139
event.preventDefault();
115140
stopPropagation(event);
116141
@@ -134,7 +159,6 @@
134159
135160
/** @param {any} event */
136161
function onDragOverCb(event) {
137-
console.log('inside drag over: ', event);
138162
event.preventDefault();
139163
stopPropagation(event);
140164
@@ -155,7 +179,6 @@
155179
156180
/** @param {any} event */
157181
function onDragLeaveCb(event) {
158-
console.log('inside drag leave: ', event);
159182
event.preventDefault();
160183
stopPropagation(event);
161184
@@ -203,52 +226,82 @@
203226
/** @type {any[]} */
204227
const fileRejections = [];
205228
229+
/** @type {Promise<any>[]} */
230+
const promises = [];
231+
isLoading = true;
206232
files.forEach(file => {
207-
const [accepted, acceptError] = fileAccepted(file, accept);
208-
const [sizeMatch, sizeError] = fileMatchSize(file, minSize, maxSize);
209-
if (accepted && sizeMatch) {
210-
acceptedFiles.push(file);
211-
} else {
212-
const errors = [acceptError, sizeError].filter(e => e);
213-
fileRejections.push({ file, errors });
214-
}
233+
promises.push(new Promise((resolve, reject) => {
234+
const [accepted, acceptError] = fileAccepted(file, accept);
235+
const [sizeMatch, sizeError] = fileMatchSize(file, minSize, maxSize);
236+
getBase64(file).then(res => {
237+
if (accepted && sizeMatch) {
238+
acceptedFiles.push({
239+
file_name: file.name,
240+
file_path: file.path,
241+
url: res
242+
});
243+
} else {
244+
const errors = [acceptError, sizeError].filter(Boolean);
245+
fileRejections.push({ file, errors });
246+
}
247+
resolve('done');
248+
});
249+
}));
215250
});
216251
217-
if (!multiple && acceptedFiles.length > 1) {
218-
// Reject everything and empty accepted files
219-
acceptedFiles.forEach(file => {
220-
fileRejections.push({ file, errors: [TOO_MANY_FILES_REJECTION] });
221-
});
222-
acceptedFiles.splice(0);
223-
}
252+
Promise.all(promises).then(() => {
253+
isLoading = false;
224254
225-
// Files dropped keep input in sync
226-
if (event.dataTransfer) {
227-
inputElement.files = event.dataTransfer.files;
228-
}
255+
if (!multiple && acceptedFiles.length > 1) {
256+
// Reject everything and empty accepted files
257+
acceptedFiles.forEach(file => {
258+
fileRejections.push({ file, errors: [TOO_MANY_FILES_REJECTION] });
259+
});
260+
acceptedFiles.splice(0);
261+
}
229262
230-
state.acceptedFiles = acceptedFiles;
231-
state.fileRejections = fileRejections;
263+
// Files dropped keep input in sync
264+
if (event.dataTransfer) {
265+
inputElement.files = event.dataTransfer.files;
266+
}
232267
233-
dispatch("drop", {
234-
acceptedFiles,
235-
fileRejections,
236-
event
237-
});
268+
state.acceptedFiles = acceptedFiles;
269+
state.fileRejections = fileRejections;
238270
239-
if (fileRejections.length > 0) {
240-
dispatch("droprejected", {
271+
dispatch("drop", {
272+
acceptedFiles,
241273
fileRejections,
242274
event
243275
});
244-
}
245276
246-
if (acceptedFiles.length > 0) {
247-
dispatch("dropaccepted", {
248-
acceptedFiles,
249-
event
250-
});
251-
}
277+
if (fileRejections.length > 0) {
278+
dispatch("droprejected", {
279+
fileRejections,
280+
event
281+
});
282+
283+
isError = true;
284+
reason = fileRejections[0]?.errors[0]?.message || "Upload failed";
285+
setTimeout(() => {
286+
reason = '';
287+
isError = false;
288+
}, duration);
289+
}
290+
291+
if (acceptedFiles.length > 0) {
292+
dispatch("dropaccepted", {
293+
acceptedFiles,
294+
event
295+
});
296+
}
297+
298+
if (acceptedFiles.length > 0 && fileRejections.length === 0) {
299+
isSuccess = true;
300+
setTimeout(() => {
301+
isSuccess = false;
302+
}, duration);
303+
}
304+
});
252305
});
253306
}
254307
resetState();
@@ -324,8 +377,8 @@
324377
bind:this={rootRef}
325378
tabindex="0"
326379
role="button"
327-
class="{disableDefaultStyles ? '' : 'dropzone'} {containerClasses}"
328-
style={containerStyles}
380+
class="{disableDefaultStyles ? '' : 'file-dropzone'} {containerClasses}"
381+
style={`${containerStyles}`}
329382
on:keydown={composeKeyboardHandler(onKeyDownCb)}
330383
on:focus={composeKeyboardHandler(onFocusCb)}
331384
on:blur={composeKeyboardHandler(onBlurCb)}
@@ -349,12 +402,12 @@
349402
style="display: none;"
350403
/>
351404
<slot>
352-
<p class="drop-text">{dropText}</p>
405+
<p class={`file-drop-text ${isError ? 'text-danger' : isSuccess ? 'text-success' : ''}`}>{innerDropText}</p>
353406
</slot>
354407
</div>
355408
356409
<style>
357-
.dropzone {
410+
.file-dropzone {
358411
flex: 1;
359412
display: flex;
360413
flex-direction: column;
@@ -368,9 +421,12 @@
368421
color: #bdbdbd;
369422
outline: none;
370423
transition: border 0.24s ease-in-out;
424+
margin-top: 3px;
425+
width: 100%;
426+
height: 10rem;
371427
}
372428
373-
.drop-text {
429+
.file-drop-text {
374430
margin-top: auto;
375431
margin-bottom: auto;
376432
}

src/lib/common/FileGallery.svelte

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<script>
2+
import { LightboxGallery, GalleryThumbnail, GalleryImage } from 'svelte-lightbox';
3+
import Viewport from 'svelte-viewport-info';
4+
5+
/** @type {any[]} */
6+
export let files = [];
7+
8+
/** @type {boolean} */
9+
export let needDelete = true;
10+
11+
/** @type {boolean} */
12+
export let disabled = false;
13+
14+
/** @type {(args0: number) => void} */
15+
export let onDelete = () => {};
16+
17+
const screenWidthThreshold = 500;
18+
19+
/**
20+
* @param {any} e
21+
* @param {number} idx
22+
*/
23+
function handleDeleteFile(e, idx) {
24+
if (disabled) return;
25+
e.preventDefault();
26+
e.stopPropagation();
27+
onDelete && onDelete(idx);
28+
}
29+
30+
</script>
31+
32+
{#if files?.length > 0}
33+
<LightboxGallery transitionDuration={100}>
34+
<svelte:fragment slot="thumbnail">
35+
<div class="image-gallery-container">
36+
{#each files as file, idx (idx)}
37+
<div class={`gallery-item ${Viewport.Width <= screenWidthThreshold ? 'lite-gallery-item' : ''}`}>
38+
<GalleryThumbnail style="width: 100%; height: 100%; display: flex;" id={idx}>
39+
<div class="gallery-item-wrapper">
40+
{#if needDelete}
41+
<div
42+
class="gallery-item-icon"
43+
tabindex="0"
44+
role="button"
45+
on:keydown={() => {}}
46+
on:click={e => handleDeleteFile(e, idx)}
47+
>
48+
<i class="bx bx-trash" />
49+
</div>
50+
{/if}
51+
<img class="gallery-item-image" src={file.url} alt={''}>
52+
</div>
53+
</GalleryThumbnail>
54+
</div>
55+
{/each}
56+
</div>
57+
</svelte:fragment>
58+
59+
{#each files as file, idx (idx)}
60+
<GalleryImage title={file.file_name}>
61+
<img src={file.url} alt={''} />
62+
<div class="item-text">{file.file_name}</div>
63+
</GalleryImage>
64+
{/each}
65+
</LightboxGallery>
66+
{/if}

src/lib/helpers/enums.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const editorType = {
4040
Phone: "phone",
4141
DateTimePicker: "datetime-picker",
4242
DateTimeRangePicker: 'datetime-range-picker',
43-
Email: 'email'
43+
Email: 'email',
44+
File: 'file'
4445
};
4546
export const EditorType = Object.freeze(editorType);

src/lib/helpers/store.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const conversationKey = "conversation";
66
const conversationUserStatesKey = "conversation_user_states";
77
const conversationSearchOptionKey = "conversation_search_option";
88
const conversationUserMessageKey = "conversation_user_messages";
9+
const conversationUserAttachmentKey = "conversation_user_attachments";
910

1011
/** @type {Writable<import('$types').UserModel>} */
1112
export const userStore = writable({ id: "", full_name: "", expires: 0, token: null });
@@ -128,4 +129,29 @@ const createConversationUserMessageStore = () => {
128129
}
129130
};
130131

131-
export const conversationUserMessageStore = createConversationUserMessageStore();
132+
export const conversationUserMessageStore = createConversationUserMessageStore();
133+
134+
135+
const createConversationUserAttachmentStore = () => {
136+
return {
137+
reset: (conversationId) => {
138+
const key = buildAttachmentKey(conversationId);
139+
localStorage.removeItem(key);
140+
},
141+
get: (conversationId) => {
142+
const key = buildAttachmentKey(conversationId);
143+
const json = localStorage.getItem(key);
144+
return json ? JSON.parse(json) : {};
145+
},
146+
put: (conversationId, value) => {
147+
const key = buildAttachmentKey(conversationId);
148+
localStorage.setItem(key, JSON.stringify(value));
149+
}
150+
}
151+
};
152+
153+
function buildAttachmentKey(conversationId){
154+
return `${conversationUserAttachmentKey}_${conversationId}`;
155+
}
156+
157+
export const conversationUserAttachmentStore = createConversationUserAttachmentStore();

0 commit comments

Comments
 (0)