2222<template >
2323 <!-- Rename input -->
2424 <form v-if =" isRenaming"
25- v-on-click-outside =" stopRenaming"
25+ ref =" renameForm"
26+ v-on-click-outside =" onRename"
2627 :aria-label =" t('files', 'Rename file')"
2728 class =" files-list__row-rename"
2829 @submit.prevent.stop =" onRename" >
3334 :required =" true"
3435 :value.sync =" newName"
3536 enterkeyhint =" done"
36- @keyup =" checkInputValidity"
3737 @keyup.esc =" stopRenaming" />
3838 </form >
3939
4646 v-bind =" linkTo.params" >
4747 <!-- File name -->
4848 <span class =" files-list__row-name-text" >
49- <!-- Keep the displayName stuck to the extension to avoid whitespace rendering issues-->
50- <span class =" files-list__row-name-" v-text =" displayName " />
49+ <!-- Keep the filename stuck to the extension to avoid whitespace rendering issues-->
50+ <span class =" files-list__row-name-" v-text =" basename " />
5151 <span class =" files-list__row-name-ext" v-text =" extension" />
5252 </span >
5353 </component >
5757import type { Node } from ' @nextcloud/files'
5858import type { PropType } from ' vue'
5959
60+ import axios from ' @nextcloud/axios'
61+ import { showError , showSuccess } from ' @nextcloud/dialogs'
6062import { emit } from ' @nextcloud/event-bus'
6163import { FileType , NodeStatus , Permission } from ' @nextcloud/files'
62- import { loadState } from ' @nextcloud/initial-state'
63- import { showError , showSuccess } from ' @nextcloud/dialogs'
6464import { translate as t } from ' @nextcloud/l10n'
65- import axios from ' @nextcloud/axios'
66- import Vue from ' vue'
65+ import { defineComponent } from ' vue'
6766
6867import NcTextField from ' @nextcloud/vue/dist/Components/NcTextField.js'
6968
7069import { useNavigation } from ' ../../composables/useNavigation'
7170import { useRenamingStore } from ' ../../store/renaming.ts'
71+ import { getFilenameValidity } from ' ../../utils/filenameValidity.ts'
7272import logger from ' ../../logger.js'
7373
74- const forbiddenCharacters = loadState <string >(' files' , ' forbiddenCharacters' , ' ' ).split (' ' )
75-
76- export default Vue .extend ({
74+ export default defineComponent ({
7775 name: ' FileEntryName' ,
7876
7977 components: {
8078 NcTextField ,
8179 },
8280
8381 props: {
84- displayName: {
82+ /**
83+ * The filename without extension
84+ */
85+ basename: {
8586 type: String ,
8687 required: true ,
8788 },
89+ /**
90+ * The extension of the filename
91+ */
8892 extension: {
8993 type: String ,
9094 required: true ,
@@ -172,7 +176,7 @@ export default Vue.extend({
172176 params: {
173177 download: this .source .basename ,
174178 href: this .source .source ,
175- title: t (' files' , ' Download file {name}' , { name: this .displayName }),
179+ title: t (' files' , ' Download file {name}' , { name: ` ${ this .basename }${ this . extension } ` }),
176180 tabindex: ' 0' ,
177181 },
178182 }
@@ -198,70 +202,51 @@ export default Vue.extend({
198202 }
199203 },
200204 },
201- },
202205
203- methods: {
204- /**
205- * Check if the file name is valid and update the
206- * input validity using browser's native validation.
207- * @param event the keyup event
208- */
209- checkInputValidity(event ? : KeyboardEvent ) {
210- const input = event .target as HTMLInputElement
206+ newName() {
207+ // Check validity of the new name
211208 const newName = this .newName .trim ?.() || ' '
212- logger .debug (' Checking input validity' , { newName })
213- try {
214- this .isFileNameValid (newName )
215- input .setCustomValidity (' ' )
216- input .title = ' '
217- } catch (e ) {
218- input .setCustomValidity (e .message )
219- input .title = e .message
220- } finally {
221- input .reportValidity ()
222- }
223- },
224- isFileNameValid(name ) {
225- const trimmedName = name .trim ()
226- const char = trimmedName .indexOf (' /' ) !== - 1
227- ? ' /'
228- : forbiddenCharacters .find ((char ) => trimmedName .includes (char ))
229-
230- if (trimmedName === ' .' || trimmedName === ' ..' ) {
231- throw new Error (t (' files' , ' "{name}" is an invalid file name.' , { name }))
232- } else if (trimmedName .length === 0 ) {
233- throw new Error (t (' files' , ' File name cannot be empty.' ))
234- } else if (char ) {
235- throw new Error (t (' files' , ' "{char}" is not allowed inside a file name.' , { char }))
236- } else if (trimmedName .match (OC .config .blacklist_files_regex )) {
237- throw new Error (t (' files' , ' "{name}" is not an allowed filetype.' , { name }))
238- } else if (this .checkIfNodeExists (name )) {
239- throw new Error (t (' files' , ' {newName} already exists.' , { newName: name }))
209+ const input = (this .$refs .renameInput as Vue | undefined )?.$el .querySelector (' input' )
210+ if (! input ) {
211+ return
240212 }
241213
242- return true
214+ let validity = getFilenameValidity (newName )
215+ // Checking if already exists
216+ if (validity === ' ' && this .checkIfNodeExists (newName )) {
217+ validity = t (' files' , ' Another entry with the same name already exists.' )
218+ }
219+ this .$nextTick (() => {
220+ if (this .isRenaming ) {
221+ input .setCustomValidity (validity )
222+ input .reportValidity ()
223+ }
224+ })
243225 },
244- checkIfNodeExists(name ) {
226+ },
227+
228+ methods: {
229+ checkIfNodeExists(name : string ) {
245230 return this .nodes .find (node => node .basename === name && node !== this .source )
246231 },
247232
248233 startRenaming() {
249234 this .$nextTick (() => {
250235 // Using split to get the true string length
251- const extLength = (this .source .extension || ' ' ).split (' ' ).length
252- const length = this .source .basename .split (' ' ).length - extLength
253- const input = this .$refs .renameInput ?.$refs ?.inputField ?.$refs ?.input
236+ const input = (this .$refs .renameInput as Vue | undefined )?.$el .querySelector (' input' )
254237 if (! input ) {
255238 logger .error (' Could not find the rename input' )
256239 return
257240 }
258- input .setSelectionRange (0 , length )
259241 input .focus ()
242+ const length = this .source .basename .length - (this .source .extension ?? ' ' ).length
243+ input .setSelectionRange (0 , length )
260244
261245 // Trigger a keyup event to update the input validity
262246 input .dispatchEvent (new Event (' keyup' ))
263247 })
264248 },
249+
265250 stopRenaming() {
266251 if (! this .isRenaming ) {
267252 return
@@ -273,28 +258,23 @@ export default Vue.extend({
273258
274259 // Rename and move the file
275260 async onRename() {
276- const oldName = this .source .basename
277- const oldEncodedSource = this .source .encodedSource
278261 const newName = this .newName .trim ?.() || ' '
279- if (newName === ' ' ) {
280- showError (t (' files' , ' Name cannot be empty' ))
262+ const form = this .$refs .renameForm as HTMLFormElement
263+ if (! form .checkValidity ()) {
264+ showError (t (' files' , ' Invalid filename.' ) + ' ' + getFilenameValidity (newName ))
281265 return
282266 }
283267
268+ const oldName = this .source .basename
269+ const oldEncodedSource = this .source .encodedSource
284270 if (oldName === newName ) {
285271 this .stopRenaming ()
286272 return
287273 }
288274
289- // Checking if already exists
290- if (this .checkIfNodeExists (newName )) {
291- showError (t (' files' , ' Another entry with the same name already exists' ))
292- return
293- }
294-
295275 // Set loading state
296276 this .loading = ' renaming'
297- Vue . set (this .source , ' status' , NodeStatus .LOADING )
277+ this . $ set (this .source , ' status' , NodeStatus .LOADING )
298278
299279 // Update node
300280 this .source .rename (newName )
@@ -338,7 +318,7 @@ export default Vue.extend({
338318 showError (t (' files' , ' Could not rename "{oldName}"' , { oldName }))
339319 } finally {
340320 this .loading = false
341- Vue . set (this .source , ' status' , undefined )
321+ this . $ set (this .source , ' status' , undefined )
342322 }
343323 },
344324
0 commit comments