<template>
<div :class="wrapClasses">
<template v-if="type !== 'textarea'">
<div :class="[prefixCls + '-group-prepend']" v-if="prepend" v-show="slotReady"><slot name="prepend">slot>div>
<i class="ivu-icon" :class="['ivu-icon-' + icon, prefixCls + '-icon', prefixCls + '-icon-normal']" v-if="icon" @click="handleIconClick">i>
<transition name="fade">
<i class="ivu-icon ivu-icon-load-c ivu-load-loop" :class="[prefixCls + '-icon', prefixCls + '-icon-validate']" v-if="!icon">i>
transition>
<input
:id="elementId"
:autocomplete="autocomplete"
:spellcheck="spellcheck"
ref="input"
:type="type"
:class="inputClasses"
:placeholder="placeholder"
:disabled="disabled"
:maxlength="maxlength"
:readonly="readonly"
:name="name"
:value="currentValue"
:number="number"
:autofocus="autofocus"
@keyup.enter="handleEnter"
@keyup="handleKeyup"
@keypress="handleKeypress"
@keydown="handleKeydown"
@focus="handleFocus"
@blur="handleBlur"
@input="handleInput"
@change="handleChange">
<div :class="[prefixCls + '-group-append']" v-if="append" v-show="slotReady"><slot name="append">slot>div>
template>
<textarea
v-else
:id="elementId"
:autocomplete="autocomplete"
:spellcheck="spellcheck"
ref="textarea"
:class="textareaClasses"
:style="textareaStyles"
:placeholder="placeholder"
:disabled="disabled"
:rows="rows"
:maxlength="maxlength"
:readonly="readonly"
:name="name"
:value="currentValue"
:autofocus="autofocus"
@keyup.enter="handleEnter"
@keyup="handleKeyup"
@keypress="handleKeypress"
@keydown="handleKeydown"
@focus="handleFocus"
@blur="handleBlur"
@input="handleInput">
textarea>
div>
template>
<script>
import { oneOf, findComponentUpward } from '../../utils/assist';
import calcTextareaHeight from '../../utils/calcTextareaHeight';
import Emitter from '../../mixins/emitter';
const prefixCls = 'ivu-input';
export default {
name: 'Input',
// 将引入的 Emitter 中的内容混合到本组件中
mixins: [ Emitter ],
props: {
// 输入框类型,必须为指定数组中的内容,默认值为 text
type: {
validator (value) {
return oneOf(value, ['text', 'textarea', 'password', 'url', 'email', 'date']);
},
default: 'text'
},
// 绑定的值,类型为字符串或者数据,默认为空串,一般使用 v-model 双向绑定
value: {
type: [String, Number],
default: ''
},
// 输入框尺寸,必须为指定值中的一个
size: {
validator (value) {
return oneOf(value, ['small', 'large', 'default']);
}
},
// 占位文本
placeholder: {
type: String,
default: ''
},
// 最大输入长度
maxlength: {
type: Number
},
// 禁用状态,默认不禁用
disabled: {
type: Boolean,
default: false
},
// 输入框尾部图标,用于向 Icon 组件传递值,只在 text 类型下有效
icon: String,
// 自适应内容高度,用于控制 textarea 的高度,也可以传入 Ojbect 来确定输入框的高度,只在 textara 下有效
autosize: {
type: [Boolean, Object],
default: false
},
// 文本域默认行数,仅在 textarea 下有效,默认为 2 行
rows: {
type: Number,
default: 2
},
// 只读状态,默认可写
readonly: {
type: Boolean,
default: false
},
// 元素name
name: {
type: String
},
// 将用户输入转换为 Number 类型,默认不开启
number: {
type: Boolean,
default: false
},
// 自动获取焦点,默认不开启
autofocus: {
type: Boolean,
default: false
},
// 拼写检查,默认不开启
spellcheck: {
type: Boolean,
default: false
},
// 自动补全,传入值必须为 on, off 之一,默认为 off
autocomplete: {
validator (value) {
return oneOf(value, ['on', 'off']);
},
default: 'off'
},
// 元素 id
elementId: {
type: String
}
},
data () {
return {
currentValue: this.value,
prefixCls: prefixCls,
prepend: true,
append: true,
slotReady: false,
textareaStyles: {}
};
},
computed: {
wrapClasses () {
return [
`${prefixCls}-wrapper`,
{
// 根据传入的 size 决定包裹标签的 size 类名,与是否渲染此类
[`${prefixCls}-wrapper-${this.size}`]: !!this.size,
// 根据传入的 type 决定包裹标签使用的 type 类
[`${prefixCls}-type`]: this.type,
// 根据是否有前后置内容来决定是否渲染 group 类
[`${prefixCls}-group`]: this.prepend || this.append,
// 如果设置了 size 且有前后置内容,那么设置对应的 group size 类
[`${prefixCls}-group-${this.size}`]: (this.prepend || this.append) && !!this.size,
// 根据是否有前后置内容来确定样式
[`${prefixCls}-group-with-prepend`]: this.prepend,
[`${prefixCls}-group-with-append`]: this.append,
[`${prefixCls}-hide-icon`]: this.append
}
];
},
inputClasses () {
return [
// 基础的 button 样式
`${prefixCls}`,
{
// 根据设置的 size 来设置对应的 size 样式
[`${prefixCls}-${this.size}`]: !!this.size,
// 根据设置的禁用状态来设置 disabled 样式
[`${prefixCls}-disabled`]: this.disabled
}
];
},
textareaClasses () {
return [
`${prefixCls}`,
{
[`${prefixCls}-disabled`]: this.disabled
}
];
}
},
methods: {
handleEnter (event) {
this.$emit('on-enter', event);
},
handleKeydown (event) {
this.$emit('on-keydown', event);
},
handleKeypress(event) {
this.$emit('on-keypress', event);
},
handleKeyup (event) {
this.$emit('on-keyup', event);
},
handleIconClick (event) {
this.$emit('on-click', event);
},
handleFocus (event) {
this.$emit('on-focus', event);
},
handleBlur (event) {
this.$emit('on-blur', event);
// 如果触发 blur 事件的不是 'DatePicker', 'TimePicker', 'Cascader', 'Search'中的一个,那么对于 FormItem 组件触发 on-form-blur 事件
if (!findComponentUpward(this, ['DatePicker', 'TimePicker', 'Cascader', 'Search'])) {
this.dispatch('FormItem', 'on-form-blur', this.currentValue);
}
},
handleInput (event) {
let value = event.target.value;
// 如果开启了转化成数字模式,那么转化成数字
if (this.number) value = Number.isNaN(Number(value)) ? value : Number(value);
this.$emit('input', value);
this.setCurrentValue(value);
this.$emit('on-change', event);
},
handleChange (event) {
this.$emit('on-input-change', event);
},
setCurrentValue (value) {
if (value === this.currentValue) return;
// 立即执行 resizeTextara()
this.$nextTick(() => {
this.resizeTextarea();
});
// 更新内容
this.currentValue = value;
// 如果当前组件的父组件不是 'DatePicker', 'TimePicker', 'Cascader', 'Search'中的一个,那么对于 FormItem 组件触发 on-form-change 事件
if (!findComponentUpward(this, ['DatePicker', 'TimePicker', 'Cascader', 'Search'])) {
this.dispatch('FormItem', 'on-form-change', value);
}
},
resizeTextarea () {
const autosize = this.autosize;
// 禁止自适应高度或者不是 textarea 那么可以不操作
if (!autosize || this.type !== 'textarea') {
return false;
}
const minRows = autosize.minRows;
const maxRows = autosize.maxRows;
// 计算应该要有的样式,然后更新
this.textareaStyles = calcTextareaHeight(this.$refs.textarea, minRows, maxRows);
},
focus () {
if (this.type === 'textarea') {
this.$refs.textarea.focus();
} else {
this.$refs.input.focus();
}
},
blur () {
if (this.type === 'textarea') {
this.$refs.textarea.blur();
} else {
this.$refs.input.blur();
}
}
},
watch: {
// 其实就是把 props 中传入的 value 与本组件内的 currentValue 内容同步
value (val) {
this.setCurrentValue(val);
}
},
mounted () {
// 如果根据 type 类型与是否传入插槽内容来判断插槽内容是否显示出来
if (this.type !== 'textarea') {
this.prepend = this.$slots.prepend !== undefined;
this.append = this.$slots.append !== undefined;
} else {
this.prepend = false;
this.append = false;
}
this.slotReady = true;
this.resizeTextarea();
}
};
script>
/**
* 寻找指定名称的父组件
*
* @param {any} context
* @param {any} componentName
* @param {any} componentNames
* @returns
*/
function findComponentUpward (context, componentName, componentNames) {
// 如果第二个参数是字符串,那就说明没有传入第三个参数,修改一下,使得参数统一
if (typeof componentName === 'string') {
componentNames = [componentName];
} else {
componentNames = componentName;
}
// 取传入组件的父组件
let parent = context.$parent;
// 取传入组件的父组件名称
let name = parent.$options.name;
// 如果父组件存在,但是父组件名称没有或者不在指定的名称列表中,那么循环,递归向上查找
while (parent && (!name || componentNames.indexOf(name) < 0)) {
parent = parent.$parent;
if (parent) name = parent.$options.name;
}
// 返回最终找到的父组件,因为 Vue 规定顶层必须要有一个标签用于注册,所以不用担心找不到
return parent;
}
// emmiter.js
// 广播事件
function broadcast(componentName, eventName, params) {
// 遍历当前组件的子组件,并执行操作
this.$children.forEach(child => {
const name = child.$options.name;
// 先取子组件的名称
// 如果子组件名称等于 componentName,那么子组件触发 eventName 事件,传入参数 params
// 如果子组件名称不等于 componentName, 那么递归调用,查找孙组件
if (name === componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
// todo 如果 params 是空数组,接收到的会是 undefined
broadcast.apply(child, [componentName, eventName].concat([params]));
}
});
}
export default {
methods: {
// 触发 componentName 组件的 eventName 事件,参数是 params
dispatch(componentName, eventName, params) {
// 找寻当前组件的父组件,如果不存在,那么直接使用根组件,赋值给 parent
let parent = this.$parent || this.$root;
// 取 parent 组件的名称
let name = parent.$options.name;
// 如果 parent 存在,但是 parent 名称没有或者不为指定值,那么循环,递归向上查找
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.name;
}
}
// 如果 parent 存在,那么 parent 触发 eventName 事件并传入参数 params
// [eventName].concat(params) 这里这么写的原因是,调用 apply 方法,参数是要作为一个数组,一次性传入的,也可以这么写:
// parent.$emit.call(parent, eventName, params);
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
// 广播事件
broadcast(componentName, eventName, params) {
// 调用 broadcast 方法并传入对应的参数
broadcast.call(this, componentName, eventName, params);
}
}
};
// calcTextareaHeight.js
let hiddenTextarea;
// 隐藏时的样式
const HIDDEN_STYLE = `
height:0 !important;
min-height:0 !important;
max-height:none !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(node) {
// 使用 window.getComputedStyle() 方法来获取指定节点在页面上渲染出来的样式
const style = window.getComputedStyle(node);
// 拿到指定节点 box-sizing 的值,可以使用 getPropertyValue() 方法来获取,也可以直接使用 style.boxSizing
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(targetNode, minRows = null, maxRows = null) {
// 如果 hiddenTextarea 不存在
if (!hiddenTextarea) {
// 创建一个 textarea 对象赋值给 hiddenTextarea ,并插入到 body 中
hiddenTextarea = document.createElement('textarea');
document.body.appendChild(hiddenTextarea);
}
// 计算指定节点的样式,并通过解构赋值赋给 paddingSize, borderSize, boxSizing, contextStyle 中
let {
paddingSize,
borderSize,
boxSizing,
contextStyle
} = calculateNodeStyling(targetNode);
// 给 hiddenTextarea 设置 style ,主要是设置盒模型类型与隐藏样式
hiddenTextarea.setAttribute('style', `${contextStyle};${HIDDEN_STYLE}`);
// 通过短路方式给 hiddenTextarea 设置值
hiddenTextarea.value = targetNode.value || targetNode.placeholder || '';
let height = hiddenTextarea.scrollHeight;
let minHeight = -Infinity;
let maxHeight = Infinity;
// 根据盒模型的类型与内容的高度给设置内容高度
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) {
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: `${height}px`,
minHeight: `${minHeight}px`,
maxHeight: `${maxHeight}px`
};
}
// value
<Input v-model="value">Input>
// size
<Input v-model="value" size="large">Input>
// placehoder
<Input v-model="value" placeholder="large size">Input>
// icon
<Input v-model="value" icon="ios-clock-outline">Input>
// type
<Input v-model="value" type="textarea">Input>
<Input v-model="value" type="password">Input>
<Input v-model="value" type="url">Input>
<Input v-model="value" type="email">Input>
<Input v-model="value" type="date">Input>
// textarea autosize
<Input v-model="value" type="textarea" :autosize="true">Input>
<Input v-model="value8" type="textarea" :autosize="{minRows: 2,maxRows: 5}">Input>
// disabled
<Input v-model="value" disabled>Input>
// prepend append slot
<Input v-model="value11">
<span slot="prepend">http://span>
<span slot="append">.comspan>
Input>
Vue 过渡动画的使用
将需要过渡动画的内容使用 transition 标签包起来, transition 标签仅需要 name 属性用于指定操作动画的 CSS 类名,一共有六组,执行顺序如下,选填
graph LR
nameAttributeValue-enter-->nameAttributeValue-enter-active
nameAttributeValue-enter-active-->nameAttributeValue-enter-to
nameAttributeValue-enter-to-->nameAttributeValue-leave
nameAttributeValue-leave-->nameAttributeValue-leave-active
nameAttributeValue-leave-active-->nameAttributeValue-leave-to