UNPKG

@coreui/vue-pro

Version:

UI Components Library for Vue.js

309 lines (306 loc) 12.4 kB
import { defineComponent, ref, computed, watch, nextTick, onUnmounted, provide, h } from 'vue'; import { usePopper } from '../../composables/usePopper.js'; import getNextActiveElement from '../../utils/getNextActiveElement.js'; import isRTL from '../../utils/isRTL.js'; import { getPlacement, getReferenceElement } from './utils.js'; import { CFocusTrap } from '../focus-trap/CFocusTrap.js'; const CDropdown = defineComponent({ name: 'CDropdown', props: { /** * Set aligment of dropdown menu. * * @values { 'start' | 'end' | { xs: 'start' | 'end' } | { sm: 'start' | 'end' } | { md: 'start' | 'end' } | { lg: 'start' | 'end' } | { xl: 'start' | 'end'} | { xxl: 'start' | 'end'} } */ alignment: { type: [String, Object], // eslint-disable-next-line @typescript-eslint/no-explicit-any validator: (value) => { if (value === 'start' || value === 'end') { return true; } else { if (value.xs !== undefined && (value.xs === 'start' || value.xs === 'end')) { return true; } if (value.sm !== undefined && (value.sm === 'start' || value.sm === 'end')) { return true; } if (value.md !== undefined && (value.md === 'start' || value.md === 'end')) { return true; } if (value.lg !== undefined && (value.lg === 'start' || value.lg === 'end')) { return true; } if (value.xl !== undefined && (value.xl === 'start' || value.xl === 'end')) { return true; } if (value.xxl !== undefined && (value.xxl === 'start' || value.xxl === 'end')) { return true; } return false; } }, }, /** * Configure the auto close behavior of the dropdown: * - `true` - the dropdown will be closed by clicking outside or inside the dropdown menu. * - `false` - the dropdown will be closed by clicking the toggle button and manually calling hide or toggle method. (Also will not be closed by pressing esc key) * - `'inside'` - the dropdown will be closed (only) by clicking inside the dropdown menu. * - `'outside'` - the dropdown will be closed (only) by clicking outside the dropdown menu. */ autoClose: { type: [Boolean, String], default: true, validator: (value) => { return typeof value === 'boolean' || ['inside', 'outside'].includes(value); }, }, /** * Appends the vue dropdown menu to a specific element. You can pass an HTML element or function that returns a single element. By default `document.body`. * * @since 5.0.0 */ container: { type: [Object, String], default: 'body', }, /** * Sets a darker color scheme to match a dark navbar. */ dark: Boolean, /** * Sets a specified direction and location of the dropdown menu. * * @values 'center', 'dropup', 'dropup-center', 'dropend', 'dropstart' */ direction: { type: String, validator: (value) => { return ['center', 'dropup', 'dropup-center', 'dropend', 'dropstart'].includes(value); }, }, /** * Toggle the disabled state for the component. */ disabled: Boolean, /** * Offset of the dropdown menu relative to its target. * * @since 4.9.0 */ offset: { type: Array, default: () => [0, 2], }, /** * Describes the placement of your component after Popper.js has applied all the modifiers that may have flipped or altered the originally provided placement property. * * @values 'auto', 'top-end', 'top', 'top-start', 'bottom-end', 'bottom', 'bottom-start', 'right-start', 'right', 'right-end', 'left-start', 'left', 'left-end' */ placement: { type: String, default: 'bottom-start', }, /** * If you want to disable dynamic positioning set this property to `true`. */ popper: { type: Boolean, default: true, }, /** * Sets the reference element for positioning the Vue Dropdown Menu. * - `toggle` - The Vue Dropdown Toggle button (default). * - `parent` - The Vue Dropdown wrapper element. * - `HTMLElement` - A custom HTML element. * - `Ref` - A custom reference element. * * @since 5.7.0 */ reference: { type: [String, Object], default: 'toggle', }, /** * Generates dropdown menu using Teleport. * * @since 5.0.0 */ teleport: { type: Boolean, default: false, }, /** * Sets which event handlers you’d like provided to your toggle prop. You can specify one trigger or an array of them. */ trigger: { type: String, default: 'click', }, /** * Set the dropdown variant to an btn-group, dropdown, input-group, and nav-item. * * @values 'btn-group', 'dropdown', 'input-group', 'nav-item' */ variant: { type: String, default: 'btn-group', validator: (value) => { return ['btn-group', 'dropdown', 'input-group', 'nav-item'].includes(value); }, }, /** * Toggle the visibility of dropdown menu component. */ visible: Boolean, }, emits: [ /** * Callback fired when the component requests to be hidden. */ 'hide', /** * Callback fired when the component requests to be shown. */ 'show', ], setup(props, { slots, emit }) { const dropdownRef = ref(null); const dropdownMenuRef = ref(null); const dropdownToggleRef = ref(null); const pendingKeyDownEventRef = ref(null); const popper = ref(typeof props.alignment === 'object' ? false : props.popper); const visible = ref(props.visible); const { initPopper, destroyPopper } = usePopper(); const popperConfig = computed(() => ({ modifiers: [ { name: 'offset', options: { offset: props.offset, }, }, ], placement: getPlacement(props.placement, props.direction, props.alignment, isRTL(dropdownMenuRef.value)), })); watch(() => props.visible, () => { visible.value = props.visible; }); watch(visible, () => { if (visible.value && dropdownToggleRef.value && dropdownMenuRef.value) { const referenceElement = getReferenceElement(props.reference, dropdownToggleRef, dropdownRef); if (referenceElement && popper.value) { initPopper(referenceElement, dropdownMenuRef.value, popperConfig.value); } window.addEventListener('click', handleClick); window.addEventListener('keyup', handleKeyup); dropdownToggleRef.value.addEventListener('keydown', handleKeydown); dropdownMenuRef.value.addEventListener('keydown', handleKeydown); if (pendingKeyDownEventRef.value) { nextTick(() => { handleKeydown(pendingKeyDownEventRef.value); pendingKeyDownEventRef.value = null; }); } emit('show'); return; } if (popper.value) { destroyPopper(); } window.removeEventListener('click', handleClick); window.removeEventListener('keyup', handleKeyup); dropdownMenuRef.value && dropdownMenuRef.value.removeEventListener('keydown', handleKeydown); dropdownToggleRef.value && dropdownToggleRef.value.removeEventListener('keydown', handleKeydown); emit('hide'); }); onUnmounted(() => { dropdownToggleRef.value && dropdownToggleRef.value.removeEventListener('keydown', handleKeydown); dropdownMenuRef.value && dropdownMenuRef.value.removeEventListener('keydown', handleKeydown); }); provide('config', { alignment: props.alignment, container: props.container, dark: props.dark, popper: props.popper, teleport: props.teleport, }); provide('variant', props.variant); provide('visible', visible); provide('dropdownToggleRef', dropdownToggleRef); provide('dropdownMenuRef', dropdownMenuRef); provide('pendingKeyDownEventRef', pendingKeyDownEventRef); const handleKeydown = (event) => { if (dropdownMenuRef.value && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) { event.preventDefault(); const target = event.target; const items = Array.from(dropdownMenuRef.value.querySelectorAll('.dropdown-item:not(.disabled):not(:disabled)')); getNextActiveElement(items, target, event.key === 'ArrowDown', true).focus(); } }; const handleKeyup = (event) => { if (props.autoClose === false) { return; } if (event.key === 'Escape') { setVisible(false); dropdownToggleRef.value?.focus(); } }; const handleClick = (event) => { if (!dropdownToggleRef.value || !dropdownMenuRef.value) { return; } if (event.button === 2) { return; } const composedPath = event.composedPath(); const isOnToggle = composedPath.includes(dropdownToggleRef.value); const isOnMenu = composedPath.includes(dropdownMenuRef.value); if (isOnToggle) { return; } const target = event.target; const FORM_TAG_RE = /^(input|select|option|textarea|form|button|label)$/i; if (isOnMenu && target && FORM_TAG_RE.test(target.tagName)) { return; } if (props.autoClose === true || (props.autoClose === 'inside' && isOnMenu) || (props.autoClose === 'outside' && !isOnMenu)) { setVisible(false); } }; const setVisible = (_visible, event) => { if (props.disabled) { return; } if (typeof _visible === 'boolean') { if (event) { pendingKeyDownEventRef.value = event || null; } visible.value = _visible; return; } }; provide('setVisible', setVisible); return () => h(CFocusTrap, { active: props.teleport && visible.value, additionalContainer: dropdownMenuRef }, () => props.variant === 'input-group' ? [slots.default && slots.default()] : h('div', { class: [ props.variant === 'nav-item' ? 'nav-item dropdown' : props.variant, props.direction === 'center' ? 'dropdown-center' : props.direction === 'dropup-center' ? 'dropup dropup-center' : props.direction, ], ref: dropdownRef, }, slots.default && slots.default())); }, }); export { CDropdown }; //# sourceMappingURL=CDropdown.js.map