@vuecs/navigation
Version:
A package for multi level navigations.
607 lines (592 loc) • 19.9 kB
JavaScript
import { createComponentOptionsManager, mergeOption, inject, provide, hasNormalizedSlot, normalizeSlot, installStoreManager, applyStoreManagerOptions } from '@vuecs/core';
import { EventEmitter } from '@posva/event-emitter';
import { VCLink } from '@vuecs/link';
import { defineComponent, resolveComponent, toRef, computed, h, ref, onMounted, onUnmounted } from 'vue';
function buildComponentOptions() {
const manager = createComponentOptionsManager({
name: 'navigation'
});
return {
groupClass: mergeOption('class', manager.get('groupClass'), 'vc-nav-items'),
groupTag: manager.get('groupTag') || 'ul',
itemClass: mergeOption('class', manager.get('itemClass'), 'vc-nav-item'),
itemNestedClass: mergeOption('class', manager.get('itemNestedClass'), 'vc-nav-item-nested'),
itemTag: manager.get('itemTag') || 'li',
separatorTag: manager.get('separatorTag') || 'div',
separatorClass: mergeOption('class', manager.get('linkIconClass'), 'vc-nav-separator'),
linkIconTag: manager.get('linkIconTag') || 'div',
linkIconClass: mergeOption('class', manager.get('linkIconClass'), 'vc-nav-link-icon'),
linkClass: mergeOption('class', manager.get('linkClass'), 'vc-nav-link'),
linkRootClass: mergeOption('class', manager.get('linkRootClass'), 'vc-nav-link-root'),
linkTextTag: manager.get('linkTextTag') || 'div',
linkTextClass: mergeOption('class', manager.get('linkTextClass'), 'vc-nav-link-text')
};
}
function calculateItemScoreForPath(item, currentPath) {
if (item.url === '/') {
return 1;
}
if (item.activeMatch) {
if (item.activeMatch === currentPath) {
return 6;
}
if (currentPath.startsWith(item.activeMatch)) {
return 4;
}
}
if (item.url) {
if (item.url === currentPath) {
return 3;
}
if (currentPath.startsWith(item.url)) {
return 2;
}
}
return 0;
}
function findItemMatchesIF(items, options, parent) {
const output = [];
for(let i = 0; i < items.length; i++){
const item = items[i];
let { score } = parent;
if (options.path) {
score += calculateItemScoreForPath(item, options.path);
}
if (item.default) {
score += 1;
}
if (item.children) {
const childMatches = findItemMatchesIF(item.children, options, {
score
});
output.push(...childMatches);
}
output.push({
data: item,
score
});
}
return output.sort((a, b)=>b.score - a.score);
}
function findBestItemMatches(items, options = {}) {
const result = findItemMatchesIF(items, options, {
score: 0
});
const [first] = result;
if (!first) {
return [];
}
return result.filter((match)=>match.score === first.score).map((match)=>match.data);
}
/*
* Copyright (c) 2024.
* Author Peter Placzek (tada5hi)
* For the full copyright and license information,
* view the LICENSE file that was distributed with this source code.
*/ function normalizeItemIF(item, defaults, trace) {
const output = {
...item,
level: defaults.level,
children: [],
trace: [
...trace,
item.name
],
meta: item.meta || {}
};
if (!item.children) {
return output;
}
for(let i = 0; i < item.children.length; i++){
output.children.push(normalizeItemIF(item.children[i], defaults, output.trace));
}
return output;
}
function normalizeItem(item, defaults) {
return normalizeItemIF(item, defaults, []);
}
function normalizeItems(items, options) {
return items.map((item)=>normalizeItem(item, options));
}
/*
* Copyright (c) 2024.
* Author Peter Placzek (tada5hi)
* For the full copyright and license information,
* view the LICENSE file that was distributed with this source code.
*/ function isTraceEqual(a, b) {
if (a.length !== b.length) {
return false;
}
for(let i = 0; i < a.length; i++){
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
function isTracePartOf(item, parent) {
for(let i = 0; i < item.length; i++){
if (parent[i] !== item[i]) {
return false;
}
}
return true;
}
function resetItemsByTraceIF(items, trace) {
for(let i = 0; i < items.length; i++){
const item = items[i];
const isEqual = isTraceEqual(items[i].trace, trace);
item.active = isEqual;
item.display = true;
if (isEqual) {
item.displayChildren = true;
} else {
item.displayChildren = isTracePartOf(item.trace, trace);
}
item.children = resetItemsByTraceIF(item.children, trace);
}
return items;
}
function resetItemsByTrace(items, trace) {
return resetItemsByTraceIF(items, trace);
}
function findItemsWithLevel(items, tier) {
return items.filter((item)=>item.level === tier);
}
function findItemWithLevel(tier, items) {
const data = findItemsWithLevel(items, tier);
if (data.length >= 1) {
return data[0];
}
return undefined;
}
function removeItemsWithLevel(tier, items) {
return items.filter((item)=>item.level !== tier);
}
function replaceLevelItem(tier, input, next) {
const output = removeItemsWithLevel(tier, input);
if (next) {
next.level = tier;
return [
...output,
next
];
}
return output;
}
function replaceLevelItems(tier, src, next) {
const componentsExisting = removeItemsWithLevel(tier, src);
return [
...componentsExisting,
...next
];
}
function isAbsoluteURL(str) {
return str.substring(0, 7) === 'http://' || str.substring(0, 8) === 'https://';
}
class NavigationManager extends EventEmitter {
getItems(tier) {
if (typeof tier === 'undefined') {
return this.items;
}
return this.items.filter((item)=>item.level === tier);
}
reset() {
this.built = false;
this.items = [];
this.itemsActive = [];
}
async build(options) {
if (this.built || this.building) {
return;
}
this.building = true;
this.emit('building');
let parent;
let level = 0;
while(true){
const raw = await this.itemsFn({
level,
parent
});
if (!raw || raw.length === 0) {
break;
}
const items = normalizeItems(raw, {
level
});
const matches = findBestItemMatches(items, {
path: options.path
});
const [match] = matches;
if (!match) {
break;
}
this.itemsActive.push(match);
await this.buildLevel(level);
parent = match;
level++;
}
this.building = false;
this.built = true;
this.emit('built');
this.emit('updated', this.items);
}
async select(level, itemNew) {
const itemOld = findItemWithLevel(level, this.itemsActive);
if (itemOld && isTraceEqual(itemOld.trace, itemNew.trace)) {
return;
}
this.itemsActive = this.itemsActive.filter((el)=>el.level < level);
this.itemsActive.push(itemNew);
const startLevel = level;
while(true){
const built = await this.buildLevel(level, startLevel === level);
if (!built) {
break;
}
level++;
}
}
async toggle(level, item) {
let isMatch;
if (item.displayChildren) {
isMatch = true;
} else {
const itemOld = findItemWithLevel(level, this.itemsActive);
isMatch = !!itemOld && isTraceEqual(item.trace, itemOld.trace);
}
if (isMatch) {
this.itemsActive = removeItemsWithLevel(level, this.itemsActive);
} else {
this.itemsActive = replaceLevelItem(level, this.itemsActive, item);
}
await this.buildLevel(level, true);
}
async buildLevel(level, useCache) {
let items;
if (useCache) {
items = findItemsWithLevel(this.items, level);
} else {
const parent = findItemWithLevel(level - 1, this.itemsActive);
const raw = await this.itemsFn({
level,
parent
});
items = raw && raw.length > 0 ? normalizeItems(raw, {
level
}) : [];
}
if (!items || items.length === 0) {
this.items = this.items.filter((item)=>item.level < level);
this.emit('levelUpdated', level, []);
return false;
}
let trace = [];
const item = findItemWithLevel(level, this.itemsActive);
if (item) {
trace = item.trace;
}
resetItemsByTrace(items, trace);
this.items = replaceLevelItems(level, this.items, items);
this.emit('levelUpdated', level, items);
return true;
}
constructor(options){
super();
let itemsFn;
if (typeof options.items === 'function') {
itemsFn = options.items;
} else {
itemsFn = async ({ level })=>{
if (level > 0) {
return [];
}
return options.items;
};
}
this.itemsFn = itemsFn;
this.items = [];
this.itemsActive = [];
this.built = false;
this.building = false;
}
}
const sym = Symbol.for('VCNavigationManager');
function injectNavigationManager(app) {
const instance = inject(sym, app);
if (!instance) {
throw new Error('A navigation provider has not been provided.');
}
return instance;
}
function provideNavigationManager(manager, app) {
provide(sym, manager, app);
}
var SlotName = /*#__PURE__*/ function(SlotName) {
SlotName["ITEM"] = "item";
SlotName["SEPARATOR"] = "separator";
SlotName["LINK"] = "link";
SlotName["SUB"] = "sub";
SlotName["SUB_TITLE"] = "sub-title";
SlotName["SUB_ITEMS"] = "sub-items";
return SlotName;
}({});
var ElementType = /*#__PURE__*/ function(ElementType) {
ElementType["LINK"] = "link";
ElementType["SEPARATOR"] = "separator";
return ElementType;
}({});
const VCNavItem = defineComponent({
props: {
data: {
type: Object,
required: true
}
},
setup (props, { slots }) {
const itemsNode = resolveComponent('VCNavItems');
const options = buildComponentOptions();
const manager = injectNavigationManager();
const data = toRef(props, 'data');
const hasChildren = computed(()=>data.value.children && data.value.children.length > 0);
const select = async (value)=>{
await manager.select(data.value.level, value);
};
const toggle = async (value)=>{
await manager.toggle(data.value.level, value);
};
return ()=>{
const buildItem = ()=>{
// type: separator
if (data.value.type === ElementType.SEPARATOR) {
const hasSlot = hasNormalizedSlot(SlotName.SEPARATOR, slots);
if (hasSlot) {
return normalizeSlot(SlotName.SEPARATOR, {
data: data.value
}, slots);
}
return h(options.separatorTag, {
class: options.separatorClass
}, data.value.name);
}
// type: group
if (!hasChildren.value) {
const hasSlot = hasNormalizedSlot(SlotName.LINK, slots);
if (hasSlot) {
return normalizeSlot(SlotName.LINK, {
data: data.value,
select,
isActive: data.value.active
}, slots);
}
const linkProps = {
active: data.value.active,
disabled: false,
prefetch: true
};
if (data.value.url) {
if (isAbsoluteURL(data.value.url) || data.value.url.startsWith('#')) {
linkProps.href = data.value.url;
if (data.value.urlTarget) {
linkProps.target = data.value.urlTarget;
}
} else {
linkProps.to = data.value.url;
}
}
return h(VCLink, {
class: [
options.linkClass
],
...linkProps,
onClicked () {
if (!data.value.url) {
return select.call(null, data.value);
}
return undefined;
},
onClick () {
return select.call(null, data.value);
}
}, {
default: ()=>[
...data.value.icon ? [
h(options.linkIconTag, {
class: options.linkIconClass
}, [
h('i', {
class: data.value.icon
})
])
] : [],
h(options.linkTextTag, {
class: options.linkTextClass
}, [
data.value.name
])
]
});
}
if (hasNormalizedSlot(SlotName.SUB, slots)) {
return normalizeSlot(SlotName.SUB, {
data: data.value,
select,
toggle
}, slots);
}
let title;
if (hasNormalizedSlot(SlotName.SUB_TITLE, slots)) {
title = normalizeSlot(SlotName.SUB_TITLE, {
data: data.value,
select,
toggle
});
} else {
title = h('div', {
class: options.linkClass,
onClick ($event) {
$event.preventDefault();
return toggle(data.value);
}
}, [
...data.value.icon ? [
h(options.linkIconTag, {
class: options.linkIconClass
}, [
h('i', {
class: data.value.icon
})
])
] : [],
h(options.linkTextTag, {
class: options.linkTextClass
}, [
data.value.name
])
]);
}
if (!hasChildren.value) {
return title;
}
let vNodes;
if (hasNormalizedSlot(SlotName.SUB_ITEMS, slots)) {
vNodes = normalizeSlot(SlotName.SUB_ITEMS, {
data: data.value,
select,
toggle
});
} else {
vNodes = h(itemsNode, {
level: data.value.level,
data: data.value.children
});
}
return [
title,
vNodes
];
};
return h(options.itemTag, {
class: [
options.itemClass,
...hasChildren.value ? [
options.itemNestedClass
] : [],
{
active: data.value.active || data.value.displayChildren
}
]
}, [
buildItem()
]);
};
}
});
const VCNavItems = defineComponent({
props: {
level: {
type: Number,
default: 0
},
data: {
type: Array,
default: undefined
}
},
setup (props, { slots }) {
const options = buildComponentOptions();
const manager = injectNavigationManager();
const managerItems = ref([]);
if (!props.data) {
managerItems.value = manager.getItems(props.level);
}
const counter = ref(0);
let removeListener;
onMounted(()=>{
removeListener = manager.on('levelUpdated', (level, items)=>{
if (level !== props.level) {
return;
}
managerItems.value = items;
counter.value++;
});
});
onUnmounted(()=>{
if (typeof removeListener === 'function') {
removeListener();
removeListener = undefined;
}
});
const items = computed(()=>{
if (typeof props.data !== 'undefined') {
return props.data;
}
return managerItems.value;
});
return ()=>{
const vNodes = [];
for(let i = 0; i < items.value.length; i++){
if (!items.value[i].display && !items.value[i].displayChildren) {
continue;
}
let vNode;
if (hasNormalizedSlot(SlotName.ITEM, slots)) {
vNode = normalizeSlot(SlotName.ITEM, {
data: items.value[i]
}, slots);
} else {
vNode = h(VCNavItem, {
key: `${i}:${counter.value}`,
data: items.value[i]
});
}
vNodes.push(vNode);
}
return h(options.groupTag, {
class: options.groupClass
}, vNodes);
};
}
});
function install(instance, options) {
const manager = new NavigationManager({
items: options.items
});
provideNavigationManager(manager, instance);
const storeManager = installStoreManager(instance);
if (options.storeManager) {
applyStoreManagerOptions(storeManager, options.storeManager);
}
Object.entries({
VCNavItem,
VCNavItems
}).forEach(([componentName, component])=>{
instance.component(componentName, component);
});
}
var index = {
install
};
export { NavigationManager, VCNavItem, VCNavItems, index as default, injectNavigationManager, install, provideNavigationManager };
//# sourceMappingURL=index.mjs.map