@ishitatsuyuki/oruga-next
Version:
UI components for Vue.js and CSS framework agnostic
604 lines (595 loc) • 20.6 kB
JavaScript
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 };