Skip to content

Commit 4d458d1

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 <opensource@fthiessen.de>
1 parent 2cf1fb4 commit 4d458d1

File tree

4 files changed

+252
-0
lines changed

4 files changed

+252
-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: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
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+
// Setup directory picking if enabled
160+
if (canUploadFolders.value) {
161+
inputElement.value!.webkitdirectory = uploadFolders
162+
}
163+
// Trigger click on the input to open the file picker
164+
nextTick(() => inputElement.value!.click())
165+
}
166+
167+
/**
168+
* Handle picking some local files
169+
*/
170+
function onPick() {
171+
const files = inputElement.value?.files ? Array.from(inputElement.value.files) : []
172+
console.error('ON PICK', files)
173+
emit('pick', files)
174+
}
175+
176+
/**
177+
* Reset the picker state of the currently selected files.
178+
*/
179+
function reset() {
180+
formElement.value!.reset()
181+
}
182+
</script>
183+
184+
<template>
185+
<form ref="form"
186+
class="vue-file-picker">
187+
<NcActions :aria-label="label"
188+
:menu-name="iconOnly ? undefined : label"
189+
:force-name="!iconOnly"
190+
:variant>
191+
<template #icon>
192+
<slot name="icon">
193+
<IconPlus :size="20" />
194+
</slot>
195+
</template>
196+
197+
<NcActionButton close-after-click
198+
@click="triggerPickFiles(false)">
199+
<template #icon>
200+
<IconUpload :size="20" />
201+
</template>
202+
{{ t('Upload files') }}
203+
</NcActionButton>
204+
205+
<NcActionButton v-if="canUploadFolders"
206+
close-after-click
207+
@click="triggerPickFiles(true)">
208+
<template #icon>
209+
<IconFolderUpload style="color: var(--color-primary-element)" :size="20" />
210+
</template>
211+
{{ t('Upload folders') }}
212+
</NcActionButton>
213+
214+
<!-- App defined upload actions -->
215+
<slot name="actions" />
216+
</NcActions>
217+
218+
<!-- Hidden files picker input -->
219+
<input ref="input"
220+
:accept="accept?.join(', ')"
221+
:multiple="multiple"
222+
class="hidden-visually"
223+
type="file"
224+
@change="onPick">
225+
226+
<slot />
227+
</form>
228+
</template>
229+
230+
<style lang="scss" scoped>
231+
.vue-file-picker {
232+
display: inline-flex;
233+
align-items: center;
234+
height: var(--default-clickable-area);
235+
}
236+
</style>

src/components/NcFilePicker/index.ts

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)