401 lines
14 KiB
Vue
401 lines
14 KiB
Vue
<template>
|
|
<PanelMenuSub
|
|
:id="panelId + '_list'"
|
|
:class="cx('rootList')"
|
|
role="tree"
|
|
:tabindex="-1"
|
|
:aria-activedescendant="focused ? focusedItemId : undefined"
|
|
:panelId="panelId"
|
|
:focusedItemId="focused ? focusedItemId : undefined"
|
|
:items="processedItems"
|
|
:templates="templates"
|
|
:activeItemPath="activeItemPath"
|
|
@focus="onFocus"
|
|
@blur="onBlur"
|
|
@keydown="onKeyDown"
|
|
@item-toggle="onItemToggle"
|
|
@item-mousemove="onItemMouseMove"
|
|
:pt="pt"
|
|
:unstyled="unstyled"
|
|
v-bind="ptm('rootList')"
|
|
/>
|
|
</template>
|
|
|
|
<script>
|
|
import BaseComponent from '@primevue/core/basecomponent';
|
|
import { findSingle, focus } from '@primeuix/utils/dom';
|
|
import { resolve, isNotEmpty, isPrintableCharacter, findLast, isEmpty } from '@primeuix/utils/object';
|
|
import PanelMenuSub from './PanelMenuSub.vue';
|
|
|
|
export default {
|
|
name: 'PanelMenuList',
|
|
hostName: 'PanelMenu',
|
|
extends: BaseComponent,
|
|
emits: ['item-toggle', 'header-focus'],
|
|
props: {
|
|
panelId: {
|
|
type: String,
|
|
default: null
|
|
},
|
|
items: {
|
|
type: Array,
|
|
default: null
|
|
},
|
|
templates: {
|
|
type: Object,
|
|
default: null
|
|
},
|
|
expandedKeys: {
|
|
type: Object,
|
|
default: null
|
|
}
|
|
},
|
|
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 ? resolve(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 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':
|
|
case 'NumpadEnter':
|
|
this.onEnterKey(event);
|
|
break;
|
|
|
|
case 'Escape':
|
|
case 'Tab':
|
|
case 'PageDown':
|
|
case 'PageUp':
|
|
case 'Backspace':
|
|
case 'ShiftLeft':
|
|
case 'ShiftRight':
|
|
//NOOP
|
|
break;
|
|
|
|
default:
|
|
if (!metaKey && isPrintableCharacter(event.key)) {
|
|
this.searchItems(event, event.key);
|
|
}
|
|
|
|
break;
|
|
}
|
|
},
|
|
onArrowDownKey(event) {
|
|
const processedItem = isNotEmpty(this.focusedItem) ? this.findNextItem(this.focusedItem) : this.findFirstItem();
|
|
|
|
this.changeFocusedItem({ originalEvent: event, processedItem, focusOnNext: true });
|
|
event.preventDefault();
|
|
},
|
|
onArrowUpKey(event) {
|
|
const processedItem = isNotEmpty(this.focusedItem) ? this.findPrevItem(this.focusedItem) : this.findLastItem();
|
|
|
|
this.changeFocusedItem({ originalEvent: event, processedItem, selfCheck: true });
|
|
event.preventDefault();
|
|
},
|
|
onArrowLeftKey(event) {
|
|
if (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 = isNotEmpty(this.focusedItem.parent) ? this.focusedItem.parent : this.focusedItem;
|
|
}
|
|
|
|
event.preventDefault();
|
|
}
|
|
},
|
|
onArrowRightKey(event) {
|
|
if (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 (isNotEmpty(this.focusedItem)) {
|
|
const element = findSingle(this.$el, `li[id="${`${this.focusedItemId}`}"]`);
|
|
const anchorElement = element && (findSingle(element, '[data-pc-section="itemlink"]') || 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;
|
|
focus(this.$el);
|
|
},
|
|
onItemMouseMove(event) {
|
|
if (this.focused) {
|
|
this.focusedItem = event.processedItem;
|
|
}
|
|
},
|
|
isElementInPanel(event, element) {
|
|
const panel = event.currentTarget.closest('[data-pc-section="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) && !this.getItemProp(processedItem, 'separator');
|
|
},
|
|
findFirstItem() {
|
|
return this.visibleItems.find((processedItem) => this.isValidItem(processedItem));
|
|
},
|
|
findLastItem() {
|
|
return 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 ? 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 (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 = isEmpty(matchedItem) ? this.visibleItems.slice(0, focusedItemIndex).find((processedItem) => this.isItemMatched(processedItem)) : matchedItem;
|
|
} else {
|
|
matchedItem = this.visibleItems.find((processedItem) => this.isItemMatched(processedItem));
|
|
}
|
|
|
|
if (isNotEmpty(matchedItem)) {
|
|
matched = true;
|
|
}
|
|
|
|
if (isEmpty(matchedItem) && isEmpty(this.focusedItem)) {
|
|
matchedItem = this.findFirstItem();
|
|
}
|
|
|
|
if (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 (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 = 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 isNotEmpty(this.focusedItem) ? `${this.panelId}_${this.focusedItem.key}` : null;
|
|
}
|
|
},
|
|
components: {
|
|
PanelMenuSub: PanelMenuSub
|
|
}
|
|
};
|
|
</script>
|