Skip to content

Commit 7ba4e9e

Browse files
committed
feat(NcFilePicker): add picker component to select local files
This can be used e.g. to upload files, for example in the forms app but also for the files or photos app. Signed-off-by: Ferdinand Thiessen <[email protected]>
1 parent dd6293c commit 7ba4e9e

File tree

4 files changed

+254
-0
lines changed

4 files changed

+254
-0
lines changed

l10n/messages.pot

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,9 @@ msgstr ""
350350
msgid "Pick an emoji"
351351
msgstr ""
352352

353+
msgid "Pick files"
354+
msgstr ""
355+
353356
msgid "Please choose a date"
354357
msgstr ""
355358

@@ -506,6 +509,12 @@ msgstr ""
506509
msgid "Undo changes"
507510
msgstr ""
508511

512+
msgid "Upload files"
513+
msgstr ""
514+
515+
msgid "Upload folders"
516+
msgstr ""
517+
509518
msgid "User status: {status}"
510519
msgstr ""
511520

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<docs>
7+
This component allows to pick local files (or folders) which can be used to upload them to Nextcloud.
8+
9+
### Exposed methods
10+
11+
- `function reset(): void`
12+
This method allows to reset the internal state of the file picker to clear the current selection
13+
14+
### Example
15+
16+
```vue
17+
<template>
18+
<div>
19+
<div class="wrapper">
20+
<NcFilePicker ref="picker"
21+
allow-folders
22+
@pick="selectedFiles = $event" />
23+
24+
<NcButton variant="tertiary"
25+
@click="clearPicker">
26+
Clear
27+
</NcButton>
28+
</div>
29+
30+
<h3>Selected files:</h3>
31+
<ul>
32+
<li v-for="file in selectedFiles" key="file.name">
33+
{{ file.webkitRelativePath || file.name }}
34+
</li>
35+
</ul>
36+
</div>
37+
</template>
38+
<script>
39+
export default {
40+
data() {
41+
return {
42+
selectedFiles: [],
43+
}
44+
},
45+
methods: {
46+
/**
47+
* This will clear the selected files from the picker.
48+
*/
49+
clearPicker() {
50+
this.$refs.picker.reset()
51+
},
52+
},
53+
}
54+
</script>
55+
<style scoped>
56+
.wrapper {
57+
display: flex;
58+
gap: 10px;
59+
}
60+
</style>
61+
```
62+
</docs>
63+
64+
<script setup lang="ts">
65+
import type { Slot } from 'vue'
66+
67+
import { computed, nextTick, useTemplateRef } from 'vue'
68+
import { t } from '../../l10n.js'
69+
import IconFolderUpload from 'vue-material-design-icons/FolderUpload.vue'
70+
import IconPlus from 'vue-material-design-icons/Plus.vue'
71+
import IconUpload from 'vue-material-design-icons/Upload.vue'
72+
import NcActionButton from '../NcActionButton/index.js'
73+
import NcActions from '../NcActions/index.js'
74+
75+
const props = withDefaults(defineProps<{
76+
/**
77+
* File types to accept
78+
*/
79+
accept?: string[]
80+
81+
/**
82+
* Allow picking a folder
83+
*/
84+
allowFolders?: boolean
85+
86+
/**
87+
* Disabled state of the picker
88+
*/
89+
disabled?: boolean
90+
91+
/**
92+
* If set then the label is only used for accessibility but not shown visually
93+
*/
94+
iconOnly?: boolean
95+
96+
/**
97+
* Label of the picker
98+
*/
99+
label?: string
100+
101+
/**
102+
* Can the user pick multiple files
103+
*/
104+
multiple?: boolean
105+
106+
/**
107+
* The variant of the button
108+
*/
109+
variant?: 'primary' | 'secondary' | 'tertiary'
110+
}>(), {
111+
accept: undefined,
112+
label: () => t('Pick files'),
113+
variant: 'primary',
114+
})
115+
116+
const emit = defineEmits<{
117+
pick: [files: File[]]
118+
}>()
119+
120+
defineSlots<{
121+
/**
122+
* Custom NcAction* to be shown within the picker menu
123+
*/
124+
actions?: Slot
125+
126+
/**
127+
* Optional custom icon for the picker menu
128+
*/
129+
icon?: Slot
130+
131+
/**
132+
* Optional content to be shown in the picker.
133+
* This can be used e.g. for a progress bar or similar.
134+
*/
135+
default?: Slot
136+
}>()
137+
138+
defineExpose({
139+
reset,
140+
})
141+
142+
const formElement = useTemplateRef('form')
143+
const inputElement = useTemplateRef('input')
144+
145+
/**
146+
* Check whether the current browser supports uploading directories
147+
* Hint: This does not check if the current connection supports this, as some browsers require a secure context!
148+
*/
149+
const canUploadFolders = computed(() => {
150+
return props.allowFolders && 'webkitdirectory' in document.createElement('input')
151+
})
152+
153+
/**
154+
* Trigger file picker
155+
*
156+
* @param uploadFolders - Whether to upload folders or files
157+
*/
158+
function triggerPickFiles(uploadFolders: boolean) {
159+
// reset the form
160+
reset()
161+
162+
// Setup directory picking if enabled
163+
if (canUploadFolders.value) {
164+
inputElement.value!.webkitdirectory = uploadFolders
165+
}
166+
// Trigger click on the input to open the file picker
167+
nextTick(() => inputElement.value!.click())
168+
}
169+
170+
/**
171+
* Handle picking some local files
172+
*/
173+
function onPick() {
174+
const files = inputElement.value?.files ? Array.from(inputElement.value.files) : []
175+
emit('pick', files)
176+
}
177+
178+
/**
179+
* Reset the picker state of the currently selected files.
180+
*/
181+
function reset() {
182+
formElement.value!.reset()
183+
}
184+
</script>
185+
186+
<template>
187+
<form ref="form"
188+
class="vue-file-picker">
189+
<NcActions :aria-label="label"
190+
:menu-name="iconOnly ? undefined : label"
191+
:force-name="!iconOnly"
192+
:variant>
193+
<template #icon>
194+
<slot name="icon">
195+
<IconPlus :size="20" />
196+
</slot>
197+
</template>
198+
199+
<NcActionButton close-after-click
200+
@click="triggerPickFiles(false)">
201+
<template #icon>
202+
<IconUpload :size="20" />
203+
</template>
204+
{{ t('Upload files') }}
205+
</NcActionButton>
206+
207+
<NcActionButton v-if="canUploadFolders"
208+
close-after-click
209+
@click="triggerPickFiles(true)">
210+
<template #icon>
211+
<IconFolderUpload style="color: var(--color-primary-element)" :size="20" />
212+
</template>
213+
{{ t('Upload folders') }}
214+
</NcActionButton>
215+
216+
<!-- App defined upload actions -->
217+
<slot name="actions" />
218+
</NcActions>
219+
220+
<!-- Hidden files picker input -->
221+
<input ref="input"
222+
:accept="accept?.join(', ')"
223+
:multiple
224+
class="hidden-visually"
225+
type="file"
226+
@change="onPick">
227+
228+
<slot />
229+
</form>
230+
</template>
231+
232+
<style lang="scss" scoped>
233+
.vue-file-picker {
234+
display: inline-flex;
235+
align-items: center;
236+
height: var(--default-clickable-area);
237+
}
238+
</style>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
export { default } from './NcFilePicker.vue'

src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export { default as NcDialogButton } from './NcDialogButton/index.ts'
5454
export { default as NcEllipsisedOption } from './NcEllipsisedOption/index.js'
5555
export { default as NcEmojiPicker } from './NcEmojiPicker/index.js'
5656
export { default as NcEmptyContent } from './NcEmptyContent/index.ts'
57+
export { default as NcFilePicker } from './NcFilePicker/index.ts'
5758
export { default as NcGuestContent } from './NcGuestContent/index.ts'
5859
export { default as NcHeaderButton } from './NcHeaderButton/index.ts'
5960
export { default as NcHeaderMenu } from './NcHeaderMenu/index.ts'

0 commit comments

Comments
 (0)