diff options
Diffstat (limited to 'apps/files/src/components/NewNodeDialog.vue')
-rw-r--r-- | apps/files/src/components/NewNodeDialog.vue | 168 |
1 files changed, 168 insertions, 0 deletions
diff --git a/apps/files/src/components/NewNodeDialog.vue b/apps/files/src/components/NewNodeDialog.vue new file mode 100644 index 00000000000..ca10935940d --- /dev/null +++ b/apps/files/src/components/NewNodeDialog.vue @@ -0,0 +1,168 @@ +<!-- + - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <NcDialog data-cy-files-new-node-dialog + :name="name" + :open="open" + close-on-click-outside + out-transition + @update:open="emit('close', null)"> + <template #actions> + <NcButton data-cy-files-new-node-dialog-submit + type="primary" + :disabled="validity !== ''" + @click="submit"> + {{ t('files', 'Create') }} + </NcButton> + </template> + <form ref="formElement" + class="new-node-dialog__form" + @submit.prevent="emit('close', localDefaultName)"> + <NcTextField ref="nameInput" + data-cy-files-new-node-dialog-input + :error="validity !== ''" + :helper-text="validity" + :label="label" + :value.sync="localDefaultName" /> + + <!-- Hidden file warning --> + <NcNoteCard v-if="isHiddenFileName" + type="warning" + :text="t('files', 'Files starting with a dot are hidden by default')" /> + </form> + </NcDialog> +</template> + +<script setup lang="ts"> +import type { ComponentPublicInstance, PropType } from 'vue' +import { getUniqueName } from '@nextcloud/files' +import { t } from '@nextcloud/l10n' +import { extname } from 'path' +import { computed, nextTick, onMounted, ref, watch, watchEffect } from 'vue' +import { getFilenameValidity } from '../utils/filenameValidity.ts' + +import NcButton from '@nextcloud/vue/components/NcButton' +import NcDialog from '@nextcloud/vue/components/NcDialog' +import NcTextField from '@nextcloud/vue/components/NcTextField' +import NcNoteCard from '@nextcloud/vue/components/NcNoteCard' + +const props = defineProps({ + /** + * The name to be used by default + */ + defaultName: { + type: String, + default: t('files', 'New folder'), + }, + /** + * Other files that are in the current directory + */ + otherNames: { + type: Array as PropType<string[]>, + default: () => [], + }, + /** + * Open state of the dialog + */ + open: { + type: Boolean, + default: true, + }, + /** + * Dialog name + */ + name: { + type: String, + default: t('files', 'Create new folder'), + }, + /** + * Input label + */ + label: { + type: String, + default: t('files', 'Folder name'), + }, +}) + +const emit = defineEmits<{ + (event: 'close', name: string | null): void +}>() + +const localDefaultName = ref<string>(props.defaultName) +const nameInput = ref<ComponentPublicInstance>() +const formElement = ref<HTMLFormElement>() +const validity = ref('') + +const isHiddenFileName = computed(() => { + // Check if the name starts with a dot, which indicates a hidden file + return localDefaultName.value.trim().startsWith('.') +}) + +/** + * Focus the filename input field + */ +function focusInput() { + nextTick(() => { + // get the input element + const input = nameInput.value?.$el.querySelector('input') + if (!props.open || !input) { + return + } + + // length of the basename + const length = localDefaultName.value.length - extname(localDefaultName.value).length + // focus the input + input.focus() + // and set the selection to the basename (name without extension) + input.setSelectionRange(0, length) + }) +} + +/** + * Trigger submit on the form + */ +function submit() { + formElement.value?.requestSubmit() +} + +// Reset local name on props change +watch(() => [props.defaultName, props.otherNames], () => { + localDefaultName.value = getUniqueName(props.defaultName, props.otherNames).trim() +}) + +// Validate the local name +watchEffect(() => { + if (props.otherNames.includes(localDefaultName.value.trim())) { + validity.value = t('files', 'This name is already in use.') + } else { + validity.value = getFilenameValidity(localDefaultName.value.trim()) + } + const input = nameInput.value?.$el.querySelector('input') + if (input) { + input.setCustomValidity(validity.value) + input.reportValidity() + } +}) + +// Ensure the input is focussed even if the dialog is already mounted but not open +watch(() => props.open, () => { + nextTick(() => { + focusInput() + }) +}) + +onMounted(() => { + // on mounted lets use the unique name + localDefaultName.value = getUniqueName(localDefaultName.value, props.otherNames).trim() + nextTick(() => focusInput()) +}) +</script> + +<style scoped> +.new-node-dialog__form { + /* Ensure the dialog does not jump when there is a validity error */ + min-height: calc(2 * var(--default-clickable-area)); +} +</style> |