@@ -3,7 +3,14 @@ import { css, html, LitElement } from 'lit';
3
3
import { query , property } from 'lit/decorators.js' ;
4
4
import { UUIFileDropzoneEvent } from './UUIFileDropzoneEvent' ;
5
5
import { LabelMixin } from '@umbraco-ui/uui-base/lib/mixins' ;
6
- import { demandCustomElement } from '@umbraco-ui/uui-base/lib/utils' ;
6
+
7
+ import '@umbraco-ui/uui-symbol-file-dropzone/lib' ;
8
+
9
+ export interface UUIFileFolder {
10
+ folderName : string ;
11
+ folders : UUIFileFolder [ ] ;
12
+ files : File [ ] ;
13
+ }
7
14
8
15
/**
9
16
* @element uui-file-dropzone
@@ -66,6 +73,13 @@ export class UUIFileDropzoneElement extends LabelMixin('', LitElement) {
66
73
return this . _accept ;
67
74
}
68
75
76
+ @property ( {
77
+ type : Boolean ,
78
+ reflect : true ,
79
+ attribute : 'disallow-folder-upload' ,
80
+ } )
81
+ public disallowFolderUpload : boolean = false ;
82
+
69
83
/**
70
84
* Allows for multiple files to be selected.
71
85
* @type {boolean }
@@ -92,64 +106,95 @@ export class UUIFileDropzoneElement extends LabelMixin('', LitElement) {
92
106
this . addEventListener ( 'drop' , this . _onDrop , false ) ;
93
107
}
94
108
95
- connectedCallback ( ) : void {
96
- super . connectedCallback ( ) ;
97
- demandCustomElement ( this , 'uui-symbol-file-dropzone' ) ;
98
- }
99
-
100
- private async _getAllFileEntries (
101
- dataTransferItemList : DataTransferItemList ,
102
- ) : Promise < File [ ] > {
103
- const fileEntries : File [ ] = [ ] ;
109
+ private async _getAllEntries ( dataTransferItemList : DataTransferItemList ) {
104
110
// Use BFS to traverse entire directory/file structure
105
111
const queue = [ ...dataTransferItemList ] ;
106
112
107
- while ( queue . length > 0 ) {
108
- const entry = queue . shift ( ) ! ;
113
+ const folders : UUIFileFolder [ ] = [ ] ;
114
+ const files : File [ ] = [ ] ;
115
+
116
+ for ( const entry of queue ) {
117
+ if ( entry ?. kind !== 'file' ) continue ;
109
118
110
- if ( entry . kind === 'file' ) {
119
+ if ( entry . type ) {
120
+ // Entry is a file
111
121
const file = entry . getAsFile ( ) ;
112
122
if ( ! file ) continue ;
113
123
if ( this . _isAccepted ( file ) ) {
114
- fileEntries . push ( file ) ;
124
+ files . push ( file ) ;
125
+ }
126
+ } else if ( ! this . disallowFolderUpload ) {
127
+ // Entry is a directory
128
+ const dir = this . _getEntry ( entry ) ;
129
+
130
+ if ( dir ) {
131
+ const structure = await this . _mkdir ( dir ) ;
132
+ folders . push ( structure ) ;
115
133
}
116
- } else if ( entry . kind === 'directory' ) {
117
- if ( 'webkitGetAsEntry' in entry === false ) continue ;
118
- const directory = entry . webkitGetAsEntry ( ) ! as FileSystemDirectoryEntry ;
119
- queue . push (
120
- ...( await this . _readAllDirectoryEntries ( directory . createReader ( ) ) ) ,
121
- ) ;
122
134
}
123
135
}
124
-
125
- return fileEntries ;
136
+ return { files, folders } ;
126
137
}
127
138
128
- // Get all the entries (files or sub-directories) in a directory
129
- // by calling readEntries until it returns empty array
130
- private async _readAllDirectoryEntries (
131
- directoryReader : FileSystemDirectoryReader ,
132
- ) {
133
- const entries : any = [ ] ;
134
- let readEntries : any = await this . _readEntriesPromise ( directoryReader ) ;
135
- while ( readEntries . length > 0 ) {
136
- entries . push ( ...readEntries ) ;
137
- readEntries = await this . _readEntriesPromise ( directoryReader ) ;
139
+ /**
140
+ * Get the directory entry from a DataTransferItem.
141
+ * @remark Supports both WebKit and non-WebKit browsers.
142
+ */
143
+ private _getEntry ( entry : DataTransferItem ) : FileSystemDirectoryEntry | null {
144
+ let dir : FileSystemDirectoryEntry | null = null ;
145
+
146
+ if ( 'webkitGetAsEntry' in entry ) {
147
+ dir = entry . webkitGetAsEntry ( ) as FileSystemDirectoryEntry ;
148
+ } else if ( 'getAsEntry' in entry ) {
149
+ // non-WebKit browsers may rename webkitGetAsEntry to getAsEntry. MDN recommends looking for both.
150
+ dir = ( entry as any ) . getAsEntry ( ) ;
138
151
}
139
- return entries ;
152
+
153
+ return dir ;
140
154
}
141
155
142
- private async _readEntriesPromise (
143
- directoryReader : FileSystemDirectoryReader ,
144
- ) {
145
- return new Promise ( ( resolve , reject ) => {
146
- try {
147
- directoryReader . readEntries ( resolve , reject ) ;
148
- } catch ( err ) {
149
- console . log ( err ) ;
150
- reject ( err ) ;
151
- }
152
- } ) ;
156
+ // Make directory structure
157
+ private async _mkdir (
158
+ entry : FileSystemDirectoryEntry ,
159
+ ) : Promise < UUIFileFolder > {
160
+ const reader = entry . createReader ( ) ;
161
+ const folders : UUIFileFolder [ ] = [ ] ;
162
+ const files : File [ ] = [ ] ;
163
+
164
+ const readEntries = ( reader : FileSystemDirectoryReader ) => {
165
+ return new Promise < void > ( ( resolve , reject ) => {
166
+ reader . readEntries ( async entries => {
167
+ if ( ! entries . length ) {
168
+ resolve ( ) ;
169
+ return ;
170
+ }
171
+
172
+ for ( const en of entries ) {
173
+ if ( en . isFile ) {
174
+ const file = await this . _getAsFile ( en as FileSystemFileEntry ) ;
175
+ if ( this . _isAccepted ( file ) ) {
176
+ files . push ( file ) ;
177
+ }
178
+ } else if ( en . isDirectory ) {
179
+ const directory = await this . _mkdir (
180
+ en as FileSystemDirectoryEntry ,
181
+ ) ;
182
+ folders . push ( directory ) ;
183
+ }
184
+ }
185
+
186
+ // readEntries only reads up to 100 entries at a time. It is on purpose we call readEntries recursively.
187
+ readEntries ( reader ) ;
188
+
189
+ resolve ( ) ;
190
+ } , reject ) ;
191
+ } ) ;
192
+ } ;
193
+
194
+ await readEntries ( reader ) ;
195
+
196
+ const result : UUIFileFolder = { folderName : entry . name , folders, files } ;
197
+ return result ;
153
198
}
154
199
155
200
private _isAccepted ( file : File ) {
@@ -184,22 +229,28 @@ export class UUIFileDropzoneElement extends LabelMixin('', LitElement) {
184
229
return false ;
185
230
}
186
231
232
+ private async _getAsFile ( fileEntry : FileSystemFileEntry ) : Promise < File > {
233
+ return new Promise ( ( resolve , reject ) => fileEntry . file ( resolve , reject ) ) ;
234
+ }
235
+
187
236
private async _onDrop ( e : DragEvent ) {
188
237
e . preventDefault ( ) ;
189
238
this . _dropzone . classList . remove ( 'hover' ) ;
190
239
191
240
const items = e . dataTransfer ?. items ;
192
241
193
242
if ( items ) {
194
- let result = await this . _getAllFileEntries ( items ) ;
243
+ const fileSystemResult = await this . _getAllEntries ( items ) ;
195
244
196
- if ( this . multiple === false && result . length ) {
197
- result = [ result [ 0 ] ] ;
245
+ if ( this . multiple === false && fileSystemResult . files . length ) {
246
+ fileSystemResult . files = [ fileSystemResult . files [ 0 ] ] ;
198
247
}
199
248
249
+ this . _getAllEntries ( items ) ;
250
+
200
251
this . dispatchEvent (
201
252
new UUIFileDropzoneEvent ( UUIFileDropzoneEvent . CHANGE , {
202
- detail : { files : result } ,
253
+ detail : fileSystemResult ,
203
254
} ) ,
204
255
) ;
205
256
}
0 commit comments