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

View File

@ -7,6 +7,7 @@ export const AnchorLinkProps = {
prefixCls: PropTypes.string,
href: PropTypes.string,
title: PropTypes.any,
target: PropTypes.string,
};
export default {
@ -47,7 +48,7 @@ export default {
},
},
render() {
const { prefixCls: customizePrefixCls, href, $slots } = this;
const { prefixCls: customizePrefixCls, href, $slots, target } = this;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('anchor', customizePrefixCls);
@ -66,6 +67,7 @@ export default {
class={titleClassName}
href={href}
title={typeof title === 'string' ? title : ''}
target={target}
onClick={this.handleClick}
>
{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-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-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="#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>
@ -18,6 +19,34 @@ exports[`renders ./components/anchor/demo/basic.md correctly 1`] = `
</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`] = `
<div class="ant-anchor-wrapper" style="max-height: 100vh;">
<div class="ant-anchor fixed">
@ -45,3 +74,21 @@ exports[`renders ./components/anchor/demo/static.md correctly 1`] = `
</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-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-basic" title="Basic demo with Target" target="_blank" />
<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" />

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 Static from './static';
import OnClick from './onClick';
import CustomizeHighlight from './customizeHighlight';
import OnChange from './OnChange';
import TargetOffset from './targetOffset';
import CN from '../index.zh-CN.md';
import US from '../index.en-US.md';
@ -37,6 +40,9 @@ export default {
<Basic />
<Static />
<OnClick />
<CustomizeHighlight />
<OnChange />
<TargetOffset />
<api>
<CN slot="cn" />
<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>
<us>
#### Static Anchor
#### Static
Do not change state when page is scrolling.
</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
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| affix | Fixed mode of Anchor | boolean | true |
| bounds | Bounding distance of anchor area | number | 5(px) |
| getContainer | Scrolling container | () => HTMLElement | () => window |
| 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 |
| showInkInFixed | Whether show ink-balls in Fixed mode | boolean | false |
| wrapperClass | The class name of the container | string | - |
| wrapperStyle | The style of the container | object | - |
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| affix | Fixed mode of Anchor | boolean | true | |
| bounds | Bounding distance of anchor area | number | 5(px) | |
| getContainer | Scrolling container | () => HTMLElement | () => window | |
| 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 | |
| showInkInFixed | Whether show ink-balls in Fixed mode | boolean | false | |
| wrapperClass | The class name of the container | string | - | |
| 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 Name | Description | Arguments |
| ----------- | --------------------------------------- | -------------------------------- |
| click | set the handler to handle `click` event | Function(e: Event, link: Object) |
| Events Name | Description | Arguments | Version |
| --- | --- | --- | --- |
| 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
| Property | Description | Type | Default |
| -------- | -------------------- | ------------ | ------- |
| href | target of hyperlink | string | |
| title | content of hyperlink | string\|slot | |
| Property | Description | Type | Default | Version |
| -------- | ----------------------------------------- | ------------ | ------- | ------- |
| href | target of hyperlink | string | | |
| title | content of hyperlink | string\|slot | | |
| target | Specifies where to display the linked URL | string | | |

View File

@ -2,26 +2,30 @@
### Anchor Props
| 成员 | 说明 | 类型 | 默认值 |
| -------------- | -------------------------------- | ----------------- | ------------ |
| affix | 固定模式 | boolean | true |
| bounds | 锚点区域边界 | number | 5(px) |
| getContainer | 指定滚动的容器 | () => HTMLElement | () => window |
| offsetBottom | 距离窗口底部达到指定偏移量后触发 | number | |
| offsetTop | 距离窗口顶部达到指定偏移量后触发 | number | |
| showInkInFixed | 固定模式是否显示小圆点 | boolean | false |
| wrapperClass | 容器的类名 | string | - |
| wrapperStyle | 容器样式 | object | - |
| 成员 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| affix | 固定模式 | boolean | true | |
| bounds | 锚点区域边界 | number | 5(px) | |
| getContainer | 指定滚动的容器 | () => HTMLElement | () => window | |
| offsetBottom | 距离窗口底部达到指定偏移量后触发 | number | | |
| offsetTop | 距离窗口顶部达到指定偏移量后触发 | number | | |
| showInkInFixed | 固定模式是否显示小圆点 | boolean | false | |
| wrapperClass | 容器的类名 | string | - | |
| 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
| 成员 | 说明 | 类型 | 默认值 |
| ----- | -------- | ------------ | ------ |
| href | 锚点链接 | string | |
| title | 文字内容 | string\|slot | |
| 成员 | 说明 | 类型 | 默认值 | 版本 |
| ------ | -------------------------------- | ------------ | ------ | ---- |
| href | 锚点链接 | string | | |
| 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));