vxe-pc-ui
Version:
A vue based PC component library
208 lines (207 loc) • 7.39 kB
JavaScript
import { ref, h, reactive, provide, nextTick, onBeforeUnmount, onMounted, watch, computed } from 'vue';
import { defineVxeComponent } from '../../ui/src/comp';
import XEUtils from 'xe-utils';
import { createEvent, renderEmptyElement } from '../../ui';
import { getOffsetPos } from '../../ui/src/dom';
import VxeAnchorLinkComponent from './anchor-link';
export default defineVxeComponent({
name: 'VxeAnchor',
props: {
modelValue: String,
options: Array,
container: [String, Object, Function],
showMarker: {
type: Boolean,
default: true
}
},
emits: [
'update:modelValue',
'change',
'click'
],
setup(props, context) {
const { slots, emit } = context;
const xID = XEUtils.uniqueId();
const refElem = ref();
const refMarkerElem = ref();
const reactData = reactive({
activeHref: null,
staticLinks: [],
containerElem: null
});
const refMaps = {
refElem
};
const computeAllHrefList = computed(() => {
const list = [];
XEUtils.eachTree(reactData.staticLinks, item => {
list.push(item.href || '');
}, { children: 'children' });
return list;
});
const computeMaps = {};
const $xeAnchor = {
xID,
props,
context,
reactData,
getRefMaps: () => refMaps,
getComputeMaps: () => computeMaps
};
const anchorMethods = {
dispatchEvent(type, params, evnt) {
emit(type, createEvent(evnt, { $anchor: $xeAnchor }, params));
}
};
const getContainerElem = () => {
const { container } = props;
if (container) {
if (XEUtils.isElement(container)) {
return container;
}
if (XEUtils.isString(container)) {
return document.querySelector(container);
}
if (XEUtils.isFunction(container)) {
return container({ $anchor: $xeAnchor });
}
}
return null;
};
const emitEvent = (value) => {
reactData.activeHref = value;
emit('update:modelValue', value);
};
const handleContainerScrollEvent = () => {
const allHrefList = computeAllHrefList.value;
const { containerElem } = reactData;
if (containerElem) {
const wrapperElList = containerElem.querySelectorAll(allHrefList.map(href => `${href}`).join(','));
for (let i = 0; i < wrapperElList.length; i++) {
const wrapperEl = wrapperElList[i];
const wrapperRect = wrapperEl.getBoundingClientRect();
if (wrapperRect.top > 0) {
const href = wrapperEl.id;
reactData.activeHref = `#${href}`;
break;
}
}
}
};
const removeContainerElemScroll = () => {
const { containerElem } = reactData;
if (containerElem) {
containerElem.removeEventListener('scroll', handleContainerScrollEvent);
}
};
const updateContainerElem = () => {
const containerElem = getContainerElem();
reactData.containerElem = containerElem;
if (containerElem) {
containerElem.addEventListener('scroll', handleContainerScrollEvent, {
passive: false
});
}
};
const updateMarkerPos = () => {
nextTick(() => {
const { activeHref } = reactData;
const elem = refElem.value;
const markerEl = refMarkerElem.value;
if (elem && markerEl) {
if (activeHref) {
const linkEl = elem.querySelector(`[href="${activeHref}"]`);
if (linkEl) {
const { top } = getOffsetPos(linkEl, elem);
markerEl.style.top = `${top}px`;
}
}
}
});
};
const anchorPrivateMethods = {
handleClickLink(evnt, href) {
evnt.preventDefault();
const targetEl = document.getElementById(`${href}`.replace('#', ''));
if (targetEl) {
targetEl.scrollIntoView({
behavior: 'smooth'
});
}
emitEvent(href);
anchorMethods.dispatchEvent('click', { href }, evnt);
}
};
Object.assign($xeAnchor, anchorMethods, anchorPrivateMethods);
const renderSubItems = (options) => {
const itemVNs = [];
if (options) {
options.forEach(item => {
const subItems = item.children;
if (subItems && subItems.length) {
itemVNs.push(h(VxeAnchorLinkComponent, {
content: item.content,
title: item.title,
href: item.href
}, {
sub: () => renderSubItems(subItems)
}));
}
else {
itemVNs.push(h(VxeAnchorLinkComponent, {
content: item.content,
title: item.title,
href: item.href
}));
}
});
}
return itemVNs;
};
const renderVN = () => {
const { options, showMarker } = props;
const defaultSlot = slots.default;
return h('div', {
ref: refElem,
class: ['vxe-anchor', {
'is--marker': showMarker
}]
}, [
h('div', {
class: 'vxe-anchor--list'
}, defaultSlot ? defaultSlot({}) : renderSubItems(options)),
showMarker
? h('div', {
ref: refMarkerElem,
class: 'vxe-anchor--marker'
})
: renderEmptyElement($xeAnchor)
]);
};
watch(() => props.modelValue, (val) => {
reactData.activeHref = val;
});
watch(() => reactData.activeHref, () => {
updateMarkerPos();
});
watch(() => props.container, () => {
removeContainerElemScroll();
updateContainerElem();
});
onMounted(() => {
nextTick(() => {
updateContainerElem();
});
});
onBeforeUnmount(() => {
removeContainerElemScroll();
});
provide('$xeAnchor', $xeAnchor);
$xeAnchor.renderVN = renderVN;
return $xeAnchor;
},
render() {
return this.renderVN();
}
});