first
This commit is contained in:
8
packages/input/index.js
Normal file
8
packages/input/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import ElInput from './src/input';
|
||||
|
||||
/* istanbul ignore next */
|
||||
ElInput.install = function(Vue) {
|
||||
Vue.component(ElInput.name, ElInput);
|
||||
};
|
||||
|
||||
export default ElInput;
|
104
packages/input/src/calcTextareaHeight.js
Normal file
104
packages/input/src/calcTextareaHeight.js
Normal file
@@ -0,0 +1,104 @@
|
||||
let hiddenTextarea;
|
||||
|
||||
const HIDDEN_STYLE = `
|
||||
height:0 !important;
|
||||
visibility:hidden !important;
|
||||
overflow:hidden !important;
|
||||
position:absolute !important;
|
||||
z-index:-1000 !important;
|
||||
top:0 !important;
|
||||
right:0 !important
|
||||
`;
|
||||
|
||||
const CONTEXT_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'
|
||||
];
|
||||
|
||||
function calculateNodeStyling(targetElement) {
|
||||
const style = window.getComputedStyle(targetElement);
|
||||
|
||||
const boxSizing = style.getPropertyValue('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 contextStyle = CONTEXT_STYLE
|
||||
.map(name => `${name}:${style.getPropertyValue(name)}`)
|
||||
.join(';');
|
||||
|
||||
return { contextStyle, paddingSize, borderSize, boxSizing };
|
||||
}
|
||||
|
||||
export default function calcTextareaHeight(
|
||||
targetElement,
|
||||
minRows = 1,
|
||||
maxRows = null
|
||||
) {
|
||||
if (!hiddenTextarea) {
|
||||
hiddenTextarea = document.createElement('textarea');
|
||||
document.body.appendChild(hiddenTextarea);
|
||||
}
|
||||
|
||||
let {
|
||||
paddingSize,
|
||||
borderSize,
|
||||
boxSizing,
|
||||
contextStyle
|
||||
} = calculateNodeStyling(targetElement);
|
||||
|
||||
hiddenTextarea.setAttribute('style', `${contextStyle};${HIDDEN_STYLE}`);
|
||||
hiddenTextarea.value = targetElement.value || targetElement.placeholder || '';
|
||||
|
||||
let height = hiddenTextarea.scrollHeight;
|
||||
const result = {};
|
||||
|
||||
if (boxSizing === 'border-box') {
|
||||
height = height + borderSize;
|
||||
} else if (boxSizing === 'content-box') {
|
||||
height = height - paddingSize;
|
||||
}
|
||||
|
||||
hiddenTextarea.value = '';
|
||||
let singleRowHeight = hiddenTextarea.scrollHeight - paddingSize;
|
||||
|
||||
if (minRows !== null) {
|
||||
let minHeight = singleRowHeight * minRows;
|
||||
if (boxSizing === 'border-box') {
|
||||
minHeight = minHeight + paddingSize + borderSize;
|
||||
}
|
||||
height = Math.max(minHeight, height);
|
||||
result.minHeight = `${ minHeight }px`;
|
||||
}
|
||||
if (maxRows !== null) {
|
||||
let maxHeight = singleRowHeight * maxRows;
|
||||
if (boxSizing === 'border-box') {
|
||||
maxHeight = maxHeight + paddingSize + borderSize;
|
||||
}
|
||||
height = Math.min(maxHeight, height);
|
||||
}
|
||||
result.height = `${ height }px`;
|
||||
hiddenTextarea.parentNode && hiddenTextarea.parentNode.removeChild(hiddenTextarea);
|
||||
hiddenTextarea = null;
|
||||
return result;
|
||||
};
|
435
packages/input/src/input.vue
Normal file
435
packages/input/src/input.vue
Normal file
@@ -0,0 +1,435 @@
|
||||
<template>
|
||||
<div :class="[
|
||||
type === 'textarea' ? 'el-textarea' : 'el-input',
|
||||
inputSize ? 'el-input--' + inputSize : '',
|
||||
{
|
||||
'is-disabled': inputDisabled,
|
||||
'is-exceed': inputExceed,
|
||||
'el-input-group': $slots.prepend || $slots.append,
|
||||
'el-input-group--append': $slots.append,
|
||||
'el-input-group--prepend': $slots.prepend,
|
||||
'el-input--prefix': $slots.prefix || prefixIcon,
|
||||
'el-input--suffix': $slots.suffix || suffixIcon || clearable || showPassword
|
||||
}
|
||||
]"
|
||||
@mouseenter="hovering = true"
|
||||
@mouseleave="hovering = false"
|
||||
>
|
||||
<template v-if="type !== 'textarea'">
|
||||
<!-- 前置元素 -->
|
||||
<div class="el-input-group__prepend" v-if="$slots.prepend">
|
||||
<slot name="prepend"></slot>
|
||||
</div>
|
||||
<input
|
||||
:tabindex="tabindex"
|
||||
v-if="type !== 'textarea'"
|
||||
class="el-input__inner"
|
||||
v-bind="$attrs"
|
||||
:type="showPassword ? (passwordVisible ? 'text': 'password') : type"
|
||||
:disabled="inputDisabled"
|
||||
:readonly="readonly"
|
||||
:autocomplete="autoComplete || autocomplete"
|
||||
ref="input"
|
||||
@compositionstart="handleCompositionStart"
|
||||
@compositionupdate="handleCompositionUpdate"
|
||||
@compositionend="handleCompositionEnd"
|
||||
@input="handleInput"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@change="handleChange"
|
||||
:aria-label="label"
|
||||
>
|
||||
<!-- 前置内容 -->
|
||||
<span class="el-input__prefix" v-if="$slots.prefix || prefixIcon">
|
||||
<slot name="prefix"></slot>
|
||||
<i class="el-input__icon"
|
||||
v-if="prefixIcon"
|
||||
:class="prefixIcon">
|
||||
</i>
|
||||
</span>
|
||||
<!-- 后置内容 -->
|
||||
<span
|
||||
class="el-input__suffix"
|
||||
v-if="getSuffixVisible()">
|
||||
<span class="el-input__suffix-inner">
|
||||
<template v-if="!showClear || !showPwdVisible || !isWordLimitVisible">
|
||||
<slot name="suffix"></slot>
|
||||
<i class="el-input__icon"
|
||||
v-if="suffixIcon"
|
||||
:class="suffixIcon">
|
||||
</i>
|
||||
</template>
|
||||
<i v-if="showClear"
|
||||
class="el-input__icon el-icon-circle-close el-input__clear"
|
||||
@mousedown.prevent
|
||||
@click="clear"
|
||||
></i>
|
||||
<i v-if="showPwdVisible"
|
||||
class="el-input__icon el-icon-view el-input__clear"
|
||||
@click="handlePasswordVisible"
|
||||
></i>
|
||||
<span v-if="isWordLimitVisible" class="el-input__count">
|
||||
<span class="el-input__count-inner">
|
||||
{{ textLength }}/{{ upperLimit }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<i class="el-input__icon"
|
||||
v-if="validateState"
|
||||
:class="['el-input__validateIcon', validateIcon]">
|
||||
</i>
|
||||
</span>
|
||||
<!-- 后置元素 -->
|
||||
<div class="el-input-group__append" v-if="$slots.append">
|
||||
<slot name="append"></slot>
|
||||
</div>
|
||||
</template>
|
||||
<textarea
|
||||
v-else
|
||||
:tabindex="tabindex"
|
||||
class="el-textarea__inner"
|
||||
@compositionstart="handleCompositionStart"
|
||||
@compositionupdate="handleCompositionUpdate"
|
||||
@compositionend="handleCompositionEnd"
|
||||
@input="handleInput"
|
||||
ref="textarea"
|
||||
v-bind="$attrs"
|
||||
:disabled="inputDisabled"
|
||||
:readonly="readonly"
|
||||
:autocomplete="autoComplete || autocomplete"
|
||||
:style="textareaStyle"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@change="handleChange"
|
||||
:aria-label="label"
|
||||
>
|
||||
</textarea>
|
||||
<span v-if="isWordLimitVisible && type === 'textarea'" class="el-input__count">{{ textLength }}/{{ upperLimit }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import emitter from 'element-ui/src/mixins/emitter';
|
||||
import Migrating from 'element-ui/src/mixins/migrating';
|
||||
import calcTextareaHeight from './calcTextareaHeight';
|
||||
import merge from 'element-ui/src/utils/merge';
|
||||
import {isKorean} from 'element-ui/src/utils/shared';
|
||||
|
||||
export default {
|
||||
name: 'ElInput',
|
||||
|
||||
componentName: 'ElInput',
|
||||
|
||||
mixins: [emitter, Migrating],
|
||||
|
||||
inheritAttrs: false,
|
||||
|
||||
inject: {
|
||||
elForm: {
|
||||
default: ''
|
||||
},
|
||||
elFormItem: {
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
textareaCalcStyle: {},
|
||||
hovering: false,
|
||||
focused: false,
|
||||
isComposing: false,
|
||||
passwordVisible: false
|
||||
};
|
||||
},
|
||||
|
||||
props: {
|
||||
value: [String, Number],
|
||||
size: String,
|
||||
resize: String,
|
||||
form: String,
|
||||
disabled: Boolean,
|
||||
readonly: Boolean,
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
autosize: {
|
||||
type: [Boolean, Object],
|
||||
default: false
|
||||
},
|
||||
autocomplete: {
|
||||
type: String,
|
||||
default: 'off'
|
||||
},
|
||||
/** @Deprecated in next major version */
|
||||
autoComplete: {
|
||||
type: String,
|
||||
validator(val) {
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
console.warn('[Element Warn][Input]\'auto-complete\' property will be deprecated in next major version. please use \'autocomplete\' instead.');
|
||||
return true;
|
||||
}
|
||||
},
|
||||
validateEvent: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
suffixIcon: String,
|
||||
prefixIcon: String,
|
||||
label: String,
|
||||
clearable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showPassword: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showWordLimit: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
tabindex: String
|
||||
},
|
||||
|
||||
computed: {
|
||||
_elFormItemSize() {
|
||||
return (this.elFormItem || {}).elFormItemSize;
|
||||
},
|
||||
validateState() {
|
||||
return this.elFormItem ? this.elFormItem.validateState : '';
|
||||
},
|
||||
needStatusIcon() {
|
||||
return this.elForm ? this.elForm.statusIcon : false;
|
||||
},
|
||||
validateIcon() {
|
||||
return {
|
||||
validating: 'el-icon-loading',
|
||||
success: 'el-icon-circle-check',
|
||||
error: 'el-icon-circle-close'
|
||||
}[this.validateState];
|
||||
},
|
||||
textareaStyle() {
|
||||
return merge({}, this.textareaCalcStyle, { resize: this.resize });
|
||||
},
|
||||
inputSize() {
|
||||
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
|
||||
},
|
||||
inputDisabled() {
|
||||
return this.disabled || (this.elForm || {}).disabled;
|
||||
},
|
||||
nativeInputValue() {
|
||||
return this.value === null || this.value === undefined ? '' : String(this.value);
|
||||
},
|
||||
showClear() {
|
||||
return this.clearable &&
|
||||
!this.inputDisabled &&
|
||||
!this.readonly &&
|
||||
this.nativeInputValue &&
|
||||
(this.focused || this.hovering);
|
||||
},
|
||||
showPwdVisible() {
|
||||
return this.showPassword &&
|
||||
!this.inputDisabled &&
|
||||
!this.readonly &&
|
||||
(!!this.nativeInputValue || this.focused);
|
||||
},
|
||||
isWordLimitVisible() {
|
||||
return this.showWordLimit &&
|
||||
this.$attrs.maxlength &&
|
||||
(this.type === 'text' || this.type === 'textarea') &&
|
||||
!this.inputDisabled &&
|
||||
!this.readonly &&
|
||||
!this.showPassword;
|
||||
},
|
||||
upperLimit() {
|
||||
return this.$attrs.maxlength;
|
||||
},
|
||||
textLength() {
|
||||
if (typeof this.value === 'number') {
|
||||
return String(this.value).length;
|
||||
}
|
||||
|
||||
return (this.value || '').length;
|
||||
},
|
||||
inputExceed() {
|
||||
// show exceed style if length of initial value greater then maxlength
|
||||
return this.isWordLimitVisible &&
|
||||
(this.textLength > this.upperLimit);
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value(val) {
|
||||
this.$nextTick(this.resizeTextarea);
|
||||
if (this.validateEvent) {
|
||||
this.dispatch('ElFormItem', 'el.form.change', [val]);
|
||||
}
|
||||
},
|
||||
// native input value is set explicitly
|
||||
// do not use v-model / :value in template
|
||||
// see: https://github.com/ElemeFE/element/issues/14521
|
||||
nativeInputValue() {
|
||||
this.setNativeInputValue();
|
||||
},
|
||||
// when change between <input> and <textarea>,
|
||||
// update DOM dependent value and styles
|
||||
// https://github.com/ElemeFE/element/issues/14857
|
||||
type() {
|
||||
this.$nextTick(() => {
|
||||
this.setNativeInputValue();
|
||||
this.resizeTextarea();
|
||||
this.updateIconOffset();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
focus() {
|
||||
this.getInput().focus();
|
||||
},
|
||||
blur() {
|
||||
this.getInput().blur();
|
||||
},
|
||||
getMigratingConfig() {
|
||||
return {
|
||||
props: {
|
||||
'icon': 'icon is removed, use suffix-icon / prefix-icon instead.',
|
||||
'on-icon-click': 'on-icon-click is removed.'
|
||||
},
|
||||
events: {
|
||||
'click': 'click is removed.'
|
||||
}
|
||||
};
|
||||
},
|
||||
handleBlur(event) {
|
||||
this.focused = false;
|
||||
this.$emit('blur', event);
|
||||
if (this.validateEvent) {
|
||||
this.dispatch('ElFormItem', 'el.form.blur', [this.value]);
|
||||
}
|
||||
},
|
||||
select() {
|
||||
this.getInput().select();
|
||||
},
|
||||
resizeTextarea() {
|
||||
if (this.$isServer) return;
|
||||
const { autosize, type } = this;
|
||||
if (type !== 'textarea') return;
|
||||
if (!autosize) {
|
||||
this.textareaCalcStyle = {
|
||||
minHeight: calcTextareaHeight(this.$refs.textarea).minHeight
|
||||
};
|
||||
return;
|
||||
}
|
||||
const minRows = autosize.minRows;
|
||||
const maxRows = autosize.maxRows;
|
||||
|
||||
this.textareaCalcStyle = calcTextareaHeight(this.$refs.textarea, minRows, maxRows);
|
||||
},
|
||||
setNativeInputValue() {
|
||||
const input = this.getInput();
|
||||
if (!input) return;
|
||||
if (input.value === this.nativeInputValue) return;
|
||||
input.value = this.nativeInputValue;
|
||||
},
|
||||
handleFocus(event) {
|
||||
this.focused = true;
|
||||
this.$emit('focus', event);
|
||||
},
|
||||
handleCompositionStart() {
|
||||
this.isComposing = true;
|
||||
},
|
||||
handleCompositionUpdate(event) {
|
||||
const text = event.target.value;
|
||||
const lastCharacter = text[text.length - 1] || '';
|
||||
this.isComposing = !isKorean(lastCharacter);
|
||||
},
|
||||
handleCompositionEnd(event) {
|
||||
if (this.isComposing) {
|
||||
this.isComposing = false;
|
||||
this.handleInput(event);
|
||||
}
|
||||
},
|
||||
handleInput(event) {
|
||||
// should not emit input during composition
|
||||
// see: https://github.com/ElemeFE/element/issues/10516
|
||||
if (this.isComposing) return;
|
||||
|
||||
// hack for https://github.com/ElemeFE/element/issues/8548
|
||||
// should remove the following line when we don't support IE
|
||||
if (event.target.value === this.nativeInputValue) return;
|
||||
|
||||
this.$emit('input', event.target.value);
|
||||
|
||||
// ensure native input value is controlled
|
||||
// see: https://github.com/ElemeFE/element/issues/12850
|
||||
this.$nextTick(this.setNativeInputValue);
|
||||
},
|
||||
handleChange(event) {
|
||||
this.$emit('change', event.target.value);
|
||||
},
|
||||
calcIconOffset(place) {
|
||||
let elList = [].slice.call(this.$el.querySelectorAll(`.el-input__${place}`) || []);
|
||||
if (!elList.length) return;
|
||||
let el = null;
|
||||
for (let i = 0; i < elList.length; i++) {
|
||||
if (elList[i].parentNode === this.$el) {
|
||||
el = elList[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!el) return;
|
||||
const pendantMap = {
|
||||
suffix: 'append',
|
||||
prefix: 'prepend'
|
||||
};
|
||||
|
||||
const pendant = pendantMap[place];
|
||||
if (this.$slots[pendant]) {
|
||||
el.style.transform = `translateX(${place === 'suffix' ? '-' : ''}${this.$el.querySelector(`.el-input-group__${pendant}`).offsetWidth}px)`;
|
||||
} else {
|
||||
el.removeAttribute('style');
|
||||
}
|
||||
},
|
||||
updateIconOffset() {
|
||||
this.calcIconOffset('prefix');
|
||||
this.calcIconOffset('suffix');
|
||||
},
|
||||
clear() {
|
||||
this.$emit('input', '');
|
||||
this.$emit('change', '');
|
||||
this.$emit('clear');
|
||||
},
|
||||
handlePasswordVisible() {
|
||||
this.passwordVisible = !this.passwordVisible;
|
||||
this.focus();
|
||||
},
|
||||
getInput() {
|
||||
return this.$refs.input || this.$refs.textarea;
|
||||
},
|
||||
getSuffixVisible() {
|
||||
return this.$slots.suffix ||
|
||||
this.suffixIcon ||
|
||||
this.showClear ||
|
||||
this.showPassword ||
|
||||
this.isWordLimitVisible ||
|
||||
(this.validateState && this.needStatusIcon);
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.$on('inputSelect', this.select);
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.setNativeInputValue();
|
||||
this.resizeTextarea();
|
||||
this.updateIconOffset();
|
||||
},
|
||||
|
||||
updated() {
|
||||
this.$nextTick(this.updateIconOffset);
|
||||
}
|
||||
};
|
||||
</script>
|
Reference in New Issue
Block a user