日常学习开发记录-input组件
实现
- 1.实现
- 2.inline-table和table-cell实现
- 2.1 表格布局的特性
- 2.2 示例
- 3.clear清除事件未生效
- 3.1 原因
- 3.2 解决
- 4. 增加type为text和textarea
- 4.1 rows,autosize的实现
- 5.拓展-composition事件
1.实现
<template>
<div
class="my-input"
:class="{
'is-disabled': disabled,
'is-focus': focused,
'my-input--suffix': showSuffix,
'my-input--prefix': showPrefix,
'my-input-group': $slots.prepend || $slots.append,
'my-input-group--prepend': $slots.prepend,
'my-input-group--append': $slots.append
}"
>
<!-- 前置元素 -->
<span class="my-input-group__prepend" v-if="$slots.prepend">
<slot name="prepend"></slot>
</span>
<!-- 前缀图标-->
<span class="my-input__prefix" v-if="showPrefix">
<i :class="prefixIcon" v-if="prefixIcon"></i>
<slot name="prefix"></slot>
</span>
<!-- 输入框 -->
<input
ref="input"
class="my-input__inner"
:type="type"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
:value="value"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
@change="handleChange"
>
<!-- 后缀图标,包括清空按钮 -->
<span class="my-input__suffix" v-if="showSuffix">
<span class="my-input__suffix-inner">
<i
class="my-input__clear my-icon-circle-close"
v-if="clearable && value && !disabled && !readonly"
@click="clear"
></i>
<i :class="suffixIcon" v-if="suffixIcon"></i>
<slot name="suffix"></slot>
</span>
</span>
<!-- 后置元素 -->
<span class="my-input-group__append" v-if="$slots.append">
<slot name="append"></slot>
</span>
</div>
</template>
<script>
export default {
name: 'MyInput',
props: {
// v-model 绑定值
value: {
type: [String, Number],
default: ''
},
// 输入框类型
type: {
type: String,
default: 'text'
},
// 占位文本
placeholder: String,
// 是否禁用
disabled: Boolean,
// 是否只读
readonly: Boolean,
// 是否可清空
clearable: Boolean,
// 前缀图标
prefixIcon: String,
// 后缀图标
suffixIcon: String
},
data() {
return {
focused: false,
// 用于存储内部值,支持可控和非可控模式
currentValue: this.value
};
},
computed: {
// 是否显示前缀
showPrefix() {
return this.prefixIcon || this.$slots.prefix;
},
// 是否显示后缀
showSuffix() {
return this.suffixIcon || this.clearable || this.$slots.suffix;
}
},
watch: {
value(val) {
this.currentValue = val;
}
},
methods: {
/**
* 处理输入事件
*/
handleInput(event) {
const value = event.target.value;
this.$emit('input', value);
this.currentValue = value;
},
/**
* 处理聚焦事件
*/
handleFocus(event) {
this.focused = true;
this.$emit('focus', event);
},
/**
* 处理失焦事件
*/
handleBlur(event) {
this.focused = false;
this.$emit('blur', event);
},
/**
* 处理变更事件
*/
handleChange(event) {
this.$emit('change', event.target.value);
},
/**
* 清空输入框
*/
clear() {
this.$emit('input', '');
this.$emit('change', '');
this.$emit('clear');
this.currentValue = '';
},
/**
* 聚焦输入框
*/
focus() {
this.$refs.input.focus();
},
/**
* 失焦输入框
*/
blur() {
this.$refs.input.blur();
}
}
};
</script>
2.inline-table和table-cell实现
使用prefix和suffix插槽会有问题:
能看到在定宽的组件中重叠错位了。
Element UI的input组件中使用的display:inline-table和display:table-cell布局技术确实很巧妙。这种布局方式允许父元素有一个固定宽度,而子元素却可以根据内容自适应并且可以超出父元素的宽度限制。
2.1 表格布局的特性
CSS表格布局有一个独特的特性:表格单元格(table cells)会根据其内容自动调整大小,并且可以超出表格本身的设定宽度。这与普通的块级元素完全不同,块级元素默认会受到父元素宽度的限制。
display: inline-table:使元素像内联元素一样在行内显示,但内部使用表格布局规则
display: table-cell:使元素表现为表格单元格
为什么可以超出父元素宽度?
表格布局有一个特殊的宽度计算算法:
先考虑内容的实际宽度
然后根据可用空间和其他单元格调整。
2.2 示例
关键点是:表格单元格会优先满足内容需求,即使这意味着需要超出表格的指定宽度
<style>
.input-wrapper {
/* 普通块级元素布局 */
width: 300px;
border: 1px solid #ccc;
}
.table-input-wrapper {
/* 表格布局 */
display: inline-table;
width: 300px;
border: 1px solid #ccc;
}
.addon {
display: table-cell;
background: #f5f7fa;
padding: 0 10px;
white-space: nowrap;
}
.input {
display: table-cell;
width: 100%;
border: none;
padding: 8px;
}
</style>
<!-- 普通布局 - 会被截断或换行 -->
<div class="input-wrapper">
<span>这是一个非常长的前置文本内容可能会超出父容器</span>
<input type="text" placeholder="输入内容">
</div>
<!-- 表格布局 - 会完整显示 -->
<div class="table-input-wrapper">
<span class="addon">这是一个非常长的前置文本内容可能会超出父容器</span>
<input class="input" type="text" placeholder="输入内容">
</div>
结果:
3.clear清除事件未生效
点击清除,clear没执行。
3.1 原因
点击清空按钮时,事件顺序如下:
mousedown 事件首先触发
由于清空按钮在输入框内部,输入框会失去焦点(blur)
输入框触发 blur 事件
如果值有变化,输入框还会触发 change 事件
然后才是清空按钮的 click 事件,此时才执行 clear 方法
问题在于:change事件在clear方法之前触发了,而且由于输入框已经失去焦点,您的清空操作可能不会生效。
3.2 解决
事件处加上 @mousedown.prevent,
1.阻止了mousedown的默认行为
2.阻止了输入框失去焦点
3.因此不会触发blur和change事件
4.让清空按钮的click事件和clear方法能够正常执行
4. 增加type为text和textarea
4.1 rows,autosize的实现
直接将input标签换成一个textarea标签是可以进行双向绑定的。但是还是差了点味。
思路(学习借鉴的elementui源码):创建一个隐藏的 textarea,放到 body 标签下,将 Textarea 组件的 value 值赋值给隐藏的 textarea,通过获取这个隐藏的 textarea 的 scrollHeight(scrollHeight 会返回该元素在不使用滚动条时的高度),来设置组件上面的 textarea 的高度。主要通过calcTextareaHeight 方法用于计算 textarea 的动态高度。
/**
* 用于动态计算文本域(textarea)高度的模块
* 通过创建隐藏的textarea元素复制样式和内容,计算理想高度
*/
// 全局隐藏textarea引用,避免重复创建
let hiddenTextarea
/**
* 应用于隐藏textarea的CSS样式
* 确保元素完全不可见且不影响页面布局
*/
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
`
/**
* 需要从目标textarea复制的CSS属性列表
* 这些属性会影响文本渲染和尺寸计算
*/
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', // 盒模型类型
]
/**
* 计算目标元素的样式参数
* @param {HTMLElement} targetElement - 目标textarea元素
* @returns {Object} 包含上下文样式和关键尺寸参数的对象
*/
function calculateNodeStyling(targetElement) {
// 获取目标元素的计算样式
const style = window.getComputedStyle(targetElement)
// 获取盒模型类型(border-box或content-box)
const boxSizing = style.getPropertyValue('box-sizing')
// 计算上下padding总和
const paddingSize =
parseFloat(style.getPropertyValue('padding-bottom')) +
parseFloat(style.getPropertyValue('padding-top'))
// 计算上下border总和
const borderSize =
parseFloat(style.getPropertyValue('border-bottom-width')) +
parseFloat(style.getPropertyValue('border-top-width'))
// 构建CSS样式字符串,包含所有CONTEXT_STYLE中定义的属性
const contextStyle = CONTEXT_STYLE.map(name => `${name}:${style.getPropertyValue(name)}`).join(
';'
)
console.log({ contextStyle, paddingSize, borderSize, boxSizing })
return { contextStyle, paddingSize, borderSize, boxSizing }
}
/**
* 计算textarea的理想高度
* @param {HTMLElement} targetElement - 目标textarea元素
* @param {number} minRows - 最小行数,默认为1
* @param {number|null} maxRows - 最大行数,默认为null(无限制)
* @returns {Object} 包含计算得到的高度信息,格式为{height: 'XXpx', minHeight: 'XXpx'}
*/
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') {
// border-box模型:滚动高度不包含border,需要加上
height = height + borderSize
} else if (boxSizing === 'content-box') {
// content-box模型:滚动高度包含padding,需要减去
height = height - paddingSize
}
// 计算单行高度(用于行数限制计算)
hiddenTextarea.value = '' // 清空内容
let singleRowHeight = hiddenTextarea.scrollHeight - paddingSize // 空文本域的高度减去padding
// 应用最小行数限制
if (minRows !== null) {
let minHeight = singleRowHeight * minRows // 计算最小高度
if (boxSizing === 'border-box') {
// border-box模型下需加上padding和border
minHeight = minHeight + paddingSize + borderSize
}
// 取计算高度和最小高度的较大值
height = Math.max(minHeight, height)
result.minHeight = `${minHeight}px`
}
// 应用最大行数限制
if (maxRows !== null) {
let maxHeight = singleRowHeight * maxRows // 计算最大高度
if (boxSizing === 'border-box') {
// border-box模型下需加上padding和border
maxHeight = maxHeight + paddingSize + borderSize
}
// 取计算高度和最大高度的较小值
height = Math.min(maxHeight, height)
}
// 设置最终计算结果
result.height = `${height}px`
// 清理:移除隐藏文本域以释放资源
hiddenTextarea.parentNode && hiddenTextarea.parentNode.removeChild(hiddenTextarea)
hiddenTextarea = null
return result
}
实现效果:
5.拓展-composition事件
现在的input搭配输入法有个问题还
我输入一个“啊”字,事件分发出去了两次。
composition相关事件是HTML
DOM的标准事件,专门用于处理输入法编辑器(IME)输入过程。在使用中文、日文、韩文等需要组合多个按键输入的语言时特别重要。
这三个事件的作用是: compositionstart: 当用户开始使用输入法输入时触发 代码中将isComposing标记为true
防止在组合输入过程中触发正常的input事件处理 compositionupdate: 当输入法正在组合字符时触发
代码中检查最后输入的字符是否是韩文(isKorean函数) 根据检测结果更新isComposing标志 compositionend:
当输入法完成组合输入,确认文字时触发 代码中重置isComposing为false 然后手动触发handleInput事件
这种设计解决了一个重要问题:避免在使用输入法时,未完成的组合文字过早触发input事件导致的问题。
完善后,结果: