UNPKG

@vuecs/navigation

Version:

A package for multi level navigations.

618 lines (601 loc) 20.2 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var core = require('@vuecs/core'); var eventEmitter = require('@posva/event-emitter'); var link = require('@vuecs/link'); var vue = require('vue'); function buildComponentOptions() { const manager = core.createComponentOptionsManager({ name: 'navigation' }); return { groupClass: core.mergeOption('class', manager.get('groupClass'), 'vc-nav-items'), groupTag: manager.get('groupTag') || 'ul', itemClass: core.mergeOption('class', manager.get('itemClass'), 'vc-nav-item'), itemNestedClass: core.mergeOption('class', manager.get('itemNestedClass'), 'vc-nav-item-nested'), itemTag: manager.get('itemTag') || 'li', separatorTag: manager.get('separatorTag') || 'div', separatorClass: core.mergeOption('class', manager.get('linkIconClass'), 'vc-nav-separator'), linkIconTag: manager.get('linkIconTag') || 'div', linkIconClass: core.mergeOption('class', manager.get('linkIconClass'), 'vc-nav-link-icon'), linkClass: core.mergeOption('class', manager.get('linkClass'), 'vc-nav-link'), linkRootClass: core.mergeOption('class', manager.get('linkRootClass'), 'vc-nav-link-root'), linkTextTag: manager.get('linkTextTag') || 'div', linkTextClass: core.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.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 = core.inject(sym, app); if (!instance) { throw new Error('A navigation provider has not been provided.'); } return instance; } function provideNavigationManager(manager, app) { core.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 = vue.defineComponent({ props: { data: { type: Object, required: true } }, setup (props, { slots }) { const itemsNode = vue.resolveComponent('VCNavItems'); const options = buildComponentOptions(); const manager = injectNavigationManager(); const data = vue.toRef(props, 'data'); const hasChildren = vue.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 = core.hasNormalizedSlot(SlotName.SEPARATOR, slots); if (hasSlot) { return core.normalizeSlot(SlotName.SEPARATOR, { data: data.value }, slots); } return vue.h(options.separatorTag, { class: options.separatorClass }, data.value.name); } // type: group if (!hasChildren.value) { const hasSlot = core.hasNormalizedSlot(SlotName.LINK, slots); if (hasSlot) { return core.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 vue.h(link.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 ? [ vue.h(options.linkIconTag, { class: options.linkIconClass }, [ vue.h('i', { class: data.value.icon }) ]) ] : [], vue.h(options.linkTextTag, { class: options.linkTextClass }, [ data.value.name ]) ] }); } if (core.hasNormalizedSlot(SlotName.SUB, slots)) { return core.normalizeSlot(SlotName.SUB, { data: data.value, select, toggle }, slots); } let title; if (core.hasNormalizedSlot(SlotName.SUB_TITLE, slots)) { title = core.normalizeSlot(SlotName.SUB_TITLE, { data: data.value, select, toggle }); } else { title = vue.h('div', { class: options.linkClass, onClick ($event) { $event.preventDefault(); return toggle(data.value); } }, [ ...data.value.icon ? [ vue.h(options.linkIconTag, { class: options.linkIconClass }, [ vue.h('i', { class: data.value.icon }) ]) ] : [], vue.h(options.linkTextTag, { class: options.linkTextClass }, [ data.value.name ]) ]); } if (!hasChildren.value) { return title; } let vNodes; if (core.hasNormalizedSlot(SlotName.SUB_ITEMS, slots)) { vNodes = core.normalizeSlot(SlotName.SUB_ITEMS, { data: data.value, select, toggle }); } else { vNodes = vue.h(itemsNode, { level: data.value.level, data: data.value.children }); } return [ title, vNodes ]; }; return vue.h(options.itemTag, { class: [ options.itemClass, ...hasChildren.value ? [ options.itemNestedClass ] : [], { active: data.value.active || data.value.displayChildren } ] }, [ buildItem() ]); }; } }); const VCNavItems = vue.defineComponent({ props: { level: { type: Number, default: 0 }, data: { type: Array, default: undefined } }, setup (props, { slots }) { const options = buildComponentOptions(); const manager = injectNavigationManager(); const managerItems = vue.ref([]); if (!props.data) { managerItems.value = manager.getItems(props.level); } const counter = vue.ref(0); let removeListener; vue.onMounted(()=>{ removeListener = manager.on('levelUpdated', (level, items)=>{ if (level !== props.level) { return; } managerItems.value = items; counter.value++; }); }); vue.onUnmounted(()=>{ if (typeof removeListener === 'function') { removeListener(); removeListener = undefined; } }); const items = vue.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 (core.hasNormalizedSlot(SlotName.ITEM, slots)) { vNode = core.normalizeSlot(SlotName.ITEM, { data: items.value[i] }, slots); } else { vNode = vue.h(VCNavItem, { key: `${i}:${counter.value}`, data: items.value[i] }); } vNodes.push(vNode); } return vue.h(options.groupTag, { class: options.groupClass }, vNodes); }; } }); function install(instance, options) { const manager = new NavigationManager({ items: options.items }); provideNavigationManager(manager, instance); const storeManager = core.installStoreManager(instance); if (options.storeManager) { core.applyStoreManagerOptions(storeManager, options.storeManager); } Object.entries({ VCNavItem, VCNavItems }).forEach(([componentName, component])=>{ instance.component(componentName, component); }); } var index = { install }; exports.NavigationManager = NavigationManager; exports.VCNavItem = VCNavItem; exports.VCNavItems = VCNavItems; exports.default = index; exports.injectNavigationManager = injectNavigationManager; exports.install = install; exports.provideNavigationManager = provideNavigationManager; module.exports = Object.assign(exports.default, exports); //# sourceMappingURL=index.cjs.map