2022-09-06 12:03:37 +00:00
< template >
< Portal :appendTo ="appendTo" :disabled ="!popup" >
< transition name = "p-connected-overlay" @enter ="onEnter" @leave ="onLeave" @after-leave ="onAfterLeave" >
2023-04-26 09:58:05 +00:00
< div v-if ="popup ? overlayVisible : true" :ref="containerRef" :id="id" :class="containerClass" @click="onOverlayClick" v-bind="{ ...$attrs, ...ptm('root') }" >
< div v-if ="$slots.start" class="p-menu-start" v-bind="ptm('start')" >
2023-01-27 13:25:28 +00:00
< slot name = "start" > < / slot >
< / div >
2022-12-08 11:04:25 +00:00
< ul
: ref = "listRef"
: id = "id + '_list'"
class = "p-menu-list p-reset"
role = "menu"
: tabindex = "tabindex"
: aria - activedescendant = "focused ? focusedOptionId : undefined"
: aria - label = "ariaLabel"
: aria - labelledby = "ariaLabelledby"
@ focus = "onListFocus"
@ blur = "onListBlur"
@ keydown = "onListKeyDown"
2023-04-26 09:58:05 +00:00
v - bind = "ptm('menu')"
2022-12-08 11:04:25 +00:00
>
2022-09-06 12:03:37 +00:00
< template v-for ="(item, i) of model" :key="label(item) + i.toString()" >
< template v-if ="item.items && visible(item) && !item.separator" >
2023-04-26 09:58:05 +00:00
< li v-if ="item.items" :id="id + '_' + i" class="p-submenu-header" role="none" v-bind="ptm('submenuHeader')" >
2022-09-14 11:26:01 +00:00
< slot name = "item" :item ="item" > { { label ( item ) } } < / slot >
2022-09-06 12:03:37 +00:00
< / li >
2022-12-08 11:04:25 +00:00
< template v-for ="(child, j) of item.items" :key="child.label + i + '_' + j" >
2023-04-26 09:58:05 +00:00
< PVMenuitem v -if = " visible ( child ) & & ! child.separator " : id = "id + '_' + i + '_' + j" :item ="child" :templates ="$slots" :exact ="exact" :focusedOptionId ="focusedOptionId" @item-click ="itemClick" :pt ="pt" / >
< li v -else -if = " visible ( child ) & & child.separator " : key = "'separator' + i + j" :class ="separatorClass(item)" :style ="child.style" role = "separator" v-bind ="ptm('separator')" > < / li >
2022-09-06 12:03:37 +00:00
< / template >
< / template >
2023-04-26 09:58:05 +00:00
< li v -else -if = " visible ( item ) & & item.separator " : key = "'separator' + i.toString()" :class ="separatorClass(item)" :style ="item.style" role = "separator" v-bind ="ptm('separator')" > < / li >
< PVMenuitem v -else : key = "label(item) + i.toString()" : id = "id + '_' + i" :item ="item" :templates ="$slots" :exact ="exact" :focusedOptionId ="focusedOptionId" @item-click ="itemClick" :pt ="pt" / >
2022-09-06 12:03:37 +00:00
< / template >
< / ul >
2023-04-26 09:58:05 +00:00
< div v-if ="$slots.end" class="p-menu-end" v-bind="ptm('end')" >
2023-01-27 13:25:28 +00:00
< slot name = "end" > < / slot >
< / div >
2022-09-06 12:03:37 +00:00
< / div >
< / transition >
< / Portal >
< / template >
< script >
2023-04-26 09:58:05 +00:00
import BaseComponent from 'primevue/basecomponent' ;
2022-09-06 12:03:37 +00:00
import OverlayEventBus from 'primevue/overlayeventbus' ;
import Portal from 'primevue/portal' ;
2022-12-08 11:04:25 +00:00
import { ConnectedOverlayScrollHandler , DomHandler , UniqueComponentId , ZIndexUtils } from 'primevue/utils' ;
import Menuitem from './Menuitem.vue' ;
2022-09-06 12:03:37 +00:00
export default {
name : 'Menu' ,
2023-04-26 09:58:05 +00:00
extends : BaseComponent ,
2022-09-06 12:03:37 +00:00
inheritAttrs : false ,
2022-12-08 11:04:25 +00:00
emits : [ 'show' , 'hide' , 'focus' , 'blur' ] ,
2022-09-06 12:03:37 +00:00
props : {
popup : {
type : Boolean ,
default : false
} ,
2022-09-14 11:26:01 +00:00
model : {
2022-09-06 12:03:37 +00:00
type : Array ,
default : null
} ,
appendTo : {
type : String ,
default : 'body'
} ,
autoZIndex : {
type : Boolean ,
default : true
} ,
baseZIndex : {
type : Number ,
default : 0
} ,
exact : {
type : Boolean ,
default : true
2022-12-08 11:04:25 +00:00
} ,
tabindex : {
type : Number ,
default : 0
} ,
'aria-label' : {
type : String ,
default : null
} ,
'aria-labelledby' : {
type : String ,
default : null
2022-09-06 12:03:37 +00:00
}
} ,
data ( ) {
return {
2023-01-19 04:01:03 +00:00
id : this . $attrs . id ,
2022-12-08 11:04:25 +00:00
overlayVisible : false ,
focused : false ,
focusedOptionIndex : - 1 ,
selectedOptionIndex : - 1
2022-09-06 12:03:37 +00:00
} ;
} ,
2023-01-19 04:01:03 +00:00
watch : {
'$attrs.id' : function ( newValue ) {
this . id = newValue || UniqueComponentId ( ) ;
}
} ,
2022-09-06 12:03:37 +00:00
target : null ,
outsideClickListener : null ,
scrollHandler : null ,
resizeListener : null ,
container : null ,
2022-12-08 11:04:25 +00:00
list : null ,
mounted ( ) {
2023-01-19 04:01:03 +00:00
this . id = this . id || UniqueComponentId ( ) ;
2022-12-08 11:04:25 +00:00
if ( ! this . popup ) {
this . bindResizeListener ( ) ;
this . bindOutsideClickListener ( ) ;
}
} ,
2022-09-06 12:03:37 +00:00
beforeUnmount ( ) {
this . unbindResizeListener ( ) ;
this . unbindOutsideClickListener ( ) ;
if ( this . scrollHandler ) {
this . scrollHandler . destroy ( ) ;
this . scrollHandler = null ;
}
2022-09-14 11:26:01 +00:00
2022-09-06 12:03:37 +00:00
this . target = null ;
if ( this . container && this . autoZIndex ) {
ZIndexUtils . clear ( this . container ) ;
}
2022-09-14 11:26:01 +00:00
2022-09-06 12:03:37 +00:00
this . container = null ;
} ,
methods : {
itemClick ( event ) {
const item = event . item ;
2022-09-14 11:26:01 +00:00
2022-09-06 12:03:37 +00:00
if ( this . disabled ( item ) ) {
return ;
}
if ( item . command ) {
item . command ( event ) ;
}
if ( item . to && event . navigate ) {
event . navigate ( event . originalEvent ) ;
}
2022-12-08 11:04:25 +00:00
if ( this . overlayVisible ) this . hide ( ) ;
if ( ! this . popup && this . focusedOptionIndex !== event . id ) {
this . focusedOptionIndex = event . id ;
}
} ,
onListFocus ( event ) {
this . focused = true ;
if ( ! this . popup ) {
if ( this . selectedOptionIndex !== - 1 ) {
this . changeFocusedOptionIndex ( this . selectedOptionIndex ) ;
this . selectedOptionIndex = - 1 ;
} else this . changeFocusedOptionIndex ( 0 ) ;
}
this . $emit ( 'focus' , event ) ;
} ,
onListBlur ( event ) {
this . focused = false ;
this . focusedOptionIndex = - 1 ;
this . $emit ( 'blur' , event ) ;
} ,
onListKeyDown ( event ) {
switch ( event . code ) {
case 'ArrowDown' :
this . onArrowDownKey ( event ) ;
break ;
case 'ArrowUp' :
this . onArrowUpKey ( event ) ;
break ;
case 'Home' :
this . onHomeKey ( event ) ;
break ;
case 'End' :
this . onEndKey ( event ) ;
break ;
case 'Enter' :
this . onEnterKey ( event ) ;
break ;
case 'Space' :
this . onSpaceKey ( event ) ;
break ;
case 'Escape' :
if ( this . popup ) {
DomHandler . focus ( this . target ) ;
this . hide ( ) ;
}
case 'Tab' :
this . overlayVisible && this . hide ( ) ;
break ;
default :
break ;
}
} ,
onArrowDownKey ( event ) {
const optionIndex = this . findNextOptionIndex ( this . focusedOptionIndex ) ;
this . changeFocusedOptionIndex ( optionIndex ) ;
event . preventDefault ( ) ;
} ,
onArrowUpKey ( event ) {
if ( event . altKey && this . popup ) {
DomHandler . focus ( this . target ) ;
this . hide ( ) ;
event . preventDefault ( ) ;
} else {
const optionIndex = this . findPrevOptionIndex ( this . focusedOptionIndex ) ;
this . changeFocusedOptionIndex ( optionIndex ) ;
event . preventDefault ( ) ;
}
} ,
onHomeKey ( event ) {
this . changeFocusedOptionIndex ( 0 ) ;
event . preventDefault ( ) ;
} ,
onEndKey ( event ) {
this . changeFocusedOptionIndex ( DomHandler . find ( this . container , 'li.p-menuitem:not(.p-disabled)' ) . length - 1 ) ;
event . preventDefault ( ) ;
} ,
onEnterKey ( event ) {
const element = DomHandler . findSingle ( this . list , ` li[id=" ${ ` ${ this . focusedOptionIndex } ` } "] ` ) ;
const anchorElement = element && DomHandler . findSingle ( element , '.p-menuitem-link' ) ;
this . popup && DomHandler . focus ( this . target ) ;
anchorElement ? anchorElement . click ( ) : element && element . click ( ) ;
event . preventDefault ( ) ;
} ,
onSpaceKey ( event ) {
this . onEnterKey ( event ) ;
} ,
findNextOptionIndex ( index ) {
const links = DomHandler . find ( this . container , 'li.p-menuitem:not(.p-disabled)' ) ;
const matchedOptionIndex = [ ... links ] . findIndex ( ( link ) => link . id === index ) ;
return matchedOptionIndex > - 1 ? matchedOptionIndex + 1 : 0 ;
} ,
findPrevOptionIndex ( index ) {
const links = DomHandler . find ( this . container , 'li.p-menuitem:not(.p-disabled)' ) ;
const matchedOptionIndex = [ ... links ] . findIndex ( ( link ) => link . id === index ) ;
return matchedOptionIndex > - 1 ? matchedOptionIndex - 1 : 0 ;
} ,
changeFocusedOptionIndex ( index ) {
const links = DomHandler . find ( this . container , 'li.p-menuitem:not(.p-disabled)' ) ;
let order = index >= links . length ? links . length - 1 : index < 0 ? 0 : index ;
2023-01-16 10:07:17 +00:00
order > - 1 && ( this . focusedOptionIndex = links [ order ] . getAttribute ( 'id' ) ) ;
2022-09-06 12:03:37 +00:00
} ,
toggle ( event ) {
2022-09-14 11:26:01 +00:00
if ( this . overlayVisible ) this . hide ( ) ;
else this . show ( event ) ;
2022-09-06 12:03:37 +00:00
} ,
show ( event ) {
this . overlayVisible = true ;
this . target = event . currentTarget ;
} ,
hide ( ) {
this . overlayVisible = false ;
this . target = null ;
} ,
onEnter ( el ) {
this . alignOverlay ( ) ;
this . bindOutsideClickListener ( ) ;
this . bindResizeListener ( ) ;
this . bindScrollListener ( ) ;
if ( this . autoZIndex ) {
ZIndexUtils . set ( 'menu' , el , this . baseZIndex + this . $primevue . config . zIndex . menu ) ;
}
2022-12-08 11:04:25 +00:00
if ( this . popup ) {
DomHandler . focus ( this . list ) ;
this . changeFocusedOptionIndex ( 0 ) ;
}
2022-09-06 12:03:37 +00:00
this . $emit ( 'show' ) ;
} ,
onLeave ( ) {
this . unbindOutsideClickListener ( ) ;
this . unbindResizeListener ( ) ;
this . unbindScrollListener ( ) ;
this . $emit ( 'hide' ) ;
} ,
onAfterLeave ( el ) {
if ( this . autoZIndex ) {
ZIndexUtils . clear ( el ) ;
}
} ,
alignOverlay ( ) {
DomHandler . absolutePosition ( this . container , this . target ) ;
this . container . style . minWidth = DomHandler . getOuterWidth ( this . target ) + 'px' ;
} ,
bindOutsideClickListener ( ) {
if ( ! this . outsideClickListener ) {
this . outsideClickListener = ( event ) => {
2022-12-08 11:04:25 +00:00
const isOutsideContainer = this . container && ! this . container . contains ( event . target ) ;
const isOutsideTarget = ! ( this . target && ( this . target === event . target || this . target . contains ( event . target ) ) ) ;
if ( this . overlayVisible && isOutsideContainer && isOutsideTarget ) {
2022-09-06 12:03:37 +00:00
this . hide ( ) ;
2022-12-08 11:04:25 +00:00
} else if ( ! this . popup && isOutsideContainer && isOutsideTarget ) {
this . focusedOptionIndex = - 1 ;
2022-09-06 12:03:37 +00:00
}
} ;
2022-09-14 11:26:01 +00:00
2022-09-06 12:03:37 +00:00
document . addEventListener ( 'click' , this . outsideClickListener ) ;
}
} ,
unbindOutsideClickListener ( ) {
if ( this . outsideClickListener ) {
document . removeEventListener ( 'click' , this . outsideClickListener ) ;
this . outsideClickListener = null ;
}
} ,
bindScrollListener ( ) {
if ( ! this . scrollHandler ) {
this . scrollHandler = new ConnectedOverlayScrollHandler ( this . target , ( ) => {
if ( this . overlayVisible ) {
this . hide ( ) ;
}
} ) ;
}
this . scrollHandler . bindScrollListener ( ) ;
} ,
unbindScrollListener ( ) {
if ( this . scrollHandler ) {
this . scrollHandler . unbindScrollListener ( ) ;
}
} ,
bindResizeListener ( ) {
if ( ! this . resizeListener ) {
this . resizeListener = ( ) => {
if ( this . overlayVisible && ! DomHandler . isTouchDevice ( ) ) {
this . hide ( ) ;
}
} ;
2022-09-14 11:26:01 +00:00
2022-09-06 12:03:37 +00:00
window . addEventListener ( 'resize' , this . resizeListener ) ;
}
} ,
unbindResizeListener ( ) {
if ( this . resizeListener ) {
window . removeEventListener ( 'resize' , this . resizeListener ) ;
this . resizeListener = null ;
}
} ,
visible ( item ) {
2022-09-14 11:26:01 +00:00
return typeof item . visible === 'function' ? item . visible ( ) : item . visible !== false ;
2022-09-06 12:03:37 +00:00
} ,
disabled ( item ) {
2022-09-14 11:26:01 +00:00
return typeof item . disabled === 'function' ? item . disabled ( ) : item . disabled ;
2022-09-06 12:03:37 +00:00
} ,
label ( item ) {
2022-09-14 11:26:01 +00:00
return typeof item . label === 'function' ? item . label ( ) : item . label ;
2022-09-06 12:03:37 +00:00
} ,
2022-12-08 11:04:25 +00:00
separatorClass ( item ) {
return [ 'p-menuitem-separator' , item . class ] ;
2022-09-06 12:03:37 +00:00
} ,
onOverlayClick ( event ) {
OverlayEventBus . emit ( 'overlay-click' , {
originalEvent : event ,
target : this . target
} ) ;
2022-12-08 11:04:25 +00:00
} ,
containerRef ( el ) {
this . container = el ;
} ,
listRef ( el ) {
this . list = el ;
2022-09-06 12:03:37 +00:00
}
} ,
computed : {
containerClass ( ) {
2022-09-14 11:26:01 +00:00
return [
'p-menu p-component' ,
{
'p-menu-overlay' : this . popup ,
'p-input-filled' : this . $primevue . config . inputStyle === 'filled' ,
'p-ripple-disabled' : this . $primevue . config . ripple === false
}
] ;
2022-12-08 11:04:25 +00:00
} ,
focusedOptionId ( ) {
return this . focusedOptionIndex !== - 1 ? this . focusedOptionIndex : null ;
2022-09-06 12:03:37 +00:00
}
} ,
components : {
2022-12-08 11:04:25 +00:00
PVMenuitem : Menuitem ,
2022-09-14 11:26:01 +00:00
Portal : Portal
2022-09-06 12:03:37 +00:00
}
2022-09-14 11:26:01 +00:00
} ;
2022-09-06 12:03:37 +00:00
< / script >
< style >
. p - menu - overlay {
position : absolute ;
top : 0 ;
left : 0 ;
}
. p - menu ul {
margin : 0 ;
padding : 0 ;
list - style : none ;
}
. p - menu . p - menuitem - link {
cursor : pointer ;
display : flex ;
align - items : center ;
text - decoration : none ;
overflow : hidden ;
position : relative ;
}
. p - menu . p - menuitem - text {
line - height : 1 ;
}
< / style >