mirror of
https://github.com/primefaces/primevue.git
synced 2025-05-09 00:42:36 +00:00
Fixed #3802 - Improve folder structure for nuxt configurations
This commit is contained in:
parent
851950270b
commit
f5fe822afb
563 changed files with 1703 additions and 1095 deletions
126
components/lib/panelmenu/PanelMenu.d.ts
vendored
Executable file
126
components/lib/panelmenu/PanelMenu.d.ts
vendored
Executable file
|
@ -0,0 +1,126 @@
|
|||
/**
|
||||
*
|
||||
* PanelMenu is a hybrid of Accordion and Tree components.
|
||||
*
|
||||
* [Live Demo](https://www.primevue.org/panelmenu/)
|
||||
*
|
||||
* @module panelmenu
|
||||
*
|
||||
*/
|
||||
import { VNode } from 'vue';
|
||||
import { MenuItem } from '../menuitem';
|
||||
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
|
||||
|
||||
/**
|
||||
* Custom expanded keys metadata.
|
||||
* @see {@link PanelMenuProps.expandedKeys}
|
||||
*/
|
||||
export interface PanelMenuExpandedKeys {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom panel open event.
|
||||
* @see {@link PanelMenuEmits['panel-open']}
|
||||
*/
|
||||
export interface PanelMenuPanelOpenEvent {
|
||||
/**
|
||||
* Browser mouse event.
|
||||
* @type {MouseEvent}
|
||||
*/
|
||||
originalEvent: MouseEvent;
|
||||
/**
|
||||
* Current item.
|
||||
*/
|
||||
item: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom panel close event.
|
||||
* @see {@link PanelMenuEmits['panel-close']}
|
||||
* @extends {PanelMenuPanelOpenEvent}
|
||||
*/
|
||||
export interface PanelMenuPanelCloseEvent extends PanelMenuPanelOpenEvent {}
|
||||
|
||||
/**
|
||||
* Defines valid properties in PanelMenu component.
|
||||
*/
|
||||
export interface PanelMenuProps {
|
||||
/**
|
||||
* An array of menuitems.
|
||||
*/
|
||||
model?: MenuItem[] | undefined;
|
||||
/**
|
||||
* A map of keys to represent the expansion state in controlled mode.
|
||||
* @type {PanelMenuExpandedKeys}
|
||||
*/
|
||||
expandedKeys?: PanelMenuExpandedKeys;
|
||||
/**
|
||||
* Whether to apply 'router-link-active-exact' class if route exactly matches the item path.
|
||||
* @defaultValue true
|
||||
*/
|
||||
exact?: boolean | undefined;
|
||||
/**
|
||||
* Index of the element in tabbing order.
|
||||
*/
|
||||
tabindex?: number | string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines valid slots in PanelMenu component.
|
||||
*/
|
||||
export interface PanelMenuSlots {
|
||||
/**
|
||||
* Custom content for each item.
|
||||
* @param {Object} scope - item slot's params.
|
||||
*/
|
||||
item(scope: {
|
||||
/**
|
||||
* Menuitem instance
|
||||
*/
|
||||
item: MenuItem;
|
||||
}): VNode[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines valid emits in PanelMenu component.
|
||||
*/
|
||||
export interface PanelMenuEmits {
|
||||
/**
|
||||
* Emitted when the expandedKeys changes.
|
||||
* @param {*} value - New value.
|
||||
*/
|
||||
'update:expandedKeys'(value: any): void;
|
||||
/**
|
||||
* Callback to invoke when a panel gets expanded.
|
||||
* @param {PanelMenuPanelOpenEvent} event - Custom panel open event.
|
||||
*/
|
||||
'panel-open'(event: PanelMenuPanelOpenEvent): void;
|
||||
/**
|
||||
* Callback to invoke when an active panel is collapsed by clicking on the header.
|
||||
* @param {PanelMenuPanelCloseEvent} event - Custom panel close event.
|
||||
*/
|
||||
'panel-close'(event: PanelMenuPanelCloseEvent): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* **PrimeVue - PanelMenu**
|
||||
*
|
||||
* _PanelMenu is a hybrid of Accordion and Tree components._
|
||||
*
|
||||
* [Live Demo](https://www.primevue.org/panelmenu/)
|
||||
* --- ---
|
||||
* 
|
||||
*
|
||||
* @group Component
|
||||
*
|
||||
*/
|
||||
declare class PanelMenu extends ClassComponent<PanelMenuProps, PanelMenuSlots, PanelMenuEmits> {}
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
interface GlobalComponents {
|
||||
PanelMenu: GlobalComponentConstructor<PanelMenu>;
|
||||
}
|
||||
}
|
||||
|
||||
export default PanelMenu;
|
88
components/lib/panelmenu/PanelMenu.spec.js
Normal file
88
components/lib/panelmenu/PanelMenu.spec.js
Normal file
|
@ -0,0 +1,88 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import PanelMenu from './PanelMenu.vue';
|
||||
|
||||
describe('PanelMenu', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mount(PanelMenu, {
|
||||
global: {
|
||||
stubs: {
|
||||
'router-link': true
|
||||
}
|
||||
},
|
||||
props: {
|
||||
model: [
|
||||
{
|
||||
key: '2',
|
||||
label: 'Users',
|
||||
icon: 'pi pi-fw pi-user',
|
||||
items: [
|
||||
{
|
||||
key: '2_0',
|
||||
label: 'New',
|
||||
icon: 'pi pi-fw pi-user-plus'
|
||||
},
|
||||
{
|
||||
key: '2_1',
|
||||
label: 'Delete',
|
||||
icon: 'pi pi-fw pi-user-minus'
|
||||
},
|
||||
{
|
||||
key: '2_2',
|
||||
label: 'Search',
|
||||
icon: 'pi pi-fw pi-users',
|
||||
items: [
|
||||
{
|
||||
key: '2_2_0',
|
||||
label: 'Filter',
|
||||
icon: 'pi pi-fw pi-filter',
|
||||
items: [
|
||||
{
|
||||
key: '2_2_0_0',
|
||||
label: 'Print',
|
||||
icon: 'pi pi-fw pi-print'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: '2_2_1',
|
||||
icon: 'pi pi-fw pi-bars',
|
||||
label: 'List'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should exist', () => {
|
||||
expect(wrapper.find('.p-panelmenu.p-component').exists()).toBe(true);
|
||||
expect(wrapper.findAll('.p-menuitem').length).toBe(6);
|
||||
expect(wrapper.findAll('.p-toggleable-content').length).toBe(7);
|
||||
expect(wrapper.findAll('.p-submenu-list').length).toBe(3);
|
||||
});
|
||||
|
||||
it('should toggle', async () => {
|
||||
expect(wrapper.findAll('.p-toggleable-content')[0].attributes().style).toBe('display: none;');
|
||||
|
||||
await wrapper.vm.onHeaderClick({}, wrapper.vm.model[0]);
|
||||
|
||||
expect(wrapper.find('.p-panelmenu-header-action > .p-submenu-icon').classes()).toContain('pi-chevron-down');
|
||||
|
||||
setTimeout(() => {
|
||||
expect(wrapper.findAll('.p-toggleable-content')[0].attributes().style).toBe('');
|
||||
}, 25);
|
||||
});
|
||||
|
||||
it('should update expandedKeys', async () => {
|
||||
await wrapper.setProps({ expandedKeys: { 2: true } });
|
||||
|
||||
await wrapper.vm.onHeaderClick({}, wrapper.vm.model[0].items[2]);
|
||||
|
||||
expect(wrapper.emitted()['update:expandedKeys'][0]).toEqual([{ 2: true, '2_2': true }]);
|
||||
});
|
||||
});
|
300
components/lib/panelmenu/PanelMenu.vue
Normal file
300
components/lib/panelmenu/PanelMenu.vue
Normal file
|
@ -0,0 +1,300 @@
|
|||
<template>
|
||||
<div :id="id" class="p-panelmenu p-component">
|
||||
<template v-for="(item, index) of model" :key="getPanelKey(index)">
|
||||
<div v-if="isItemVisible(item)" :style="getItemProp(item, 'style')" :class="getPanelClass(item)">
|
||||
<div
|
||||
:id="getHeaderId(index)"
|
||||
:class="getHeaderClass(item)"
|
||||
:tabindex="isItemDisabled(item) ? -1 : tabindex"
|
||||
role="button"
|
||||
:aria-label="getItemLabel(item)"
|
||||
:aria-expanded="isItemActive(item)"
|
||||
:aria-controls="getContentId(index)"
|
||||
:aria-disabled="isItemDisabled(item)"
|
||||
@click="onHeaderClick($event, item)"
|
||||
@keydown="onHeaderKeyDown($event, item)"
|
||||
>
|
||||
<div class="p-panelmenu-header-content">
|
||||
<template v-if="!$slots.item">
|
||||
<router-link v-if="getItemProp(item, 'to') && !isItemDisabled(item)" v-slot="{ navigate, href, isActive, isExactActive }" :to="getItemProp(item, 'to')" custom>
|
||||
<a :href="href" :class="getHeaderActionClass(item, { isActive, isExactActive })" :tabindex="-1" @click="onHeaderActionClick($event, navigate)">
|
||||
<span v-if="getItemProp(item, 'icon')" :class="getHeaderIconClass(item)"></span>
|
||||
<span class="p-menuitem-text">{{ getItemLabel(item) }}</span>
|
||||
</a>
|
||||
</router-link>
|
||||
<a v-else :href="getItemProp(item, 'url')" :class="getHeaderActionClass(item)" :tabindex="-1">
|
||||
<span v-if="getItemProp(item, 'items')" :class="getHeaderToggleIconClass(item)"></span>
|
||||
<span v-if="getItemProp(item, 'icon')" :class="getHeaderIconClass(item)"></span>
|
||||
<span class="p-menuitem-text">{{ getItemLabel(item) }}</span>
|
||||
</a>
|
||||
</template>
|
||||
<component v-else :is="$slots.item" :item="item"></component>
|
||||
</div>
|
||||
</div>
|
||||
<transition name="p-toggleable-content">
|
||||
<div v-show="isItemActive(item)" :id="getContentId(index)" class="p-toggleable-content" role="region" :aria-labelledby="getHeaderId(index)">
|
||||
<div v-if="getItemProp(item, 'items')" class="p-panelmenu-content">
|
||||
<PanelMenuList :panelId="getPanelId(index)" :items="getItemProp(item, 'items')" :template="$slots.item" :expandedKeys="expandedKeys" @item-toggle="changeExpandedKeys" @header-focus="updateFocusedHeader" :exact="exact" />
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DomHandler, ObjectUtils, UniqueComponentId } from 'primevue/utils';
|
||||
import PanelMenuList from './PanelMenuList.vue';
|
||||
|
||||
export default {
|
||||
name: 'PanelMenu',
|
||||
emits: ['update:expandedKeys', 'panel-open', 'panel-close'],
|
||||
props: {
|
||||
model: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
expandedKeys: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
exact: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
tabindex: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
id: this.$attrs.id,
|
||||
activeItem: null
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
'$attrs.id': function (newValue) {
|
||||
this.id = newValue || UniqueComponentId();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.id = this.id || UniqueComponentId();
|
||||
},
|
||||
methods: {
|
||||
getItemProp(item, name) {
|
||||
return item ? ObjectUtils.getItemValue(item[name]) : undefined;
|
||||
},
|
||||
getItemLabel(item) {
|
||||
return this.getItemProp(item, 'label');
|
||||
},
|
||||
isItemActive(item) {
|
||||
return this.expandedKeys ? this.expandedKeys[this.getItemProp(item, 'key')] : ObjectUtils.equals(item, this.activeItem);
|
||||
},
|
||||
isItemVisible(item) {
|
||||
return this.getItemProp(item, 'visible') !== false;
|
||||
},
|
||||
isItemDisabled(item) {
|
||||
return this.getItemProp(item, 'disabled');
|
||||
},
|
||||
getPanelId(index) {
|
||||
return `${this.id}_${index}`;
|
||||
},
|
||||
getPanelKey(index) {
|
||||
return this.getPanelId(index);
|
||||
},
|
||||
getHeaderId(index) {
|
||||
return `${this.getPanelId(index)}_header`;
|
||||
},
|
||||
getContentId(index) {
|
||||
return `${this.getPanelId(index)}_content`;
|
||||
},
|
||||
onHeaderClick(event, item) {
|
||||
if (this.isItemDisabled(item)) {
|
||||
event.preventDefault();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.command) {
|
||||
item.command({ originalEvent: event, item });
|
||||
}
|
||||
|
||||
this.changeActiveItem(event, item);
|
||||
DomHandler.focus(event.currentTarget);
|
||||
},
|
||||
onHeaderKeyDown(event, item) {
|
||||
switch (event.code) {
|
||||
case 'ArrowDown':
|
||||
this.onHeaderArrowDownKey(event);
|
||||
break;
|
||||
|
||||
case 'ArrowUp':
|
||||
this.onHeaderArrowUpKey(event);
|
||||
break;
|
||||
|
||||
case 'Home':
|
||||
this.onHeaderHomeKey(event);
|
||||
break;
|
||||
|
||||
case 'End':
|
||||
this.onHeaderEndKey(event);
|
||||
break;
|
||||
|
||||
case 'Enter':
|
||||
case 'Space':
|
||||
this.onHeaderEnterKey(event, item);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
onHeaderArrowDownKey(event) {
|
||||
const rootList = DomHandler.hasClass(event.currentTarget, 'p-highlight') ? DomHandler.findSingle(event.currentTarget.nextElementSibling, '.p-panelmenu-root-list') : null;
|
||||
|
||||
rootList ? DomHandler.focus(rootList) : this.updateFocusedHeader({ originalEvent: event, focusOnNext: true });
|
||||
event.preventDefault();
|
||||
},
|
||||
onHeaderArrowUpKey(event) {
|
||||
const prevHeader = this.findPrevHeader(event.currentTarget.parentElement) || this.findLastHeader();
|
||||
const rootList = DomHandler.hasClass(prevHeader, 'p-highlight') ? DomHandler.findSingle(prevHeader.nextElementSibling, '.p-panelmenu-root-list') : null;
|
||||
|
||||
rootList ? DomHandler.focus(rootList) : this.updateFocusedHeader({ originalEvent: event, focusOnNext: false });
|
||||
event.preventDefault();
|
||||
},
|
||||
onHeaderHomeKey(event) {
|
||||
this.changeFocusedHeader(event, this.findFirstHeader());
|
||||
event.preventDefault();
|
||||
},
|
||||
onHeaderEndKey(event) {
|
||||
this.changeFocusedHeader(event, this.findLastHeader());
|
||||
event.preventDefault();
|
||||
},
|
||||
onHeaderEnterKey(event, item) {
|
||||
const headerAction = DomHandler.findSingle(event.currentTarget, '.p-panelmenu-header-action');
|
||||
|
||||
headerAction ? headerAction.click() : this.onHeaderClick(event, item);
|
||||
event.preventDefault();
|
||||
},
|
||||
onHeaderActionClick(event, navigate) {
|
||||
navigate && navigate(event);
|
||||
},
|
||||
findNextHeader(panelElement, selfCheck = false) {
|
||||
const nextPanelElement = selfCheck ? panelElement : panelElement.nextElementSibling;
|
||||
const headerElement = DomHandler.findSingle(nextPanelElement, '.p-panelmenu-header');
|
||||
|
||||
return headerElement ? (DomHandler.hasClass(headerElement, 'p-disabled') ? this.findNextHeader(headerElement.parentElement) : headerElement) : null;
|
||||
},
|
||||
findPrevHeader(panelElement, selfCheck = false) {
|
||||
const prevPanelElement = selfCheck ? panelElement : panelElement.previousElementSibling;
|
||||
const headerElement = DomHandler.findSingle(prevPanelElement, '.p-panelmenu-header');
|
||||
|
||||
return headerElement ? (DomHandler.hasClass(headerElement, 'p-disabled') ? this.findPrevHeader(headerElement.parentElement) : headerElement) : null;
|
||||
},
|
||||
findFirstHeader() {
|
||||
return this.findNextHeader(this.$el.firstElementChild, true);
|
||||
},
|
||||
findLastHeader() {
|
||||
return this.findPrevHeader(this.$el.lastElementChild, true);
|
||||
},
|
||||
updateFocusedHeader(event) {
|
||||
const { originalEvent, focusOnNext, selfCheck } = event;
|
||||
const panelElement = originalEvent.currentTarget.closest('.p-panelmenu-panel');
|
||||
const header = selfCheck ? DomHandler.findSingle(panelElement, '.p-panelmenu-header') : focusOnNext ? this.findNextHeader(panelElement) : this.findPrevHeader(panelElement);
|
||||
|
||||
header ? this.changeFocusedHeader(originalEvent, header) : focusOnNext ? this.onHeaderHomeKey(originalEvent) : this.onHeaderEndKey(originalEvent);
|
||||
},
|
||||
changeActiveItem(event, item, selfActive = false) {
|
||||
if (!this.isItemDisabled(item)) {
|
||||
const active = this.isItemActive(item);
|
||||
const eventName = !active ? 'panel-open' : 'panel-close';
|
||||
|
||||
this.activeItem = selfActive ? item : this.activeItem && ObjectUtils.equals(item, this.activeItem) ? null : item;
|
||||
this.changeExpandedKeys({ item, expanded: !active });
|
||||
this.$emit(eventName, { originalEvent: event, item });
|
||||
}
|
||||
},
|
||||
changeExpandedKeys({ item, expanded = false }) {
|
||||
if (this.expandedKeys) {
|
||||
let _keys = { ...this.expandedKeys };
|
||||
|
||||
if (expanded) _keys[item.key] = true;
|
||||
else delete _keys[item.key];
|
||||
|
||||
this.$emit('update:expandedKeys', _keys);
|
||||
}
|
||||
},
|
||||
changeFocusedHeader(event, element) {
|
||||
element && DomHandler.focus(element);
|
||||
},
|
||||
getPanelClass(item) {
|
||||
return ['p-panelmenu-panel', this.getItemProp(item, 'class')];
|
||||
},
|
||||
getHeaderClass(item) {
|
||||
return [
|
||||
'p-panelmenu-header',
|
||||
this.getItemProp(item, 'headerClass'),
|
||||
{
|
||||
'p-highlight': this.isItemActive(item),
|
||||
'p-disabled': this.isItemDisabled(item)
|
||||
}
|
||||
];
|
||||
},
|
||||
getHeaderActionClass(item, routerProps) {
|
||||
return [
|
||||
'p-panelmenu-header-action',
|
||||
{
|
||||
'router-link-active': routerProps && routerProps.isActive,
|
||||
'router-link-active-exact': this.exact && routerProps && routerProps.isExactActive
|
||||
}
|
||||
];
|
||||
},
|
||||
getHeaderIconClass(item) {
|
||||
return ['p-menuitem-icon', this.getItemProp(item, 'icon')];
|
||||
},
|
||||
getHeaderToggleIconClass(item) {
|
||||
return ['p-submenu-icon', this.isItemActive(item) ? 'pi pi-chevron-down' : 'pi pi-chevron-right'];
|
||||
}
|
||||
},
|
||||
components: {
|
||||
PanelMenuList: PanelMenuList
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.p-panelmenu .p-panelmenu-header-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.p-panelmenu .p-panelmenu-header-action:focus {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.p-panelmenu .p-submenu-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.p-panelmenu .p-menuitem-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.p-panelmenu .p-menuitem-text {
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
391
components/lib/panelmenu/PanelMenuList.vue
Normal file
391
components/lib/panelmenu/PanelMenuList.vue
Normal file
|
@ -0,0 +1,391 @@
|
|||
<template>
|
||||
<PanelMenuSub
|
||||
:id="panelId + '_list'"
|
||||
class="p-panelmenu-root-list"
|
||||
role="tree"
|
||||
:tabindex="-1"
|
||||
:aria-activedescendant="focused ? focusedItemId : undefined"
|
||||
:panelId="panelId"
|
||||
:focusedItemId="focused ? focusedItemId : undefined"
|
||||
:items="processedItems"
|
||||
:template="template"
|
||||
:activeItemPath="activeItemPath"
|
||||
:exact="exact"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@keydown="onKeyDown"
|
||||
@item-toggle="onItemToggle"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DomHandler, ObjectUtils } from 'primevue/utils';
|
||||
import PanelMenuSub from './PanelMenuSub.vue';
|
||||
|
||||
export default {
|
||||
name: 'PanelMenuList',
|
||||
emits: ['item-toggle', 'header-focus'],
|
||||
props: {
|
||||
panelId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
template: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
expandedKeys: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
exact: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
searchTimeout: null,
|
||||
searchValue: null,
|
||||
data() {
|
||||
return {
|
||||
focused: false,
|
||||
focusedItem: null,
|
||||
activeItemPath: []
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
expandedKeys(newValue) {
|
||||
this.autoUpdateActiveItemPath(newValue);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.autoUpdateActiveItemPath(this.expandedKeys);
|
||||
},
|
||||
methods: {
|
||||
getItemProp(processedItem, name) {
|
||||
return processedItem && processedItem.item ? ObjectUtils.getItemValue(processedItem.item[name]) : undefined;
|
||||
},
|
||||
getItemLabel(processedItem) {
|
||||
return this.getItemProp(processedItem, 'label');
|
||||
},
|
||||
isItemVisible(processedItem) {
|
||||
return this.getItemProp(processedItem, 'visible') !== false;
|
||||
},
|
||||
isItemDisabled(processedItem) {
|
||||
return this.getItemProp(processedItem, 'disabled');
|
||||
},
|
||||
isItemActive(processedItem) {
|
||||
return this.activeItemPath.some((path) => path.key === processedItem.parentKey);
|
||||
},
|
||||
isItemGroup(processedItem) {
|
||||
return ObjectUtils.isNotEmpty(processedItem.items);
|
||||
},
|
||||
onFocus(event) {
|
||||
this.focused = true;
|
||||
this.focusedItem = this.focusedItem || (this.isElementInPanel(event, event.relatedTarget) ? this.findFirstItem() : this.findLastItem());
|
||||
},
|
||||
onBlur() {
|
||||
this.focused = false;
|
||||
this.focusedItem = null;
|
||||
this.searchValue = '';
|
||||
},
|
||||
onKeyDown(event) {
|
||||
const metaKey = event.metaKey || event.ctrlKey;
|
||||
|
||||
switch (event.code) {
|
||||
case 'ArrowDown':
|
||||
this.onArrowDownKey(event);
|
||||
break;
|
||||
|
||||
case 'ArrowUp':
|
||||
this.onArrowUpKey(event);
|
||||
break;
|
||||
|
||||
case 'ArrowLeft':
|
||||
this.onArrowLeftKey(event);
|
||||
break;
|
||||
|
||||
case 'ArrowRight':
|
||||
this.onArrowRightKey(event);
|
||||
break;
|
||||
|
||||
case 'Home':
|
||||
this.onHomeKey(event);
|
||||
break;
|
||||
|
||||
case 'End':
|
||||
this.onEndKey(event);
|
||||
break;
|
||||
|
||||
case 'Space':
|
||||
this.onSpaceKey(event);
|
||||
break;
|
||||
|
||||
case 'Enter':
|
||||
this.onEnterKey(event);
|
||||
break;
|
||||
|
||||
case 'Escape':
|
||||
case 'Tab':
|
||||
case 'PageDown':
|
||||
case 'PageUp':
|
||||
case 'Backspace':
|
||||
case 'ShiftLeft':
|
||||
case 'ShiftRight':
|
||||
//NOOP
|
||||
break;
|
||||
|
||||
default:
|
||||
if (!metaKey && ObjectUtils.isPrintableCharacter(event.key)) {
|
||||
this.searchItems(event, event.key);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
},
|
||||
onArrowDownKey(event) {
|
||||
const processedItem = ObjectUtils.isNotEmpty(this.focusedItem) ? this.findNextItem(this.focusedItem) : this.findFirstItem();
|
||||
|
||||
this.changeFocusedItem({ originalEvent: event, processedItem, focusOnNext: true });
|
||||
event.preventDefault();
|
||||
},
|
||||
onArrowUpKey(event) {
|
||||
const processedItem = ObjectUtils.isNotEmpty(this.focusedItem) ? this.findPrevItem(this.focusedItem) : this.findLastItem();
|
||||
|
||||
this.changeFocusedItem({ originalEvent: event, processedItem, selfCheck: true });
|
||||
event.preventDefault();
|
||||
},
|
||||
onArrowLeftKey(event) {
|
||||
if (ObjectUtils.isNotEmpty(this.focusedItem)) {
|
||||
const matched = this.activeItemPath.some((p) => p.key === this.focusedItem.key);
|
||||
|
||||
if (matched) {
|
||||
this.activeItemPath = this.activeItemPath.filter((p) => p.key !== this.focusedItem.key);
|
||||
} else {
|
||||
this.focusedItem = ObjectUtils.isNotEmpty(this.focusedItem.parent) ? this.focusedItem.parent : this.focusedItem;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
onArrowRightKey(event) {
|
||||
if (ObjectUtils.isNotEmpty(this.focusedItem)) {
|
||||
const grouped = this.isItemGroup(this.focusedItem);
|
||||
|
||||
if (grouped) {
|
||||
const matched = this.activeItemPath.some((p) => p.key === this.focusedItem.key);
|
||||
|
||||
if (matched) {
|
||||
this.onArrowDownKey(event);
|
||||
} else {
|
||||
this.activeItemPath = this.activeItemPath.filter((p) => p.parentKey !== this.focusedItem.parentKey);
|
||||
this.activeItemPath.push(this.focusedItem);
|
||||
}
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
onHomeKey(event) {
|
||||
this.changeFocusedItem({ originalEvent: event, processedItem: this.findFirstItem(), allowHeaderFocus: false });
|
||||
event.preventDefault();
|
||||
},
|
||||
onEndKey(event) {
|
||||
this.changeFocusedItem({ originalEvent: event, processedItem: this.findLastItem(), focusOnNext: true, allowHeaderFocus: false });
|
||||
event.preventDefault();
|
||||
},
|
||||
onEnterKey(event) {
|
||||
if (ObjectUtils.isNotEmpty(this.focusedItem)) {
|
||||
const element = DomHandler.findSingle(this.$el, `li[id="${`${this.focusedItemId}`}"]`);
|
||||
const anchorElement = element && (DomHandler.findSingle(element, '.p-menuitem-link') || DomHandler.findSingle(element, 'a,button'));
|
||||
|
||||
anchorElement ? anchorElement.click() : element && element.click();
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
},
|
||||
onSpaceKey(event) {
|
||||
this.onEnterKey(event);
|
||||
},
|
||||
onItemToggle(event) {
|
||||
const { processedItem, expanded } = event;
|
||||
|
||||
if (this.expandedKeys) {
|
||||
this.$emit('item-toggle', { item: processedItem.item, expanded });
|
||||
} else {
|
||||
this.activeItemPath = this.activeItemPath.filter((p) => p.parentKey !== processedItem.parentKey);
|
||||
expanded && this.activeItemPath.push(processedItem);
|
||||
}
|
||||
|
||||
this.focusedItem = processedItem;
|
||||
DomHandler.focus(this.$el);
|
||||
},
|
||||
isElementInPanel(event, element) {
|
||||
const panel = event.currentTarget.closest('.p-panelmenu-panel');
|
||||
|
||||
return panel && panel.contains(element);
|
||||
},
|
||||
isItemMatched(processedItem) {
|
||||
return this.isValidItem(processedItem) && this.getItemLabel(processedItem).toLocaleLowerCase(this.searchLocale).startsWith(this.searchValue.toLocaleLowerCase(this.searchLocale));
|
||||
},
|
||||
isVisibleItem(processedItem) {
|
||||
return !!processedItem && (processedItem.level === 0 || this.isItemActive(processedItem)) && this.isItemVisible(processedItem);
|
||||
},
|
||||
isValidItem(processedItem) {
|
||||
return !!processedItem && !this.isItemDisabled(processedItem);
|
||||
},
|
||||
findFirstItem() {
|
||||
return this.visibleItems.find((processedItem) => this.isValidItem(processedItem));
|
||||
},
|
||||
findLastItem() {
|
||||
return ObjectUtils.findLast(this.visibleItems, (processedItem) => this.isValidItem(processedItem));
|
||||
},
|
||||
findNextItem(processedItem) {
|
||||
const index = this.visibleItems.findIndex((item) => item.key === processedItem.key);
|
||||
const matchedItem = index < this.visibleItems.length - 1 ? this.visibleItems.slice(index + 1).find((pItem) => this.isValidItem(pItem)) : undefined;
|
||||
|
||||
return matchedItem || processedItem;
|
||||
},
|
||||
findPrevItem(processedItem) {
|
||||
const index = this.visibleItems.findIndex((item) => item.key === processedItem.key);
|
||||
const matchedItem = index > 0 ? ObjectUtils.findLast(this.visibleItems.slice(0, index), (pItem) => this.isValidItem(pItem)) : undefined;
|
||||
|
||||
return matchedItem || processedItem;
|
||||
},
|
||||
searchItems(event, char) {
|
||||
this.searchValue = (this.searchValue || '') + char;
|
||||
|
||||
let matchedItem = null;
|
||||
let matched = false;
|
||||
|
||||
if (ObjectUtils.isNotEmpty(this.focusedItem)) {
|
||||
const focusedItemIndex = this.visibleItems.findIndex((processedItem) => processedItem.key === this.focusedItem.key);
|
||||
|
||||
matchedItem = this.visibleItems.slice(focusedItemIndex).find((processedItem) => this.isItemMatched(processedItem));
|
||||
matchedItem = ObjectUtils.isEmpty(matchedItem) ? this.visibleItems.slice(0, focusedItemIndex).find((processedItem) => this.isItemMatched(processedItem)) : matchedItem;
|
||||
} else {
|
||||
matchedItem = this.visibleItems.find((processedItem) => this.isItemMatched(processedItem));
|
||||
}
|
||||
|
||||
if (ObjectUtils.isNotEmpty(matchedItem)) {
|
||||
matched = true;
|
||||
}
|
||||
|
||||
if (ObjectUtils.isEmpty(matchedItem) && ObjectUtils.isEmpty(this.focusedItem)) {
|
||||
matchedItem = this.findFirstItem();
|
||||
}
|
||||
|
||||
if (ObjectUtils.isNotEmpty(matchedItem)) {
|
||||
this.changeFocusedItem({
|
||||
originalEvent: event,
|
||||
processedItem: matchedItem,
|
||||
allowHeaderFocus: false
|
||||
});
|
||||
}
|
||||
|
||||
if (this.searchTimeout) {
|
||||
clearTimeout(this.searchTimeout);
|
||||
}
|
||||
|
||||
this.searchTimeout = setTimeout(() => {
|
||||
this.searchValue = '';
|
||||
this.searchTimeout = null;
|
||||
}, 500);
|
||||
|
||||
return matched;
|
||||
},
|
||||
changeFocusedItem(event) {
|
||||
const { originalEvent, processedItem, focusOnNext, selfCheck, allowHeaderFocus = true } = event;
|
||||
|
||||
if (ObjectUtils.isNotEmpty(this.focusedItem) && this.focusedItem.key !== processedItem.key) {
|
||||
this.focusedItem = processedItem;
|
||||
this.scrollInView();
|
||||
} else if (allowHeaderFocus) {
|
||||
this.$emit('header-focus', { originalEvent, focusOnNext, selfCheck });
|
||||
}
|
||||
},
|
||||
scrollInView() {
|
||||
const element = DomHandler.findSingle(this.$el, `li[id="${`${this.focusedItemId}`}"]`);
|
||||
|
||||
if (element) {
|
||||
element.scrollIntoView && element.scrollIntoView({ block: 'nearest', inline: 'start' });
|
||||
}
|
||||
},
|
||||
autoUpdateActiveItemPath(expandedKeys) {
|
||||
this.activeItemPath = Object.entries(expandedKeys || {}).reduce((acc, [key, val]) => {
|
||||
if (val) {
|
||||
const processedItem = this.findProcessedItemByItemKey(key);
|
||||
|
||||
processedItem && acc.push(processedItem);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
},
|
||||
findProcessedItemByItemKey(key, processedItems, level = 0) {
|
||||
processedItems = processedItems || (level === 0 && this.processedItems);
|
||||
|
||||
if (!processedItems) return null;
|
||||
|
||||
for (let i = 0; i < processedItems.length; i++) {
|
||||
const processedItem = processedItems[i];
|
||||
|
||||
if (this.getItemProp(processedItem, 'key') === key) return processedItem;
|
||||
|
||||
const matchedItem = this.findProcessedItemByItemKey(key, processedItem.items, level + 1);
|
||||
|
||||
if (matchedItem) return matchedItem;
|
||||
}
|
||||
},
|
||||
createProcessedItems(items, level = 0, parent = {}, parentKey = '') {
|
||||
const processedItems = [];
|
||||
|
||||
items &&
|
||||
items.forEach((item, index) => {
|
||||
const key = (parentKey !== '' ? parentKey + '_' : '') + index;
|
||||
const newItem = {
|
||||
item,
|
||||
index,
|
||||
level,
|
||||
key,
|
||||
parent,
|
||||
parentKey
|
||||
};
|
||||
|
||||
newItem['items'] = this.createProcessedItems(item.items, level + 1, newItem, key);
|
||||
processedItems.push(newItem);
|
||||
});
|
||||
|
||||
return processedItems;
|
||||
},
|
||||
flatItems(processedItems, processedFlattenItems = []) {
|
||||
processedItems &&
|
||||
processedItems.forEach((processedItem) => {
|
||||
if (this.isVisibleItem(processedItem)) {
|
||||
processedFlattenItems.push(processedItem);
|
||||
this.flatItems(processedItem.items, processedFlattenItems);
|
||||
}
|
||||
});
|
||||
|
||||
return processedFlattenItems;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
processedItems() {
|
||||
return this.createProcessedItems(this.items || []);
|
||||
},
|
||||
visibleItems() {
|
||||
return this.flatItems(this.processedItems);
|
||||
},
|
||||
focusedItemId() {
|
||||
return ObjectUtils.isNotEmpty(this.focusedItem) ? `${this.panelId}_${this.focusedItem.key}` : null;
|
||||
}
|
||||
},
|
||||
components: {
|
||||
PanelMenuSub: PanelMenuSub
|
||||
}
|
||||
};
|
||||
</script>
|
169
components/lib/panelmenu/PanelMenuSub.vue
Executable file
169
components/lib/panelmenu/PanelMenuSub.vue
Executable file
|
@ -0,0 +1,169 @@
|
|||
<template>
|
||||
<ul class="p-submenu-list">
|
||||
<template v-for="(processedItem, index) of items" :key="getItemKey(processedItem)">
|
||||
<li
|
||||
v-if="isItemVisible(processedItem) && !getItemProp(processedItem, 'separator')"
|
||||
:id="getItemId(processedItem)"
|
||||
:style="getItemProp(processedItem, 'style')"
|
||||
:class="getItemClass(processedItem)"
|
||||
role="treeitem"
|
||||
:aria-label="getItemLabel(processedItem)"
|
||||
:aria-expanded="isItemGroup(processedItem) ? isItemActive(processedItem) : undefined"
|
||||
:aria-level="level + 1"
|
||||
:aria-setsize="getAriaSetSize()"
|
||||
:aria-posinset="getAriaPosInset(index)"
|
||||
>
|
||||
<div class="p-menuitem-content" @click="onItemClick($event, processedItem)">
|
||||
<template v-if="!template">
|
||||
<router-link v-if="getItemProp(processedItem, 'to') && !isItemDisabled(processedItem)" v-slot="{ navigate, href, isActive, isExactActive }" :to="getItemProp(processedItem, 'to')" custom>
|
||||
<a v-ripple :href="href" :class="getItemActionClass(processedItem, { isActive, isExactActive })" tabindex="-1" aria-hidden="true" @click="onItemActionClick($event, navigate)">
|
||||
<span v-if="getItemProp(processedItem, 'icon')" :class="getItemIconClass(processedItem)"></span>
|
||||
<span class="p-menuitem-text">{{ getItemLabel(processedItem) }}</span>
|
||||
</a>
|
||||
</router-link>
|
||||
<a v-else v-ripple :href="getItemProp(processedItem, 'url')" :class="getItemActionClass(processedItem)" :target="getItemProp(processedItem, 'target')" tabindex="-1" aria-hidden="true">
|
||||
<span v-if="isItemGroup(processedItem)" :class="getItemToggleIconClass(processedItem)"></span>
|
||||
<span v-if="getItemProp(processedItem, 'icon')" :class="getItemIconClass(processedItem)"></span>
|
||||
<span class="p-menuitem-text">{{ getItemLabel(processedItem) }}</span>
|
||||
</a>
|
||||
</template>
|
||||
<component v-else :is="template" :item="processedItem.item"></component>
|
||||
</div>
|
||||
<transition name="p-toggleable-content">
|
||||
<div v-show="isItemActive(processedItem)" class="p-toggleable-content">
|
||||
<PanelMenuSub
|
||||
v-if="isItemVisible(processedItem) && isItemGroup(processedItem)"
|
||||
:id="getItemId(processedItem) + '_list'"
|
||||
role="group"
|
||||
:panelId="panelId"
|
||||
:focusedItemId="focusedItemId"
|
||||
:items="processedItem.items"
|
||||
:level="level + 1"
|
||||
:template="template"
|
||||
:activeItemPath="activeItemPath"
|
||||
:exact="exact"
|
||||
@item-toggle="onItemToggle"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</li>
|
||||
<li v-if="isItemVisible(processedItem) && getItemProp(processedItem, 'separator')" :style="getItemProp(processedItem, 'style')" :class="getSeparatorItemClass(processedItem)" role="separator"></li>
|
||||
</template>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Ripple from 'primevue/ripple';
|
||||
import { ObjectUtils } from 'primevue/utils';
|
||||
|
||||
export default {
|
||||
name: 'PanelMenuSub',
|
||||
emits: ['item-toggle'],
|
||||
props: {
|
||||
panelId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
focusedItemId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
template: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
activeItemPath: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
exact: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getItemId(processedItem) {
|
||||
return `${this.panelId}_${processedItem.key}`;
|
||||
},
|
||||
getItemKey(processedItem) {
|
||||
return this.getItemId(processedItem);
|
||||
},
|
||||
getItemProp(processedItem, name, params) {
|
||||
return processedItem && processedItem.item ? ObjectUtils.getItemValue(processedItem.item[name], params) : undefined;
|
||||
},
|
||||
getItemLabel(processedItem) {
|
||||
return this.getItemProp(processedItem, 'label');
|
||||
},
|
||||
isItemActive(processedItem) {
|
||||
return this.activeItemPath.some((path) => path.key === processedItem.key);
|
||||
},
|
||||
isItemVisible(processedItem) {
|
||||
return this.getItemProp(processedItem, 'visible') !== false;
|
||||
},
|
||||
isItemDisabled(processedItem) {
|
||||
return this.getItemProp(processedItem, 'disabled');
|
||||
},
|
||||
isItemFocused(processedItem) {
|
||||
return this.focusedItemId === this.getItemId(processedItem);
|
||||
},
|
||||
isItemGroup(processedItem) {
|
||||
return ObjectUtils.isNotEmpty(processedItem.items);
|
||||
},
|
||||
onItemClick(event, processedItem) {
|
||||
this.getItemProp(processedItem, 'command', { originalEvent: event, item: processedItem.item });
|
||||
this.$emit('item-toggle', { processedItem, expanded: !this.isItemActive(processedItem) });
|
||||
},
|
||||
onItemToggle(event) {
|
||||
this.$emit('item-toggle', event);
|
||||
},
|
||||
onItemActionClick(event, navigate) {
|
||||
navigate && navigate(event);
|
||||
},
|
||||
getAriaSetSize() {
|
||||
return this.items.filter((processedItem) => this.isItemVisible(processedItem) && !this.getItemProp(processedItem, 'separator')).length;
|
||||
},
|
||||
getAriaPosInset(index) {
|
||||
return index - this.items.slice(0, index).filter((processedItem) => this.isItemVisible(processedItem) && this.getItemProp(processedItem, 'separator')).length + 1;
|
||||
},
|
||||
getItemClass(processedItem) {
|
||||
return [
|
||||
'p-menuitem',
|
||||
this.getItemProp(processedItem, 'class'),
|
||||
{
|
||||
'p-focus': this.isItemFocused(processedItem),
|
||||
'p-disabled': this.isItemDisabled(processedItem)
|
||||
}
|
||||
];
|
||||
},
|
||||
getItemActionClass(processedItem, routerProps) {
|
||||
return [
|
||||
'p-menuitem-link',
|
||||
{
|
||||
'router-link-active': routerProps && routerProps.isActive,
|
||||
'router-link-active-exact': this.exact && routerProps && routerProps.isExactActive
|
||||
}
|
||||
];
|
||||
},
|
||||
getItemIconClass(processedItem) {
|
||||
return ['p-menuitem-icon', this.getItemProp(processedItem, 'icon')];
|
||||
},
|
||||
getItemToggleIconClass(processedItem) {
|
||||
return ['p-submenu-icon', this.isItemActive(processedItem) ? 'pi pi-fw pi-chevron-down' : 'pi pi-fw pi-chevron-right'];
|
||||
},
|
||||
getSeparatorItemClass(processedItem) {
|
||||
return ['p-menuitem-separator', this.getItemProp(processedItem, 'class')];
|
||||
}
|
||||
},
|
||||
directives: {
|
||||
ripple: Ripple
|
||||
}
|
||||
};
|
||||
</script>
|
9
components/lib/panelmenu/package.json
Normal file
9
components/lib/panelmenu/package.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"main": "./panelmenu.cjs.js",
|
||||
"module": "./panelmenu.esm.js",
|
||||
"unpkg": "./panelmenu.min.js",
|
||||
"types": "./PanelMenu.d.ts",
|
||||
"browser": {
|
||||
"./sfc": "./PanelMenu.vue"
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue