|
1 | 1 | <template>
|
2 |
| - <div class="bold"> |
3 |
| - hello |
4 |
| - <div class="italic"> |
5 |
| - {{ value }} |
| 2 | + <div class="fs-file-selector"> |
| 3 | + <div class="fs-loader" v-if="isLoading"> |
| 4 | + <slot name="loader"> |
| 5 | + Loading... |
| 6 | + </slot> |
| 7 | + </div> |
| 8 | + |
| 9 | + <div class="fs-droppable" |
| 10 | + ref="fsDroppable" |
| 11 | + :class="{ 'fs-drag-enter': isDragEnter }" |
| 12 | + :style="{ height: height + 'px' }" |
| 13 | + @dragenter.stop.prevent="isDragEnter = true" |
| 14 | + @dragover.stop.prevent="() => {}" |
| 15 | + @dragleave.stop.prevent="isDragEnter = false" |
| 16 | + @drop.stop.prevent="handleDrop"> |
| 17 | + <input |
| 18 | + ref="fsFileInput" |
| 19 | + type="file" |
| 20 | + tabindex="-1" |
| 21 | + :multiple="multiple" |
| 22 | + :accept="acceptExtensions" |
| 23 | + @change="handleFilesChange" |
| 24 | + /> |
| 25 | + <slot name="top"></slot> |
| 26 | + |
| 27 | + <div href="#" class="fs-btn-select" @click="$refs.fsFileInput.click()"> |
| 28 | + <slot>Select</slot> |
| 29 | + </div> |
| 30 | + |
| 31 | + <slot name="bottom"></slot> |
6 | 32 | </div>
|
7 | 33 | </div>
|
8 | 34 | </template>
|
9 | 35 |
|
| 36 | + |
10 | 37 | <script>
|
11 | 38 | export default {
|
| 39 | + props: { |
| 40 | + multiple: { |
| 41 | + type: Boolean, |
| 42 | + default: false, |
| 43 | + }, |
| 44 | +
|
| 45 | + isLoading: { |
| 46 | + type: Boolean, |
| 47 | + default: false, |
| 48 | + }, |
| 49 | +
|
| 50 | + acceptExtensions: { |
| 51 | + type: String, |
| 52 | + default: '', |
| 53 | + }, |
| 54 | +
|
| 55 | + maxFileSize: { // in bytes |
| 56 | + type: Number, |
| 57 | + default: NaN, |
| 58 | + }, |
| 59 | +
|
| 60 | + height: { |
| 61 | + type: Number, |
| 62 | + default: NaN, |
| 63 | + }, |
| 64 | +
|
| 65 | + validateFn: { |
| 66 | + type: Function, |
| 67 | + default: () => true, |
| 68 | + }, |
| 69 | + }, |
| 70 | +
|
12 | 71 | data() {
|
13 | 72 | return {
|
14 |
| - value: 10, |
| 73 | + isDragEnter: false, |
15 | 74 | };
|
16 | 75 | },
|
17 | 76 |
|
18 |
| - create() { |
19 |
| - console.log('created'); |
| 77 | + methods: { |
| 78 | + handleFilesChange($event) { |
| 79 | + this.preprocessFiles($event.target.files); |
| 80 | + }, |
| 81 | +
|
| 82 | + handleDrop($event) { |
| 83 | + this.isDragEnter = false; |
| 84 | + this.preprocessFiles($event.dataTransfer.files); |
| 85 | + }, |
| 86 | +
|
| 87 | + checkFileExtensions(files) { |
| 88 | + // get non-empty, unique extension items |
| 89 | + const extList = [...new Set( |
| 90 | + this.acceptExtensions.toLowerCase() |
| 91 | + .split(',') |
| 92 | + .filter(Boolean) |
| 93 | + )]; |
| 94 | + const list = Array.from(files); |
| 95 | +
|
| 96 | + // check if the selected files are in supported extensions |
| 97 | + const invalidFileIndex = list.findIndex(file => { |
| 98 | + const ext = `.${file.name.toLowerCase().split('.').pop()}`; |
| 99 | +
|
| 100 | + return !extList.includes(ext); |
| 101 | + }); |
| 102 | +
|
| 103 | + // all exts are valid |
| 104 | + return invalidFileIndex === -1; |
| 105 | + }, |
| 106 | +
|
| 107 | + checkFileSize(files) { |
| 108 | + if (Number.isNaN(this.maxFileSize)) { |
| 109 | + return true; |
| 110 | + } |
| 111 | +
|
| 112 | + const list = Array.from(files); |
| 113 | +
|
| 114 | + // find invalid file size |
| 115 | + const invalidFileIndex = list.findIndex(file => file.size > this.maxFileSize); |
| 116 | +
|
| 117 | + // all file size are valid |
| 118 | + return invalidFileIndex === -1; |
| 119 | + }, |
| 120 | +
|
| 121 | + validate(files) { |
| 122 | + // file selection |
| 123 | + if (!this.multiple && files.length > 1) { |
| 124 | + return 'MULTIFILES_ERROR'; |
| 125 | + } |
| 126 | +
|
| 127 | + // extension |
| 128 | + if (!this.checkFileExtensions(files)) { |
| 129 | + return 'EXTENSION_ERROR'; |
| 130 | + } |
| 131 | +
|
| 132 | + // file size |
| 133 | + if (!this.checkFileSize(files)) { |
| 134 | + return 'FILE_SIZE_ERROR'; |
| 135 | + } |
| 136 | +
|
| 137 | + // custom validation |
| 138 | + return this.validateFn(files); |
| 139 | + }, |
| 140 | +
|
| 141 | + preprocessFiles(files) { |
| 142 | + const result = this.validate(files); |
| 143 | + this.$emit('validate', result, files); |
| 144 | +
|
| 145 | + // validation |
| 146 | + if (result === true) { |
| 147 | + this.$emit('change', files); |
| 148 | + } |
| 149 | +
|
| 150 | + // clear selected files |
| 151 | + this.$refs.fsFileInput.value = ''; |
| 152 | + }, |
20 | 153 | },
|
21 | 154 | };
|
22 | 155 | </script>
|
23 | 156 |
|
| 157 | + |
24 | 158 | <style lang="scss" scoped>
|
25 |
| -.bold { |
26 |
| - font-weight: 800; |
| 159 | +.fs-file-selector { |
| 160 | + position: relative; |
| 161 | +
|
| 162 | + .fs-loader { |
| 163 | + background: rgba(#fff, 0.8); |
| 164 | + position: absolute; |
| 165 | + top: 0; |
| 166 | + bottom: 0; |
| 167 | + left: 0; |
| 168 | + right: 0; |
| 169 | + z-index: 1; |
| 170 | +
|
| 171 | + display: flex; |
| 172 | + justify-content: center; |
| 173 | + align-items: center; |
| 174 | + } |
| 175 | +
|
| 176 | + .fs-droppable { |
| 177 | + display: flex; |
| 178 | + flex-direction: column; |
| 179 | + align-items: center; |
| 180 | + justify-content: center; |
| 181 | + position: relative; |
| 182 | +
|
| 183 | + text-align: center; |
| 184 | + border-radius: 8px; |
| 185 | + border: 1px dashed #000; |
27 | 186 |
|
28 |
| - .italic { |
29 |
| - font-style: italic; |
| 187 | + input[type="file"] { |
| 188 | + visibility: hidden; |
| 189 | + position: absolute; |
| 190 | + width: 1px; |
| 191 | + height: 1px; |
| 192 | + } |
30 | 193 | }
|
31 | 194 | }
|
32 | 195 | </style>
|
0 commit comments