![](https://webmobtuts.com/wp-content/uploads/2025/02/Building-Simple-File-Manager-With-Laravel-and-Vue-3-1-800x400.jpg)
We will continue what we done in the last part of file manager implementation and in this part we will handle file selection and un-selection and double clicking on directory.
Handling dblClick Event On Directory
When user double click on directory it should open this directory and list it’s files. To do so open file-list.vue file, and add the defineEmits()
function as shown:
resources/js/app/components/file-list.vue
<script setup> import FileRow from "./file-row.vue"; ..... const emits = defineEmits(['onDblClickFile']); </script> <template> <div id="file-list" class="mt-3"> <table class="table table-hover"> <thead class="table-light"> <tr> <th>Name</th> <th>Size</th> <th>Last Modified</th> <th>Type</th> <th>Permissions</th> </tr> </thead> <tbody class="table-group-divider"> <FileRow v-for="(file_item, index) in file_list" :key="index" :fileItem="file_item" @dblclick="$emit('onDblClickFile', file_item)" /> </tbody> </table> </div> </template>
In this code we listen for the @dblclick
event and emit a custom event that we defined above which is “onDblClickFile
” and sending the file_item
object as an argument.
Then in the <App />
component let’s listen for the emitted event on <FileList />
component:
App.vue
<template> .... .... <FileList :file_list="file_list" @onDblClickFile="handleDblClickFile" /> .... </template>
Next in component <script setup>
add this function after navigateToDir()
function like so:
<script setup> import {removeLastPathSegment} from "./helpers/functions.js" .... .... const navigateToDir = (ev) => { ... } const handleDblClickFile = (ev) => { if(ev.type === 'directory') { // If type=directory open it if(ev.name === '..') { // if name=".." then go to previous directory navigation_dir.value = removeLastPathSegment(navigation_dir.value) + "/"; } else { const append_dir = (navigation_dir.value.endsWith('/') ? navigation_dir.value : navigation_dir.value+'/') + ev.name + "/"; navigation_dir.value = append_dir; } fetchFiles(navigation_dir.value); } } </script>
At first i imported the removeLastPathSegment()
function we declared in the previous part. In the handleDblClickFile()
i check for ev.type
which tells that this is a directory or file:
if(ev.type === 'directory') {
If the case is directory then we do another check to determine if the directory name='..'
in that case we navigate up one level from the current directory by removing the last segment of the path:
if(ev.name === '..') { navigation_dir.value = removeLastPathSegment(navigation_dir.value) + "/"; }
Otherwise we append current directory to the current path:
const append_dir = (navigation_dir.value.endsWith('/') ? navigation_dir.value : navigation_dir.value+'/') + ev.name + "/"; navigation_dir.value = append_dir;
Then we invoke the fetchFiles()
function which make axios request to retrieve the files.
Handling Selecting and Deselecting Files
The file manager should handle selecting and deselecting files so that we can make actions on files like deleting, downloading, editing, etc.
We will handle three kinds of file selection operations:
- Selecting single file: Using the normal mouse click.
- Selecting multiple files with ctrl key: Using the normal mouse click while holding the ctrl key.
- Selecting range of files with shift key: Using the normal mouse click while holding the shift key.
I created a new composable file which contains the logic of file selections:
resources/js/app/composables/useSelectedFiles.js
import {ref} from "vue"; export const useSelectedFiles = (file_list) => { const selected_files = ref([]); const clearSelectedFiles = () => { selected_files.value = []; } const handleSelectSingleFile = (ev) => { selected_files.value = []; selected_files.value.push(ev); } const handleSelectFileWithCtrl = (ev) => { if(selected_files.value.find((f) => f.index===ev.index)) { selected_files.value = selected_files.value.filter((f) => f.index !== ev.index); } else { selected_files.value.push(ev); } } const handleSelectFileWithShift = (ev) => { if(!selected_files.value.length) { selected_files.value.push(ev); return; } // If select a file with index greater than the selected files first file index if(ev.index > selected_files.value[0].index) { for(var i=selected_files.value[0].index; i<=ev.index; i++) { if(i !== selected_files.value[0].index && selected_files.value.find((f) => f.index === i) === undefined) { selected_files.value.push({index: i, file_item: file_list.value[i] }); } } } // If select a file with index lower than the selected files last file index if(ev.index < selected_files.value[selected_files.value.length-1].index) { for(var j=selected_files.value[selected_files.value.length-1].index; j >= ev.index; j--) { if(selected_files.value.find((f) => f.index === j) === undefined) { selected_files.value.push({index: j, file_item: file_list.value[j] }); } } } } const handleSelectAll = () => { selected_files.value = []; selected_files.value = file_list.value.map((item, index) => ({index, file_item: item})); } return { selected_files, handleSelectSingleFile, handleSelectFileWithCtrl, handleSelectFileWithShift, clearSelectedFiles, handleSelectAll } }
In the above code the useSelectedFiles()
composable accept the file_list
as an argument. The selected_files
reactive variable is declared which stores an array of selected files. Then i added several functions to deal with file selections operations
The handleSelectSingleFile()
store a single file entry in the selected_files
array.
The handleSelectWithWithCtrl()
supposed to be called while holding ctrl key and stores multiples files in the selected_files
array.
The handleSelectWithShift()
is called while holding the keyboard shift key and stores a range of files. To do so we have to check for the current file selected index. If the file index greater than the selected files first file index then we select all files downwards:
if(ev.index > selected_files.value[0].index) { for(var i=selected_files.value[0].index; i<=ev.index; i++) { } }
If the file index lower than the selected files last file index then we select all files upwards:
if(ev.index < selected_files.value[selected_files.value.length-1].index) { for(var j=selected_files.value[selected_files.value.length-1].index; j >= ev.index; j--) { } }
The last function handleSelectAll()
select all files in the current screen. Then we return an object from the composable with the required data and functions that we need.
Let’s use this composable in our <App /> component:
App.vue
First update the <script setup> as shown:
<script setup> .... .... import {useSelectedFiles} from "./composables/useSelectedFiles.js"; // add this line .... .... const {file_list, fetchFiles} = useNavigation(navigation_dir.value); const {selected_files, handleSelectSingleFile, handleSelectFileWithShift, handleSelectFileWithCtrl, clearSelectedFiles, handleSelectAll} = useSelectedFiles(file_list); // add this line const navigateToDir = (ev) => { fetchFiles(ev); navigation_dir.value = ev; clearSelectedFiles(); // add this line } const handleDblClickFile = (ev) => { if(ev.type === 'directory') { // If type=directory open it if(ev.name === '..') { // if name=".." then go to previous directory navigation_dir.value = removeLastPathSegment(navigation_dir.value) + "/"; } else { const append_dir = (navigation_dir.value.endsWith('/') ? navigation_dir.value : navigation_dir.value+'/') + ev.name + "/"; navigation_dir.value = append_dir; } fetchFiles(navigation_dir.value); clearSelectedFiles(); // add this line } } </script>
Next update the used components in <template> tag:
<template> ..... ..... <DirectoryNavigation :nav_dir="navigation_dir" @navigate="navigateToDir($event)" @select-all="handleSelectAll" @unselect-all="clearSelectedFiles" /> <FileList :file_list="file_list" @onDblClickFile="handleDblClickFile" :selected_files="selected_files" @select-file-single="handleSelectSingleFile" @select-multiple-files="handleSelectFileWithCtrl" @select-file-range="handleSelectFileWithShift" /> .... .... </template>
As you see in the component <script> i imported the {useSelectedFiles
} composable, then we called it extracting the object return:
const {selected_files, handleSelectSingleFile, handleSelectFileWithShift, handleSelectFileWithCtrl, clearSelectedFiles, handleSelectAll} = useSelectedFiles(file_list);
Next we have done several updates to the navigateToDir()
function by calling clearSelectedFiles()
which is essential after fetching the file list. The same is done for handleDblClickFile()
function.
In the <template> tag i updated components <DirectoryNavigation/>
and <FileList />
by adding these props and events:
@select-all
@unselect-all
-
:selected_files
@select-file-single
@select-multiple-files
@select-file-range
Now let’s open each of these components and update as shown:
file-list.vue
<script setup> import FileRow from "./file-row.vue"; const props = defineProps({ file_list: { type: Array }, selected_files: { type: Array } }); const emits = defineEmits(['selectFileSingle', 'selectFileRange', 'selectMultipleFiles', 'onDblClickFile']); const is_selected = (index) => { return (props.selected_files.length && (props.selected_files.find((f) => f.index === index) !== undefined))===true; }; const handleSelectFile = (event, index, file_item) => { if(event.ctrlKey) { // Triggered if clicked while holding ctrl key emits('selectMultipleFiles', {index, file_item}) } else if(event.shiftKey) { // Triggered if clicked while holding shift key emits('selectFileRange', {index, file_item}) } else { // Triggered if clicked only emits('selectFileSingle', {index, file_item}) } } </script> <template> <div id="file-list" class="mt-3"> <table class="table table-hover"> <thead class="table-light"> <tr> <th>Name</th> <th>Size</th> <th>Last Modified</th> <th>Type</th> <th>Permissions</th> </tr> </thead> <tbody class="table-group-divider"> <FileRow v-for="(file_item, index) in file_list" :key="index" :fileItem="file_item" @click.prevent="handleSelectFile($event, index, file_item)" @dblclick="$emit('onDblClickFile', file_item)" :is_selected="is_selected(index)" /> </tbody> </table> </div> </template>
In the above code i added the selected_files
prop to the defineProps()
function. Next i modified the defineEmits()
function to include the other events like selectFileSingle
, selectFileRange
, etc.
The is_selected()
function return whether the file record is selected or not.
The handleSelectFile()
function triggered when selecting particular file(s) on the <FileRow/>
component.
The function accept three arguments: event, index
, and the file_item
respectively. Then we check for the event keyboard key using event.ctrl
or event.shift
then we emit the appropriate event.
Modify the other component directory-navigation.vue
as follows:
<script setup> ..... ..... const emits = defineEmits(['navigate', 'selectAll', 'unselectAll']); // update this line .... .... </script> <template> <div id="navigator" class="d-flex gap-2 pt-2 pb-2"> .... .... <div id="directory-select-all"> <a href="#" class="text-decoration-none" @click.prevent="$emit('selectAll')"> <i class="bi bi-check-square mx-1"></i> <span>Select all</span> </a> </div> <div id="directory-unselect-all"> <a href="#" class="text-decoration-none" @click.prevent="$emit('unselectAll')"> <i class="bi bi-square mx-1"></i> <span>Unselect all</span> </a> </div> </div> </template>
In this code i updated the defineEmits()
function and added selectAll
and unselectAll
events. In template i added the @click
events for both the “Select all” and “Unselect all” buttons.