Add autosize for Input of textarea type (#1951)

This commit is contained in:
afc163 2016-06-10 16:44:01 +08:00
parent 82a55adfc7
commit 06220f3319
3 changed files with 223 additions and 0 deletions

View File

@ -1,5 +1,6 @@
import React, { Component, PropTypes } from 'react';
import classNames from 'classnames';
import calculateNodeHeight from './calculateNodeHeight';
function fixControlledValue(value) {
if (typeof value === 'undefined' || value === null) {
@ -8,6 +9,21 @@ function fixControlledValue(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 {
static defaultProps = {
defaultValue: '',
@ -16,6 +32,7 @@ export default class Input extends Component {
type: 'text',
onPressEnter() {},
onKeyDown() {},
autosize: false,
}
static propTypes = {
@ -32,10 +49,32 @@ export default class Input extends Component {
addonBefore: PropTypes.node,
addonAfter: PropTypes.node,
prefixCls: PropTypes.string,
autosize: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
onPressEnter: 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) => {
if (e.keyCode === 13) {
this.props.onPressEnter(e);
@ -43,6 +82,24 @@ export default class Input extends Component {
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) {
const props = this.props;
const wrapperClassName = `${props.prefixCls}-group`;
@ -99,8 +156,13 @@ export default class Input extends Component {
return (
<textarea
{...props}
style={{
...props.style,
...this.state.textareaStyles,
}}
className={inputClassName}
onKeyDown={this.handleKeyDown}
onChange={this.handleTextareaChange}
ref="input"
/>
);

View 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 };
}

View 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);
````