vxe-pc-ui
Version:
A vue based PC component library
237 lines (207 loc) • 6.89 kB
text/typescript
import { defineComponent, ref, h, reactive, PropType, VNode, provide, nextTick, onBeforeUnmount, onMounted, watch, createCommentVNode, computed } from 'vue'
import XEUtils from 'xe-utils'
import { createEvent } from '@vxe-ui/core'
import { getOffsetPos } from '../../ui/src/dom'
import VxeAnchorLinkComponent from './anchor-link'
import type { VxeAnchorPropTypes, AnchorReactData, VxeAnchorEmits, AnchorPrivateRef, VxeAnchorPrivateComputed, VxeAnchorConstructor, VxeAnchorPrivateMethods, AnchorMethods, AnchorPrivateMethods } from '../../../types'
export default defineComponent({
name: 'VxeAnchor',
props: {
modelValue: String as PropType<VxeAnchorPropTypes.ModelValue>,
options: Array as PropType<VxeAnchorPropTypes.Options>,
container: [String, Object, Function] as PropType<VxeAnchorPropTypes.Container>,
showMarker: {
type: Boolean as PropType<VxeAnchorPropTypes.ShowMarker>,
default: true
}
},
emits: [
'update:modelValue',
'change',
'click'
] as VxeAnchorEmits,
setup (props, context) {
const { slots, emit } = context
const xID = XEUtils.uniqueId()
const refElem = ref<HTMLDivElement>()
const refMarkerElem = ref<HTMLDivElement>()
const reactData = reactive<AnchorReactData>({
activeHref: null,
staticLinks: [],
containerElem: null
})
const refMaps: AnchorPrivateRef = {
refElem
}
const computeAllHrefList = computed(() => {
const list: VxeAnchorPropTypes.ModelValue[] = []
XEUtils.eachTree(reactData.staticLinks, item => {
list.push(item.href || '')
}, { children: 'children' })
return list
})
const computeMaps: VxeAnchorPrivateComputed = {
}
const $xeAnchor = {
xID,
props,
context,
reactData,
getRefMaps: () => refMaps,
getComputeMaps: () => computeMaps
} as unknown as VxeAnchorConstructor & VxeAnchorPrivateMethods
const anchorMethods: 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) as HTMLElement | null
}
if (XEUtils.isFunction(container)) {
return container({ $anchor: $xeAnchor })
}
}
return null
}
const emitEvent = (value?: VxeAnchorPropTypes.ModelValue) => {
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: 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?: VxeAnchorPropTypes.Options) => {
const itemVNs: VNode[] = []
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'
})
: createCommentVNode()
])
}
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()
}
})