携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第19天,点击查看活动详情
简介
书接上文,将继续深入分析组件的文本域功能实现,耐心读完,相信会对您有所帮助。
为了更好的理解本文,请先阅读以下文章
更多组件分析详见 👉 📚 Element UI 源码剖析组件总览 。
本专栏的 gitbook
版本地址已经发布 📚《learning element-ui》 ,内容同步更新中!
文本域 textarea
组件通过封装原生 textarea
控件实现多行文本输入文本域功能。组件做了封装统一处理,所以textarea
控件的属性/事件跟input
控件是相似的。
<div :class="[type === 'textarea' ? '' : 'el-input']">
<!-- 单行文本输入框 -->
<template v-if="type !== 'textarea'">
<!-- 表单输入控件 -->
<input>
</template>
<!-- 多行文本输入的文本域 -->
<textarea
v-else
:tabindex="tabindex" // 元素是否可以聚焦 键盘导航
class="el-textarea__inner"
ref="textarea"
v-bind="$attrs" // 透传 Attributes
:disabled="inputDisabled" // 是否禁用
:readonly="readonly" // 是否只读
:autocomplete="autoComplete || autocomplete" // 自动补全
:style="textareaStyle" // 自定义样式
@compositionstart="handleCompositionStart" // 输入法编辑器 (IME) 事件
@compositionupdate="handleCompositionUpdate"
@compositionend="handleCompositionEnd"
@input="handleInput" // 输入内容
@focus="handleFocus" // 获取焦点
@blur="handleBlur" // 失去焦点
@change="handleChange" // 输入值变化
:aria-label="label" // ARIA 无障碍属性
>
</textarea>
<!-- 展示字数统计 -->
<span v-if="isWordLimitVisible && type === 'textarea'" class="el-input__count">
{{ textLength }}/{{ upperLimit }}
</span>
</div>
控件缩放
控件textarea
控件通过绑定计算属性textareaStyle
设置内联样式,实现控件的高度自适应、文本区大小可调整。
计算属性textareaStyle
中使用了内部属性textareaCalcStyle
和prop属性 resize
。
- 属性
textareaCalcStyle
值为文本域的高度属性(height
、min-height
)样式,在方法resizeTextarea
中由文本域内容和配置项计算生成。 - 属性
resize
控制文本区是否可调整大小。none
元素不能被用户缩放。both
允许用户在水平和垂直方向上调整元素的大小。horizontal
允许用户在水平方向上调整元素的大小。vertical
允许用户在垂直方向上调整元素的大小。
data() {
return {
textareaCalcStyle: {},
};
},
computed: {
textareaStyle() {
return merge({}, this.textareaCalcStyle, { resize: this.resize });
},
},
methods: {
resizeTextarea() {
// 计算 textareaCalcStyle
},
},
可自适应文本高度
方法 resizeTextarea
用来改变文本域的高度大小的。实例挂载时会调用该方法,当实例类型、输入框的值改变时也会多次调用该方法。
通过设置 autosize
属性可以使得文本域的高度能够根据文本内容自动进行调整,并且 autosize
还可以设定为一个对象,指定最小行数和最大行数。
mounted() {
this.resizeTextarea();
},
watch: {
value(val) {
this.$nextTick(this.resizeTextarea);
},
type() {
this.$nextTick(() => {
this.resizeTextarea();
});
}
},
methods: {
resizeTextarea() {
// 若服务端渲染,方法中断返回
if (this.$isServer) return;
const { autosize, type } = this;
// 此方法只用于 `textarea` 控件
if (type !== 'textarea') return;
// 属性 autosize 未开启自适应内容高度 只计算控件最小高度
if (!autosize) {
this.textareaCalcStyle = {
minHeight: calcTextareaHeight(this.$refs.textarea).minHeight
};
return;
}
// autosize 也可传入对象,如 { minRows: 2, maxRows: 6 }
const minRows = autosize.minRows; // 最少行数
const maxRows = autosize.maxRows; // 最大行数
// 控件自适应高度样式
this.textareaCalcStyle = calcTextareaHeight(this.$refs.textarea, minRows, maxRows);
},
},
calcTextareaHeight.js
类库导出方法calcTextareaHeight
,用于动态计算文本域的高度样式。
// packages\input\src\calcTextareaHeight.js
let hiddenTextarea; // 一个临时的文本域元素
// 用于隐藏创建的临时的文本域元素 hiddenTextarea
const HIDDEN_STYLE = `
height:0 !important;
// ...
`;
// 指定实例中文本域元素样式属性列表,获取属性值后用于创建临时的文本域元素
const CONTEXT_STYLE = [
'width',
// ...
];
// 获取指定元素节点的样式
function calculateNodeStyling(targetElement) {
// ...
}
// 计算文本域的高度
export default function calcTextareaHeight(
targetElement,
minRows = 1,
maxRows = null
) {
// ...
};
calculateNodeStyling()
方法 calculateNodeStyling
用于获取实例文本域元素节点的样式,并计算box-sizing
相关属性值。
contextStyle
复制当前实例元素的样式用于创建隐藏文本域。通过window.getComputedStyle()
获取元素计算后/渲染后的所有 CSS 属性的值,然后获取数组CONTEXT_STYLE
中指定属性值生成样式对象并转化字符串。boxSizing
获取元素box-sizing
属性值。paddingSize
获取元素上下内边距和。borderSize
获取元素上下边框宽度和。
// 指定实例中文本域元素样式属性列表,获取属性值后用于隐藏文本域创建
const CONTEXT_STYLE = [
'line-height',
'padding-top',
'padding-bottom',
'font-weight',
'font-size',
'width',
// ...
];
// 获取指定元素节点的样式
function calculateNodeStyling(targetElement) {
// 获取元素计算后/渲染后的所有 CSS 属性的值
const style = window.getComputedStyle(targetElement);
// box-sizing 属性定义了如何计算一个元素的总宽度和总高度。
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 };
}
calcTextareaHeight()
方法 calcTextareaHeight
通过创建一个跟实例元素一样的临时文本域,用于计算出自适应高度。
- 创建临时文本域元素
hiddenTextarea
,获取实例元素的内容和样式值、内容赋值给临时元素,使其作为实例元素的复制镜像。通过样式HIDDEN_STYLE
用于隐藏临时元素使其不可见。 - 获取元素内容高度
scrollHeight
,根据不同box-sizing
属性计算出元素总高度。 - 计算出单行文本行高度
singleRowHeight
。 - 如果设置
minRows
,计算出元素属性min-height
值。 - 如果设置
maxRows
,计算出元素最大高度,实际 height 不能超过最大高度。 - 清除临时文本域元素。
- 返回计算结果,格式
{ height:20px }
或{ height:20px; minHeight:20px; }
。
// 用于隐藏创建的临时的文本域元素 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
`;
// 计算文本域内容高度
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);
// contextStyle 让临时文本域元素样式跟实例元素保持一致
// HIDDEN_STYLE 用于隐藏临时文本域元素
hiddenTextarea.setAttribute('style', `${contextStyle};${HIDDEN_STYLE}`);
// 设置临时文本域内容
hiddenTextarea.value = targetElement.value || targetElement.placeholder || '';
// 临时文本域内容高度,包括由于溢出导致的视图中不可见内容。
let height = hiddenTextarea.scrollHeight;
const result = {};
// 计算文本内容高度 因为 scrollHeight 包括元素的 padding,但不包括元素的 border 和 margin。
if (boxSizing === 'border-box') {
height = height + borderSize; // border-box 加上上下边框宽度和
} else if (boxSizing === 'content-box') {
height = height - paddingSize; // content-box 减去上下内边距和
}
hiddenTextarea.value = ''; // 清空内容计算单行高度,
let singleRowHeight = hiddenTextarea.scrollHeight - paddingSize; // 单行文本高度
if (minRows !== null) {
let minHeight = singleRowHeight * minRows; // 最小行数高度和
if (boxSizing === 'border-box') {
minHeight = minHeight + paddingSize + borderSize; // border + padding + 内容的高度
}
height = Math.max(minHeight, height);
result.minHeight = `${ minHeight }px`; // 设置样式 { minHeight:20px; }
}
if (maxRows !== null) {
let maxHeight = singleRowHeight * maxRows; // 最大行数高度和
if (boxSizing === 'border-box') {
maxHeight = maxHeight + paddingSize + borderSize; // border + padding + 内容的高度
}
height = Math.min(maxHeight, height); // 选择最小高度
}
result.height = `${height}px`; // 设置样式 { height:20px; }
// 计算结束,清除临时文本域元素
hiddenTextarea.parentNode && hiddenTextarea.parentNode.removeChild(hiddenTextarea);
hiddenTextarea = null;
// 返回 { height:20px } 或 { height:20px; minHeight:20px; }
return result;
};
属性scrollHeight
是一个元素内容高度,包括由于溢出导致的视图中不可见内容。包括元素的 padding,但不包括元素的 border 和 margin。
属性 box-sizing
定义了如何计算一个元素的总宽度和总高度。
content-box
默认值,标准盒子模型。width
与height
只包括内容的宽和高,不包括边框(border),内边距(padding),外边距(margin)。width
= 内容的宽度height
= 内容的高度
border-box
width
与height
属性包括内容,内边距和边框,但不包括外边距。width
= border + padding + 内容的宽度height
= border + padding + 内容的高度
样式实现
组件样式源码 packages\theme-chalk\src\input.scss
使用混合指令嵌套生成组件样式。
// packages\theme-chalk\src\input.scss
// 生成 .el-textarea
@include b(textarea) {
// ...
// 生成 .el-textarea__inner
@include e(inner) {
// ...
// 生成 .el-textarea__inner::placeholder
&::placeholder { /* ... */ }
// 生成 .el-textarea__inner:hover
&:hover { /* ... */ }
// 生成 .el-textarea__inner:focus
&:focus { /* ... */ }
}
// 生成 .el-textarea .el-input__count
& .el-input__count { /* ... */ }
@include when(disabled) {
// 生成 .el-textarea.is-disabled .el-textarea__inner
.el-textarea__inner {
// ...
// 生成 .el-textarea.is-disabled .el-textarea__inner::placeholder
&::placeholder { /* ... */ }
}
}
@include when(exceed) {
// 生成 .el-textarea.is-exceed .el-textarea__inner
.el-textarea__inner { /* ... */ }
// 生成 .el-textarea.is-exceed .el-input__count
.el-input__count { /* ... */ }
}
}
// 生成 .el-input
@include b(input) {
// ...
// @include scroll-bar;
// 生成 .el-input .el-input__clear
& .el-input__clear {
// ...
// 生成 .el-input .el-input__clear:hover
&:hover { /* ... */ }
}
// 生成 .el-input .el-input__count
& .el-input__count {
// ...
// 生成 .el-input .el-input__count .el-input__count-inner
.el-input__count-inner { /* ... */ }
}
// 生成 .el-input__inner
@include e(inner) {
// ...
// 生成 .el-input__inner::-ms-reveal
&::-ms-reveal { /* ... */ }
// 生成 .el-input__inner::placeholder
&::placeholder { /* ... */ }
// 生成 .el-input__inner:hover
&:hover { /* ... */ }
// 生成 .el-input__inner:focus
&:focus { /* ... */ }
}
// 生成 .el-input__suffix
@include e(suffix) { /* ... */ }
// 生成 .el-input__suffix-inner
@include e(suffix-inner) { /* ... */ }
// 生成 .el-input__prefix
@include e(prefix) { /* ... */ }
// 生成 .el-input__icon
@include e(icon) {
// ...
// 生成 .el-input__icon:after
&:after { /* ... */ }
}
// 生成 .el-input__validateIcon
@include e(validateIcon) { /* ... */ }
@include when(active) {
// 生成.el-input.is-active .el-input__inner
.el-input__inner { /* ... */ }
}
@include when(disabled) {
// 生成 .el-input.is-disabled .el-input__inner
.el-input__inner {
// ...
// 生成 .el-input.is-disabled::placeholder
&::placeholder { /* ... */ }
}
// 生成 .el-input.is-disabled .el-input__icon
.el-input__icon { /* ... */ }
}
@include when(exceed) {
// 生成 .el-input.is-exceed .el-input__inner
.el-input__inner { /* ... */ }
.el-input__suffix {
// 生成 .el-input.is-exceed .el-input__suffix .el-input__count
.el-input__count { /* ... */ }
}
}
@include m(suffix) {
// 生成 .el-input--suffix .el-input__inner
.el-input__inner { /* ... */ }
}
@include m(prefix) {
// 生成 .el-input--prefix .el-input__inner
.el-input__inner { /* ... */ }
}
// 生成 .el-input--medium
@include m(medium) {
// ...
// 生成 .el-input--medium .el-input__inner
@include e(inner) { /* ... */ }
// 生成 .el-input--medium .el-input__icon
.el-input__icon { /* ... */ }
}
// small/mini ...
}
// 生成 .el-input-group
@include b(input-group) {
// ...
// 生成 .el-input-group > .el-input__inner
> .el-input__inner { /* ... */ }
// 生成 .el-input-group__append, .el-input-group__prepend
@include e((append, prepend)) {
// ...
// 生成 .el-input-group__append:focus,.el-input-group__prepend:focus
&:focus { /* ... */ }
/* 生成
.el-input-group__append .el-button,
.el-input-group__append .el-select,
.el-input-group__prepend .el-button,
.el-input-group__prepend .el-select */
.el-select,.el-button { /* ... */ }
/* 生成
.el-input-group__append button.el-button,
.el-input-group__append div.el-select .el-input__inner,
.el-input-group__append div.el-select:hover .el-input__inner,
.el-input-group__prepend button.el-button,
.el-input-group__prepend div.el-select .el-input__inner,
.el-input-group__prepend div.el-select:hover .el-input__inner */
button.el-button,
div.el-select .el-input__inner,
div.el-select:hover .el-input__inner {
// ...
}
/* 生成
.el-input-group__append .el-button,
.el-input-group__append .el-select,
.el-input-group__prepend .el-button,
.el-input-group__prepend .el-select */
.el-button,.el-input { /* ... */ }
}
// 生成 .el-input-group__prepend
@include e(prepend) { /* ... */ }
// 生成 .el-input-group__append
@include e(append) { /* ... */ }
// 生成
@include m(prepend) {
// 生成 .el-input-group--prepend .el-input__inner
.el-input__inner { /* ... */ }
// 生成 .el-input-group--prepend .el-select .el-input.is-focus .el-input__inner
.el-select .el-input.is-focus .el-input__inner { /* ... */ }
}
@include m(append) {
// 生成 .el-input-group--append .el-input__inner
.el-input__inner { /* ... */ }
// 生成 .el-input-group--append .el-select .el-input.is-focus .el-input__inner
.el-select .el-input.is-focus .el-input__inner { /* ... */ }
}
}
/** disalbe default clear on IE */
.el-input__inner::-ms-clear { /* ... */ }
混合指令scroll-bar
定义如下:
/* Scrollbar
-------------------------- */
@mixin scroll-bar {
$--scrollbar-thumb-background: #b4bccc;
$--scrollbar-track-background: #fff;
&::-webkit-scrollbar {
z-index: 11;
width: 6px;
&:horizontal {
height: 6px;
}
&-thumb {
border-radius: 5px;
width: 6px;
background: $--scrollbar-thumb-background;
}
&-corner {
background: $--scrollbar-track-background;
}
&-track {
background: $--scrollbar-track-background;
&-piece {
background: $--scrollbar-track-background;
width: 6px;
}
}
}
}
📚参考&关联阅读
"getComputedStyle",MDN
"CSS/box-sizing",MDN
"Element/scrollHeight",MDN
关注专栏
如果本文对您有所帮助请关注➕、 点赞👍、 收藏⭐!您的认可就是对我的最大支持!
此文章已收录到专栏中 👇,可以直接关注。