iview/src/components/anchor/anchor.vue

191 lines
6.9 KiB
Vue

<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',
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>