UNPKG

vuetify

Version:

Vue Material Component Framework

313 lines (310 loc) 10.7 kB
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