primevue-mirror/components/panelmenu/PanelMenuList.vue

392 lines
14 KiB
Vue

<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>