feat: update anchor

pull/1790/head
tangjinzhou 2020-02-08 13:07:57 +08:00
parent 2ab5d86672
commit 22b28b34c2
13 changed files with 314 additions and 106 deletions

View File

@ -1,5 +1,5 @@
module.exports = { module.exports = {
dev: { dev: {
componentName: 'alert', // dev components componentName: 'anchor', // dev components
}, },
}; };

View File

@ -2,8 +2,8 @@ import PropTypes from '../_util/vue-types';
import classNames from 'classnames'; import classNames from 'classnames';
import addEventListener from '../vc-util/Dom/addEventListener'; import addEventListener from '../vc-util/Dom/addEventListener';
import Affix from '../affix'; import Affix from '../affix';
import scrollTo from '../_util/scrollTo';
import getScroll from '../_util/getScroll'; import getScroll from '../_util/getScroll';
import raf from 'raf';
import { initDefaultProps } from '../_util/props-util'; import { initDefaultProps } from '../_util/props-util';
import BaseMixin from '../_util/BaseMixin'; import BaseMixin from '../_util/BaseMixin';
import { ConfigConsumerProps } from '../config-provider'; import { ConfigConsumerProps } from '../config-provider';
@ -34,47 +34,47 @@ function getOffsetTop(element, container) {
return rect.top; return rect.top;
} }
function easeInOutCubic(t, b, c, d) { // function easeInOutCubic(t, b, c, d) {
const cc = c - b; // const cc = c - b;
t /= d / 2; // t /= d / 2;
if (t < 1) { // if (t < 1) {
return (cc / 2) * t * t * t + b; // return (cc / 2) * t * t * t + b;
} // }
return (cc / 2) * ((t -= 2) * t * t + 2) + b; // return (cc / 2) * ((t -= 2) * t * t + 2) + b;
} // }
const sharpMatcherRegx = /#([^#]+)$/; const sharpMatcherRegx = /#([^#]+)$/;
function scrollTo(href, offsetTop = 0, getContainer, callback = () => {}) { // function scrollTo(href, offsetTop = 0, getContainer, callback = () => {}) {
const container = getContainer(); // const container = getContainer();
const scrollTop = getScroll(container, true); // const scrollTop = getScroll(container, true);
const sharpLinkMatch = sharpMatcherRegx.exec(href); // const sharpLinkMatch = sharpMatcherRegx.exec(href);
if (!sharpLinkMatch) { // if (!sharpLinkMatch) {
return; // return;
} // }
const targetElement = document.getElementById(sharpLinkMatch[1]); // const targetElement = document.getElementById(sharpLinkMatch[1]);
if (!targetElement) { // if (!targetElement) {
return; // return;
} // }
const eleOffsetTop = getOffsetTop(targetElement, container); // const eleOffsetTop = getOffsetTop(targetElement, container);
const targetScrollTop = scrollTop + eleOffsetTop - offsetTop; // const targetScrollTop = scrollTop + eleOffsetTop - offsetTop;
const startTime = Date.now(); // const startTime = Date.now();
const frameFunc = () => { // const frameFunc = () => {
const timestamp = Date.now(); // const timestamp = Date.now();
const time = timestamp - startTime; // const time = timestamp - startTime;
const nextScrollTop = easeInOutCubic(time, scrollTop, targetScrollTop, 450); // const nextScrollTop = easeInOutCubic(time, scrollTop, targetScrollTop, 450);
if (container === window) { // if (container === window) {
window.scrollTo(window.pageXOffset, nextScrollTop); // window.scrollTo(window.pageXOffset, nextScrollTop);
} else { // } else {
container.scrollTop = nextScrollTop; // container.scrollTop = nextScrollTop;
} // }
if (time < 450) { // if (time < 450) {
raf(frameFunc); // raf(frameFunc);
} else { // } else {
callback(); // callback();
} // }
}; // };
raf(frameFunc); // raf(frameFunc);
} // }
export const AnchorProps = { export const AnchorProps = {
prefixCls: PropTypes.string, prefixCls: PropTypes.string,
@ -85,6 +85,8 @@ export const AnchorProps = {
getContainer: PropTypes.func, getContainer: PropTypes.func,
wrapperClass: PropTypes.string, wrapperClass: PropTypes.string,
wrapperStyle: PropTypes.object, wrapperStyle: PropTypes.object,
getCurrentAnchor: PropTypes.func,
targetOffset: PropTypes.number,
}; };
export default { export default {
@ -130,43 +132,38 @@ export default {
mounted() { mounted() {
this.$nextTick(() => { this.$nextTick(() => {
const { getContainer } = this; const { getContainer } = this;
this.scrollEvent = addEventListener(getContainer(), 'scroll', this.handleScroll); this.scrollContainer = getContainer();
this.scrollEvent = addEventListener(this.scrollContainer, 'scroll', this.handleScroll);
this.handleScroll(); this.handleScroll();
}); });
}, },
updated() {
this.$nextTick(() => {
if (this.scrollEvent) {
const { getContainer } = this;
const currentContainer = getContainer();
if (this.scrollContainer !== currentContainer) {
this.scrollContainer = currentContainer;
this.scrollEvent.remove();
this.scrollEvent = addEventListener(this.scrollContainer, 'scroll', this.handleScroll);
this.handleScroll();
}
}
this.updateInk();
});
},
beforeDestroy() { beforeDestroy() {
if (this.scrollEvent) { if (this.scrollEvent) {
this.scrollEvent.remove(); this.scrollEvent.remove();
} }
}, },
updated() {
this.$nextTick(() => {
this.updateInk();
});
},
methods: { methods: {
handleScroll() { getCurrentActiveLink(offsetTop = 0, bounds = 5) {
if (this.animating) { const { getCurrentAnchor } = this;
return;
if (typeof getCurrentAnchor === 'function') {
return getCurrentAnchor();
} }
const { offsetTop, bounds } = this;
this.setState({
activeLink: this.getCurrentAnchor(offsetTop, bounds),
});
},
handleScrollTo(link) {
const { offsetTop, getContainer } = this;
this.animating = true;
this.setState({ activeLink: link });
scrollTo(link, offsetTop, getContainer, () => {
this.animating = false;
});
},
getCurrentAnchor(offsetTop = 0, bounds = 5) {
const activeLink = ''; const activeLink = '';
if (typeof document === 'undefined') { if (typeof document === 'undefined') {
return activeLink; return activeLink;
@ -199,6 +196,56 @@ export default {
return ''; return '';
}, },
handleScrollTo(link) {
const { offsetTop, getContainer, targetOffset } = this;
this.setCurrentActiveLink(link);
const container = getContainer();
const scrollTop = getScroll(container, true);
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,
});
},
setCurrentActiveLink(link) {
const { activeLink } = this;
if (activeLink !== link) {
this.setState({
activeLink: link,
});
this.$emit('change', link);
}
},
handleScroll() {
if (this.animating) {
return;
}
const { offsetTop, bounds, targetOffset } = this;
const currentActiveLink = this.getCurrentActiveLink(
targetOffset !== undefined ? targetOffset : offsetTop || 0,
bounds,
);
this.setCurrentActiveLink(currentActiveLink);
},
updateInk() { updateInk() {
if (typeof document === 'undefined') { if (typeof document === 'undefined') {
return; return;
@ -206,7 +253,7 @@ export default {
const { _sPrefixCls } = this; const { _sPrefixCls } = this;
const linkNode = this.$el.getElementsByClassName(`${_sPrefixCls}-link-title-active`)[0]; const linkNode = this.$el.getElementsByClassName(`${_sPrefixCls}-link-title-active`)[0];
if (linkNode) { if (linkNode) {
this.$refs.linkNode.style.top = `${linkNode.offsetTop + linkNode.clientHeight / 2 - 4.5}px`; this.$refs.inkNode.style.top = `${linkNode.offsetTop + linkNode.clientHeight / 2 - 4.5}px`;
} }
}, },
}, },
@ -245,7 +292,7 @@ export default {
<div class={wrapperClass} style={wrapperStyle}> <div class={wrapperClass} style={wrapperStyle}>
<div class={anchorClass}> <div class={anchorClass}>
<div class={`${prefixCls}-ink`}> <div class={`${prefixCls}-ink`}>
<span class={inkClass} ref="linkNode" /> <span class={inkClass} ref="inkNode" />
</div> </div>
{$slots.default} {$slots.default}
</div> </div>

View File

@ -7,6 +7,7 @@ export const AnchorLinkProps = {
prefixCls: PropTypes.string, prefixCls: PropTypes.string,
href: PropTypes.string, href: PropTypes.string,
title: PropTypes.any, title: PropTypes.any,
target: PropTypes.string,
}; };
export default { export default {
@ -47,7 +48,7 @@ export default {
}, },
}, },
render() { render() {
const { prefixCls: customizePrefixCls, href, $slots } = this; const { prefixCls: customizePrefixCls, href, $slots, target } = this;
const getPrefixCls = this.configProvider.getPrefixCls; const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('anchor', customizePrefixCls); const prefixCls = getPrefixCls('anchor', customizePrefixCls);
@ -66,6 +67,7 @@ export default {
class={titleClassName} class={titleClassName}
href={href} href={href}
title={typeof title === 'string' ? title : ''} title={typeof title === 'string' ? title : ''}
target={target}
onClick={this.handleClick} onClick={this.handleClick}
> >
{title} {title}

View File

@ -8,6 +8,7 @@ exports[`renders ./components/anchor/demo/basic.md correctly 1`] = `
<div class="ant-anchor-ink"><span class="ant-anchor-ink-ball"></span></div> <div class="ant-anchor-ink"><span class="ant-anchor-ink-ball"></span></div>
<div class="ant-anchor-link"><a href="#components-anchor-demo-basic" title="Basic demo" class="ant-anchor-link-title">Basic demo</a></div> <div class="ant-anchor-link"><a href="#components-anchor-demo-basic" title="Basic demo" class="ant-anchor-link-title">Basic demo</a></div>
<div class="ant-anchor-link"><a href="#components-anchor-demo-static" title="Static demo" class="ant-anchor-link-title">Static demo</a></div> <div class="ant-anchor-link"><a href="#components-anchor-demo-static" title="Static demo" class="ant-anchor-link-title">Static demo</a></div>
<div class="ant-anchor-link"><a href="#components-anchor-demo-basic" title="Basic demo with Target" target="_blank" class="ant-anchor-link-title">Basic demo with Target</a></div>
<div class="ant-anchor-link"><a href="#API" title="API" class="ant-anchor-link-title">API</a> <div class="ant-anchor-link"><a href="#API" title="API" class="ant-anchor-link-title">API</a>
<div class="ant-anchor-link"><a href="#Anchor-Props" title="Anchor Props" class="ant-anchor-link-title">Anchor Props</a></div> <div class="ant-anchor-link"><a href="#Anchor-Props" title="Anchor Props" class="ant-anchor-link-title">Anchor Props</a></div>
<div class="ant-anchor-link"><a href="#Link-Props" title="Link Props" class="ant-anchor-link-title">Link Props</a></div> <div class="ant-anchor-link"><a href="#Link-Props" title="Link Props" class="ant-anchor-link-title">Link Props</a></div>
@ -18,6 +19,34 @@ exports[`renders ./components/anchor/demo/basic.md correctly 1`] = `
</div> </div>
`; `;
exports[`renders ./components/anchor/demo/customizeHighlight.md correctly 1`] = `
<div class="ant-anchor-wrapper" style="max-height: 100vh;">
<div class="ant-anchor fixed">
<div class="ant-anchor-ink"><span class="ant-anchor-ink-ball"></span></div>
<div class="ant-anchor-link"><a href="#components-anchor-demo-basic" title="Basic demo" class="ant-anchor-link-title">Basic demo</a></div>
<div class="ant-anchor-link"><a href="#components-anchor-demo-static" title="Static demo" class="ant-anchor-link-title">Static demo</a></div>
<div class="ant-anchor-link"><a href="#API" title="API" class="ant-anchor-link-title">API</a>
<div class="ant-anchor-link"><a href="#Anchor-Props" title="Anchor Props" class="ant-anchor-link-title">Anchor Props</a></div>
<div class="ant-anchor-link"><a href="#Link-Props" title="Link Props" class="ant-anchor-link-title">Link Props</a></div>
</div>
</div>
</div>
`;
exports[`renders ./components/anchor/demo/onChange.md correctly 1`] = `
<div class="ant-anchor-wrapper" style="max-height: 100vh;">
<div class="ant-anchor fixed">
<div class="ant-anchor-ink"><span class="ant-anchor-ink-ball"></span></div>
<div class="ant-anchor-link"><a href="#components-anchor-demo-basic" title="Basic demo" class="ant-anchor-link-title">Basic demo</a></div>
<div class="ant-anchor-link"><a href="#components-anchor-demo-static" title="Static demo" class="ant-anchor-link-title">Static demo</a></div>
<div class="ant-anchor-link"><a href="#API" title="API" class="ant-anchor-link-title">API</a>
<div class="ant-anchor-link"><a href="#Anchor-Props" title="Anchor Props" class="ant-anchor-link-title">Anchor Props</a></div>
<div class="ant-anchor-link"><a href="#Link-Props" title="Link Props" class="ant-anchor-link-title">Link Props</a></div>
</div>
</div>
</div>
`;
exports[`renders ./components/anchor/demo/onClick.md correctly 1`] = ` exports[`renders ./components/anchor/demo/onClick.md correctly 1`] = `
<div class="ant-anchor-wrapper" style="max-height: 100vh;"> <div class="ant-anchor-wrapper" style="max-height: 100vh;">
<div class="ant-anchor fixed"> <div class="ant-anchor fixed">
@ -45,3 +74,21 @@ exports[`renders ./components/anchor/demo/static.md correctly 1`] = `
</div> </div>
</div> </div>
`; `;
exports[`renders ./components/anchor/demo/targetOffset.md correctly 1`] = `
<div>
<div class="">
<div class="ant-anchor-wrapper" style="max-height: 100vh;">
<div class="ant-anchor">
<div class="ant-anchor-ink"><span class="ant-anchor-ink-ball"></span></div>
<div class="ant-anchor-link"><a href="#components-anchor-demo-basic" title="Basic demo" class="ant-anchor-link-title">Basic demo</a></div>
<div class="ant-anchor-link"><a href="#components-anchor-demo-static" title="Static demo" class="ant-anchor-link-title">Static demo</a></div>
<div class="ant-anchor-link"><a href="#API" title="API" class="ant-anchor-link-title">API</a>
<div class="ant-anchor-link"><a href="#Anchor-Props" title="Anchor Props" class="ant-anchor-link-title">Anchor Props</a></div>
<div class="ant-anchor-link"><a href="#Link-Props" title="Link Props" class="ant-anchor-link-title">Link Props</a></div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -13,6 +13,7 @@ The simplest usage.
<a-anchor> <a-anchor>
<a-anchor-link href="#components-anchor-demo-basic" title="Basic demo" /> <a-anchor-link href="#components-anchor-demo-basic" title="Basic demo" />
<a-anchor-link href="#components-anchor-demo-static" title="Static demo" /> <a-anchor-link href="#components-anchor-demo-static" title="Static demo" />
<a-anchor-link href="#components-anchor-demo-basic" title="Basic demo with Target" target="_blank" />
<a-anchor-link href="#API" title="API"> <a-anchor-link href="#API" title="API">
<a-anchor-link href="#Anchor-Props" title="Anchor Props" /> <a-anchor-link href="#Anchor-Props" title="Anchor Props" />
<a-anchor-link href="#Link-Props" title="Link Props" /> <a-anchor-link href="#Link-Props" title="Link Props" />

View File

@ -0,0 +1,31 @@
<cn>
#### 自定义锚点高亮
自定义锚点高亮。
</cn>
<us>
#### Customize the anchor highlight
Customize the anchor highlight.
</us>
```tpl
<template>
<a-anchor :affix="false" :getCurrentAnchor="getCurrentAnchor">
<a-anchor-link href="#components-anchor-demo-basic" title="Basic demo" />
<a-anchor-link href="#components-anchor-demo-static" title="Static demo" />
<a-anchor-link href="#API" title="API">
<a-anchor-link href="#Anchor-Props" title="Anchor Props" />
<a-anchor-link href="#Link-Props" title="Link Props" />
</a-anchor-link>
</a-anchor>
</template>
<script>
export default {
methods: {
getCurrentAnchor() {
return '#components-anchor-demo-static';
}
}
}
</script>
```

View File

@ -2,6 +2,9 @@
import Basic from './basic'; import Basic from './basic';
import Static from './static'; import Static from './static';
import OnClick from './onClick'; import OnClick from './onClick';
import CustomizeHighlight from './customizeHighlight';
import OnChange from './OnChange';
import TargetOffset from './targetOffset';
import CN from '../index.zh-CN.md'; import CN from '../index.zh-CN.md';
import US from '../index.en-US.md'; import US from '../index.en-US.md';
@ -37,6 +40,9 @@ export default {
<Basic /> <Basic />
<Static /> <Static />
<OnClick /> <OnClick />
<CustomizeHighlight />
<OnChange />
<TargetOffset />
<api> <api>
<CN slot="cn" /> <CN slot="cn" />
<US /> <US />

View File

@ -0,0 +1,31 @@
<cn>
#### 监听锚点链接改变
监听锚点链接改变
</cn>
<us>
#### Listening for anchor link change
Listening for anchor link change.
</us>
```tpl
<template>
<a-anchor :affix="false" @change="onChange">
<a-anchor-link href="#components-anchor-demo-basic" title="Basic demo" />
<a-anchor-link href="#components-anchor-demo-static" title="Static demo" />
<a-anchor-link href="#API" title="API">
<a-anchor-link href="#Anchor-Props" title="Anchor Props" />
<a-anchor-link href="#Link-Props" title="Link Props" />
</a-anchor-link>
</a-anchor>
</template>
<script>
export default {
methods: {
onChange(link) {
console.log('Anchor:OnChange', link);
}
}
}
</script>
```

View File

@ -4,7 +4,7 @@
</cn> </cn>
<us> <us>
#### Static Anchor #### Static
Do not change state when page is scrolling. Do not change state when page is scrolling.
</us> </us>

View File

@ -0,0 +1,34 @@
<cn>
#### 设置锚点滚动偏移量
锚点目标滚动到屏幕正中间。
</cn>
<us>
#### Set Anchor scroll offset
Anchor target scroll to screen center.
</us>
```tpl
<template>
<a-anchor :targetOffset="targetOffset">
<a-anchor-link href="#components-anchor-demo-basic" title="Basic demo" />
<a-anchor-link href="#components-anchor-demo-static" title="Static demo" />
<a-anchor-link href="#API" title="API">
<a-anchor-link href="#Anchor-Props" title="Anchor Props" />
<a-anchor-link href="#Link-Props" title="Link Props" />
</a-anchor-link>
</a-anchor>
</template>
<script>
export default {
data() {
return {
targetOffset: undefined,
}
},
mounted() {
this.targetOffset = window.innerHeight / 2;
}
}
</script>
```

View File

@ -2,26 +2,30 @@
### Anchor Props ### Anchor Props
| Property | Description | Type | Default | | Property | Description | Type | Default | Version |
| --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| affix | Fixed mode of Anchor | boolean | true | | affix | Fixed mode of Anchor | boolean | true | |
| bounds | Bounding distance of anchor area | number | 5(px) | | bounds | Bounding distance of anchor area | number | 5(px) | |
| getContainer | Scrolling container | () => HTMLElement | () => window | | getContainer | Scrolling container | () => HTMLElement | () => window | |
| offsetBottom | Pixels to offset from bottom when calculating position of scroll | number | - | | offsetBottom | Pixels to offset from bottom when calculating position of scroll | number | - | |
| offsetTop | Pixels to offset from top when calculating position of scroll | number | 0 | | offsetTop | Pixels to offset from top when calculating position of scroll | number | 0 | |
| showInkInFixed | Whether show ink-balls in Fixed mode | boolean | false | | showInkInFixed | Whether show ink-balls in Fixed mode | boolean | false | |
| wrapperClass | The class name of the container | string | - | | wrapperClass | The class name of the container | string | - | |
| wrapperStyle | The style of the container | object | - | | wrapperStyle | The style of the container | object | - | |
| getCurrentAnchor | Customize the anchor highlight | () => string | - | 1.5.0 |
| targetOffset | Anchor scroll offset, default as `offsetTop`, [example](#components-anchor-demo-targetOffset) | number | `offsetTop` | 1.5.0 |
### Events ### Events
| Events Name | Description | Arguments | | Events Name | Description | Arguments | Version |
| ----------- | --------------------------------------- | -------------------------------- | | --- | --- | --- | --- |
| click | set the handler to handle `click` event | Function(e: Event, link: Object) | | click | set the handler to handle `click` event | Function(e: Event, link: Object) | |
| change | Listening for anchor link change | (currentActiveLink: string) => void | | 1.5.0 |
### Link Props ### Link Props
| Property | Description | Type | Default | | Property | Description | Type | Default | Version |
| -------- | -------------------- | ------------ | ------- | | -------- | ----------------------------------------- | ------------ | ------- | ------- |
| href | target of hyperlink | string | | | href | target of hyperlink | string | | |
| title | content of hyperlink | string\|slot | | | title | content of hyperlink | string\|slot | | |
| target | Specifies where to display the linked URL | string | | |

View File

@ -2,26 +2,30 @@
### Anchor Props ### Anchor Props
| 成员 | 说明 | 类型 | 默认值 | | 成员 | 说明 | 类型 | 默认值 | 版本 |
| -------------- | -------------------------------- | ----------------- | ------------ | | --- | --- | --- | --- | --- |
| affix | 固定模式 | boolean | true | | affix | 固定模式 | boolean | true | |
| bounds | 锚点区域边界 | number | 5(px) | | bounds | 锚点区域边界 | number | 5(px) | |
| getContainer | 指定滚动的容器 | () => HTMLElement | () => window | | getContainer | 指定滚动的容器 | () => HTMLElement | () => window | |
| offsetBottom | 距离窗口底部达到指定偏移量后触发 | number | | | offsetBottom | 距离窗口底部达到指定偏移量后触发 | number | | |
| offsetTop | 距离窗口顶部达到指定偏移量后触发 | number | | | offsetTop | 距离窗口顶部达到指定偏移量后触发 | number | | |
| showInkInFixed | 固定模式是否显示小圆点 | boolean | false | | showInkInFixed | 固定模式是否显示小圆点 | boolean | false | |
| wrapperClass | 容器的类名 | string | - | | wrapperClass | 容器的类名 | string | - | |
| wrapperStyle | 容器样式 | object | - | | wrapperStyle | 容器样式 | object | - | |
| getCurrentAnchor | 自定义高亮的锚点 | () => string | - | 1.5.0 |
| targetOffset | 锚点滚动偏移量,默认与 offsetTop 相同,[例子](#components-anchor-demo-targetOffset) | number | `offsetTop` | 1.5.0 |
### 事件 ### 事件
| 事件名称 | 说明 | 回调参数 | | 事件名称 | 说明 | 回调参数 | 版本 |
| -------- | ---------------------- | -------------------------------- | | -------- | ---------------------- | ----------------------------------- | ---- |
| click | `click` 事件的 handler | Function(e: Event, link: Object) | | click | `click` 事件的 handler | Function(e: Event, link: Object) | |
| change | 监听锚点链接改变 | (currentActiveLink: string) => void | | 1.5.0 |
### Link Props ### Link Props
| 成员 | 说明 | 类型 | 默认值 | | 成员 | 说明 | 类型 | 默认值 | 版本 |
| ----- | -------- | ------------ | ------ | | ------ | -------------------------------- | ------------ | ------ | ---- |
| href | 锚点链接 | string | | | href | 锚点链接 | string | | |
| title | 文字内容 | string\|slot | | | title | 文字内容 | string\|slot | | |
| target | 该属性指定在何处显示链接的资源。 | string | | |

View File

@ -25,3 +25,4 @@ export function asyncExpect(fn, timeout) {
} }
}); });
} }
export const sleep = (timeout = 0) => new Promise(resolve => setTimeout(resolve, timeout));