UNPKG

@ishitatsuyuki/oruga-next

Version:

UI components for Vue.js and CSS framework agnostic

604 lines (595 loc) 20.6 kB
import { defineComponent, resolveDirective, openBlock, createBlock, withModifiers, renderSlot, createCommentVNode, createVNode, Transition, withCtx, withDirectives, vShow, resolveDynamicComponent } from 'vue'; import { getValueByPath, toCssDimension, createAbsoluteElement, removeElement } from './helpers.mjs'; import { getOptions } from './config.mjs'; import { B as BaseComponentMixin } from './BaseComponentMixin-d78ed3dc.mjs'; import { M as MatchMediaMixin } from './MatchMediaMixin-6f49ea3f.mjs'; import { d as directive } from './trapFocus-dc03669f.mjs'; /** * Dropdowns are very versatile, can used as a quick menu or even like a select for discoverable content * @displayName Dropdown * @requires ./DropdownItem.vue * @style _dropdown.scss */ var script = defineComponent({ name: 'ODropdown', directives: { trapFocus: directive }, configField: 'dropdown', mixins: [BaseComponentMixin, MatchMediaMixin], provide() { return { $dropdown: this }; }, emits: ['update:modelValue', 'active-change', 'change'], props: { /** @model */ modelValue: { type: [String, Number, Boolean, Object, Array], default: null }, /** * Dropdown disabled */ disabled: Boolean, /** * Dropdown content (items) are shown inline, trigger is removed */ inline: Boolean, /** * Dropdown content will be scrollable */ scrollable: Boolean, /** * Max height of dropdown content */ maxHeight: { type: [String, Number], default: () => { return getValueByPath(getOptions(), 'dropdown.maxHeight', 200); } }, /** * Optional, position of the dropdown relative to the trigger * @values top-right, top-left, bottom-left */ position: { type: String, validator: (value) => { return [ 'top-right', 'top-left', 'bottom-left', 'bottom-right' ].indexOf(value) > -1; } }, /** * Dropdown content (items) are shown into a modal on mobile */ mobileModal: { type: Boolean, default: () => { return getValueByPath(getOptions(), 'dropdown.mobileModal', true); } }, /** * Role attribute to be passed to list container for better accessibility. Use menu only in situations where your dropdown is related to navigation menus * @values list, menu, dialog */ ariaRole: { type: String, validator: (value) => { return [ 'menu', 'list', 'dialog' ].indexOf(value) > -1; }, default: null }, /** * Custom animation (transition name) */ animation: { type: String, default: () => { return getValueByPath(getOptions(), 'dropdown.animation', 'fade'); } }, /** * Allows multiple selections */ multiple: Boolean, /** * Trap focus inside the dropdown. */ trapFocus: { type: Boolean, default: () => { return getValueByPath(getOptions(), 'dropdown.trapFocus', true); } }, /** * Close dropdown when content is clicked */ closeOnClick: { type: Boolean, default: true }, /** * Can close dropdown by pressing escape or by clicking outside * @values escape, outside */ canClose: { type: [Array, Boolean], default: true }, /** * Dropdown will be expanded (full-width) */ expanded: Boolean, /** * Dropdown will be triggered by any events * @values click, hover, contextmenu, focus */ triggers: { type: Array, default: () => ['click'] }, /** * Append dropdown content to body */ appendToBody: Boolean, /** * Dropdown menu tag name */ menuTag: { type: String, default: () => { return getValueByPath(getOptions(), 'dropdown.menuTag', 'div'); } }, /** * @ignore */ appendToBodyCopyParent: Boolean, rootClass: [String, Function, Array], triggerClass: [String, Function, Array], inlineClass: [String, Function, Array], menuMobileOverlayClass: [String, Function, Array], menuClass: [String, Function, Array], menuPositionClass: [String, Function, Array], menuActiveClass: [String, Function, Array], mobileClass: [String, Function, Array], disabledClass: [String, Function, Array], expandedClass: [String, Function, Array] }, data() { return { selected: this.modelValue, isActive: false, isHoverable: false, bodyEl: undefined // Used to append to body }; }, computed: { rootClasses() { return [ this.computedClass('rootClass', 'o-drop'), { [this.computedClass('disabledClass', 'o-drop--disabled')]: this.disabled }, { [this.computedClass('expandedClass', 'o-drop--expanded')]: this.expanded }, { [this.computedClass('inlineClass', 'o-drop--inline')]: this.inline }, { [this.computedClass('mobileClass', 'o-drop--mobile')]: this.isMobileModal && this.isMatchMedia && !this.hoverable }, ]; }, triggerClasses() { return [ this.computedClass('triggerClass', 'o-drop__trigger') ]; }, menuMobileOverlayClasses() { return [ this.computedClass('menuMobileOverlayClass', 'o-drop__overlay') ]; }, menuClasses() { return [ this.computedClass('menuClass', 'o-drop__menu'), { [this.computedClass('menuPositionClass', 'o-drop__menu--', this.position)]: this.position }, { [this.computedClass('menuActiveClass', 'o-drop__menu--active')]: (this.isActive || this.inline) } ]; }, isMobileModal() { return this.mobileModal && !this.inline; }, cancelOptions() { return typeof this.canClose === 'boolean' ? this.canClose ? ['escape', 'outside'] : [] : this.canClose; }, menuStyle() { return { maxHeight: this.scrollable ? toCssDimension(this.maxHeight) : null, overflow: this.scrollable ? 'auto' : null }; }, hoverable() { return this.triggers.indexOf('hover') >= 0; } }, watch: { /** * When v-model is changed set the new selected item. */ modelValue(value) { this.selected = value; }, /** * Emit event when isActive value is changed. */ isActive(value) { this.$emit('active-change', value); if (this.appendToBody) { this.$nextTick(() => { this.updateAppendToBody(); }); } } }, methods: { /** * Click listener from DropdownItem. * 1. Set new selected item. * 2. Emit input event to update the user v-model. * 3. Close the dropdown. */ selectItem(value) { if (this.multiple) { if (this.selected) { if (this.selected.indexOf(value) === -1) { // Add value this.selected = [...this.selected, value]; } else { // Remove value this.selected = this.selected.filter((val) => val !== value); } } else { this.selected = [value]; } this.$emit('change', this.selected); } else { if (this.selected !== value) { this.selected = value; this.$emit('change', this.selected); } } this.$emit('update:modelValue', this.selected); if (!this.multiple) { this.isActive = !this.closeOnClick; if (this.hoverable && this.closeOnClick) { this.isHoverable = false; } } }, /** * White-listed items to not close when clicked. */ isInWhiteList(el) { if (el === this.$refs.dropdownMenu) return true; if (el === this.$refs.trigger) return true; // All chidren from dropdown if (this.$refs.dropdownMenu !== undefined) { const children = this.$refs.dropdownMenu.querySelectorAll('*'); for (const child of children) { if (el === child) { return true; } } } // All children from trigger if (this.$refs.trigger !== undefined) { const children = this.$refs.trigger.querySelectorAll('*'); for (const child of children) { if (el === child) { return true; } } } return false; }, /** * Close dropdown if clicked outside. */ clickedOutside(event) { if (this.cancelOptions.indexOf('outside') < 0) return; if (this.inline) return; if (!this.isInWhiteList(event.target)) this.isActive = false; }, /** * Keypress event that is bound to the document */ keyPress({ key }) { if (this.isActive && (key === 'Escape' || key === 'Esc')) { if (this.cancelOptions.indexOf('escape') < 0) return; this.isActive = false; } }, onClick() { if (this.triggers.indexOf('click') < 0) return; this.toggle(); }, onContextMenu() { if (this.triggers.indexOf('contextmenu') < 0) return; this.toggle(); }, onHover() { if (this.triggers.indexOf('hover') < 0) return; this.isHoverable = true; }, onFocus() { if (this.triggers.indexOf('focus') < 0) return; this.toggle(); }, /** * Toggle dropdown if it's not disabled. */ toggle() { if (this.disabled) return; if (!this.isActive) { // if not active, toggle after clickOutside event // this fixes toggling programmatic this.$nextTick(() => { const value = !this.isActive; this.isActive = value; // Vue 2.6.x ??? setTimeout(() => (this.isActive = value)); }); } else { this.isActive = !this.isActive; } }, updateAppendToBody() { const dropdownMenu = this.$refs.dropdownMenu; const trigger = this.$refs.trigger; if (dropdownMenu && trigger) { // update wrapper dropdown const dropdown = this.$data.bodyEl.children[0]; dropdown.classList.forEach((item) => dropdown.classList.remove(...item.split(' '))); this.rootClasses.forEach((item) => { if (item) { if (typeof item === 'object') { Object.keys(item).filter(key => key && item[key]).forEach(key => dropdown.classList.add(key)); } else { dropdown.classList.add(...item.split(' ')); } } }); if (this.appendToBodyCopyParent) { const parentNode = this.$refs.dropdown.parentNode; const parent = this.$data.bodyEl; parent.classList.forEach((item) => parent.classList.remove(...item.split(' '))); parentNode.classList.forEach((item) => parent.classList.add(...item.split(' '))); } const rect = trigger.getBoundingClientRect(); let top = rect.top + window.scrollY; let left = rect.left + window.scrollX; if (!this.position || this.position.indexOf('bottom') >= 0) { top += trigger.clientHeight; } else { top -= dropdownMenu.clientHeight; } if (this.position && this.position.indexOf('left') >= 0) { left -= (dropdownMenu.clientWidth - trigger.clientWidth); } dropdownMenu.style.position = 'absolute'; dropdownMenu.style.top = `${top}px`; dropdownMenu.style.left = `${left}px`; dropdownMenu.style.zIndex = '9999'; } } }, mounted() { if (this.appendToBody) { this.$data.bodyEl = createAbsoluteElement(this.$refs.dropdownMenu); this.updateAppendToBody(); } }, created() { if (typeof window !== 'undefined') { document.addEventListener('click', this.clickedOutside); document.addEventListener('keyup', this.keyPress); } }, beforeUnmount() { if (typeof window !== 'undefined') { document.removeEventListener('click', this.clickedOutside); document.removeEventListener('keyup', this.keyPress); } if (this.appendToBody) { removeElement(this.$data.bodyEl); } } }); function render(_ctx, _cache, $props, $setup, $data, $options) { const _directive_trap_focus = resolveDirective("trap-focus"); return openBlock(), createBlock("div", { ref: "dropdown", class: _ctx.rootClasses, onMouseleave: _cache[5] || (_cache[5] = $event => _ctx.isHoverable = false) }, [!_ctx.inline ? (openBlock(), createBlock("div", { key: 0, tabindex: _ctx.disabled ? null : 0, ref: "trigger", class: _ctx.triggerClasses, onClick: _cache[1] || (_cache[1] = (...args) => _ctx.onClick(...args)), onContextmenu: _cache[2] || (_cache[2] = withModifiers((...args) => _ctx.onContextMenu(...args), ["prevent"])), onMouseenter: _cache[3] || (_cache[3] = (...args) => _ctx.onHover(...args)), onFocusCapture: _cache[4] || (_cache[4] = (...args) => _ctx.onFocus(...args)), "aria-haspopup": "true" }, [renderSlot(_ctx.$slots, "trigger", { active: _ctx.isActive })], 42 /* CLASS, PROPS, HYDRATE_EVENTS */ , ["tabindex"])) : createCommentVNode("v-if", true), createVNode(Transition, { name: _ctx.animation }, { default: withCtx(() => [_ctx.isMobileModal ? withDirectives((openBlock(), createBlock("div", { key: 0, class: _ctx.menuMobileOverlayClasses, "aria-hidden": !_ctx.isActive }, null, 10 /* CLASS, PROPS */ , ["aria-hidden"])), [[vShow, _ctx.isActive]]) : createCommentVNode("v-if", true)]), _: 1 }, 8 /* PROPS */ , ["name"]), createVNode(Transition, { name: _ctx.animation }, { default: withCtx(() => [withDirectives(createVNode("div", { ref: "dropdownMenu", is: _ctx.menuTag, class: _ctx.menuClasses, "aria-hidden": !_ctx.isActive, role: _ctx.ariaRole, "aria-modal": !_ctx.inline, style: _ctx.menuStyle }, [renderSlot(_ctx.$slots, "default")], 14 /* CLASS, STYLE, PROPS */ , ["is", "aria-hidden", "role", "aria-modal"]), [[vShow, !_ctx.disabled && (_ctx.isActive || _ctx.isHoverable) || _ctx.inline], [_directive_trap_focus, _ctx.trapFocus]])]), _: 3 }, 8 /* PROPS */ , ["name"])], 34 /* CLASS, HYDRATE_EVENTS */ ); } script.render = render; script.__file = "src/components/dropdown/Dropdown.vue"; /** * @displayName Dropdown Item */ var script$1 = defineComponent({ name: 'ODropdownItem', mixins: [BaseComponentMixin], configField: 'dropdown', inject: ["$dropdown"], emits: ['click'], props: { /** * The value that will be returned on events and v-model */ value: { type: [String, Number, Boolean, Object, Array] }, /** * Item is disabled */ disabled: Boolean, /** * Item is clickable and emit an event */ clickable: { type: Boolean, default: true }, /** * Dropdown item tag name */ tag: { type: String, default: () => { return getValueByPath(getOptions(), 'dropdown.itemTag', 'div'); } }, tabindex: { type: [Number, String], default: 0 }, ariaRole: { type: String, default: '' }, itemClass: [String, Function, Array], itemActiveClass: [String, Function, Array], itemDisabledClass: [String, Function, Array], }, computed: { parent() { return this.$dropdown; }, rootClasses() { return [ this.computedClass('itemClass', 'o-drop__item'), { [this.computedClass('itemDisabledClass', 'o-drop__item--disabled')]: (this.parent.disabled || this.disabled) }, { [this.computedClass('itemActiveClass', 'o-drop__item--active')]: this.isActive } ]; }, ariaRoleItem() { return this.ariaRole === 'menuitem' || this.ariaRole === 'listitem' ? this.ariaRole : null; }, isClickable() { return !this.parent.disabled && !this.disabled && this.clickable; }, isActive() { if (this.parent.selected === null) return false; if (this.parent.multiple) return this.parent.selected.indexOf(this.value) >= 0; return this.value === this.parent.selected; } }, methods: { /** * Click listener, select the item. */ selectItem() { if (!this.isClickable) return; this.parent.selectItem(this.value); this.$emit('click'); } }, created() { if (!this.parent) { throw new Error('You should wrap oDropdownItem on a oDropdown'); } } }); function render$1(_ctx, _cache, $props, $setup, $data, $options) { return openBlock(), createBlock(resolveDynamicComponent(_ctx.tag), { class: _ctx.rootClasses, onClick: _ctx.selectItem, role: _ctx.ariaRoleItem, tabindex: _ctx.tabindex }, { default: withCtx(() => [renderSlot(_ctx.$slots, "default")]), _: 3 }, 8 /* PROPS */ , ["class", "onClick", "role", "tabindex"]); } script$1.render = render$1; script$1.__file = "src/components/dropdown/DropdownItem.vue"; export { script$1 as a, script as s };