hongluan-ui
Version:
Hongluan Component Library for Vue 3
1 lines • 10.4 kB
Source Map (JSON)
{"version":3,"file":"anchor.mjs","sources":["../../../../../../packages/components/anchor/src/anchor.vue"],"sourcesContent":["<template>\n <div ref=\"anchorRef\" :class=\"cls\">\n <div\n v-if=\"!sliderLess\"\n ref=\"lineSliderRef\"\n :class=\"`slider-bar`\"\n ></div>\n <div :class=\"`anchor-list`\">\n <slot></slot>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport {\n computed,\n defineComponent,\n nextTick,\n onBeforeUnmount,\n onMounted,\n provide,\n reactive,\n ref,\n watch,\n} from 'vue'\nimport { isString } from '@vue/shared'\nimport computeScrollIntoView from 'compute-scroll-into-view'\nimport { isNumber, throttleByRaf } from '@hongluan-ui/utils'\nimport { useNamespace } from '@hongluan-ui/hooks'\nimport { findNode, slide } from './utils'\nimport { anchorInjectionKey } from './types'\n\nimport type { PropType } from 'vue'\nconst BOUNDARY_POSITIONS = ['start', 'end', 'center', 'nearest'] as const\ntype BoundaryPosition = typeof BOUNDARY_POSITIONS[number];\n\nexport default defineComponent({\n name: 'Anchor',\n props: {\n /**\n * @zh 滚动边界值,设置该值为数字后,将会在距离滚动容器 `boundary` 距离时停止滚动。\n * @en Scrolling boundary value. After setting the value to a number, it will stop scrolling when the distance is `boundary` from the scrolling container.\n * @values 'start', 'end', 'center', 'nearest', number\n */\n boundary: {\n type: [Number, String] as PropType<BoundaryPosition | number>,\n default: 'start',\n validator: (value: any) => {\n return isNumber(value) || BOUNDARY_POSITIONS.includes(value)\n },\n },\n /**\n * @zh 是否显示左侧轴线\n * @en Whether to show the left axis\n */\n sliderLess: {\n type: Boolean,\n default: false,\n },\n /**\n * @zh 滚动容器\n * @en Scroll container\n */\n scrollContainer: {\n type: [String, Object] as PropType<string | HTMLElement | Window>,\n },\n /**\n * @zh 是否改变hash。设置为 `false` 时点击锚点不会改变页面的 hash\n * @en Whether to change the hash. When set to `false`, clicking on the anchor will not change the hash of the page\n */\n changeHash: {\n type: Boolean,\n default: true,\n },\n /**\n * @zh 是否使用平滑滚动\n * @en Whether to use smooth scrolling\n */\n smooth: {\n type: Boolean,\n default: true,\n },\n right: Boolean,\n },\n emits: [\n /**\n * @zh 用户点击链接时触发\n * @en Triggered when the user clicks on the link\n * @property {string} hash\n * @property {string} preHash\n */\n 'select',\n /**\n * @zh 链接发生改变时触发\n * @en Triggered when the link changes\n * @property {string} hash\n */\n 'change',\n ],\n setup(props, { emit }) {\n const { namespace } = useNamespace('anchor')\n const anchorRef = ref<HTMLElement | null>(null)\n const lineSliderRef = ref<HTMLElement | null>(null)\n const links = reactive<Record<string, HTMLElement>>({})\n const currentLink = ref('')\n const isScrolling = ref(false)\n // throttle(this.handleScroll, 30, { trailing: true })\n const addLink = (hash: string, node: HTMLElement) => {\n if (!hash) return\n links[hash] = node\n }\n const removeLink = (hash: string) => {\n delete links[hash]\n }\n const getContainer = (targetContainer?: HTMLElement) => {\n let scrollContainer: string | HTMLElement | Window = targetContainer\n if (!scrollContainer) {\n scrollContainer = props.scrollContainer\n }\n if (isString(scrollContainer)) {\n return findNode(document, scrollContainer) as HTMLElement\n }\n return scrollContainer || window\n }\n const getContainerElement = (): HTMLElement => {\n if (isString(props.scrollContainer)) {\n return findNode(document, props.scrollContainer) as HTMLElement\n }\n if (props.scrollContainer && props.scrollContainer !== window) {\n return props.scrollContainer as HTMLElement\n }\n return document.documentElement ?? document.body\n }\n const handleClick = (e: MouseEvent, hash?: string) => {\n // const { onSelect, hash: changeHash } = this.props;\n if (!props.changeHash) {\n e.preventDefault()\n }\n handleAnchorChange(hash)\n scrollIntoView(hash)\n emit('select', hash, currentLink.value)\n }\n const scrollIntoView = (hash: string) => {\n if (!hash) return\n try {\n const node = findNode(document, hash)\n if (!node) return\n let block = props.boundary as BoundaryPosition\n let diff = 0\n if (isNumber(props.boundary)) {\n block = 'start'\n diff = props.boundary as number\n }\n const actions = computeScrollIntoView(node, { block })\n if (!actions.length) return\n const { el, top } = actions[0]\n const targetTop = top - diff\n // if (!this.props.animation) {\n // return;\n // }\n slide(el as HTMLElement, targetTop, () => {\n isScrolling.value = false\n })\n isScrolling.value = true\n } catch (e) {\n // eslint-disable-next-line no-console\n console.error(e)\n }\n }\n const handleScroll = throttleByRaf(() => {\n if (isScrolling.value) return\n const element = getFirstInViewportEle()\n if (element && element.id) {\n const hash = `#${element.id}`\n handleAnchorChange(hash)\n }\n })\n const handleAnchorChange = (hash: string) => {\n if (!hash) return\n if (!links[hash] && anchorRef.value) {\n const node = findNode(\n anchorRef.value,\n `a[data-href='${hash}']`,\n ) as HTMLElement\n if (!node) return\n links[hash] = node\n }\n if (hash !== currentLink.value) {\n currentLink.value = hash\n nextTick(() => {\n emit('change', hash)\n })\n }\n }\n const getFirstInViewportEle = (): HTMLElement | undefined => {\n const boundary = isNumber(props.boundary) ? props.boundary as number : 0\n const container = getContainerElement()\n const containerRect = container.getBoundingClientRect()\n const { clientHeight } = document.documentElement\n const ele = getContainer()\n for (const hash of Object.keys(links)) {\n const node = findNode(document, hash)\n if (node) {\n const { top } = node.getBoundingClientRect()\n if (ele === window) {\n const offsetTop = top - boundary\n if (offsetTop >= 0 && offsetTop <= clientHeight / 2) {\n return node as HTMLElement\n }\n } else {\n const offsetTop = top - containerRect.top - boundary\n if (offsetTop >= 0 && offsetTop <= containerRect.height / 2) {\n return node as HTMLElement\n }\n }\n }\n }\n return undefined\n }\n onMounted(() => {\n handleScroll()\n bindScrollEvent()\n })\n onBeforeUnmount(() => {\n unbindScrollEvent()\n })\n watch(currentLink, () => {\n const link = links[currentLink.value]\n if (!props.sliderLess && link && lineSliderRef.value) {\n lineSliderRef.value.style.top = `${link.offsetTop}px`\n }\n })\n const bindScrollEvent = (scrollContainer?: HTMLElement) => {\n getContainer(scrollContainer).addEventListener('scroll', handleScroll)\n }\n const unbindScrollEvent = (scrollContainer?: HTMLElement) => {\n getContainer(scrollContainer).removeEventListener('scroll', handleScroll)\n }\n provide(\n anchorInjectionKey,\n reactive({\n name: 'Anchor',\n currentLink,\n addLink,\n removeLink,\n handleClick,\n }),\n )\n const cls = computed(() => [\n namespace.value,\n {\n 'slider-less': props.sliderLess,\n 'right': props.right,\n },\n ])\n return {\n cls,\n anchorRef,\n lineSliderRef,\n }\n },\n})\n</script>\n"],"names":["_openBlock"],"mappings":"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"}