<template> <component :is="wrapperComponent" :offset-top="offsetTop" :offset-bottom="offsetBottom" @on-change="handleAffixStateChange"> <div :class="`${prefix}-wrapper`" :style="wrapperStyle"> <div :class="`${prefix}`"> <div :class="`${prefix}-ink`"> <span v-show="showInk" :class="`${prefix}-ink-ball`" :style="{top: `${inkTop}px`}"></span> </div> <slot></slot> </div> </div> </component> </template> <script> import { scrollTop, findComponentsDownward, sharpMatcherRegx } from '../../utils/assist'; import { on, off } from '../../utils/dom'; export default { name: 'Anchor', provide () { return { anchorCom: this }; }, data () { return { prefix: 'ivu-anchor', isAffixed: false, // current affixed state inkTop: 0, animating: false, // if is scrolling now currentLink: '', // current show link => #href -> currentLink = #href currentId: '', // current show title id => #href -> currentId = href scrollContainer: null, scrollElement: null, titlesOffsetArr: [], wrapperTop: 0, upperFirstTitle: true }; }, props: { affix: { type: Boolean, default: true }, offsetTop: { type: Number, default: 0 }, offsetBottom: Number, bounds: { type: Number, default: 5 }, // container: [String, HTMLElement], // HTMLElement 在 SSR 下不支持 container: null, showInk: { type: Boolean, default: false }, scrollOffset: { type: Number, default: 0 } }, computed: { wrapperComponent () { return this.affix ? 'Affix' : 'div'; }, wrapperStyle () { return { maxHeight: this.offsetTop ? `calc(100vh - ${this.offsetTop}px)` : '100vh' }; }, containerIsWindow () { return this.scrollContainer === window; } }, methods: { handleAffixStateChange (state) { this.isAffixed = this.affix && state; }, handleScroll (e) { this.upperFirstTitle = e.target.scrollTop < this.titlesOffsetArr[0].offset; if (this.animating) return; this.updateTitleOffset(); const scrollTop = document.documentElement.scrollTop || document.body.scrollTop || e.target.scrollTop; this.getCurrentScrollAtTitleId(scrollTop); }, handleHashChange () { const url = window.location.href; const sharpLinkMatch = sharpMatcherRegx.exec(url); if (!sharpLinkMatch) return; this.currentLink = sharpLinkMatch[0]; this.currentId = sharpLinkMatch[1]; }, handleScrollTo () { const anchor = document.getElementById(this.currentId); const currentLinkElementA = document.querySelector(`a[data-href="${this.currentLink}"]`); let offset = this.scrollOffset; if (currentLinkElementA) { offset = parseFloat(currentLinkElementA.getAttribute('data-scroll-offset')); } if (!anchor) return; const offsetTop = anchor.offsetTop - this.wrapperTop - offset; this.animating = true; scrollTop(this.scrollContainer, this.scrollElement.scrollTop, offsetTop, 600, () => { this.animating = false; }); this.handleSetInkTop(); }, handleSetInkTop () { const currentLinkElementA = document.querySelector(`a[data-href="${this.currentLink}"]`); if (!currentLinkElementA) return; const elementATop = currentLinkElementA.offsetTop; const top = (elementATop < 0 ? this.offsetTop : elementATop); this.inkTop = top; }, updateTitleOffset () { const links = findComponentsDownward(this, 'AnchorLink').map(link => { return link.href; }); const idArr = links.map(link => { return link.split('#')[1]; }); let offsetArr = []; idArr.forEach(id => { const titleEle = document.getElementById(id); if (titleEle) offsetArr.push({ link: `#${id}`, offset: titleEle.offsetTop - this.scrollElement.offsetTop }); }); this.titlesOffsetArr = offsetArr; }, getCurrentScrollAtTitleId (scrollTop) { let i = -1; let len = this.titlesOffsetArr.length; let titleItem = { link: '#', offset: 0 }; scrollTop += this.bounds; while (++i < len) { let currentEle = this.titlesOffsetArr[i]; let nextEle = this.titlesOffsetArr[i + 1]; if (scrollTop >= currentEle.offset && scrollTop < ((nextEle && nextEle.offset) || Infinity)) { titleItem = this.titlesOffsetArr[i]; break; } } this.currentLink = titleItem.link; this.handleSetInkTop(); }, getContainer () { this.scrollContainer = this.container ? (typeof this.container === 'string' ? document.querySelector(this.container) : this.container) : window; this.scrollElement = this.container ? this.scrollContainer : (document.documentElement || document.body); }, removeListener () { off(this.scrollContainer, 'scroll', this.handleScroll); off(window, 'hashchange', this.handleHashChange); }, init () { // const anchorLink = findComponentDownward(this, 'AnchorLink'); this.handleHashChange(); this.$nextTick(() => { this.removeListener(); this.getContainer(); this.wrapperTop = this.containerIsWindow ? 0 : this.scrollElement.offsetTop; this.handleScrollTo(); this.handleSetInkTop(); this.updateTitleOffset(); this.upperFirstTitle = this.scrollElement.scrollTop < this.titlesOffsetArr[0].offset; on(this.scrollContainer, 'scroll', this.handleScroll); on(window, 'hashchange', this.handleHashChange); }); } }, watch: { '$route' () { this.handleHashChange(); this.$nextTick(() => { this.handleScrollTo(); }); }, container () { this.init(); }, currentLink (newHref, oldHref) { this.$emit('on-change', newHref, oldHref); } }, mounted () { this.init(); }, beforeDestroy () { this.removeListener(); } }; </script>