木灵鱼儿

木灵鱼儿

阅读:98

最后更新:2022/09/15/ 1:28:18

实现一个自动高度的输入框

前言

大概在上个月的时候我就看了对应的一些资料,但是一直拖着,因为这个功能其实vue的框架就有提供,比如element ui的input组件,它的这个功能叫自适应文本域,属性为autosize

所以本文也不过多讲解具体实现,主要还是它的原理层的东西。

这个功能可以用在哪呢?比如移动端的聊天输入框的高度判断,拿我们的QQ来讲,在没有内容的时候它就只有1行的高度,如果内容过高,就会自动变高,然后也会有一个max的最大高度阈值,这个高度一般是几行,比如最大6行高度,多了就滚动条处理。

那么就开始吧!

最基本的实现变高原理

其实非常简单,假设我现在有一个textarea元素用于用户的输入,然后我希望它在用户内容过多时自动加高,element是这么实现的,他会创建一份用户不可见的textarea元素,我们称之为影子textarea;然后监听用户输入的内容,将内容赋值给影子textarea。

此时影子textarea与真实textarea内容相同,那么我们只需要计算出影子textarea的高度,然后将这个高度赋值给真实textarea,不就可以实现自动变高的需求了。

<div>
    <textarea id="real-textarea"> </textarea>
</div>
<div style="margin-top: 20px">
    <textarea id="shadow-textarea"> </textarea>
</div>
textarea {
    box-sizing: border-box;
    height: 20px;
}

这里我们创建两个textarea元素,为了省事我使用了box-sizing: border-box;css,由于是demo演示,所以不用尽善尽美,后续也会讲这块怎么处理。

const realTextarea = document.getElementById("real-textarea");
const shadowTextarea = document.getElementById("shadow-textarea");

realTextarea.addEventListener("input", (e) => {
    shadowTextarea.value = e.target.value;
    //计算高度
    let height = shadowTextarea.scrollHeight;
    realTextarea.style.height = height + "px";
});

此时我们的原理就已经展示完毕了,scrollHeight获取到textarea元素的内容区域的实际大小;包括不在页面中的可滚动部分(内容和内边距)。

也就是说我们这里拿到的是:content高度 + 上下padding高度 + scroll滚动区域高度

如果没有滚动条,那么滚动区域高度为0。

但是其实这里会有一个小问题,我们看图:

你会发现真实的输入框它的高度虽然变了,但是会有滚动条,这说明我们的高度计算有问题,为什么?

细心的同学相信已经知道为什么了,我们的scrollHeight获得的高度不是完整的textarea高度,一个textarea它的高度还需要上下border的高度。

所以我们的代码需要这么完善一下:

const realTextarea = document.getElementById("real-textarea");
const shadowTextarea = document.getElementById("shadow-textarea");

realTextarea.addEventListener("input", (e) => {
    shadowTextarea.value = e.target.value;
    //计算高度
    let height = shadowTextarea.scrollHeight;
    const style = window.getComputedStyle(e.target);
    height += Number.parseFloat(style.getPropertyValue("border-bottom-width"));
    height += Number.parseFloat(style.getPropertyValue("border-top-width"));
    realTextarea.style.height = height + "px";
});

需要注意getComputedStyle是用于计算当前元素实际渲染的大小,所以这个元素绝对不能被display:none;,切记!!!

由于getPropertyValue得到的是一个带px单位的值,所以我们需要利用parseFloat方法解析这个字符串得到浮点数。

此时我们再去看看效果图:

问题已经解决。

样式带来的问题

从上面最基本的实现中我们可以联想到,元素的样式是可以影响到高度的,除了padding,border,甚至受box-sizing、字体大小、字体缩进,字体间距,行高,等等...。

非常多的css会影响元素高度,所以我们的影子textarea,它需要将真实textarea中,会影响到高度的样式自己也复制一份,这样才能保证两个元素的基建是相同的。这样计算得到的高度才是正确的。

那么我们也不需要自己去想有哪些会影响,在element中提供了一个常量数组,它里面保存了能影响到高度计算的所有样式。

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 =
        Number.parseFloat(style.getPropertyValue("padding-bottom")) +
        Number.parseFloat(style.getPropertyValue("padding-top"));

    const borderSize =
        Number.parseFloat(style.getPropertyValue("border-bottom-width")) +
        Number.parseFloat(style.getPropertyValue("border-top-width"));

    const contextStyle = CONTEXT_STYLE.map((name) => `${name}:${style.getPropertyValue(name)}`).join(";");

    return {
        contextStyle,
        paddingSize,
        borderSize,
        boxSizing
    };
}

element中有这么一个计算函数,它返回了一些我们将会用到的属性:

  1. contextStyle 真实元素的样式属性,用于给影子textarea赋值样式用;
  2. paddingSize padding上下的大小
  3. borderSize 上下边框的大小
  4. boxSizing 盒子模型,用于针对不同盒子模型的计算
realTextarea.addEventListener("input", (e) => {
    shadowTextarea.value = e.target.value;
    //计算高度
    const {
        contextStyle,
        paddingSize,
        borderSize,
        boxSizing
    } = calculateNodeStyling(e.target);
    //影子复制样式
    shadowTextarea.setAttribute("style", contextStyle);
    //设置高度
    let height = shadowTextarea.scrollHeight;
    if (boxSizing === "border-box") {
        height = height + borderSize;
    } else if (boxSizing === "content-box") {
        height = height - paddingSize;
    }

    realTextarea.style.height = height + "px";
});

这里boxSizing的if判断我照搬过来了:

  • 如果是"border-box",那自然是得加上边框border的高度;
  • 如果是"content-box",元素的高度就是content高度,scrollHeight中包含padding高度,所以需要减去

此时我们得到的就是一个非常正确的高度了。

如何影藏影子textarea

element也提供了一个常量样式,我们可以在复制样式的时候同时将其使用:

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;
`
realTextarea.addEventListener("input", (e) => {
    shadowTextarea.value = e.target.value;
    //计算高度
    const {
        contextStyle,
        paddingSize,
        borderSize,
        boxSizing
    } = calculateNodeStyling(e.target);
    //影子复制样式
    shadowTextarea.setAttribute("style", `${contextStyle};${HIDDEN_STYLE}`);
    //设置高度
    let height = shadowTextarea.scrollHeight;
    if (boxSizing === "border-box") {
        height = height + borderSize;
    } else if (boxSizing === "content-box") {
        height = height - paddingSize;
    }

    realTextarea.style.height = height + "px";
});

最大最小行数高度限制

最低行数高度

有时候我们希望textarea在不满足多少行时,默认有指定行数的高度,假设我们要求3行高度。

const minRows = 3;

realTextarea.addEventListener("input", (e) => {
    shadowTextarea.value = e.target.value;
    //计算高度
    const {
        contextStyle,
        paddingSize,
        borderSize,
        boxSizing
    } = calculateNodeStyling(e.target);
    //影子复制样式
    shadowTextarea.setAttribute("style", `${contextStyle};${HIDDEN_STYLE}`);
    //设置高度
    let height = shadowTextarea.scrollHeight;
    if (boxSizing === "border-box") {
        height = height + borderSize;
    } else if (boxSizing === "content-box") {
        height = height - paddingSize;
    }

    //最小3行
    shadowTextarea.value = "";
    const singleRowHeight = shadowTextarea.scrollHeight - paddingSize; //得到单行内容高度

    if (typeof minRows === "number") {
        let miniHeight = singleRowHeight * minRows; //3行的高度
        if (boxSizing === "border-box") {
            miniHeight = miniHeight + paddingSize + borderSize;
        }
        height = Math.max(height, miniHeight); //取最大值,如果实际高度小于3行高度,就取3行高度
    }

    realTextarea.style.height = height + "px";
});

原理也很简单,由于我们的height已经算好了,现在只需要处理最大最小行数高度就行了,那么直接将影子textarea的value复制空字符,这样他就一定会是一行的内容了。

此时再去获取scrollHeight的高度,再减去padding的大小,得到的就是纯行号的高度,这样就可以省去判断boxSizing === "content-box"了。

此时我们在乘以最小行数得到纯内容的最小高度,再通过判断boxSizing得到实际元素的高度。

此时再通过Math.max去一个最大值即可。

最大行数高度

做法和最小是一样的,只是变动了一点点。假设我们最大是6行的高度。

const minRows = 3;
const maxRows = 6;

realTextarea.addEventListener("input", (e) => {
    shadowTextarea.value = e.target.value;
    //计算高度
    const {
        contextStyle,
        paddingSize,
        borderSize,
        boxSizing
    } = calculateNodeStyling(e.target);
    //影子复制样式
    shadowTextarea.setAttribute("style", `${contextStyle};${HIDDEN_STYLE}`);
    //设置高度
    let height = shadowTextarea.scrollHeight;
    if (boxSizing === "border-box") {
        height = height + borderSize;
    } else if (boxSizing === "content-box") {
        height = height - paddingSize;
    }

    //最小3行
    shadowTextarea.value = "";
    const singleRowHeight = shadowTextarea.scrollHeight - paddingSize; //得到单行内容高度

    if (typeof minRows === "number") {
        let miniHeight = singleRowHeight * minRows; //3行的高度
        if (boxSizing === "border-box") {
            miniHeight = miniHeight + paddingSize + borderSize;
        }
        height = Math.max(height, miniHeight); //取最大值,如果实际高度小于3行高度,就取3行高度
    }

    //最大6行
    if (typeof maxRows === "number") {
        let maxHeight = singleRowHeight * maxRows; //6行的高度
        if (boxSizing === "border-box") {
            maxHeight = maxHeight + paddingSize + borderSize;
        }
        height = Math.min(height, maxHeight); //取最小值,如果实际高度大于6行高度,就取6行高度
    }

    realTextarea.style.height = height + "px";
});

效果展示

由于只是demo,所以一些展示逻辑没有完善,但是主要原理已全部阐述明白,不再过多赘述。

可以看到,我们在input事件触发后,影子textarea就会消失,因为我们给他的style赋值了HIDDEN_STYLE常量的值,然后高度也突然从1行变成3行的高度,当内容不断填充时,超出6行高度后,高度不再变化了。

此时完全符合我们的代码逻辑,展示没问题。

element源码

核心代码:input.ts

版权申明

本文系作者 @木灵鱼儿 原创发布在木灵鱼儿 - 有梦就能远航站点。未经许可,禁止转载。

关于作者

站点职位 博主
获得点赞 0
文章被阅读 98

相关文章