vuetify
Version:
Vue Material Component Framework
313 lines (310 loc) • 10.7 kB
JavaScript
import { createVNode as _createVNode, createElementVNode as _createElementVNode, mergeProps as _mergeProps, normalizeClass as _normalizeClass, normalizeStyle as _normalizeStyle } from "vue";
// Styles
import "./VCommandPalette.css";
// Components
import { VCommandPaletteSymbol } from "./shared.js";
import { VCommandPaletteItem } from "./VCommandPaletteItem.js";
import { VDialog } from "../../components/VDialog/index.js";
import { makeVDialogProps } from "../../components/VDialog/VDialog.js";
import { VList } from "../../components/VList/index.js";
import { VSheet } from "../../components/VSheet/index.js";
import { VTextField } from "../../components/VTextField/index.js"; // Composables
import { useCommandPaletteNavigation } from "./composables/useCommandPaletteNavigation.js";
import { makeDensityProps } from "../../composables/density.js";
import { makeFilterProps, useFilter } from "../../composables/filter.js";
import { useHotkey } from "../../composables/hotkey/index.js";
import { useLocale } from "../../composables/locale.js";
import { useProxiedModel } from "../../composables/proxiedModel.js"; // Utilities
import { computed, nextTick, onUnmounted, provide, ref, shallowRef, toRef, watch, watchEffect } from 'vue';
import { isActionItem } from "./types.js";
import { genericComponent, omit, propsFactory, useRender } from "../../util/index.js"; // Types
export const makeVCommandPaletteProps = propsFactory({
modelValue: Boolean,
search: String,
items: {
type: Array,
default: () => []
},
placeholder: {
type: String,
default: '$vuetify.command.search'
},
inputIcon: {
type: String,
default: '$search'
},
hotkey: String,
closeOnSelect: {
type: Boolean,
default: true
},
noDataText: {
type: String,
default: '$vuetify.noDataText'
},
listProps: Object,
...makeFilterProps({
filterKeys: ['title', 'subtitle']
}),
...makeDensityProps(),
...omit(makeVDialogProps({
maxWidth: 500
}), ['modelValue'])
}, 'VCommandPalette');
export const VCommandPalette = genericComponent()({
name: 'VCommandPalette',
props: makeVCommandPaletteProps(),
emits: {
'update:modelValue': value => true,
'update:search': value => true,
'click:item': (item, event) => true,
'before-select': payload => true
},
setup(props, {
emit,
slots
}) {
const {
t
} = useLocale();
const isOpen = useProxiedModel(props, 'modelValue');
const searchQuery = useProxiedModel(props, 'search');
const searchInputRef = ref();
const dialogRef = ref();
const previouslyFocusedElement = shallowRef(null);
const internalItems = computed(() => props.items.map((item, index) => ({
value: index,
type: item.type,
raw: item,
...('title' in item && {
title: item.title
}),
...('subtitle' in item && {
subtitle: item.subtitle
})
})));
const {
filteredItems: filterResult
} = useFilter(props, internalItems, searchQuery);
const filteredItems = computed(() => filterResult.value.map(item => item.raw));
const itemsForList = computed(() => {
return filteredItems.value.map((item, idx) => ({
...item,
value: idx
}));
});
function executeItem(item, event) {
if ('onClick' in item && item.onClick) {
item.onClick(event, item.value);
}
emit('click:item', item, event);
if (!isActionItem(item) || !props.closeOnSelect) return;
let shouldClose = true;
emit('before-select', {
item,
event,
preventDefault: () => {
shouldClose = false;
}
});
if (shouldClose) {
isOpen.value = false;
}
}
const navigation = useCommandPaletteNavigation({
filteredItems,
onItemClick: (item, event) => {
executeItem(item, event);
}
});
provide(VCommandPaletteSymbol, {
items: computed(() => props.items),
filteredItems,
selectedIndex: navigation.selectedIndex,
search: searchQuery,
setSelectedIndex: navigation.setSelectedIndex
});
// Register main hotkey with cleanup - using toRef for reactivity
const hotkeyUnsubscribe = useHotkey(toRef(props, 'hotkey'), () => {
isOpen.value = !isOpen.value;
});
watchEffect(onCleanup => {
if (!isOpen.value) {
return;
}
const hotkeyUnsubscribes = [];
function registerItemHotkeys(items) {
items.forEach(item => {
if (isActionItem(item) && item.hotkey) {
const unsubscribe = useHotkey(item.hotkey, event => {
event.preventDefault();
executeItem(item, event);
}, {
inputs: true
});
hotkeyUnsubscribes.push(unsubscribe);
}
});
}
registerItemHotkeys(props.items);
onCleanup(() => {
hotkeyUnsubscribes.forEach(unsubscribe => unsubscribe?.());
});
});
function findNextSelectableIndex(startIndex, direction) {
const items = filteredItems.value;
if (items.length === 0) return -1;
let index = startIndex;
const maxIterations = items.length;
for (let i = 0; i < maxIterations; i++) {
index += direction;
if (index >= items.length) index = 0;
if (index < 0) index = items.length - 1;
if (isActionItem(items[index])) {
return index;
}
}
return -1;
}
function handleSearchKeydown(e) {
switch (e.key) {
case 'ArrowDown':
{
e.preventDefault();
const nextIndex = findNextSelectableIndex(navigation.selectedIndex.value, 1);
if (nextIndex !== -1) {
navigation.setSelectedIndex(nextIndex);
}
break;
}
case 'ArrowUp':
{
e.preventDefault();
const prevIndex = findNextSelectableIndex(navigation.selectedIndex.value, -1);
if (prevIndex !== -1) {
navigation.setSelectedIndex(prevIndex);
}
break;
}
case 'Enter':
e.preventDefault();
navigation.executeSelected(e);
break;
case 'Escape':
e.preventDefault();
isOpen.value = false;
break;
}
}
watch(isOpen, (newValue, oldValue) => {
if (newValue && !oldValue) {
previouslyFocusedElement.value = document.activeElement;
searchQuery.value = '';
navigation.reset();
// Use requestAnimationFrame to ensure DOM is fully rendered
nextTick(() => {
requestAnimationFrame(() => {
if (searchInputRef.value && typeof searchInputRef.value.focus === 'function') {
searchInputRef.value.focus();
}
});
});
} else if (!newValue && oldValue) {
nextTick(() => {
previouslyFocusedElement.value?.focus({
preventScroll: true
});
previouslyFocusedElement.value = null;
});
}
}, {
immediate: true
});
onUnmounted(() => {
hotkeyUnsubscribe();
previouslyFocusedElement.value = null;
});
useRender(() => {
const dialogProps = VDialog.filterProps(omit(props, ['modelValue', 'class', 'style']));
return _createVNode(VDialog, _mergeProps({
"ref": dialogRef,
"class": "v-command-palette",
"modelValue": isOpen.value,
"onUpdate:modelValue": $event => isOpen.value = $event,
"scrollable": true
}, dialogProps), {
activator: slots.activator,
default: () => _createVNode(VSheet, {
"class": _normalizeClass(props.class),
"style": _normalizeStyle(props.style)
}, {
default: () => [slots.prepend?.(), _createElementVNode("div", {
"class": "v-command-palette__input-container"
}, [slots.input?.() ?? _createVNode(VTextField, {
"ref": searchInputRef,
"modelValue": searchQuery.value,
"onUpdate:modelValue": $event => searchQuery.value = $event,
"density": props.density,
"placeholder": t(props.placeholder),
"prependInnerIcon": props.inputIcon,
"autocomplete": "off",
"autofocus": true,
"singleLine": true,
"hideDetails": true,
"variant": "solo",
"flat": true,
"bgColor": "transparent",
"onKeydown": handleSearchKeydown
}, {
'append-inner': slots['input.append-inner']
})]), _createElementVNode("div", {
"class": "v-command-palette__content"
}, [filteredItems.value.length > 0 ? _createVNode(VList, _mergeProps({
"key": "list",
"class": "v-command-palette__list",
"density": props.density,
"items": itemsForList.value,
"itemType": "type",
"itemProps": true,
"activatable": true
}, props.listProps, {
"navigationStrategy": "track",
"navigationIndex": navigation.selectedIndex.value,
"onUpdate:navigationIndex": navigation.setSelectedIndex
}), {
prepend: slots['list.prepend'],
subheader: slots['list.subheader'],
item: ({
props: itemProps
}) => slots.item?.({
item: itemProps,
index: itemProps.index
}) ?? _createVNode(VCommandPaletteItem, {
"key": `item-${itemProps.index}`,
"item": itemProps,
"index": itemProps.index,
"onExecute": event => navigation.execute(itemProps.index, event)
}, {
prepend: slots['item.prepend'] ? () => slots['item.prepend']?.({
item: itemProps,
index: itemProps.index
}) : undefined,
title: slots['item.title'] ? () => slots['item.title']?.({
item: itemProps,
index: itemProps.index
}) : undefined,
append: slots['item.append'] ? () => slots['item.append']?.({
item: itemProps,
index: itemProps.index
}) : undefined
})
}) : _createElementVNode("div", {
"key": "no-data",
"class": "v-command-palette__no-data"
}, [slots['no-data']?.() || t(props.noDataText)])]), slots.append?.()]
})
});
});
}
});
//# sourceMappingURL=VCommandPalette.js.map