如何自己实现锚点跳转

前言:在做项目中,由于使用的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>

Attributes
参数说明类型默认值
affix

固定模式

booleantrue
getContainer指定滚动的容器() => HTMLElement() => HTMLElement
getCurrentAnchor自定义高亮的锚点() => stringnull
wrapperClass容器的类名string-
wrapperStyle容器样式object-

Link Props
参数说明类型默认值
href锚点链接string-
target该属性指定在何处显示链接的资源。string-
title

文字内容

stringslot

结语小提示:如果想要在vue2中也使用此代码,可将ready换成mounted,setTimeout换成this.$nextTick,由于vue1无法使用provide,更换比较麻烦可忽略。

最后祝愿大家钱多事少快乐摸鱼~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值