add affix demo
parent
08f9f6afbb
commit
dd1266ee61
|
@ -0,0 +1,23 @@
|
|||
<cn>
|
||||
#### 基本
|
||||
最简单的用法。
|
||||
</cn>
|
||||
|
||||
<us>
|
||||
#### basic
|
||||
The simplest usage.
|
||||
</us>
|
||||
|
||||
```html
|
||||
<template>
|
||||
<div>
|
||||
<a-affix>
|
||||
<a-button type="primary">Affix top</a-button>
|
||||
</a-affix>
|
||||
<br />
|
||||
<a-affix :offsetBottom="0">
|
||||
<a-button type="primary">Affix bottom</a-button>
|
||||
</a-affix>
|
||||
</div>
|
||||
</template>
|
||||
```
|
|
@ -0,0 +1,37 @@
|
|||
<script>
|
||||
import Basic from './basic'
|
||||
import Onchange from './on-change'
|
||||
import Target from './target'
|
||||
import CN from '../index.zh-CN.md'
|
||||
import US from '../index.en-US.md'
|
||||
const md = {
|
||||
cn: `# Affix 固钉
|
||||
将页面元素钉在可视范围。
|
||||
## 何时使用
|
||||
当内容区域比较长,需要滚动页面时,这部分内容对应的操作或者导航需要在滚动范围内始终展现。常用于侧边菜单和按钮组合。
|
||||
页面可视范围过小时,慎用此功能以免遮挡页面内容。
|
||||
## 代码演示`,
|
||||
us: `# Affix
|
||||
Make an element stick to viewport.
|
||||
## When To Use
|
||||
When user browses a long web page, some content need to stick to the viewport. This is common for menus and actions.
|
||||
Please note that Affix should not cover other content on the page, especially when the size of the viewport is small.
|
||||
`,
|
||||
}
|
||||
export default {
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<md cn={md.cn} us={md.us}/>
|
||||
<Basic />
|
||||
<Onchange />
|
||||
<Target />
|
||||
<api>
|
||||
<CN slot='cn' />
|
||||
<US/>
|
||||
</api>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,26 @@
|
|||
<cn>
|
||||
#### 固定状态改变的回调
|
||||
可以获得是否固定的状态。
|
||||
</cn>
|
||||
|
||||
<us>
|
||||
#### Callback
|
||||
Callback with affixed state.
|
||||
</us>
|
||||
|
||||
```html
|
||||
<template>
|
||||
<a-affix :offsetTop="120" @change="change">
|
||||
<a-button>120px to affix top</a-button>
|
||||
</a-affix>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
change(affixed) {
|
||||
console.log(affixed)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
|
@ -0,0 +1,34 @@
|
|||
<cn>
|
||||
#### 滚动容器
|
||||
用 `target` 设置 `Affix` 需要监听其滚动事件的元素,默认为 `window`。
|
||||
</cn>
|
||||
|
||||
<us>
|
||||
#### Container to scroll.
|
||||
Set a `target` for 'Affix', which is listen to scroll event of target element (default is `window`).
|
||||
</us>
|
||||
|
||||
```html
|
||||
<template>
|
||||
<div id="components-affix-demo-target" class="scrollable-container" ref="container">
|
||||
<div class="background">
|
||||
<a-affix :target="() => this.$refs.container">
|
||||
<a-button type="primary">
|
||||
Fixed at the top of container
|
||||
</a-button>
|
||||
</a-affix>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style>
|
||||
#components-affix-demo-target.scrollable-container {
|
||||
height: 100px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
#components-affix-demo-target .background {
|
||||
padding-top: 60px;
|
||||
height: 300px;
|
||||
background-image: url('https://zos.alipayobjects.com/rmsportal/RmjwQiJorKyobvI.jpg');
|
||||
}
|
||||
</style>
|
||||
```
|
|
@ -0,0 +1,21 @@
|
|||
|
||||
## API
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
| -------- | ----------- | ---- | ------- |
|
||||
| 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 |
|
||||
| target | specifies the scrollable area dom node | () => HTMLElement | () => window |
|
||||
|
||||
### events
|
||||
| Events Name | Description | Arguments |
|
||||
| --- | --- | --- |
|
||||
| onChange | Callback for when affix state is changed | Function(affixed) |
|
||||
|
||||
**Note:** Children of `Affix` can not be `position: absolute`, but you can set `Affix` as `position: absolute`:
|
||||
|
||||
````html
|
||||
<a-affix :style="{ position: 'absolute', top: y, left: x}">
|
||||
...
|
||||
</a-affix>
|
||||
````
|
|
@ -5,7 +5,8 @@ import classNames from 'classnames'
|
|||
import shallowequal from 'shallowequal'
|
||||
import omit from 'omit.js'
|
||||
import getScroll from '../_util/getScroll'
|
||||
import { throttleByAnimationFrameDecorator } from '../_util/throttleByAnimationFrame'
|
||||
import BaseMixin from '../_util/BaseMixin'
|
||||
import throttleByAnimationFrame from '../_util/throttleByAnimationFrame'
|
||||
|
||||
function getTargetRect (target) {
|
||||
return target !== window
|
||||
|
@ -55,14 +56,10 @@ const AffixProps = {
|
|||
prefixCls: PropTypes.string,
|
||||
}
|
||||
|
||||
// export interface AffixState {
|
||||
// affixStyle: React.CSSProperties | undefined;
|
||||
// placeholderStyle: React.CSSProperties | undefined;
|
||||
// }
|
||||
|
||||
export default {
|
||||
name: 'Affix',
|
||||
props: AffixProps,
|
||||
mixins: [BaseMixin],
|
||||
data () {
|
||||
this.events = [
|
||||
'resize',
|
||||
|
@ -79,6 +76,9 @@ export default {
|
|||
placeholderStyle: undefined,
|
||||
}
|
||||
},
|
||||
beforeMount () {
|
||||
this.updatePosition = throttleByAnimationFrame(this.updatePosition)
|
||||
},
|
||||
mounted () {
|
||||
const target = this.target || getDefaultTarget
|
||||
// Wait for parent component ref has its value
|
||||
|
@ -96,7 +96,6 @@ export default {
|
|||
})
|
||||
},
|
||||
},
|
||||
|
||||
beforeDestroy () {
|
||||
this.clearEventListeners()
|
||||
clearTimeout(this.timeout)
|
||||
|
@ -130,7 +129,6 @@ export default {
|
|||
this.setState({ placeholderStyle: placeholderStyle })
|
||||
},
|
||||
|
||||
// @throttleByAnimationFrameDecorator()
|
||||
updatePosition (e) {
|
||||
let { offsetTop } = this
|
||||
const { offsetBottom, offset, target = getDefaultTarget } = this
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
|
||||
## API
|
||||
|
||||
| 成员 | 说明 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| offsetBottom | 距离窗口底部达到指定偏移量后触发 | number | |
|
||||
| offsetTop | 距离窗口顶部达到指定偏移量后触发 | number | |
|
||||
| target | 设置 `Affix` 需要监听其滚动事件的元素,值为一个返回对应 DOM 元素的函数 | () => HTMLElement | () => window |
|
||||
|
||||
|
||||
### 事件
|
||||
| 事件名称 | 说明 | 回调参数 |
|
||||
| --- | --- | --- |
|
||||
| change | 固定状态改变时触发的回调函数 | Function(affixed) |
|
||||
|
||||
|
||||
**注意:**`Affix` 内的元素不要使用绝对定位,如需要绝对定位的效果,可以直接设置 `Affix` 为绝对定位:
|
||||
|
||||
````html
|
||||
<a-affix :style="{ position: 'absolute', top: y, left: x}">
|
||||
...
|
||||
</a-affix>
|
||||
````
|
|
@ -11,7 +11,7 @@ There are `primary` button, `default` button, `dashed` button and `danger` butto
|
|||
|
||||
```html
|
||||
<template>
|
||||
<div class='test'>
|
||||
<div>
|
||||
<a-button type="primary">Primary</a-button>
|
||||
<a-button>Default</a-button>
|
||||
<a-button type="dashed">Dashed</a-button>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
<script>
|
||||
import { Tabs } from 'antd'
|
||||
import omit from 'omit.js'
|
||||
import PropTypes from '../_util/vue-types'
|
||||
import addEventListener from '../_util/Dom/addEventListener'
|
||||
import { hasProp, getComponentFromProp, getComponentName } from '../_util/props-util'
|
||||
import { getComponentFromProp, getComponentName } from '../_util/props-util'
|
||||
import throttleByAnimationFrame from '../_util/throttleByAnimationFrame'
|
||||
import BaseMixin from '../_util/BaseMixin'
|
||||
|
||||
|
@ -18,32 +17,25 @@
|
|||
bordered: PropTypes.bool.def(true),
|
||||
bodyStyle: PropTypes.object,
|
||||
loading: PropTypes.bool.def(false),
|
||||
noHovering: PropTypes.bool.def(false),
|
||||
hoverable: PropTypes.bool.def(false),
|
||||
type: PropTypes.string,
|
||||
actions: PropTypes.any,
|
||||
tabList: PropTypes.array,
|
||||
},
|
||||
data () {
|
||||
this.updateWiderPaddingCalled = false
|
||||
return {
|
||||
widerPadding: false,
|
||||
updateWiderPaddingCalled: false,
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
beforeMount () {
|
||||
this.updateWiderPadding = throttleByAnimationFrame(this.updateWiderPadding)
|
||||
},
|
||||
mounted () {
|
||||
this.updateWiderPadding()
|
||||
this.resizeEvent = addEventListener(window, 'resize', this.updateWiderPadding)
|
||||
|
||||
// if (hasProp(this, 'noHovering')) {
|
||||
// warning(
|
||||
// !this.noHovering,
|
||||
// '`noHovering` of Card is deperated, you can remove it safely or use `hoverable` instead.',
|
||||
// )
|
||||
// warning(!!this.noHovering, '`noHovering={false}` of Card is deperated, use `hoverable` instead.')
|
||||
// }
|
||||
},
|
||||
beforeMount () {
|
||||
beforeDestroy () {
|
||||
if (this.resizeEvent) {
|
||||
this.resizeEvent.remove()
|
||||
}
|
||||
|
@ -84,19 +76,11 @@
|
|||
})
|
||||
return containGrid
|
||||
},
|
||||
// For 2.x compatible
|
||||
getCompatibleHoverable () {
|
||||
const { noHovering, hoverable } = this.$props
|
||||
if (hasProp(this, 'noHovering')) {
|
||||
return !noHovering || hoverable
|
||||
}
|
||||
return !!hoverable
|
||||
},
|
||||
},
|
||||
render () {
|
||||
const {
|
||||
prefixCls = 'ant-card', extra, bodyStyle, title, loading,
|
||||
bordered = true, type, tabList, ...others
|
||||
bordered = true, type, tabList, hoverable,
|
||||
} = this.$props
|
||||
|
||||
const { $slots } = this
|
||||
|
@ -105,7 +89,7 @@
|
|||
[`${prefixCls}`]: true,
|
||||
[`${prefixCls}-loading`]: loading,
|
||||
[`${prefixCls}-bordered`]: bordered,
|
||||
[`${prefixCls}-hoverable`]: this.getCompatibleHoverable(),
|
||||
[`${prefixCls}-hoverable`]: !!hoverable,
|
||||
[`${prefixCls}-wider-padding`]: this.widerPadding,
|
||||
[`${prefixCls}-padding-transition`]: this.updateWiderPaddingCalled,
|
||||
[`${prefixCls}-contain-grid`]: this.isContainGrid($slots.default),
|
||||
|
@ -166,11 +150,9 @@
|
|||
)
|
||||
const actions = getComponentFromProp(this, 'actions')
|
||||
const actionDom = actions || null
|
||||
const divProps = omit(others, [
|
||||
'tabChange',
|
||||
])
|
||||
|
||||
return (
|
||||
<div {...divProps} class={classString} ref='cardContainerRef'>
|
||||
<div class={classString} ref='cardContainerRef'>
|
||||
{head}
|
||||
{coverDom}
|
||||
{children ? body : null}
|
||||
|
|
|
@ -89,3 +89,5 @@ export { default as LocaleProvider } from './locale-provider'
|
|||
|
||||
export { default as AutoComplete } from './auto-complete'
|
||||
|
||||
export { default as Affix } from './affix'
|
||||
|
||||
|
|
|
@ -24,3 +24,4 @@ import './spin/style'
|
|||
import './select/style'
|
||||
import './switch/style'
|
||||
import './auto-complete/style'
|
||||
import './affix/style'
|
||||
|
|
|
@ -0,0 +1,313 @@
|
|||
<script>
|
||||
import React, { Component, cloneElement } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Trigger from 'rc-trigger';
|
||||
import Menus from './Menus';
|
||||
import KeyCode from 'rc-util/lib/KeyCode';
|
||||
import arrayTreeFilter from 'array-tree-filter';
|
||||
import shallowEqualArrays from 'shallow-equal/arrays';
|
||||
|
||||
const BUILT_IN_PLACEMENTS = {
|
||||
bottomLeft: {
|
||||
points: ['tl', 'bl'],
|
||||
offset: [0, 4],
|
||||
overflow: {
|
||||
adjustX: 1,
|
||||
adjustY: 1,
|
||||
},
|
||||
},
|
||||
topLeft: {
|
||||
points: ['bl', 'tl'],
|
||||
offset: [0, -4],
|
||||
overflow: {
|
||||
adjustX: 1,
|
||||
adjustY: 1,
|
||||
},
|
||||
},
|
||||
bottomRight: {
|
||||
points: ['tr', 'br'],
|
||||
offset: [0, 4],
|
||||
overflow: {
|
||||
adjustX: 1,
|
||||
adjustY: 1,
|
||||
},
|
||||
},
|
||||
topRight: {
|
||||
points: ['br', 'tr'],
|
||||
offset: [0, -4],
|
||||
overflow: {
|
||||
adjustX: 1,
|
||||
adjustY: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
class Cascader extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
let initialValue = [];
|
||||
if ('value' in props) {
|
||||
initialValue = props.value || [];
|
||||
} else if ('defaultValue' in props) {
|
||||
initialValue = props.defaultValue || [];
|
||||
}
|
||||
|
||||
this.state = {
|
||||
popupVisible: props.popupVisible,
|
||||
activeValue: initialValue,
|
||||
value: initialValue,
|
||||
};
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if ('value' in nextProps && !shallowEqualArrays(this.props.value, nextProps.value)) {
|
||||
const newValues = {
|
||||
value: nextProps.value || [],
|
||||
activeValue: nextProps.value || [],
|
||||
};
|
||||
// allow activeValue diff from value
|
||||
// https://github.com/ant-design/ant-design/issues/2767
|
||||
if ('loadData' in nextProps) {
|
||||
delete newValues.activeValue;
|
||||
}
|
||||
this.setState(newValues);
|
||||
}
|
||||
if ('popupVisible' in nextProps) {
|
||||
this.setState({
|
||||
popupVisible: nextProps.popupVisible,
|
||||
});
|
||||
}
|
||||
}
|
||||
getPopupDOMNode() {
|
||||
return this.trigger.getPopupDomNode();
|
||||
}
|
||||
getCurrentLevelOptions() {
|
||||
const { options } = this.props;
|
||||
const { activeValue = [] } = this.state;
|
||||
const result = arrayTreeFilter(options, (o, level) => o.value === activeValue[level]);
|
||||
if (result[result.length - 2]) {
|
||||
return result[result.length - 2].children;
|
||||
}
|
||||
return [...options].filter(o => !o.disabled);
|
||||
}
|
||||
getActiveOptions(activeValue) {
|
||||
return arrayTreeFilter(this.props.options, (o, level) => o.value === activeValue[level]);
|
||||
}
|
||||
setPopupVisible = (popupVisible) => {
|
||||
if (!('popupVisible' in this.props)) {
|
||||
this.setState({ popupVisible });
|
||||
}
|
||||
// sync activeValue with value when panel open
|
||||
if (popupVisible && !this.state.visible) {
|
||||
this.setState({
|
||||
activeValue: this.state.value,
|
||||
});
|
||||
}
|
||||
this.props.onPopupVisibleChange(popupVisible);
|
||||
}
|
||||
handleChange = (options, setProps, e) => {
|
||||
if (e.type !== 'keydown' || e.keyCode === KeyCode.ENTER) {
|
||||
this.props.onChange(options.map(o => o.value), options);
|
||||
this.setPopupVisible(setProps.visible);
|
||||
}
|
||||
}
|
||||
handlePopupVisibleChange = (popupVisible) => {
|
||||
this.setPopupVisible(popupVisible);
|
||||
}
|
||||
handleMenuSelect = (targetOption, menuIndex, e) => {
|
||||
// Keep focused state for keyboard support
|
||||
const triggerNode = this.trigger.getRootDomNode();
|
||||
if (triggerNode && triggerNode.focus) {
|
||||
triggerNode.focus();
|
||||
}
|
||||
const { changeOnSelect, loadData, expandTrigger } = this.props;
|
||||
if (!targetOption || targetOption.disabled) {
|
||||
return;
|
||||
}
|
||||
let { activeValue } = this.state;
|
||||
activeValue = activeValue.slice(0, menuIndex + 1);
|
||||
activeValue[menuIndex] = targetOption.value;
|
||||
const activeOptions = this.getActiveOptions(activeValue);
|
||||
if (targetOption.isLeaf === false && !targetOption.children && loadData) {
|
||||
if (changeOnSelect) {
|
||||
this.handleChange(activeOptions, { visible: true }, e);
|
||||
}
|
||||
this.setState({ activeValue });
|
||||
loadData(activeOptions);
|
||||
return;
|
||||
}
|
||||
const newState = {};
|
||||
if (!targetOption.children || !targetOption.children.length) {
|
||||
this.handleChange(activeOptions, { visible: false }, e);
|
||||
// set value to activeValue when select leaf option
|
||||
newState.value = activeValue;
|
||||
// add e.type judgement to prevent `onChange` being triggered by mouseEnter
|
||||
} else if (changeOnSelect && (e.type === 'click' || e.type === 'keydown')) {
|
||||
if (expandTrigger === 'hover') {
|
||||
this.handleChange(activeOptions, { visible: false }, e);
|
||||
} else {
|
||||
this.handleChange(activeOptions, { visible: true }, e);
|
||||
}
|
||||
// set value to activeValue on every select
|
||||
newState.value = activeValue;
|
||||
}
|
||||
newState.activeValue = activeValue;
|
||||
// not change the value by keyboard
|
||||
if ('value' in this.props ||
|
||||
(e.type === 'keydown' && e.keyCode !== KeyCode.ENTER)) {
|
||||
delete newState.value;
|
||||
}
|
||||
this.setState(newState);
|
||||
}
|
||||
handleKeyDown = (e) => {
|
||||
const { children } = this.props;
|
||||
// https://github.com/ant-design/ant-design/issues/6717
|
||||
// Don't bind keyboard support when children specify the onKeyDown
|
||||
if (children && children.props.onKeyDown) {
|
||||
children.props.onKeyDown(e);
|
||||
return;
|
||||
}
|
||||
const activeValue = [...this.state.activeValue];
|
||||
const currentLevel = activeValue.length - 1 < 0 ? 0 : activeValue.length - 1;
|
||||
const currentOptions = this.getCurrentLevelOptions();
|
||||
const currentIndex = currentOptions.map(o => o.value).indexOf(activeValue[currentLevel]);
|
||||
if (e.keyCode !== KeyCode.DOWN &&
|
||||
e.keyCode !== KeyCode.UP &&
|
||||
e.keyCode !== KeyCode.LEFT &&
|
||||
e.keyCode !== KeyCode.RIGHT &&
|
||||
e.keyCode !== KeyCode.ENTER &&
|
||||
e.keyCode !== KeyCode.BACKSPACE &&
|
||||
e.keyCode !== KeyCode.ESC) {
|
||||
return;
|
||||
}
|
||||
// Press any keys above to reopen menu
|
||||
if (!this.state.popupVisible &&
|
||||
e.keyCode !== KeyCode.BACKSPACE &&
|
||||
e.keyCode !== KeyCode.LEFT &&
|
||||
e.keyCode !== KeyCode.RIGHT &&
|
||||
e.keyCode !== KeyCode.ESC) {
|
||||
this.setPopupVisible(true);
|
||||
return;
|
||||
}
|
||||
if (e.keyCode === KeyCode.DOWN || e.keyCode === KeyCode.UP) {
|
||||
let nextIndex = currentIndex;
|
||||
if (nextIndex !== -1) {
|
||||
if (e.keyCode === KeyCode.DOWN) {
|
||||
nextIndex += 1;
|
||||
nextIndex = nextIndex >= currentOptions.length ? 0 : nextIndex;
|
||||
} else {
|
||||
nextIndex -= 1;
|
||||
nextIndex = nextIndex < 0 ? currentOptions.length - 1 : nextIndex;
|
||||
}
|
||||
} else {
|
||||
nextIndex = 0;
|
||||
}
|
||||
activeValue[currentLevel] = currentOptions[nextIndex].value;
|
||||
} else if (e.keyCode === KeyCode.LEFT || e.keyCode === KeyCode.BACKSPACE) {
|
||||
activeValue.splice(activeValue.length - 1, 1);
|
||||
} else if (e.keyCode === KeyCode.RIGHT) {
|
||||
if (currentOptions[currentIndex] && currentOptions[currentIndex].children) {
|
||||
activeValue.push(currentOptions[currentIndex].children[0].value);
|
||||
}
|
||||
} else if (e.keyCode === KeyCode.ESC) {
|
||||
this.setPopupVisible(false);
|
||||
return;
|
||||
}
|
||||
if (!activeValue || activeValue.length === 0) {
|
||||
this.setPopupVisible(false);
|
||||
}
|
||||
const activeOptions = this.getActiveOptions(activeValue);
|
||||
const targetOption = activeOptions[activeOptions.length - 1];
|
||||
this.handleMenuSelect(targetOption, activeOptions.length - 1, e);
|
||||
|
||||
if (this.props.onKeyDown) {
|
||||
this.props.onKeyDown(e);
|
||||
}
|
||||
}
|
||||
|
||||
saveTrigger = (node) => {
|
||||
this.trigger = node;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
prefixCls, transitionName, popupClassName, options, disabled,
|
||||
builtinPlacements, popupPlacement, children, ...restProps,
|
||||
} = this.props;
|
||||
// Did not show popup when there is no options
|
||||
let menus = <div />;
|
||||
let emptyMenuClassName = '';
|
||||
if (options && options.length > 0) {
|
||||
menus = (
|
||||
<Menus
|
||||
{...this.props}
|
||||
value={this.state.value}
|
||||
activeValue={this.state.activeValue}
|
||||
onSelect={this.handleMenuSelect}
|
||||
visible={this.state.popupVisible}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
emptyMenuClassName = ` ${prefixCls}-menus-empty`;
|
||||
}
|
||||
return (
|
||||
<Trigger
|
||||
ref={this.saveTrigger}
|
||||
{...restProps}
|
||||
options={options}
|
||||
disabled={disabled}
|
||||
popupPlacement={popupPlacement}
|
||||
builtinPlacements={builtinPlacements}
|
||||
popupTransitionName={transitionName}
|
||||
action={disabled ? [] : ['click']}
|
||||
popupVisible={disabled ? false : this.state.popupVisible}
|
||||
onPopupVisibleChange={this.handlePopupVisibleChange}
|
||||
prefixCls={`${prefixCls}-menus`}
|
||||
popupClassName={popupClassName + emptyMenuClassName}
|
||||
popup={menus}
|
||||
>
|
||||
{cloneElement(children, {
|
||||
onKeyDown: this.handleKeyDown,
|
||||
tabIndex: disabled ? undefined : 0,
|
||||
})}
|
||||
</Trigger>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Cascader.defaultProps = {
|
||||
options: [],
|
||||
onChange() {},
|
||||
onPopupVisibleChange() {},
|
||||
disabled: false,
|
||||
transitionName: '',
|
||||
prefixCls: 'rc-cascader',
|
||||
popupClassName: '',
|
||||
popupPlacement: 'bottomLeft',
|
||||
builtinPlacements: BUILT_IN_PLACEMENTS,
|
||||
expandTrigger: 'click',
|
||||
};
|
||||
|
||||
Cascader.propTypes = {
|
||||
value: PropTypes.array,
|
||||
defaultValue: PropTypes.array,
|
||||
options: PropTypes.array.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
onPopupVisibleChange: PropTypes.func,
|
||||
popupVisible: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
transitionName: PropTypes.string,
|
||||
popupClassName: PropTypes.string,
|
||||
popupPlacement: PropTypes.string,
|
||||
prefixCls: PropTypes.string,
|
||||
dropdownMenuColumnStyle: PropTypes.object,
|
||||
builtinPlacements: PropTypes.object,
|
||||
loadData: PropTypes.func,
|
||||
changeOnSelect: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
onKeyDown: PropTypes.func,
|
||||
expandTrigger: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Cascader;
|
||||
|
||||
</script>
|
|
@ -0,0 +1,156 @@
|
|||
<script>
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import arrayTreeFilter from 'array-tree-filter'
|
||||
import { findDOMNode } from 'react-dom'
|
||||
|
||||
class Menus extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.menuItems = {}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.scrollActiveItemToView()
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (!prevProps.visible && this.props.visible) {
|
||||
this.scrollActiveItemToView()
|
||||
}
|
||||
}
|
||||
|
||||
getOption (option, menuIndex) {
|
||||
const { prefixCls, expandTrigger } = this.props
|
||||
const onSelect = this.props.onSelect.bind(this, option, menuIndex)
|
||||
let expandProps = {
|
||||
onClick: onSelect,
|
||||
}
|
||||
let menuItemCls = `${prefixCls}-menu-item`
|
||||
const hasChildren = option.children && option.children.length > 0
|
||||
if (hasChildren || option.isLeaf === false) {
|
||||
menuItemCls += ` ${prefixCls}-menu-item-expand`
|
||||
}
|
||||
if (expandTrigger === 'hover' && hasChildren) {
|
||||
expandProps = {
|
||||
onMouseEnter: this.delayOnSelect.bind(this, onSelect),
|
||||
onMouseLeave: this.delayOnSelect.bind(this),
|
||||
onClick: onSelect,
|
||||
}
|
||||
}
|
||||
if (this.isActiveOption(option, menuIndex)) {
|
||||
menuItemCls += ` ${prefixCls}-menu-item-active`
|
||||
expandProps.ref = this.saveMenuItem(menuIndex)
|
||||
}
|
||||
if (option.disabled) {
|
||||
menuItemCls += ` ${prefixCls}-menu-item-disabled`
|
||||
}
|
||||
if (option.loading) {
|
||||
menuItemCls += ` ${prefixCls}-menu-item-loading`
|
||||
}
|
||||
let title = ''
|
||||
if (option.title) {
|
||||
title = option.title
|
||||
} else if (typeof option.label === 'string') {
|
||||
title = option.label
|
||||
}
|
||||
return (
|
||||
<li
|
||||
key={option.value}
|
||||
className={menuItemCls}
|
||||
title={title}
|
||||
{...expandProps}
|
||||
>
|
||||
{option.label}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
getActiveOptions (values) {
|
||||
const activeValue = values || this.props.activeValue
|
||||
const options = this.props.options
|
||||
return arrayTreeFilter(options, (o, level) => o.value === activeValue[level])
|
||||
}
|
||||
|
||||
getShowOptions () {
|
||||
const { options } = this.props
|
||||
const result = this.getActiveOptions()
|
||||
.map(activeOption => activeOption.children)
|
||||
.filter(activeOption => !!activeOption)
|
||||
result.unshift(options)
|
||||
return result
|
||||
}
|
||||
|
||||
delayOnSelect (onSelect, ...args) {
|
||||
if (this.delayTimer) {
|
||||
clearTimeout(this.delayTimer)
|
||||
this.delayTimer = null
|
||||
}
|
||||
if (typeof onSelect === 'function') {
|
||||
this.delayTimer = setTimeout(() => {
|
||||
onSelect(args)
|
||||
this.delayTimer = null
|
||||
}, 150)
|
||||
}
|
||||
}
|
||||
|
||||
scrollActiveItemToView () {
|
||||
// scroll into view
|
||||
const optionsLength = this.getShowOptions().length
|
||||
for (let i = 0; i < optionsLength; i++) {
|
||||
const itemComponent = this.menuItems[i]
|
||||
if (itemComponent) {
|
||||
const target = findDOMNode(itemComponent)
|
||||
target.parentNode.scrollTop = target.offsetTop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isActiveOption (option, menuIndex) {
|
||||
const { activeValue = [] } = this.props
|
||||
return activeValue[menuIndex] === option.value
|
||||
}
|
||||
|
||||
saveMenuItem = (index) => (node) => {
|
||||
this.menuItems[index] = node
|
||||
}
|
||||
|
||||
render () {
|
||||
const { prefixCls, dropdownMenuColumnStyle } = this.props
|
||||
return (
|
||||
<div>
|
||||
{this.getShowOptions().map((options, menuIndex) =>
|
||||
<ul className={`${prefixCls}-menu`} key={menuIndex} style={dropdownMenuColumnStyle}>
|
||||
{options.map(option => this.getOption(option, menuIndex))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Menus.defaultProps = {
|
||||
options: [],
|
||||
value: [],
|
||||
activeValue: [],
|
||||
onSelect () {},
|
||||
prefixCls: 'rc-cascader-menus',
|
||||
visible: false,
|
||||
expandTrigger: 'click',
|
||||
}
|
||||
|
||||
Menus.propTypes = {
|
||||
value: PropTypes.array,
|
||||
activeValue: PropTypes.array,
|
||||
options: PropTypes.array.isRequired,
|
||||
prefixCls: PropTypes.string,
|
||||
expandTrigger: PropTypes.string,
|
||||
onSelect: PropTypes.func,
|
||||
visible: PropTypes.bool,
|
||||
dropdownMenuColumnStyle: PropTypes.object,
|
||||
}
|
||||
|
||||
export default Menus
|
||||
|
||||
</script>
|
|
@ -0,0 +1,167 @@
|
|||
.effect() {
|
||||
animation-duration: .3s;
|
||||
animation-fill-mode: both;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.rc-cascader {
|
||||
font-size: 12px;
|
||||
&-menus {
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
position: absolute;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 4px rgba(0,0,0,0.17);
|
||||
white-space: nowrap;
|
||||
|
||||
&-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.slide-up-enter, &.slide-up-appear {
|
||||
.effect();
|
||||
opacity: 0;
|
||||
animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1);
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
&.slide-up-leave {
|
||||
.effect();
|
||||
opacity: 1;
|
||||
animation-timing-function: cubic-bezier(0.6, 0.04, 0.98, 0.34);
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
&.slide-up-enter.slide-up-enter-active&-placement-bottomLeft,
|
||||
&.slide-up-appear.slide-up-appear-active&-placement-bottomLeft {
|
||||
animation-name: SlideUpIn;
|
||||
animation-play-state: running;
|
||||
}
|
||||
|
||||
&.slide-up-enter.slide-up-enter-active&-placement-topLeft,
|
||||
&.slide-up-appear.slide-up-appear-active&-placement-topLeft {
|
||||
animation-name: SlideDownIn;
|
||||
animation-play-state: running;
|
||||
}
|
||||
|
||||
&.slide-up-leave.slide-up-leave-active&-placement-bottomLeft {
|
||||
animation-name: SlideUpOut;
|
||||
animation-play-state: running;
|
||||
}
|
||||
|
||||
&.slide-up-leave.slide-up-leave-active&-placement-topLeft {
|
||||
animation-name: SlideDownOut;
|
||||
animation-play-state: running;
|
||||
}
|
||||
}
|
||||
&-menu {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
height: 192px;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-right: 1px solid #e9e9e9;
|
||||
overflow: auto;
|
||||
&:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
&-menu-item {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
padding: 0 16px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
&:hover {
|
||||
background: tint(#2db7f5, 90%);
|
||||
}
|
||||
&-disabled {
|
||||
cursor: not-allowed;
|
||||
color: #ccc;
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
&-loading:after {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
content: 'loading';
|
||||
color: #aaa;
|
||||
font-style: italic;
|
||||
}
|
||||
&-active {
|
||||
background: tint(#2db7f5, 80%);
|
||||
&:hover {
|
||||
background: tint(#2db7f5, 80%);
|
||||
}
|
||||
}
|
||||
&-expand {
|
||||
position: relative;
|
||||
&:after {
|
||||
content: '>';
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes SlideUpIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform-origin: 0% 0%;
|
||||
transform: scaleY(.8);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform-origin: 0% 0%;
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
@keyframes SlideUpOut {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform-origin: 0% 0%;
|
||||
transform: scaleY(1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform-origin: 0% 0%;
|
||||
transform: scaleY(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes SlideDownIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform-origin: 0% 100%;
|
||||
transform: scaleY(0.8);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform-origin: 0% 100%;
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
@keyframes SlideDownOut {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform-origin: 0% 100%;
|
||||
transform: scaleY(1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform-origin: 0% 100%;
|
||||
transform: scaleY(0.8);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
// export this package's api
|
||||
import Cascader from './Cascader'
|
||||
export default Cascader
|
|
@ -15,9 +15,6 @@ export default {
|
|||
props: {
|
||||
...switchPropTypes,
|
||||
prefixCls: switchPropTypes.prefixCls.def('rc-switch'),
|
||||
checkedChildren: switchPropTypes.checkedChildren,
|
||||
unCheckedChildren: switchPropTypes.unCheckedChildren,
|
||||
defaultChecked: switchPropTypes.defaultChecked.def(''),
|
||||
// onChange: switchPropTypes.onChange.def(noop),
|
||||
// onClick: switchPropTypes.onClick.def(noop),
|
||||
},
|
||||
|
|
|
@ -19,6 +19,8 @@ Select | done
|
|||
Input | done
|
||||
InputNumber
|
||||
AutoComplete | done
|
||||
Affix | done
|
||||
Cascader
|
||||
Modal
|
||||
Alert
|
||||
Calendar
|
||||
|
@ -32,7 +34,6 @@ Mention
|
|||
##万
|
||||
Grid
|
||||
Col
|
||||
Affix
|
||||
BackTop
|
||||
Layout
|
||||
Anchor
|
||||
|
|
|
@ -25,4 +25,5 @@ export { default as message } from 'antd/message/demo/index.vue'
|
|||
export { default as spin } from 'antd/spin/demo/index.vue'
|
||||
export { default as switch } from 'antd/switch/demo/index.vue'
|
||||
export { default as autoComplete } from 'antd/auto-complete/demo/index.vue'
|
||||
export { default as affix } from 'antd/Affix/demo/index.vue'
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ const AsyncComp = () => {
|
|||
const hashs = window.location.hash.split('/')
|
||||
const d = hashs[hashs.length - 1]
|
||||
return {
|
||||
component: import(`../components/auto-complete/demo/${d}`),
|
||||
component: import(`../components/affix/demo/${d}`),
|
||||
}
|
||||
}
|
||||
export default [
|
||||
|
|
|
@ -100,6 +100,7 @@
|
|||
"lodash.isplainobject": "^4.0.6",
|
||||
"moment": "^2.20.1",
|
||||
"omit.js": "^1.0.0",
|
||||
"shallowequal": "^1.0.2",
|
||||
"vue": "^2.5.13",
|
||||
"vue-clipboard2": "0.0.8",
|
||||
"vue-types": "^1.0.2",
|
||||
|
|
Loading…
Reference in New Issue