@bagelink/vue 1.2.89 → 1.2.97

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,7 @@
1
1
  <script setup lang="ts">
2
- import type { BglFile, QueueFile, UploadInputProps } from './upload.types'
3
- import { Btn, IMAGE_FORMATS_REGEXP, Icon, useBagel, Card, Image, pathKeyToURL, bagelInjectionKey } from '@bagelink/vue'
4
- import { watch } from 'vue'
5
- import { files } from './upload'
2
+ import type { UploadInputProps } from './upload.types'
3
+ import { Btn, IMAGE_FORMATS_REGEXP, Icon, Card, Image, pathKeyToURL } from '@bagelink/vue'
4
+ import { useFileUpload } from './useFileUpload'
6
5
 
7
6
  const props = withDefaults(defineProps<UploadInputProps>(), {
8
7
  height: '215px',
@@ -11,122 +10,48 @@ const props = withDefaults(defineProps<UploadInputProps>(), {
11
10
  })
12
11
 
13
12
  const emit = defineEmits(['update:modelValue', 'addFileStart'])
14
- const bagel = useBagel(bagelInjectionKey)
15
13
 
16
- files.setBaseUrl(bagel.host)
17
-
18
- const fileQueue = $ref<QueueFile[]>([])
19
- let storageFiles = $ref<BglFile[]>([])
20
- const pk = $ref<string[]>([props.modelValue].flat().filter(Boolean) as string[])
21
-
22
- const pathKeys = $computed<string[]>(() => {
23
- const sf = storageFiles.map(file => file.path_key)
24
- return [...pk, ...sf]
14
+ const {
15
+ fileQueue,
16
+ pathKeys,
17
+ removeFile,
18
+ flushQueue,
19
+ fileToUrl,
20
+ addFile,
21
+ browse,
22
+ } = useFileUpload({
23
+ disabled: props.disabled,
24
+ dirPath: props.dirPath,
25
+ multiple: props.multiple,
26
+ accept: props.accept
25
27
  })
26
28
 
27
- watch(() => pk, (value) => {
28
- if (props.multiple) emit('update:modelValue', value)
29
- else emit('update:modelValue', value[0] || undefined)
30
- }, { deep: true })
31
-
32
29
  const isImage = (str: string) => IMAGE_FORMATS_REGEXP.test(str)
33
- const fileToUrl = (file: File) => URL.createObjectURL(file)
34
30
 
35
31
  let isDragOver = $ref(false)
36
32
 
37
- // File handling methods
38
- async function removeFile(path_key: string) {
39
- storageFiles = storageFiles.filter(file => file.path_key !== path_key)
40
- const pki = pk.indexOf(path_key)
41
- if (pki !== -1) pk.splice(pki, 1)
42
-
43
- try {
44
- await files.delete(path_key)
45
- } catch (error) {
46
- console.error(error)
47
- }
48
- }
49
-
50
- async function flushQueue() {
51
- emit('addFileStart')
52
- for (const file of fileQueue) {
53
- file.uploading = true
54
- if (!props.multiple) pk.splice(0, 1)
55
- try {
56
- const { data } = await files.upload(file.file, {
57
- onUploadProgress: (e: ProgressEvent) => {
58
- file.progress = (e.loaded / e.total) * 100 - 1
59
- },
60
- dirPath: props.dirPath,
61
- })
62
- pk.push(data.path_key)
63
- } catch (error) {
64
- console.error('error flushing queue', error)
65
- }
66
- }
67
- fileQueue.splice(0, fileQueue.length)
68
- }
69
-
70
- // Event handlers
71
- function browse() {
72
- if (props.disabled) return
73
- const input = document.createElement('input')
74
- input.type = 'file'
75
- input.multiple = props.multiple
76
- input.accept = props.accept
77
- input.onchange = (e: Event) => {
78
- const files = Array.from((e.target as HTMLInputElement).files || [])
79
- fileQueue.push(...files.map(file => ({ name: file.name, file, progress: 0 })))
80
- flushQueue()
81
- }
82
- input.click()
83
- }
84
-
85
33
  function handleDrag(e: DragEvent, isDragging = false) {
86
34
  e.preventDefault()
87
35
  e.stopPropagation()
88
36
  if (!props.disabled) isDragOver = isDragging
89
37
  }
90
38
 
91
- function handleDrop(e: DragEvent) {
39
+ async function handleDrop(e: DragEvent) {
92
40
  if (props.disabled) return
93
41
  e.preventDefault()
94
42
  e.stopPropagation()
95
-
96
- if (e.dataTransfer?.files) {
97
- fileQueue.push(...Array.from(e.dataTransfer.files)
98
- .map(file => ({ name: file.name, file, progress: 0 })))
99
- flushQueue()
100
- }
43
+ emit('addFileStart')
44
+ addFile(e.dataTransfer?.files)
45
+ await flushQueue()
46
+ emit('update:modelValue', pathKeys.value)
101
47
  isDragOver = false
102
48
  }
103
-
104
- if (props.dirPath) {
105
- files.list(props.dirPath)
106
- .then(response => storageFiles.push(...([response.data].flat())))
107
- .catch(console.error)
108
- }
109
-
110
- watch(() => props.dirPath, () => {
111
- if (props.dirPath) {
112
- files.list(props.dirPath)
113
- .then(response => storageFiles.push(...([response.data].flat())))
114
- .catch(console.error)
115
- }
116
- })
117
49
  </script>
118
50
 
119
51
  <template>
120
52
  <div class="bagel-input">
121
53
  <label v-if="label">{{ label }}</label>
122
- <input
123
- v-if="required && !pathKeys.length"
124
- placeholder="required"
125
- type="text"
126
- required
127
- class="pixel"
128
- >
129
-
54
+ <input v-if="required && !pathKeys.length" placeholder="required" type="text" required class="pixel">
130
55
  <Card
131
56
  v-if="theme === 'basic'"
132
57
  outline
@@ -140,7 +65,7 @@ watch(() => props.dirPath, () => {
140
65
  icon="upload"
141
66
  outline
142
67
  :value="btnPlaceholder || 'Upload'"
143
- @click="browse"
68
+ @click="browse(true)"
144
69
  />
145
70
 
146
71
  <template v-for="path_key in pathKeys" :key="path_key">
@@ -158,7 +83,7 @@ watch(() => props.dirPath, () => {
158
83
  color="gray"
159
84
  thin
160
85
  icon="autorenew"
161
- @click="browse"
86
+ @click="browse(true)"
162
87
  />
163
88
  <Btn
164
89
  icon="download"
@@ -197,7 +122,7 @@ watch(() => props.dirPath, () => {
197
122
  'bgl_oval-upload': oval,
198
123
  }"
199
124
  :style="{ width, height }"
200
- @click="browse"
125
+ @click="browse(true)"
201
126
  @dragover="(e) => handleDrag(e, true)"
202
127
  @drop="handleDrop"
203
128
  @dragleave="(e) => handleDrag(e)"
@@ -281,7 +206,7 @@ watch(() => props.dirPath, () => {
281
206
  color="gray"
282
207
  thin
283
208
  icon="autorenew"
284
- @click="browse"
209
+ @click="browse(true)"
285
210
  />
286
211
  <Btn
287
212
  v-tooltip="'Download'"
@@ -0,0 +1,144 @@
1
+ import type { BglFile, QueueFile } from './upload.types'
2
+ import { useBagel } from '@bagelink/vue'
3
+ import { ref, computed, onMounted } from 'vue'
4
+ import { files } from './upload'
5
+
6
+ interface UseFileUploadProps {
7
+ multiple?: boolean
8
+ dirPath?: string
9
+ accept?: string
10
+ disabled?: boolean
11
+ }
12
+
13
+ export function useFileUpload(props: UseFileUploadProps = {}) {
14
+ files.setBaseUrl(useBagel().host)
15
+
16
+ const fileQueue = ref<QueueFile[]>([])
17
+ const storageFiles = ref<BglFile[]>([])
18
+ const pk = ref<string[]>([])
19
+
20
+ // Computed
21
+ const pathKeys = computed(() => {
22
+ const storagePathKeys = storageFiles.value.map(file => file.path_key)
23
+ return [...pk.value, ...storagePathKeys]
24
+ })
25
+
26
+ // File handling methods
27
+ const fileToUrl = (file: File) => URL.createObjectURL(file)
28
+
29
+ const addFile = (file?: File | File[] | FileList | null) => {
30
+ if (!file) return
31
+
32
+ let filesToAdd: File[] = []
33
+
34
+ if (file instanceof File) {
35
+ filesToAdd = [file]
36
+ } else if (file instanceof FileList) {
37
+ filesToAdd = Array.from(file)
38
+ } else if (Array.isArray(file)) {
39
+ filesToAdd = file
40
+ }
41
+
42
+ const newQueueFiles = filesToAdd.map(f => ({
43
+ name: f.name,
44
+ file: f,
45
+ progress: 0
46
+ }))
47
+
48
+ fileQueue.value.push(...newQueueFiles)
49
+ }
50
+
51
+ const removeFile = async (pathKeyOrFile: string | File) => {
52
+ if (typeof pathKeyOrFile === 'string') {
53
+ // Remove from both lists
54
+ storageFiles.value = storageFiles.value.filter(file => file.path_key !== pathKeyOrFile)
55
+
56
+ const pathKeyIndex = pk.value.indexOf(pathKeyOrFile)
57
+ if (pathKeyIndex !== -1) {
58
+ pk.value.splice(pathKeyIndex, 1)
59
+ }
60
+
61
+ try {
62
+ await files.delete(pathKeyOrFile)
63
+ } catch (error) {
64
+ console.error('Error deleting file:', error)
65
+ }
66
+ } else if (pathKeyOrFile) {
67
+ const index = fileQueue.value.findIndex(({ file }) => file.name === pathKeyOrFile.name)
68
+ if (index !== -1) {
69
+ fileQueue.value.splice(index, 1)
70
+ }
71
+ }
72
+ }
73
+
74
+ const flushQueue = async () => {
75
+ for (const file of fileQueue.value) {
76
+ file.uploading = true
77
+
78
+ // If not multiple, replace the existing file
79
+ if (!props.multiple) {
80
+ pk.value.splice(0, 1)
81
+ }
82
+
83
+ try {
84
+ const { data } = await files.upload(file.file, {
85
+ onUploadProgress: (e: ProgressEvent) => {
86
+ file.progress = (e.loaded / e.total) * 100 - 1
87
+ },
88
+ dirPath: props.dirPath,
89
+ })
90
+ pk.value.push(data.path_key)
91
+ } catch (error) {
92
+ console.error('Error uploading file:', error)
93
+ }
94
+ }
95
+
96
+ // Clear the queue after processing
97
+ fileQueue.value = []
98
+ }
99
+
100
+ // UI interaction
101
+ const browse = (upload = false) => {
102
+ if (props.disabled) return
103
+
104
+ const input = document.createElement('input')
105
+ input.type = 'file'
106
+ input.multiple = !!props.multiple
107
+ input.accept = props.accept || ''
108
+
109
+ input.onchange = (e: Event) => {
110
+ addFile((e.target as HTMLInputElement).files)
111
+ if (upload) {
112
+ flushQueue()
113
+ }
114
+ }
115
+
116
+ input.click()
117
+ }
118
+
119
+ // Load initial files
120
+ onMounted(() => {
121
+ if (props.dirPath) {
122
+ files.list(props.dirPath)
123
+ .then((response) => {
124
+ const responseData = Array.isArray(response.data)
125
+ ? response.data
126
+ : [response.data]
127
+ storageFiles.value.push(...responseData)
128
+ })
129
+ .catch((error) => { console.error('Error loading files:', error) })
130
+ }
131
+ })
132
+
133
+ return {
134
+ fileQueue,
135
+ storageFiles,
136
+ pk,
137
+ pathKeys,
138
+ fileToUrl,
139
+ removeFile,
140
+ flushQueue,
141
+ addFile,
142
+ browse,
143
+ }
144
+ }
@@ -21,3 +21,5 @@ export { default as TelInput } from './TelInput.vue'
21
21
  export { default as TextInput } from './TextInput.vue'
22
22
  export { default as ToggleInput } from './ToggleInput.vue'
23
23
  export { default as UploadInput } from './Upload/UploadInput.vue'
24
+
25
+ export { useFileUpload } from './Upload/useFileUpload'
@@ -32,6 +32,7 @@ export { default as ModalConfirm } from './ModalConfirm.vue'
32
32
  export { default as ModalForm } from './ModalForm.vue'
33
33
  export { default as NavBar } from './NavBar.vue'
34
34
  export { default as PageTitle } from './PageTitle.vue'
35
+ export { default as Pagination } from './Pagination.vue'
35
36
  export { default as Pill } from './Pill.vue'
36
37
  export { default as RouterWrapper } from './RouterWrapper.vue'
37
38
  export { default as Slider } from './Slider.vue'
@@ -63,6 +63,16 @@ function clickOutside() {
63
63
 
64
64
  const upgradeHeaders = (url: string) => url.replace(/http:\/\//, '//')
65
65
 
66
+ function downloadFile() {
67
+ const link = document.createElement('a')
68
+ const src = currentItem.src || ''
69
+ link.target = '_blank'
70
+ link.href = upgradeHeaders(src)
71
+ link.download = src ? src.split('/').pop() || 'download' : 'download'
72
+ document.body.appendChild(link)
73
+ link.click()
74
+ document.body.removeChild(link)
75
+ }
66
76
  defineExpose({ open, close })
67
77
  </script>
68
78
 
@@ -108,8 +118,7 @@ defineExpose({ open, close })
108
118
  <Btn
109
119
  v-if="currentItem?.download && currentItem?.src" class="color-white" round thin flat icon="download"
110
120
  value="Download File"
111
- :href="upgradeHeaders(currentItem?.src)"
112
- download
121
+ @click="downloadFile"
113
122
  />
114
123
  <div v-if="!currentItem?.openFile && !currentItem?.download" />
115
124
  </div>
@@ -128,34 +137,40 @@ defineExpose({ open, close })
128
137
  class="vw90"
129
138
  />
130
139
 
131
- <embed
140
+ <div
132
141
  v-else-if="item?.type === 'pdf' && item?.src"
133
- :src="normalizeURL(item?.src)"
134
- type="application/pdf"
135
- width="100%"
136
- height="1080"
137
- :title="item?.name"
138
142
  class="vw90"
139
143
  >
140
- <div v-else class="file-info txt-white flex m_block align-items-start gap-025">
141
- <Icon class="m-0 m_none" icon="draft" :size="10" weight="12" />
142
- <Icon class="m-0 none m_block m_-mb-1" icon="draft" :size="4" weight="2" />
144
+ <embed
145
+ :src="normalizeURL(item?.src)"
146
+ type="application/pdf"
147
+ width="100%"
148
+ height="1080"
149
+ :title="item?.name"
150
+ class="vw90"
151
+ >
152
+ </div>
153
+ <div v-else class="vw90">
154
+ <div class="file-info txt-white flex m_block align-items-start gap-025">
155
+ <Icon class="m-0 m_none" icon="draft" :size="10" weight="12" />
156
+ <Icon class="m-0 none m_block m_-mb-1" icon="draft" :size="4" weight="2" />
143
157
 
144
- <div class="txt-start">
145
- <p class="mx-0 light">
146
- File:
147
- <span class="semi word-break-all ">
148
- {{ item?.name }}
149
- </span>
150
- </p>
151
- <p class="mx-0 ">
152
- Type:
153
- <span class="semi">
154
- {{ item?.type }}
155
- </span>
156
- </p>
157
- <Btn :href="item?.src" target="_blank" round thin class="mt-1" value="Open file" />
158
- <!-- <a :href="currentItem?.src" target="_blank">Open file</a> -->
158
+ <div class="txt-start">
159
+ <p class="mx-0 light">
160
+ File:
161
+ <span class="semi word-break-all ">
162
+ {{ item?.name }}
163
+ </span>
164
+ </p>
165
+ <p class="mx-0 ">
166
+ Type:
167
+ <span class="semi">
168
+ {{ item?.type }}
169
+ </span>
170
+ </p>
171
+ <Btn :href="item?.src" target="_blank" round thin class="mt-1" value="Open file" />
172
+ <!-- <a :href="currentItem?.src" target="_blank">Open file</a> -->
173
+ </div>
159
174
  </div>
160
175
  </div>
161
176
  </template>