Skip to content

Commit

Permalink
Merge branch 'master' of github.com:ant-design/ant-design
Browse files Browse the repository at this point in the history
  • Loading branch information
afc163 committed Sep 1, 2016
2 parents 1dd810e + 570b0bf commit 5f8b176
Show file tree
Hide file tree
Showing 34 changed files with 709 additions and 353 deletions.
12 changes: 10 additions & 2 deletions components/affix/demo/target.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
---
order: 4
title: 参考对象
title:
zh-CN: 滚动容器
en-US: Container to scroll.
---

## zh-CN

`target` 设置 `Affix` 需要监听其滚动事件的元素,默认为 `window`

## en-US

Set a `target` for 'Affix', which is listen to scroll event of target element (default is `window`).

````jsx
import { Affix, Button } from 'antd';

Expand All @@ -17,7 +25,7 @@ const Demo = () => {
<br />
<br />
<Affix target={() => document.getElementById('affix-target')} offsetTop={20}>
<Button type="primary">固定在容器顶部</Button>
<Button type="primary">Fixed at the top of container</Button>
</Affix>
</div>
</div>
Expand Down
50 changes: 50 additions & 0 deletions components/cascader/demo/search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
order: 9
title:
zh-CN: 搜索
en-US: Search
---

## zh-CN

可以直接搜索选项并选择。

## en-US

Search and select options directly.

````jsx
import { Cascader } from 'antd';

const options = [{
value: 'zhejiang',
label: '浙江',
children: [{
value: 'hangzhou',
label: '杭州',
children: [{
value: 'xihu',
label: '西湖',
}],
}],
}, {
value: 'jiangsu',
label: '江苏',
children: [{
value: 'nanjing',
label: '南京',
children: [{
value: 'zhonghuamen',
label: '中华门',
}],
}],
}];

function onChange(value) {
console.log(value);
}

ReactDOM.render(
<Cascader options={options} onChange={onChange} placeholder="请选择地区" showSearch />
, mountNode);
````
12 changes: 12 additions & 0 deletions components/cascader/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,15 @@ Cascade selection box.
| allowClear | whether allow clear | boolean | true |
| expandTrigger | expand current item when click or hover, one of 'click' 'hover' | string | 'click' |
| changeOnSelect | change value on each selection if set to true, see above demo for details | boolean | false |
| showSearch | Whether show search input in single mode. | Boolean or Object | false |
| notFoundContent | Specify content to show when no result matches. | String | 'Not Found' |

Fields in `showSearch`:

| Property | Description | Type | Default |
|----------|-------------|------|---------|
| filter | The function will receive two arguments, inputValue and option, if the function returns true, the option will be included in the filtered set; Otherwise, it will be excluded. | `function(inputValue, path): boolean` | |
| render | Used to render filtered options. | `function(inputValue, path): React.ReactNode` | |
| sort | Used to sort filtered options. | `function(a, b, inputValue)` | |
| matchInputWidth | Whether the width of result list equals to input's | boolean | |

193 changes: 172 additions & 21 deletions components/cascader/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ export interface CascaderOptionType {
}

export type CascaderExpandTrigger = 'click' | 'hover'

export interface ShowSearchType {
filter?: (inputValue: string, path: CascaderOptionType[]) => boolean;
render?: (inputValue: string, path: CascaderOptionType[]) => React.ReactNode;
sort?: (a: CascaderOptionType[], b: CascaderOptionType[], inputValue: string) => number;
matchInputWidth?: boolean;
}

export interface CascaderProps {
/** 可选项数据源 */
options: Array<CascaderOptionType>;
Expand Down Expand Up @@ -42,6 +50,8 @@ export interface CascaderProps {
disabled?: boolean;
/** 是否支持清除*/
allowClear?: boolean;
showSearch?: boolean | ShowSearchType;
notFoundContent?: React.ReactNode;
/** 次级菜单的展开方式,可选 'click' 和 'hover' */
expandTrigger?: CascaderExpandTrigger;
/** 当此项为 true 时,点选每级菜单选项值都会发生变化 */
Expand All @@ -50,6 +60,33 @@ export interface CascaderProps {
onPopupVisibleChange?: (popupVisible: boolean) => void;
}

function highlightKeyword(str: string, keyword: string) {
return str.split(keyword)
.map((node: string, index: number) => index === 0 ? node : [
<span className="ant-cascader-menu-item-keyword" key="seperator">{keyword}</span>,
node,
]);
}

function defaultFilterOption(inputValue, path) {
return path.some(option => option.label.indexOf(inputValue) > -1);
}

function defaultRenderFilteredOption(inputValue, path) {
return path.map(({ label }, index) => {
const node = label.indexOf(inputValue) > -1 ? highlightKeyword(label, inputValue) : label;
return index === 0 ? node : [' / ', node];
});
}

function defaultSortFilteredOption(a, b, inputValue) {
function callback(elem) {
return elem.label.indexOf(inputValue) > -1;
}

return a.findIndex(callback) - b.findIndex(callback);
}

export default class Cascader extends React.Component<CascaderProps, any> {
static defaultProps = {
prefixCls: 'ant-cascader',
Expand All @@ -61,38 +98,73 @@ export default class Cascader extends React.Component<CascaderProps, any> {
displayRender: label => label.join(' / '),
disabled: false,
allowClear: true,
showSearch: false,
notFoundContent: 'Not Found',
onPopupVisibleChange() {},
};

cachedOptions: CascaderOptionType[];
refs: {
[key: string]: any;
input: {
refs: { input: HTMLElement }
};
};

constructor(props) {
super(props);
let value;
if ('value' in props) {
value = props.value;
} else if ('defaultValue' in props) {
value = props.defaultValue;
}
this.state = {
value: value || [],
value: props.value || props.defautValue || [],
inputValue: '',
inputFocused: false,
popupVisible: false,
flattenOptions: props.showSearch && this.flattenTree(props.options, props.changeOnSelect),
};
}

componentWillReceiveProps(nextProps) {
if ('value' in nextProps) {
this.setState({ value: nextProps.value || [] });
}
if (nextProps.showSearch && this.props.options !== nextProps.options) {
this.setState({ flattenOptions: this.flattenTree(nextProps.options, nextProps.changeOnSelect) });
}
}

handleChange = (value, selectedOptions) => {
this.setValue(value, selectedOptions);
const unwrappedValue = Array.isArray(value[0]) ? value[0] : value;
this.setState({ inputValue: '' });
this.setValue(unwrappedValue, selectedOptions);
}

handlePopupVisibleChange = (popupVisible) => {
this.setState({ popupVisible });
this.setState({
popupVisible,
inputFocused: popupVisible,
});
this.props.onPopupVisibleChange(popupVisible);
}

handleInputBlur = () => {
this.setState({
inputFocused: false,
});
}

handleInputClick = (e) => {
const { inputFocused, popupVisible } = this.state;
// Prevent `Trigger` behaviour.
if (inputFocused || popupVisible) {
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
}
}

handleInputChange = (e) => {
const inputValue = e.target.value;
this.setState({ inputValue });
}

setValue = (value, selectedOptions = []) => {
if (!('value' in this.props)) {
this.setState({ value });
Expand All @@ -102,36 +174,81 @@ export default class Cascader extends React.Component<CascaderProps, any> {

getLabel() {
const { options, displayRender } = this.props;
const selectedOptions = arrayTreeFilter(options, (o, level) => o.value === this.state.value[level]);
const value = this.state.value;
const unwrappedValue = Array.isArray(value[0]) ? value[0] : value;
const selectedOptions = arrayTreeFilter(options, (o, level) => o.value === unwrappedValue[level]);
const label = selectedOptions.map(o => o.label);
return displayRender(label, selectedOptions);
}

clearSelection = (e) => {
e.preventDefault();
e.stopPropagation();
this.setValue([]);
this.setState({ popupVisible: false });
if (!this.state.inputValue) {
this.setValue([]);
this.setState({ popupVisible: false });
} else {
this.setState({ inputValue: '' });
}
}

flattenTree(options, changeOnSelect, ancestor = []) {
let flattenOptions = [];
options.forEach((option) => {
const path = ancestor.concat(option);
if (changeOnSelect || !option.children) {
flattenOptions.push(path);
}
if (option.children) {
flattenOptions = flattenOptions.concat(this.flattenTree(option.children, changeOnSelect, path));
}
});
return flattenOptions;
}

generateFilteredOptions() {
const { showSearch, notFoundContent } = this.props;
const {
filter = defaultFilterOption,
render = defaultRenderFilteredOption,
sort = defaultSortFilteredOption,
} = showSearch as ShowSearchType;
const { flattenOptions, inputValue } = this.state;
const filtered = flattenOptions.filter((path) => filter(this.state.inputValue, path))
.sort((a, b) => sort(a, b, inputValue));

if (filtered.length > 0) {
return filtered.map((path) => {
return {
label: render(inputValue, path),
value: path.map(o => o.value),
};
});
}
return [{ label: notFoundContent, value: 'ANT_CASCADER_NOT_FOUND', disabled: true }];
}

render() {
const props = this.props;
const state = this.state;
const [{ prefixCls, children, placeholder, size, disabled,
className, style, allowClear }, otherProps] = splitObject(props,
['prefixCls', 'children', 'placeholder', 'size', 'disabled', 'className', 'style', 'allowClear']);
className, style, allowClear, showSearch }, otherProps] = splitObject(props,
['prefixCls', 'children', 'placeholder', 'size', 'disabled', 'className',
'style', 'allowClear', 'showSearch']);
const value = state.value;

const sizeCls = classNames({
'ant-input-lg': size === 'large',
'ant-input-sm': size === 'small',
});
const clearIcon = (allowClear && !disabled && this.state.value.length > 0) ?
const clearIcon = (allowClear && !disabled && value.length > 0) || state.inputValue ?
<Icon type="cross-circle"
className={`${prefixCls}-picker-clear`}
onClick={this.clearSelection}
/> : null;
const arrowCls = classNames({
[`${prefixCls}-picker-arrow`]: true,
[`${prefixCls}-picker-arrow-expand`]: this.state.popupVisible,
[`${prefixCls}-picker-arrow-expand`]: state.popupVisible,
});
const pickerCls = classNames({
[className]: !!className,
Expand All @@ -154,26 +271,60 @@ export default class Cascader extends React.Component<CascaderProps, any> {
'getPopupContainer',
'loadData',
'popupClassName',
'filterOption',
'renderFilteredOption',
'sortFilteredOption',
'notFoundContent',
]);

let options = props.options;
if (state.inputValue) {
options = this.generateFilteredOptions();
}
// Dropdown menu should keep previous status until it is fully closed.
if (!state.popupVisible) {
options = this.cachedOptions;
} else {
this.cachedOptions = options;
}

const dropdownMenuColumnStyle = {
width: undefined,
height: undefined,
};
const isNotFound = (options || []).length === 1 && options[0].value === 'ANT_CASCADER_NOT_FOUND';
if (isNotFound) {
dropdownMenuColumnStyle.height = 'auto'; // Height of one row.
}
// The default value of `matchInputWidth` is `true`
const resultListMatchInputWidth = showSearch.matchInputWidth === false ? false : true;
if (resultListMatchInputWidth && state.inputValue && this.refs.input) {
dropdownMenuColumnStyle.width = this.refs.input.refs.input.offsetWidth;
}
return (
<RcCascader {...props}
value={this.state.value}
popupVisible={this.state.popupVisible}
options={options}
value={value}
popupVisible={state.popupVisible}
onPopupVisibleChange={this.handlePopupVisibleChange}
onChange={this.handleChange}
dropdownMenuColumnStyle={dropdownMenuColumnStyle}
>
{children ||
<span
style={style}
className={pickerCls}
>
<Input {...inputProps}
placeholder={this.state.value && this.state.value.length > 0 ? null : placeholder}
ref="input"
placeholder={value && value.length > 0 ? null : placeholder}
className={`${prefixCls}-input ${sizeCls}`}
value=""
value={state.inputValue}
disabled={disabled}
readOnly
readOnly={!showSearch}
onClick={showSearch ? this.handleInputClick : null}
onBlur={showSearch ? this.handleInputBlur : null}
onChange={showSearch ? this.handleInputChange : null}
/>
<span className={`${prefixCls}-picker-label`}>{this.getLabel()}</span>
{clearIcon}
Expand Down
Loading

0 comments on commit 5f8b176

Please sign in to comment.