前言:在做项目中,由于使用的Vue1+iview1.0.1版本,版本过于老旧,无法使用业务中需要的锚点,经过借鉴自己创建一个Vue1可以使用的锚点跳转组件
首先安装需要用到的依赖
yarn add throttle-debounce
affix.vue 固定组件
<template>
<div ref="placeholder" class="affix-placeholder" :style="placeholderStyle">
<div class="affix" :style="affixStyle">
<slot></slot>
</div>
</div>
</template>
<script>
import { throttle } from "throttle-debounce";
import { getTargetRect, getFixedTop } from "../../utils/dom";
const events = ["scroll", "resize"];
export default {
data() {
return {
eventHandler: throttle(16, this.updatePosition.bind(this)),
placeholderStyle: {},
affixStyle: {},
affixContainer: null,
};
},
props: {
getContainer: {
type: Function,
default: () => window,
},
offsetTop: {
type: Number,
default: 0,
},
},
watch: {
getContainer(val) {
let newTarget = null;
if (val) {
newTarget = val() || null;
}
if (this.affixContainer !== newTarget) {
events.forEach(
(event) =>
this.affixContainer &&
this.affixContainer.removeEventListener(event, this.eventHandler)
);
events.forEach(
(event) =>
newTarget && newTarget.addEventListener(event, this.eventHandler)
);
this.updatePosition();
this.affixContainer = newTarget;
}
},
offsetTop() {
this.updatePosition();
},
},
methods: {
updatePosition() {
const $container = this.getContainer();
const $placeholder = this.$refs.placeholder;
if (!$container || !$placeholder) {
return;
}
const containerRect = getTargetRect($container);
const placeholderRect = getTargetRect($placeholder);
const fixedTop = getFixedTop(
placeholderRect,
containerRect,
this.offsetTop
);
if (fixedTop !== undefined) {
this.affixStyle = {
position: "fixed",
top: fixedTop,
width: placeholderRect.width + "px",
height: placeholderRect.height + "px",
};
this.placeholderStyle = {
width: placeholderRect.width + "px",
height: placeholderRect.height + "px",
};
} else {
this.affixStyle = {};
this.placeholderStyle = {};
}
},
},
ready() {
setTimeout(() => {
this.affixContainer = this.getContainer();
events.forEach(
(event) =>
this.affixContainer &&
this.affixContainer.addEventListener(event, this.eventHandler)
);
}, 0);
},
beforeDestroy() {
events.forEach(
(event) =>
this.affixContainer &&
this.affixContainer.removeEventListener(event, this.eventHandler)
);
},
};
</script>
anchor-link.vue 锚点item项
<template>
<div class="anchor-link">
<slot name="title">
<a
:href="href"
:target="target"
:title="title"
class="anchor-link__title"
:class="bizAnchorActiveLink === href ? 'is-active' : ''"
@click="handleClick"
>
{{ title }}
</a>
</slot>
<slot></slot>
</div>
</template>
<script>
export default {
name: "AnchorLink",
props: {
title: {
type: String,
default: "",
},
href: {
type: String,
default: "",
},
target: {
type: String,
default: "",
},
},
watch: {
href(val, oldVal) {
setTimeout(() => {
this.bizAnchor.unregisterLink(oldVal);
this.bizAnchor.registerLink(val);
}, 0);
},
},
data() {
return {
bizAnchor: null,
};
},
computed: {
bizAnchorActiveLink() {
return this.$parent.bizAnchorActiveLink;
},
},
beforeDestroy() {
this.bizAnchor.unregisterLink(this.href);
},
ready() {
this.bizAnchor = this.getBizAnchor();
this.bizAnchor.registerLink(this.href);
},
methods: {
handleClick(e) {
this.bizAnchor.scrollTo(this.href);
this.$emit("click", e, { title: this.title, href: this.href });
},
getBizAnchor() {
return this.$parent.getBizAnchor();
},
},
};
</script>
anchor.vue 锚点链接主组件
css三个组件的样式全在里面
<template>
<affix v-if="affix" :offset-top="offsetTop" :get-container="getContainer">
<div class="anchor">
<div
v-if="showInk"
:class="['anchor-ink', inkHeight === '0px' ? '' : 'visible']"
:style="{ top: inkPositionTop, height: inkHeight }"
></div>
<slot></slot>
</div>
</affix>
<div v-else class="anchor">
<div
v-if="showInk"
class="anchor-ink"
:style="{ top: inkPositionTop, height: inkHeight }"
></div>
<slot></slot>
</div>
</template>
<script>
import { throttle } from 'throttle-debounce';
import { getScrollTop, getOffsetTop, scrollTo } from "../../utils/dom";
import Affix from "./affix.vue";
const sharpMatcherRegx = /#([^#]+)$/;
export default {
name: "Anchor",
components: {
Affix,
},
data() {
return {
links: [],
scrollContainer: window,
animating: false,
inkPositionTop: "",
inkHeight: "0px",
bizAnchorActiveLink: "",
};
},
props: {
affix: {
type: Boolean,
default: true,
},
bounds: {
type: Number,
default: 5,
},
getContainer: {
type: Function,
default: () => window,
},
offsetTop: {
type: Number,
default: 0,
},
showInk: {
type: Boolean,
default: true,
},
targetOffset: {
type: Number,
},
getCurrentAnchor: {
type: Function,
default: null,
},
},
methods: {
scrollTo(link) {
const { offsetTop, getContainer, targetOffset } = this;
this.setCurrentActiveLink(link);
const container = getContainer();
const scrollTop = getScrollTop(container);
const sharpLinkMatch = sharpMatcherRegx.exec(link);
if (!sharpLinkMatch) {
return;
}
const targetElement = document.getElementById(sharpLinkMatch[1]);
if (!targetElement) {
return;
}
const eleOffsetTop = getOffsetTop(targetElement, container);
let y = scrollTop + eleOffsetTop;
y -= targetOffset !== undefined ? targetOffset : offsetTop || 0;
this.animating = true;
scrollTo(y, {
callback: () => {
this.animating = false;
},
getContainer,
});
},
handleScroll() {
if (this.animating) {
return;
}
const currentActiveLink = this.getCurrentActiveLink(
this.targetOffset !== undefined
? this.targetOffset
: this.offsetTop || 0,
this.bounds
);
this.setCurrentActiveLink(currentActiveLink);
},
setCurrentActiveLink(link) {
this.bizAnchorActiveLink = link;
setTimeout(() => {
const linkNode = this.$el.parentElement.getElementsByClassName("is-active")[0];
if (linkNode) {
this.inkPositionTop = `${linkNode.offsetTop}px`;
this.inkHeight = `${linkNode.clientHeight}px`;
} else {
this.inkHeight = "0px";
}
}, 0);
},
getCurrentActiveLink(offsetTop = 0, bounds = 5) {
if (typeof this.getCurrentAnchor === "function") {
return this.getCurrentAnchor();
}
const activeLink = "";
if (typeof document === "undefined") {
return activeLink;
}
const linkSections = [];
const { getContainer } = this;
const container = getContainer();
this.links.forEach((link) => {
const sharpLinkMatch = sharpMatcherRegx.exec(link.toString());
if (!sharpLinkMatch) {
return;
}
const target = document.getElementById(sharpLinkMatch[1]);
if (target) {
const top = getOffsetTop(target, container);
if (top < offsetTop + bounds) {
linkSections.push({
link,
top,
});
}
}
});
if (linkSections.length) {
const maxSection = linkSections.reduce((prev, curr) =>
curr.top > prev.top ? curr : prev
);
return maxSection.link;
}
return "";
},
getBizAnchor() {
const registerLink = (link) => {
if (!this.links.includes(link)) {
this.links.push(link);
}
};
const unregisterLink = (link) => {
this.links = this.links.filter((item) => item !== link);
};
const scrollTo = this.scrollTo.bind(this);
return {
registerLink,
unregisterLink,
scrollTo,
};
},
},
ready() {
this.scrollHandLer = throttle(30, this.handleScroll.bind(this));
setTimeout(() => {
this.scrollContainer = this.getContainer();
if (this.scrollContainer) {
this.scrollContainer.addEventListener("scroll", this.scrollHandLer);
}
const sharpLinkMatch = sharpMatcherRegx.exec(window.location.href);
if (!sharpLinkMatch) {
return;
}
this.scrollTo(sharpLinkMatch[0]);
}, 500);
},
beforeDestroy() {
if (this.scrollContainer) {
this.scrollContainer.removeEventListener("scroll", this.scrollHandLer);
}
},
};
</script>
<style lang="less">
.anchor {
position: relative;
border-left: 1px solid #ccc;
&-ink {
position: absolute;
left: -1px;
top: 0;
width: 1px;
height: 32px;
background: #2e6be6;
transition: top 0.3s, height 0.3s;
}
.visible::after {
display: block;
width: 8px;
height: 8px;
position: absolute;
content: "";
top: 50%;
background: white;
border-radius: 50%;
transform: translate(-50%, -50%);
border: 1px solid #2e6be6;
}
&-link {
.anchor-link {
padding-left: 16px;
}
}
&-link__title {
padding-left: 16px;
display: inline-block;
font-size: 12px;
color: #666;
height: 32px;
line-height: 32px;
cursor: pointer;
text-decoration: none;
outline: none;
transition: color 0.3s;
width: 100%;
&.is-active {
color: #2e6be6;
// background: #E6EEFF;
}
&:hover {
color: #2e6be6;
}
}
}
</style>
dom.js 具体使用的js
export function easeInOutCubic(t, b, c, d) {
const cc = c - b;
t /= d / 2;
if (t < 1) {
return (cc / 2) * t * t * t + b;
}
return (cc / 2) * ((t -= 2) * t * t + 2) + b;
}
export function getScroll(target, fromTop) {
if (typeof window === "undefined") {
return 0;
}
const prop = fromTop ? "pageYOffset" : "pageXOffset";
const method = fromTop ? "scrollTop" : "scrollLeft";
let ret = target instanceof Window ? target[prop] : target[method];
if (target instanceof Window && typeof ret !== "number") {
ret = window.document.documentElement[method];
}
return ret;
}
export function getScrollTop(target) {
return getScroll(target, true);
}
export function scrollTo(y, options = {}) {
const { getContainer = () => window, callback, duration = 450 } = options;
const container = getContainer();
const scrollTop = getScrollTop(container);
const startTime = Date.now();
const frameFunc = () => {
const timestamp = Date.now();
const time = timestamp - startTime;
const nextScrollTop = easeInOutCubic(
time > duration ? duration : time,
scrollTop,
y,
duration
);
if (container instanceof Window) {
window.scrollTo(window.pageXOffset, nextScrollTop);
} else {
container.scrollTop = nextScrollTop;
}
if (time < duration) {
requestAnimationFrame(frameFunc);
} else if (typeof callback === "function") {
callback();
}
};
requestAnimationFrame(frameFunc);
}
export function getOffsetTop(element, container) {
if (!element) {
return 0;
}
if (!element.getClientRects().length) {
return 0;
}
const rect = element.getBoundingClientRect();
if (rect.width || rect.height) {
if (container instanceof Window) {
container = element.ownerDocument.documentElement;
return rect.top - container.clientTop;
}
return rect.top - container.getBoundingClientRect().top;
}
return rect.top;
}
export function getTargetRect(target) {
return target instanceof Window
? { top: 0, bottom: window.innerHeight }
: target.getBoundingClientRect();
}
export function getFixedTop(placeholderRect, containerRect, offsetTop) {
if (
offsetTop !== undefined &&
containerRect.top > placeholderRect.top - offsetTop
) {
return offsetTop + containerRect.top + "px";
}
return undefined;
}
具体如何使用
基础使用方法:
<template>
<anchor>
<anchor-link href="#shiyong" title="使用" />
<anchor-link href="#yongfa" title="用法" />
<anchor-link href="#xin-ye-mian" target="_blank" title="新页面" />
<anchor-link title="API" href="#API">
<anchor-link href="#props" title="Props" />
<anchor-link href="#slots" title="Slots" />
</anchor-link>
</anchor>
</template>
指定容器使用方法:
<template>
<anchor :get-container="getContainer">
<anchor-link href="#shiyong" title="使用" />
<anchor-link href="#yongfa" title="用法" />
<anchor-link href="#xin-ye-mian" target="_blank" title="新页面" />
<anchor-link title="API" href="#API">
<anchor-link href="#props" title="Props" />
<anchor-link href="#slots" title="Slots" />
</anchor-link>
</anchor>
</template>
<script>
export default {
methods: {
getContainer() {
return document.body;
}
}
}
</script>
参数 | 说明 | 类型 | 默认值 |
affix | 固定模式 | boolean | true |
getContainer | 指定滚动的容器 | () => HTMLElement | () => HTMLElement |
getCurrentAnchor | 自定义高亮的锚点 | () => string | null |
wrapperClass | 容器的类名 | string | - |
wrapperStyle | 容器样式 | object | - |
参数 | 说明 | 类型 | 默认值 |
href | 锚点链接 | string | - |
target | 该属性指定在何处显示链接的资源。 | string | - |
title | 文字内容 | string | slot |
结语小提示:如果想要在vue2中也使用此代码,可将ready换成mounted,setTimeout换成this.$nextTick,由于vue1无法使用provide,更换比较麻烦可忽略。
最后祝愿大家钱多事少快乐摸鱼~