mirror of
https://github.com/ant-design/ant-design.git
synced 2025-01-18 06:03:38 +08:00
Merge pull request #2001 from ant-design/feat-autosize-textarea
Feature autosize textarea
This commit is contained in:
commit
d5f77412ca
@ -1,5 +1,6 @@
|
|||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import calculateNodeHeight from './calculateNodeHeight';
|
||||||
|
|
||||||
function fixControlledValue(value) {
|
function fixControlledValue(value) {
|
||||||
if (typeof value === 'undefined' || value === null) {
|
if (typeof value === 'undefined' || value === null) {
|
||||||
@ -8,6 +9,21 @@ function fixControlledValue(value) {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onNextFrame(cb) {
|
||||||
|
if (window.requestAnimationFrame) {
|
||||||
|
return window.requestAnimationFrame(cb);
|
||||||
|
}
|
||||||
|
return window.setTimeout(cb, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearNextFrameAction(nextFrameId) {
|
||||||
|
if (window.cancelAnimationFrame) {
|
||||||
|
window.cancelAnimationFrame(nextFrameId);
|
||||||
|
} else {
|
||||||
|
window.clearTimeout(nextFrameId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default class Input extends Component {
|
export default class Input extends Component {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
@ -16,6 +32,7 @@ export default class Input extends Component {
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
onPressEnter() {},
|
onPressEnter() {},
|
||||||
onKeyDown() {},
|
onKeyDown() {},
|
||||||
|
autosize: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -32,10 +49,32 @@ export default class Input extends Component {
|
|||||||
addonBefore: PropTypes.node,
|
addonBefore: PropTypes.node,
|
||||||
addonAfter: PropTypes.node,
|
addonAfter: PropTypes.node,
|
||||||
prefixCls: PropTypes.string,
|
prefixCls: PropTypes.string,
|
||||||
|
autosize: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
|
||||||
onPressEnter: PropTypes.func,
|
onPressEnter: PropTypes.func,
|
||||||
onKeyDown: PropTypes.func,
|
onKeyDown: PropTypes.func,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
textareaStyles: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.resizeTextarea();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
// Re-render with the new content then recalculate the height as required.
|
||||||
|
if (this.props.value !== nextProps.value) {
|
||||||
|
if (this.nextFrameActionId) {
|
||||||
|
clearNextFrameAction(this.nextFrameActionId);
|
||||||
|
}
|
||||||
|
this.nextFrameActionId = onNextFrame(this.resizeTextarea);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleKeyDown = (e) => {
|
handleKeyDown = (e) => {
|
||||||
if (e.keyCode === 13) {
|
if (e.keyCode === 13) {
|
||||||
this.props.onPressEnter(e);
|
this.props.onPressEnter(e);
|
||||||
@ -43,6 +82,24 @@ export default class Input extends Component {
|
|||||||
this.props.onKeyDown(e);
|
this.props.onKeyDown(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleTextareaChange = (e) => {
|
||||||
|
this.resizeTextarea();
|
||||||
|
if (this.props.onChange) {
|
||||||
|
this.props.onChange(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resizeTextarea = () => {
|
||||||
|
const { type, autosize } = this.props;
|
||||||
|
if (type !== 'textarea' || !autosize || !this.refs.input) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const minRows = autosize ? autosize.minRows : null;
|
||||||
|
const maxRows = autosize ? autosize.maxRows : null;
|
||||||
|
const textareaStyles = calculateNodeHeight(this.refs.input, false, minRows, maxRows);
|
||||||
|
this.setState({ textareaStyles });
|
||||||
|
}
|
||||||
|
|
||||||
renderLabledInput(children) {
|
renderLabledInput(children) {
|
||||||
const props = this.props;
|
const props = this.props;
|
||||||
const wrapperClassName = `${props.prefixCls}-group`;
|
const wrapperClassName = `${props.prefixCls}-group`;
|
||||||
@ -99,8 +156,13 @@ export default class Input extends Component {
|
|||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
{...props}
|
{...props}
|
||||||
|
style={{
|
||||||
|
...props.style,
|
||||||
|
...this.state.textareaStyles,
|
||||||
|
}}
|
||||||
className={inputClassName}
|
className={inputClassName}
|
||||||
onKeyDown={this.handleKeyDown}
|
onKeyDown={this.handleKeyDown}
|
||||||
|
onChange={this.handleTextareaChange}
|
||||||
ref="input"
|
ref="input"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
143
components/input/calculateNodeHeight.js
Normal file
143
components/input/calculateNodeHeight.js
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
// Thanks to https://github.com/andreypopp/react-textarea-autosize/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* calculateNodeHeight(uiTextNode, useCache = false)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const HIDDEN_TEXTAREA_STYLE = `
|
||||||
|
min-height:0 !important;
|
||||||
|
max-height:none !important;
|
||||||
|
height:0 !important;
|
||||||
|
visibility:hidden !important;
|
||||||
|
overflow:hidden !important;
|
||||||
|
position:absolute !important;
|
||||||
|
z-index:-1000 !important;
|
||||||
|
top:0 !important;
|
||||||
|
right:0 !important
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SIZING_STYLE = [
|
||||||
|
'letter-spacing',
|
||||||
|
'line-height',
|
||||||
|
'padding-top',
|
||||||
|
'padding-bottom',
|
||||||
|
'font-family',
|
||||||
|
'font-weight',
|
||||||
|
'font-size',
|
||||||
|
'text-rendering',
|
||||||
|
'text-transform',
|
||||||
|
'width',
|
||||||
|
'text-indent',
|
||||||
|
'padding-left',
|
||||||
|
'padding-right',
|
||||||
|
'border-width',
|
||||||
|
'box-sizing',
|
||||||
|
];
|
||||||
|
|
||||||
|
let computedStyleCache = {};
|
||||||
|
let hiddenTextarea;
|
||||||
|
|
||||||
|
function calculateNodeStyling(node, useCache = false) {
|
||||||
|
const nodeRef = (
|
||||||
|
node.getAttribute('id') ||
|
||||||
|
node.getAttribute('data-reactid') ||
|
||||||
|
node.getAttribute('name')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (useCache && computedStyleCache[nodeRef]) {
|
||||||
|
return computedStyleCache[nodeRef];
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = window.getComputedStyle(node);
|
||||||
|
|
||||||
|
const boxSizing = (
|
||||||
|
style.getPropertyValue('box-sizing') ||
|
||||||
|
style.getPropertyValue('-moz-box-sizing') ||
|
||||||
|
style.getPropertyValue('-webkit-box-sizing')
|
||||||
|
);
|
||||||
|
|
||||||
|
const paddingSize = (
|
||||||
|
parseFloat(style.getPropertyValue('padding-bottom')) +
|
||||||
|
parseFloat(style.getPropertyValue('padding-top'))
|
||||||
|
);
|
||||||
|
|
||||||
|
const borderSize = (
|
||||||
|
parseFloat(style.getPropertyValue('border-bottom-width')) +
|
||||||
|
parseFloat(style.getPropertyValue('border-top-width'))
|
||||||
|
);
|
||||||
|
|
||||||
|
const sizingStyle = SIZING_STYLE
|
||||||
|
.map(name => `${name}:${style.getPropertyValue(name)}`)
|
||||||
|
.join(';');
|
||||||
|
|
||||||
|
const nodeInfo = {
|
||||||
|
sizingStyle,
|
||||||
|
paddingSize,
|
||||||
|
borderSize,
|
||||||
|
boxSizing,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (useCache && nodeRef) {
|
||||||
|
computedStyleCache[nodeRef] = nodeInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodeInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function calculateNodeHeight(
|
||||||
|
uiTextNode,
|
||||||
|
useCache = false,
|
||||||
|
minRows = null,
|
||||||
|
maxRows = null
|
||||||
|
) {
|
||||||
|
if (!hiddenTextarea) {
|
||||||
|
hiddenTextarea = document.createElement('textarea');
|
||||||
|
document.body.appendChild(hiddenTextarea);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy all CSS properties that have an impact on the height of the content in
|
||||||
|
// the textbox
|
||||||
|
let {
|
||||||
|
paddingSize, borderSize,
|
||||||
|
boxSizing, sizingStyle,
|
||||||
|
} = calculateNodeStyling(uiTextNode, useCache);
|
||||||
|
|
||||||
|
// Need to have the overflow attribute to hide the scrollbar otherwise
|
||||||
|
// text-lines will not calculated properly as the shadow will technically be
|
||||||
|
// narrower for content
|
||||||
|
hiddenTextarea.setAttribute('style', `${sizingStyle};${HIDDEN_TEXTAREA_STYLE}`);
|
||||||
|
hiddenTextarea.value = uiTextNode.value || uiTextNode.placeholder || '';
|
||||||
|
|
||||||
|
let minHeight = -Infinity;
|
||||||
|
let maxHeight = Infinity;
|
||||||
|
let height = hiddenTextarea.scrollHeight;
|
||||||
|
|
||||||
|
if (boxSizing === 'border-box') {
|
||||||
|
// border-box: add border, since height = content + padding + border
|
||||||
|
height = height + borderSize;
|
||||||
|
} else if (boxSizing === 'content-box') {
|
||||||
|
// remove padding, since height = content
|
||||||
|
height = height - paddingSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minRows !== null || maxRows !== null) {
|
||||||
|
// measure height of a textarea with a single row
|
||||||
|
hiddenTextarea.value = '';
|
||||||
|
let singleRowHeight = hiddenTextarea.scrollHeight - paddingSize;
|
||||||
|
if (minRows !== null) {
|
||||||
|
minHeight = singleRowHeight * minRows;
|
||||||
|
if (boxSizing === 'border-box') {
|
||||||
|
minHeight = minHeight + paddingSize + borderSize;
|
||||||
|
}
|
||||||
|
height = Math.max(minHeight, height);
|
||||||
|
}
|
||||||
|
if (maxRows !== null) {
|
||||||
|
maxHeight = singleRowHeight * maxRows;
|
||||||
|
if (boxSizing === 'border-box') {
|
||||||
|
maxHeight = maxHeight + paddingSize + borderSize;
|
||||||
|
}
|
||||||
|
height = Math.min(maxHeight, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { height, minHeight, maxHeight };
|
||||||
|
}
|
18
components/input/demo/autosize-textarea.md
Normal file
18
components/input/demo/autosize-textarea.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
order: 6
|
||||||
|
title: 适应文本高度的文本域
|
||||||
|
---
|
||||||
|
|
||||||
|
`autosize` 属性适用于 `textarea` 节点,并且只有高度会自动变化。另外 `autosize` 可以设定为一个对象,指定最小行数和最大行数。
|
||||||
|
|
||||||
|
````jsx
|
||||||
|
import { Input } from 'antd';
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<div>
|
||||||
|
<Input type="textarea" placeholder="自适应内容高度" autosize />
|
||||||
|
<div style={{ margin: '24px 0' }} />
|
||||||
|
<Input type="textarea" placeholder="有最大高度和最小高度" autosize={{ minRows: 2, maxRows: 6 }} />
|
||||||
|
</div>
|
||||||
|
, mountNode);
|
||||||
|
````
|
@ -16,8 +16,8 @@ english: Input
|
|||||||
|
|
||||||
### Input
|
### Input
|
||||||
|
|
||||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||||
|-----------|------------------------------------------|------------|-------|--------|
|
|-----------|-----------------------------------------|------------|-------|--------|
|
||||||
| type | 【必须】声明 input 类型,同原生 input 标签的 type 属性。另外提供 `type="textarea"`。 | string | | 'text' |
|
| type | 【必须】声明 input 类型,同原生 input 标签的 type 属性。另外提供 `type="textarea"`。 | string | | 'text' |
|
||||||
| id | id | number 或 string | | |
|
| id | id | number 或 string | | |
|
||||||
| value | value 值 | any | | |
|
| value | value 值 | any | | |
|
||||||
@ -26,7 +26,8 @@ english: Input
|
|||||||
| disabled | 是否禁用状态,默认为 false | bool | | false |
|
| disabled | 是否禁用状态,默认为 false | bool | | false |
|
||||||
| addonBefore | 带标签的 input,设置前置标签 | node | | |
|
| addonBefore | 带标签的 input,设置前置标签 | node | | |
|
||||||
| addonAfter | 带标签的 input,设置后置标签 | node | | |
|
| addonAfter | 带标签的 input,设置后置标签 | node | | |
|
||||||
| onPressEnter | 按下回车的回调 | function(e) | | |
|
| onPressEnter | 按下回车的回调 | function(e) | | |
|
||||||
|
| autosize | 自适应内容高度,只对 `type="textarea"` 有效 | bool or object | `true` or `{ minRows: 2, maxRows: 6 }` | false |
|
||||||
|
|
||||||
> 如果 `Input` 在 `Form.Item` 内,并且 `Form.Item` 设置了 `id` 和 `options` 属性,则 `value` `defaultValue` 和 `id` 属性会被自动设置。
|
> 如果 `Input` 在 `Form.Item` 内,并且 `Form.Item` 设置了 `id` 和 `options` 属性,则 `value` `defaultValue` 和 `id` 属性会被自动设置。
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user