diff --git a/components/index.js b/components/index.js index 0f80d68f4..d430782f9 100644 --- a/components/index.js +++ b/components/index.js @@ -25,3 +25,5 @@ export { default as Avatar } from './avatar' export { default as Badge } from './badge' export { default as Tabs } from './tabs' + +export { default as Input } from './input' diff --git a/components/input/Group.vue b/components/input/Group.vue new file mode 100644 index 000000000..169feccc5 --- /dev/null +++ b/components/input/Group.vue @@ -0,0 +1,36 @@ + + diff --git a/components/input/Input.vue b/components/input/Input.vue new file mode 100644 index 000000000..f7b332b05 --- /dev/null +++ b/components/input/Input.vue @@ -0,0 +1,181 @@ + diff --git a/components/input/Search.vue b/components/input/Search.vue new file mode 100644 index 000000000..f3d839bac --- /dev/null +++ b/components/input/Search.vue @@ -0,0 +1,61 @@ + diff --git a/components/input/TextArea.vue b/components/input/TextArea.vue new file mode 100644 index 000000000..e69de29bb diff --git a/components/input/calculateNodeHeight.js b/components/input/calculateNodeHeight.js new file mode 100644 index 000000000..8f568a19e --- /dev/null +++ b/components/input/calculateNodeHeight.js @@ -0,0 +1,157 @@ +// 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', +] + +const 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) + } + + // Fix wrap="off" issue + // https://github.com/ant-design/ant-design/issues/6577 + if (uiTextNode.getAttribute('wrap')) { + hiddenTextarea.setAttribute('wrap', uiTextNode.getAttribute('wrap')) + } else { + hiddenTextarea.removeAttribute('wrap') + } + + // Copy all CSS properties that have an impact on the height of the content in + // the textbox + const { + 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 + let overflowY + + 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 = '' + const 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 + } + overflowY = height > maxHeight ? '' : 'hidden' + height = Math.min(maxHeight, height) + } + } + // Remove scroll bar flash when autosize without maxRows + if (!maxRows) { + overflowY = 'hidden' + } + return { height, minHeight, maxHeight, overflowY } +} diff --git a/components/input/demo/basic.vue b/components/input/demo/basic.vue new file mode 100644 index 000000000..43218815f --- /dev/null +++ b/components/input/demo/basic.vue @@ -0,0 +1,16 @@ + + diff --git a/components/input/index.js b/components/input/index.js new file mode 100644 index 000000000..5e39b58b5 --- /dev/null +++ b/components/input/index.js @@ -0,0 +1,9 @@ +import Input from './Input' +import Group from './Group' +import Search from './Search' +// import TextArea from './TextArea' + +Input.Group = Group +Input.Search = Search +// Input.TextArea = TextArea +export default Input diff --git a/components/input/inputProps.js b/components/input/inputProps.js new file mode 100644 index 000000000..9d629cfb8 --- /dev/null +++ b/components/input/inputProps.js @@ -0,0 +1,39 @@ +export default { + prefixCls: { + default: 'ant-input', + type: String, + }, + defaultValue: [String, Number], + value: [String, Number], + placeholder: [String, Number], + type: { + default: 'text', + type: String, + }, + id: [String, Number], + name: String, + size: { + validator (value) { + return ['small', 'large', 'default'].includes(value) + }, + }, + maxLength: String, + disabled: { + default: false, + type: Boolean, + }, + readOnly: Boolean, + // addonBefore: React.ReactNode, + // addonAfter: React.ReactNode, + // onPressEnter?: React.FormEventHandler; + // onKeyDown?: React.FormEventHandler; + // onChange?: React.ChangeEventHandler; + // onClick?: React.FormEventHandler; + // onFocus?: React.FormEventHandler; + // onBlur?: React.FormEventHandler; + // autoComplete: String; + // prefix: React.ReactNode, + // suffix: React.ReactNode, + spellCheck: Boolean, + autoFocus: Boolean, +} diff --git a/components/input/style/index.js b/components/input/style/index.js new file mode 100644 index 000000000..cf31ed80f --- /dev/null +++ b/components/input/style/index.js @@ -0,0 +1,2 @@ +import '../../style/index.less' +import './index.less' diff --git a/components/input/style/index.less b/components/input/style/index.less new file mode 100644 index 000000000..6271e4e96 --- /dev/null +++ b/components/input/style/index.less @@ -0,0 +1,29 @@ +@import "../../style/themes/default"; +@import "../../style/mixins/index"; +@import "./mixin"; +@import "./search-input"; + +// Input styles +.@{ant-prefix}-input { + .input; +} + +//== Style for input-group: input with label, with button or dropdown... +.@{ant-prefix}-input-group { + .input-group(~"@{ant-prefix}-input"); + &-wrapper { + display: inline-block; + vertical-align: top; // https://github.com/ant-design/ant-design/issues/6403 + width: 100%; + } +} + +// Input with affix: prefix or suffix +.@{ant-prefix}-input-affix-wrapper { + .input-affix-wrapper(~"@{ant-prefix}-input"); + + // https://github.com/ant-design/ant-design/issues/6144 + .@{ant-prefix}-input { + min-height: 100%; // use min-height, assume that no smaller height to override + } +} diff --git a/components/input/style/mixin.less b/components/input/style/mixin.less new file mode 100644 index 000000000..d465800d5 --- /dev/null +++ b/components/input/style/mixin.less @@ -0,0 +1,345 @@ +@import "../../style/themes/default"; +@import "../../style/mixins/index"; + +@input-affix-width: 17px; + +// size mixins for input +.input-lg() { + padding: @input-padding-vertical-lg @input-padding-horizontal-lg; + height: @input-height-lg; +} + +.input-sm() { + padding: @input-padding-vertical-sm @input-padding-horizontal-sm; + height: @input-height-sm; +} + +// input status +// == when focus or actived +.active(@color: @outline-color) { + border-color: ~`colorPalette("@{color}", 5)`; + outline: 0; + box-shadow: 0 0 @outline-blur-size @outline-width fade(@color, 20%); +} + +// == when hoverd +.hover(@color: @input-hover-border-color) { + border-color: ~`colorPalette("@{color}", 5)`; +} + +.disabled() { + background-color: @input-disabled-bg; + opacity: 1; + cursor: not-allowed; + color: @disabled-color; + &:hover { + .hover(@input-border-color); + } +} + +// Basic style for input +.input() { + position: relative; + display: inline-block; + padding: @input-padding-vertical-base @input-padding-horizontal-base; + width: 100%; + height: @input-height-base; + font-size: @font-size-base; + line-height: @line-height-base; + color: @input-color; + background-color: @input-bg; + background-image: none; + border: @border-width-base @border-style-base @input-border-color; + border-radius: @border-radius-base; + .placeholder(); // Reset placeholder + transition: all .3s; + + &:hover { + .hover(); + } + + &:focus { + .active(); + } + + &-disabled { + .disabled(); + } + + // Reset height for `textarea`s + textarea& { + max-width: 100%; // prevent textearea resize from coming out of its container + height: auto; + vertical-align: bottom; + transition: all .3s, height 0s; + } + + // Size + &-lg { + .input-lg(); + } + + &-sm { + .input-sm(); + } +} + +// label input +.input-group(@inputClass) { + position: relative; + display: table; + border-collapse: separate; + border-spacing: 0; + width: 100%; + + // Undo padding and float of grid classes + &[class*="col-"] { + float: none; + padding-left: 0; + padding-right: 0; + } + + > [class*="col-"] { + padding-right: 8px; + &:last-child { + padding-right: 0; + } + } + + &-addon, + &-wrap, + > .@{inputClass} { + display: table-cell; + + &:not(:first-child):not(:last-child) { + border-radius: 0; + } + } + + &-addon, + &-wrap { + width: 1px; // To make addon/wrap as small as possible + white-space: nowrap; + vertical-align: middle; + } + + &-wrap > * { + display: block !important; + } + + .@{inputClass} { + float: left; + width: 100%; + margin-bottom: 0; + &:focus { + z-index: 1; // Fix https://gw.alipayobjects.com/zos/rmsportal/DHNpoqfMXSfrSnlZvhsJ.png + } + } + + &-addon { + padding: @input-padding-vertical-base @input-padding-horizontal-base; + font-size: @font-size-base; + font-weight: normal; + line-height: 1; + color: @input-color; + text-align: center; + background-color: @input-addon-bg; + border: @border-width-base @border-style-base @input-border-color; + border-radius: @border-radius-base; + position: relative; + transition: all .3s; + + // Reset Select's style in addon + .@{ant-prefix}-select { + margin: -(@input-padding-vertical-base + 1px) (-@input-padding-horizontal-base); // lesshint spaceAroundOperator: false + + .@{ant-prefix}-select-selection { + background-color: inherit; + margin: -1px; + border: @border-width-base @border-style-base transparent; + box-shadow: none; + } + + &-open, + &-focused { + .@{ant-prefix}-select-selection { + color: @primary-color; + } + } + } + + // Expand addon icon click area + // https://github.com/ant-design/ant-design/issues/3714 + > i:only-child:after { + position: absolute; + content: ''; + top: 0; + left: 0; + right: 0; + bottom: 0; + } + } + + // Reset rounded corners + > .@{inputClass}:first-child, + &-addon:first-child { + border-bottom-right-radius: 0; + border-top-right-radius: 0; + + // Reset Select's style in addon + .@{ant-prefix}-select .@{ant-prefix}-select-selection { + border-bottom-right-radius: 0; + border-top-right-radius: 0; + } + } + + > .@{inputClass}-affix-wrapper { + &:not(:first-child) .@{inputClass} { + border-bottom-left-radius: 0; + border-top-left-radius: 0; + } + + &:not(:last-child) .@{inputClass} { + border-bottom-right-radius: 0; + border-top-right-radius: 0; + } + } + + &-addon:first-child { + border-right: 0; + } + &-addon:last-child { + border-left: 0; + } + + > .@{inputClass}:last-child, + &-addon:last-child { + border-bottom-left-radius: 0; + border-top-left-radius: 0; + + // Reset Select's style in addon + .@{ant-prefix}-select .@{ant-prefix}-select-selection { + border-bottom-left-radius: 0; + border-top-left-radius: 0; + } + } + + // Sizing options + &-lg .@{inputClass}, + &-lg > &-addon { + .input-lg(); + } + + &-sm .@{inputClass}, + &-sm > &-addon { + .input-sm(); + } + + // Fix https://github.com/ant-design/ant-design/issues/5754 + &-lg .@{ant-prefix}-select-selection--single { + height: @input-height-lg; + } + + &-sm .@{ant-prefix}-select-selection--single { + height: @input-height-sm; + } + + .@{inputClass}-affix-wrapper { + display: table-cell; + width: 100%; + float: left; + } + + &&-compact { + display: block; + .clearfix; + + & > * { + border-radius: 0; + border-right-width: 0; + vertical-align: middle; + float: none; + display: inline-block; + } + + // Undo float for .ant-input-group .ant-input + .@{inputClass} { + float: none; + z-index: auto; + } + + // reset border for Select, DatePicker, AutoComplete, Cascader, Mention, TimePicker + & > .@{ant-prefix}-select > .@{ant-prefix}-select-selection, + & > .@{ant-prefix}-calendar-picker .@{ant-prefix}-input, + & > .@{ant-prefix}-select-auto-complete .@{ant-prefix}-input, + & > .@{ant-prefix}-cascader-picker .@{ant-prefix}-input, + & > .@{ant-prefix}-mention-wrapper .@{ant-prefix}-mention-editor, + & > .@{ant-prefix}-time-picker .@{ant-prefix}-time-picker-input { + border-radius: 0; + border-right-width: 0; + } + + & > *:first-child, + & > .@{ant-prefix}-select:first-child > .@{ant-prefix}-select-selection, + & > .@{ant-prefix}-calendar-picker:first-child .@{ant-prefix}-input, + & > .@{ant-prefix}-select-auto-complete:first-child .@{ant-prefix}-input, + & > .@{ant-prefix}-cascader-picker:first-child .@{ant-prefix}-input, + & > .@{ant-prefix}-mention-wrapper:first-child .@{ant-prefix}-mention-editor, + & > .@{ant-prefix}-time-picker:first-child .@{ant-prefix}-time-picker-input { + border-top-left-radius: @border-radius-base; + border-bottom-left-radius: @border-radius-base; + } + + & > *:last-child, + & > .@{ant-prefix}-select:last-child > .@{ant-prefix}-select-selection, + & > .@{ant-prefix}-calendar-picker:last-child .@{ant-prefix}-input, + & > .@{ant-prefix}-select-auto-complete:last-child .@{ant-prefix}-input, + & > .@{ant-prefix}-cascader-picker:last-child .@{ant-prefix}-input, + & > .@{ant-prefix}-mention-wrapper:last-child .@{ant-prefix}-mention-editor, + & > .@{ant-prefix}-time-picker:last-child .@{ant-prefix}-time-picker-input { + border-top-right-radius: @border-radius-base; + border-bottom-right-radius: @border-radius-base; + border-right-width: 1px; + } + } +} + +.input-affix-wrapper(@inputClass) { + position: relative; + display: inline-block; + width: 100%; + + .@{inputClass} { + z-index: 1; + } + + &:hover .@{inputClass}:not(.@{inputClass}-disabled) { + .hover(); + } + + .@{inputClass}-prefix, + .@{inputClass}-suffix { + position: absolute; + top: 50%; + transform: translateY(-50%); + z-index: 2; + line-height: 0; + color: @input-color; + } + + .@{inputClass}-prefix { + left: @input-padding-horizontal-base; + } + + .@{inputClass}-suffix { + right: @input-padding-horizontal-base; + } + + .@{inputClass}:not(:first-child) { + padding-left: @input-padding-horizontal-base + @input-affix-width; + } + + .@{inputClass}:not(:last-child) { + padding-right: @input-padding-horizontal-base + @input-affix-width; + } +} diff --git a/components/input/style/search-input.less b/components/input/style/search-input.less new file mode 100644 index 000000000..6a7ecddb5 --- /dev/null +++ b/components/input/style/search-input.less @@ -0,0 +1,58 @@ +@import "../../style/themes/default"; +@import "../../style/mixins/index"; +@import "../../button/style/mixin"; +@import "./mixin"; + +.@{ant-prefix}-input-search-icon { + cursor: pointer; + transition: all .3s; + font-size: 14px; + &:hover { + color: @input-hover-border-color; + } +} + +// code blow is keeped for compatibility +// for this demo: http://1x.ant.design/components/select/#components-select-demo-search-box +// do not delete until 3.x +.@{ant-prefix}-search-input-wrapper { + display: inline-block; + vertical-align: middle; +} + +.@{ant-prefix}-search-input { + &.@{ant-prefix}-input-group .@{ant-prefix}-input:first-child, + &.@{ant-prefix}-input-group .@{ant-prefix}-select:first-child { + border-radius: @border-radius-base; + position: absolute; + top: -1px; + width: 100%; + } + + &.@{ant-prefix}-input-group .@{ant-prefix}-input:first-child { + padding-right: 36px; + } + + .@{ant-prefix}-search-btn { + .btn-default; + border-radius: 0 @border-radius-base - 1px @border-radius-base - 1px 0; + left: -1px; + position: relative; + border-width: 0 0 0 1px; + z-index: 2; + padding-left: 8px; + padding-right: 8px; + &:hover { + border-color: @border-color-base; + } + } + &&-focus .@{ant-prefix}-search-btn-noempty, + &:hover .@{ant-prefix}-search-btn-noempty { + .btn-primary; + } + .@{ant-prefix}-select-combobox { + .@{ant-prefix}-select-selection__rendered { + margin-right: 29px; + } + } +} diff --git a/components/style.js b/components/style.js index 8c8c5c93f..b811afd8e 100644 --- a/components/style.js +++ b/components/style.js @@ -9,3 +9,4 @@ import './pagination/style' import './avatar/style' import './badge/style' import './tabs/style' +import './input/style' diff --git a/contributors.md b/contributors.md index 631161c29..bbda32e08 100644 --- a/contributors.md +++ b/contributors.md @@ -5,19 +5,19 @@ Button | done Icon | done Checkbox | done Radio | done -AutoComplete -Calendar +Tabs | done +Tag | done Carousel -DatePicker -Form +Mention Input InputNumber +AutoComplete Select -Tabs -Tag -Mention -TimePicker Upload +Form +Calendar +DatePicker +TimePicker ##万 Grid diff --git a/package.json b/package.json index 209ef2de3..473e40b41 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "dependencies": { "add-dom-event-listener": "^1.0.2", "eslint-plugin-vue": "^3.13.0", - "lodash.debounce": "^4.0.8" + "lodash.debounce": "^4.0.8", + "omit.js": "^1.0.0" } }