anchor.vue 6.97 KB
<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="showInkBall" :class="`${prefix}-ink-ball`" :style="{top: `${inkTop}px`}"></span>
                </div>
                <slot></slot>
            </div>
        </div>
	</component>
</template>
<script>
import { scrollTop, findComponentDownward, 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,
            linkHeight: 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],
        showInkInFixed: {
            type: Boolean,
            default: false
        }
    },
    computed: {
        wrapperComponent () {
            return this.affix ? 'Affix' : 'div';
        },
        wrapperStyle () {
            return {
                maxHeight: this.offsetTop ? `calc(100vh - ${this.offsetTop}px)` : '100vh'
            };
        },
        containerIsWindow () {
            return this.scrollContainer === window;
        },
        showInkBall () {
            return this.showInkInFixed && (this.isAffixed || (!this.isAffixed && !this.upperFirstTitle && 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);
        },
        turnTo (href) {
            this.currentLink = href;
            this.$router.push({
                path: href
            });
            this.$emit('on-select', href);
        },
        handleHashChange () {
            const url = window.location.href;
            const sharpLinkMatch = sharpMatcherRegx.exec(url);
            this.currentLink = sharpLinkMatch[0];
            this.currentId = sharpLinkMatch[1];
        },
        handleScrollTo () {
            const anchor = document.getElementById(this.currentId);
            if (!anchor) return;
            const offsetTop = anchor.offsetTop - this.wrapperTop;
            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.linkHeight = anchorLink ? anchorLink.$el.getBoundingClientRect().height : 0;
            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.handleScrollTo();
        },
        container () {
            this.init();
        },
        currentLink (newHref, oldHref) {
            this.$emit('on-change', newHref, oldHref);
        }
    },
    mounted () {
        this.init();
    },
    beforeDestroy () {
        this.removeListener();
    }
};
</script>