Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 224 additions & 0 deletions components/anchor/Anchor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
<script>
import debounce from 'lodash.debounce'
import getScroll from '../_util/getScroll'
import getRequestAnimationFrame from '../_util/getRequestAnimationFrame'
import Affix from '@/affix'

function getOffsetTop (element) {
if (!element) {
return 0
}

if (!element.getClientRects().length) {
return 0
}

const rect = element.getBoundingClientRect()

if (rect.width || rect.height) {
const doc = element.ownerDocument
const docElem = doc.documentElement
return rect.top - docElem.clientTop
}

return rect.top
}

const easeInOutCubic = (t, b, c, d) => {
const cc = c - b
t /= d / 2
if (t < 1) {
return cc / 2 * t * t * t + b
} else {
return cc / 2 * ((t -= 2) * t * t + 2) + b
}
}

const reqAnimFrame = getRequestAnimationFrame()
const sharpMatcherRegx = /#([^#]+)$/

function scrollTo (href, offsetTop = 0, target, callback = () => { }) {
const scrollTop = getScroll(target(), true)
const sharpLinkMatch = sharpMatcherRegx.exec(href)
if (!sharpLinkMatch) { return }
const targetElement = document.getElementById(sharpLinkMatch[1])
if (!targetElement) {
return
}
const eleOffsetTop = getOffsetTop(targetElement)
const targetScrollTop = scrollTop + eleOffsetTop - offsetTop
const startTime = Date.now()
const frameFunc = () => {
const timestamp = Date.now()
const time = timestamp - startTime
window.scrollTo(window.pageXOffset, easeInOutCubic(time, scrollTop, targetScrollTop, 450))
if (time < 450) {
reqAnimFrame(frameFunc)
} else {
callback()
}
}
reqAnimFrame(frameFunc)
history.pushState(null, '', href)
}

export default {
name: 'Anchor',
provide () {
return {
anchorRoot: this
}
},
props: {
prefixCls: {
type: String,
default: 'ant-anchor'
},
affix: {
type: Boolean,
default: true
},
bounds: {
type: Number,
default: 5
},
getContainer: {
type: Function,
default: () => window
},
offsetBottom: {
type: Number,
default: 0
},
offsetTop: {
type: Number,
default: 0
},
showInkInFixed: {
type: Boolean,
default: false
}
},
data () {
return {
links: [],
activeLink: null,
scrollHandler: debounce(this.handleScroll, 50),
animating: false
}
},
computed: {
baseCls () {
return [
`${this.prefixCls}`,
{
'fixed': !this.affix && !this.showInkInFixed
}
]
},
ballCls () {
return [
`${this.prefixCls}-ink-ball`,
{
'visible': !!this.activeLink
}
]
}
},
watch: {
activeLink (newv, oldv) {
this.updateInk()
if (oldv !== null) oldv.active = false
if (newv !== null) newv.active = true
}
},
components: {
Affix
},
methods: {
registerLink (link) {
if (!this.links.includes(link)) this.links.push(link)
},
unregisterLink (link) {
const index = this.links.indexOf(link)
if (index !== -1) {
this.links.splice(index, 1)
}
},
handleAnchorClick (link) {
this.activeLink = link
this.handleScrollTo(this.activeLink.href)
},
updateInk () {
if (typeof document === 'undefined') {
return
}
if (!this.activeLink) return
const linkNode = this.activeLink.$el.firstElementChild
if (linkNode) {
this.$refs.ink.style.top = `${linkNode.offsetTop + linkNode.clientHeight / 2 - 4.5}px`
}
},
handleScroll () {
if (this.animating) {
return
}
this.activeLink = this.getCurrentAnchor()
},
handleScrollTo (link) {
this.animating = true
scrollTo(link, this.offsetTop, this.getContainer, () => {
this.animating = false
})
},
getCurrentAnchor () {
let activeLink = null
if (typeof document === 'undefined') {
return activeLink
}
const inArea = (target) => {
const areaHeight = this.offsetTop + this.bounds
return getOffsetTop(target) < areaHeight && (getOffsetTop(target) + target.offsetHeight) > areaHeight
}
if (this.links.length > 0) {
for (let i = 0; i < this.links.length; i++) {
if (typeof document === 'undefined') {
return
}
const target = document.querySelector(this.links[i].href)
if (target && inArea(target)) {
return this.links[i]
}
}
}
return null
}
},
mounted () {
this.$on('anchorClick', this.handleAnchorClick)
this.getContainer().addEventListener('scroll', this.scrollHandler)
},
beforeDestroy () {
this.getContainer().removeEventListener('scroll', this.scrollHandler)
},
render (h) {
const { prefixCls, baseCls, ballCls, offsetTop, affix } = this
const anchorContent = (
<div class={`${prefixCls}-wrapper`} style={{ maxHeight: '100vh' }}>
<div class={baseCls}>
<div class={`${prefixCls}-ink`}>
<span class={ballCls} style={{ top: '10.5px' }} ref="ink"></span>
</div>
{ this.$slots.default }
</div>
</div>
)
return !affix ? anchorContent : (
<Affix offsetTop={offsetTop}>
{anchorContent}
</Affix>
)
}
}
</script>

63 changes: 63 additions & 0 deletions components/anchor/AnchorLink.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<template>
<div :class="linkCls">
<a :class="titleCls" :href="href" :title="title" @click.prevent="handleClick">{{title}}</a>
<slot></slot>
</div>
</template>

<script>
export default {
name: 'AnchorLink',
inject: ['anchorRoot'],
props: {
prefixCls: {
type: String,
default: 'ant-anchor'
},
href: {
type: String,
default: '#'
},
title: {
type: String,
default: ''
}
},
data () {
return {
active: false
}
},
computed: {
linkCls () {
return [
`${this.prefixCls}-link`,
{
[`${this.prefixCls}-link-active`]: this.active
}
]
},
titleCls () {
return [
`${this.prefixCls}-link-title`,
{
[`${this.prefixCls}-link-title-active`]: this.active
}
]
}
},
methods: {
handleClick () {
if (this.active) return
this.anchorRoot.$emit('anchorClick', this)
}
},
mounted () {
this.anchorRoot.registerLink(this)
},
beforeDestroy () {
this.anchorRoot.unRegisterLink(this)
}
}
</script>

49 changes: 49 additions & 0 deletions components/anchor/demo/basic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
order: 0
title:
zh-CN: 基本
en-US: Basic
---

## zh-CN

最简单的用法。

## en-US

The simplest usage.

``` html
<template>
<div id="components-anchor-demo-basic">
<Anchor>
<Link href="#components-anchor-demo-basic" title="Basic demo" />
<Link href="#components-anchor-demo-fixed" title="Fixed demo" />
<Link href="#api" title="API">
<Link href="#anchor-props" title="Anchor Props" />
<Link href="#link-props" title="Link Props" />
</Link>
</Anchor>
</div>
</template>

<script>
import Anchor from '@/anchor'

export default {
name: 'base-demo',
components: {
Anchor,
Link: Anchor.Link
}
}
</script>

<style>
.code-box-demo .ant-affix {
z-index: 11;
}
</style>

```

42 changes: 42 additions & 0 deletions components/anchor/demo/fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
order: 2
title:
zh-CN: 固定
en-US: Fixed Anchor
---

## zh-CN

不浮动,状态不随页面滚动变化。

## en-US

Do not change state when page is scrolling.

``` html
<template>
<div id="components-anchor-demo-fixed">
<Anchor :affix="false">
<Link href="#components-anchor-demo-basic" title="Basic demo" />
<Link href="#components-anchor-demo-fixed" title="Fixed demo" />
<Link href="#API" title="API">
<Link href="#Anchor-Props" title="Anchor Props" />
<Link href="#Link-Props" title="Link Props" />
</Link>
</Anchor>
</div>
</template>

<script>
import Anchor from '@/anchor'

export default {
name: 'base-demo',
components: {
Anchor,
Link: Anchor.Link
}
}
</script>

```
Loading