Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make context menu customizable #83

Merged
merged 2 commits into from
Dec 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 57 additions & 17 deletions examples/App.vue
Original file line number Diff line number Diff line change
@@ -1,42 +1,71 @@
<template>
<div class="wrapper">
<div style="font-weight: bold;padding: 10px">Inline select button example</div>

<label for="example">
Example
</label>
<div>
<select id="example" v-model="example">
<option v-for="(name, key) in examples" :value="key">{{ name }}</option>
</select>
</div>

<div style="font-weight: bold;padding: 10px">{{ examples[example] }}</div>
<vue-finder
v-if="example === 'default'"
id='my_vuefinder'
:request="request"
:max-file-size="maxFileSize"
:features="features"
:select-button="handleSelectButton"
/>

<br>
<br>
<div style="font-weight: bold;padding: 10px">External select example</div>
<div v-if="example === 'externalSelect'">
<vue-finder
id='my_vuefinder2'
:request="request"
:max-file-size="maxFileSize"
:features="features"
loadingIndicator="linear"
@select="handleSelect"
/>

<button class="btn" @click="handleButton" :disabled="!selectedFiles.length">Show Selected ({{ selectedFiles.length ?? 0 }} selected)</button>

<div v-show="selectedFiles.length">
<h3>Selected Files ({{ selectedFiles.length }} selected)</h3>
<ul>
<li v-for="file in selectedFiles" :key="file.path">
{{ file.path }}
</li>
</ul>
</div>
</div>

<vue-finder
id='my_vuefinder2'
v-if="example === 'contextmenu'"
id='my_vuefinder3'
:request="request"
:max-file-size="maxFileSize"
:features="features"
@select="handleSelect"
:select-button="handleSelectButton"
:context-menu-items="customContextMenuItems"
/>

<button class="btn" @click="handleButton" :disabled="!selectedFiles.length">Show Selected ({{ selectedFiles.length ?? 0 }} selected)</button>

<div v-show="selectedFiles.length">
<h3>Selected Files ({{ selectedFiles.length }} selected)</h3>
<ul>
<li v-for="file in selectedFiles" :key="file.path">
{{ file.path }}
</li>
</ul>
</div>

</div>
</template>

<script setup>
import { ref } from 'vue';
import { FEATURES, FEATURE_ALL_NAMES } from '../src/features.js';
import { contextMenuItems, SimpleContextMenuItem } from '../src/index.js';

const example = ref('default')
const examples = {
default: "Inline select button example",
externalSelect: "External select example",
contextmenu: "Custom context menu example",
}

/** @type {import('../src/utils/ajax.js').RequestConfig} */

Expand Down Expand Up @@ -100,6 +129,17 @@ const handleSelectButton = {
}
}

const customContextMenuItems = [
...contextMenuItems,
new SimpleContextMenuItem(() => 'Log Info', (app, selectedItems) => {
const info = selectedItems.value.map((i) => `Name: ${i.basename}, Type: ${i.type}, Path: ${i.path}`)
console.log(selectedItems.value.length + " item(s) selected:\n", info.join('\n'))
console.log(selectedItems.value)
}, undefined, {
target: undefined
})
]

</script>

<style>
Expand Down
2 changes: 2 additions & 0 deletions src/ServiceContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ export default (props, options) => {
showThumbnails: storage.getStore('show-thumbnails', props.showThumbnails),
// type of progress indicator
loadingIndicator: props.loadingIndicator,
// possible items of the context menu
contextMenuItems: props.contextMenuItems,

// file system
fs: useData(adapter, path),
Expand Down
154 changes: 17 additions & 137 deletions src/components/ContextMenu.vue
Original file line number Diff line number Diff line change
@@ -1,34 +1,26 @@
<template>
<ul ref="contextmenu" v-show="context.active" :style="context.positions"
class="vuefinder__context-menu">
<li class="vuefinder__context-menu__item" v-for="(item) in filteredItems" :key="item.title">
<li class="vuefinder__context-menu__item" v-for="(item) in context.items" :key="item.title">
<template v-if="item.link">
<a class="vuefinder__context-menu__link" target="_blank" :href="item.link" :download="item.link"
<a class="vuefinder__context-menu__link" target="_blank" :href="link(item)" :download="link(item)"
@click="app.emitter.emit('vf-contextmenu-hide')">
<span>{{ item.title() }}</span>
<span>{{ item.title(app.i18n) }}</span>
</a>
</template>
<template v-else>
<div class="vuefinder__context-menu__action" @click="run(item)">
<span>{{ item.title() }}</span>
<span>{{ item.title(app.i18n) }}</span>
</div>
</template>
</li>
</ul>
</template>

<script setup>
import {computed, inject, nextTick, reactive, ref} from 'vue';
import {FEATURES} from "../features.js";
import ModalNewFolder from "./modals/ModalNewFolder.vue";
import ModalPreview from "./modals/ModalPreview.vue";
import ModalArchive from "./modals/ModalArchive.vue";
import ModalUnarchive from "./modals/ModalUnarchive.vue";
import ModalRename from "./modals/ModalRename.vue";
import ModalDelete from "./modals/ModalDelete.vue";
import { inject, nextTick, reactive, ref} from 'vue';

const app = inject('ServiceContainer');
const {t} = app.i18n

const contextmenu = ref(null);
const selectedItems = ref([]);
Expand All @@ -43,160 +35,48 @@ const context = reactive({
}
});

const filteredItems = computed(() => {
return context.items.filter(item => item.key == null || app.features.includes(item.key))
});


app.emitter.on('vf-context-selected', (items) => {
selectedItems.value = items;
})

const menuItems = {
newfolder: {
key: FEATURES.NEW_FOLDER,
title: () => t('New Folder'),
action: () => app.modal.open(ModalNewFolder),
},
selectAll: {
title: () => t('Select All'),
action: () => app.dragSelect.selectAll(),
},
pinFolder: {
title: () => t('Pin Folder'),
action: () => {
app.pinnedFolders = app.pinnedFolders.concat(selectedItems.value);
app.storage.setStore('pinned-folders', app.pinnedFolders);
},
},

unpinFolder: {
title: () => t('Unpin Folder'),
action: () => {
app.pinnedFolders = app.pinnedFolders.filter(fav => !selectedItems.value.find(item => item.path === fav.path));
app.storage.setStore('pinned-folders', app.pinnedFolders);
},
},
delete: {
key: FEATURES.DELETE,
title: () => t('Delete'),
action: () => {
app.modal.open(ModalDelete, {items: selectedItems});
},
},
refresh: {
title: () => t('Refresh'),
action: () => {
app.emitter.emit('vf-fetch', {params: {q: 'index', adapter: app.fs.adapter, path: app.fs.data.dirname}});
},
},
preview: {
key: FEATURES.PREVIEW,
title: () => t('Preview'),
action: () => app.modal.open(ModalPreview, {adapter: app.fs.adapter, item: selectedItems.value[0]}),
},
open: {
title: () => t('Open'),
action: () => {
app.emitter.emit('vf-search-exit');
app.emitter.emit('vf-fetch', {
params: {
q: 'index',
adapter: app.fs.adapter,
path: selectedItems.value[0].path
}
});
},
},
openDir: {
title: () => t('Open containing folder'),
action: () => {
app.emitter.emit('vf-search-exit');
app.emitter.emit('vf-fetch', {
params: {
q: 'index',
adapter: app.fs.adapter,
path: (selectedItems.value[0].dir)
}
});
},
},
download: {
key: FEATURES.DOWNLOAD,
link: computed(() => app.requester.getDownloadUrl(app.fs.adapter, selectedItems.value[0])),
title: () => t('Download'),
action: () => {
},
},
archive: {
key: FEATURES.ARCHIVE,
title: () => t('Archive'),
action: () => app.modal.open(ModalArchive, {items: selectedItems}),
},
unarchive: {
key: FEATURES.UNARCHIVE,
title: () => t('Unarchive'),
action: () => app.modal.open(ModalUnarchive, {items: selectedItems}),
},
rename: {
key: FEATURES.RENAME,
title: () => t('Rename'),
action: () => app.modal.open(ModalRename, {items: selectedItems}),
}
};
const link = (item) => {
return item.link(app, selectedItems)
}

const run = (item) => {
app.emitter.emit('vf-contextmenu-hide');
item.action();
item.action(app, selectedItems);
};


app.emitter.on('vf-search-query', ({newQuery}) => {
searchQuery.value = newQuery;
});

app.emitter.on('vf-contextmenu-show', ({event, items, target = null}) => {
context.items = [];
context.items = app.contextMenuItems.filter((item) => {
return item.show(app, {
searchQuery: searchQuery.value,
items,
target
})
});

if (searchQuery.value) {
if (target) {
context.items.push(menuItems.openDir);
app.emitter.emit('vf-context-selected', [target]);
// console.log('search item selected');
} else {
return;
}
} else if (!target && !searchQuery.value) {
context.items.push(menuItems.refresh);
context.items.push(menuItems.selectAll);
context.items.push(menuItems.newfolder);
app.emitter.emit('vf-context-selected', []);
// console.log('no files selected');
} else if (items.length > 1 && items.some(el => el.path === target.path)) {
context.items.push(menuItems.refresh);
context.items.push(menuItems.archive);
context.items.push(menuItems.delete);
app.emitter.emit('vf-context-selected', items);
// console.log(items.length + ' selected (more than 1 item.)');
} else {
if (target.type === 'dir') {
context.items.push(menuItems.open);
if (app.pinnedFolders.findIndex((item) => item.path === target.path) !== -1) {
context.items.push(menuItems.unpinFolder);
} else {
context.items.push(menuItems.pinFolder);
}
} else {
context.items.push(menuItems.preview);
context.items.push(menuItems.download);
}
context.items.push(menuItems.rename);

if (target.mime_type === 'application/zip') {
context.items.push(menuItems.unarchive);
} else {
context.items.push(menuItems.archive);
}
context.items.push(menuItems.delete);
app.emitter.emit('vf-context-selected', [target]);
// console.log(target.type + ' is selected');
}
Expand Down
8 changes: 6 additions & 2 deletions src/components/VueFinder.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import Explorer from '../components/Explorer.vue';
import ContextMenu from '../components/ContextMenu.vue';
import Statusbar from '../components/Statusbar.vue';
import TreeView from '../components/TreeView.vue';
import { menuItems as contextMenuItems } from '../utils/contextmenu.js';


const emit = defineEmits(['select', 'update:path'])
Expand Down Expand Up @@ -119,8 +120,11 @@ const props = defineProps({
loadingIndicator: {
type: String,
default: 'circular'

}
},
contextMenuItems: {
type: Array,
default: () => contextMenuItems
},
});

// the object is passed to all components as props
Expand Down
6 changes: 5 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import VueFinder from './components/VueFinder.vue';
import './assets/css/style.scss';
import { menuItems, SimpleItem } from './utils/contextmenu';

export default {
/**
Expand All @@ -20,4 +21,7 @@ export default {
}
};


export {
menuItems as contextMenuItems,
SimpleItem as SimpleContextMenuItem,
}
Loading