移动端输入框填坑系列

背景

需求预沟通

  • 产品:我们需要做一个 IM,和微信一样
  • 我:打扰了,走错片场了,你们继续,,,,,,,
  • ....
  • 我:这个需求虽然不合理,但是我还是接了。。。。。。

输入框

来来来,我们来想一下我们一天中使用频率最高的 App——微信,英文名是 WeChat,它的聊天界面长啥样子,输入框在最底部(不知道你什么反应,我反正当场就打开了 weibo,facebook, twitter 等等等的 mobile 端,就没有一个是把输入框放在最底部的,好的,我当场就对产品发动了强烈的反抗,当然依旧没有成功),输入框默认单行展示,随着用户输入,输入框中的内容自动换行,最多显示 3 行,超出部分可以滚动,要求可以输入超链接,输入框左边有个插入表情按钮,右边是发送按钮,输入框下面有个 checkbox,,,,,

产品:这个需求很简单,怎么实现我不管,反正下个月要上线。

技术方案

虽然我很绝望,但是需求还是要做,,,,

输入框技术选型

来,我们来具体看需求,“输入框内容要自动换行”,排除 input;“输入框中可以输入超链接”,排除 textarea;还有啥,只有 contentEditable 为 true 的 div 了。

插入表情

我们知道,如果想做各端样式统一的表情的话,必然是用图片展示表情,这样的话,我们的成本就会增加很多,所幸在这一点上说服了产品,可以直接使用 emoji,那这里我们只需要找到一个比较全面的 emoji 字符列表就好了。

实现

输入框实现

这些能难住 CSS 高手我吗?不能够吧,直接上代码

  • placeholder 的实现
  • 自动换行,最多 3 行
.input-editor {

    color: rgba(0, 0, 0, 0.8);

    min-height: 32px;

    max-height: 72px;

    word-break: break-all;

    overflow-y: scroll;

    -webkit-overflow-scrolling: touch;

    -webkit-tap-highlight-color: transparent;

    user-select: text;



    &:empty {

        &::after {

            content: attr(data-placeholder);

            color: rgba(0, 0, 0, 0.24);

        }

    }

}


emoji 实现

这里和设计同学确认了一下,表情列表的展示逻辑,一行展示 8 个效果最好,列表的高度最好与键盘高度一致

const EMOJI_SIZE = (window.innerWidth - 24) / 8 - 6; // 一行放八个表情

const emojiStyles = {

    width: EMOJI_SIZE,

    height: EMOJI_SIZE,

    fontSize: `${EMOJI_SIZE / 1.43}px`, // 1.43为line-height

};


checkbox 实现

这个没什么好说的,CSS 背景图,js 控制选中态

发送按钮

按钮分为可以 active 与 disabled 两种状态

问题

做完了吗,嗯,是的,我用 chrome 的模拟器没啥问题了,那我们赶紧真机看一下效果吧,,,迫不及待搓手中,,,,然鹅,,,,,我是不是很菜?这不是真的吧?也许,睡一觉就好了吧!

键盘高度获取方案

  • Android:键盘弹起时,会触发 resize 事件,页面的高度会被挤压,也就说输入框只要一直固定在页面底部就可以了。

    监听 window 的 resize 事件,记得区分横竖屏的情况,计算 resize 事件触发前后 window.innerHeight 的变化,差值即为键盘高度。

  • iOS:键盘弹起前后,不会触发 resize 事件,页面的高度不变,同时会把页面上 fixed 的元素变为 absolute 处理(万恶之源)

    这里没有成熟的前端获取键盘高度的方法,所以依赖了客户端的接口。这里有降级方案,输入框在顶部就可以了。

键盘弹起时,禁止页面滚动,只允许输入框内容的滚动

如果你知道将外层容器改成 fixed,让其无法滚动的方法,其实你已经很棒了,但是我们前面有提到过,iOS 键盘弹起时,会把 fixed 布局变为 absolute 布局,显然这种方法是不行的;只有监听 touchmove 事件了,关键代码如下:

handleTouchStart = (event) => {

    const { touches } = event;

    const touch = touches[touches.length - 1];

    this.touchStartPageY = touch.pageY;

}



handleTouchMoveEvent = (event) => {

    const { editable } = this.props;

    const inputDom = this.inputRef.current;

    // 页面不处于编辑状态

    if (!editable || !inputDom) return;

    const { target, touches } = event;

    const isSlideInInput = target === inputDom || inputDom.contains(target); // 在输入框中滑动

    const isMultiTouch = touches.length > 1; // 多指拖动

    const isSlideUp = touches[0].pageY - this.touchStartPageY < 0; // 向上滑动

    const isSlideUpOverRange = isSlideUp && inputDom.scrollTop + inputDom.clientHeight >= inputDom.scrollHeight; // 向上滑动超出边界

    const isSlideDownOverRange = !isSlideUp && inputDom.scrollTop <= 0; // 向下滑动超出边界

    // input 允许滚动

    if (isSlideInInput && !isMultiTouch && !isSlideUpOverRange && !isSlideDownOverRange) {

        return;

    } else {

        event.preventDefault();

        event.stopPropagation();

        event.stopImmediatePropagation();

    }

}


重新聚焦 input,光标在所有文字的最前面

这个是 contentEditable 属性的浏览器支持的 bug,解决方法其实也很简单,就是输入框聚焦后,手动的调整光标的位置,关键代码如下

const input = this.inputRef.current;

// 矫正光标位置

if (!input || !input.lastChild) return;

const range = document.createRange();

const selection = window.getSelection();

range.selectNodeContents(input.lastChild);

range.collapse(false);

selection.removeAllRanges();

selection.addRange(range);


iOS 下,输入内容后再删除至空,不会显示 placeholder

经过排查发现,经过用户输入和删除等操作后,光标后会多出一个 br 标签,这里监听一下 KeyUp 事件做一下处理就好,关键代码如下

const keyCode = event.keyCode;

switch (keyCode) {

    // backspace

    case 8:

    // delete

    case 46:

        if (!this.getContent()) {

            this.setContent('');

        }

        break;

    default:

        // todo

}


部分安卓手机下,将键盘收起后,输入框不会失去焦点

键盘收起后,也会触发 resize 事件,这个时候手动触发 blur 就可以

输入内容安全问题

将输入框元素的 innerHTML,设置为一个新创建的 div 的内容,递归遍历所有子节点,将块级元素变为内容加上 BR 标签,A 标签只取 href 属性,重新生成 A 标签,保证最后得到的内容是文本、BR 标签与 A 标签的组合。

输入内容发送成功后的显示问题

如果把用户输入的 HTML 字符串直接放到 react 组件的 child 中,出于安全考虑,react 会把 HTML 标签作为字符串显示,这个时候需要用到 react 的 dangerouslySetInnerHTML 属性。

<div dangerouslySetInnerHTML={{ __html: content, }} />

切换 checkbox 时,input 会失去焦点

第一反应就是,我明明在 checkbox 的 click 事件回调中,preventDefault 了啊,怎么会呢!如果你了解过 Passive event listeners,你就会知道原因了,曾经一度绝望,react 不支持事件回调中传 passive 参数,翻了一下 react 的 issue,发现解决方案是有的,绑定 touchend 事件是可以成功阻止 input 失焦的。

插入表情时,每一次都是插在 input 最后,不能记住上次光标的位置

这里我们需要维护一下用户的光标位置,在插入表情时,直接插在上一次光标位置后,关键代码如下

updateLastRange() {

    // 更新range

    const selection = window.getSelection();

    if (!selection.rangeCount) return;

    this.lastRange = selection.getRangeAt(0);

}

总结

经过本次填坑,对移动端的输入框又有了更加深入的了解,遇到问题不要慌,先来翻一翻 AlloyTeam 的博客,AlloyTeam 长期招优秀前端工程师,欢迎入坑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

短暂又灿烂的

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值