H5: div与textarea输入框的交互(聚焦、失去焦点、键盘收起)

简介

本文是基于 VUE3+TS 的代码说明。

记录自己遇到的 div 与 textarea 输入框交互的聚焦、失去焦点、键盘收起、表情插入不失去焦点的需求实现。

需求分析

在这里插入图片描述在这里插入图片描述

1.固定在页面底部;
2.默认显示纯文字与发送图标按钮,文字超出的省略显示;
3.点击文字后,显示文本输入框、表情、半透明遮罩层,自动聚焦;
4.有输入内容时,文本输入框右侧显示发送按钮;
5.点击表情,将表情加入到输入框最后,并且输入法键盘不收起;
6.输入框失去焦点、点击键盘上的收起或完成时,隐藏文本输入框和表情,显示默认的纯文字样式。

注意

以下代码是伪代码

1.输入框聚焦后,可能存在输入框位置不正确的问题

如输入框被遮挡、输入框没有挨着键盘等类似的问题。
这些问题在网上的解决方案较多,可自行查阅。

我的处理思路如下:
1)在 app.vue 中建立一个传送目标

<template><router-view /><!-- 这里是输入框显示时,传送的目标DOM --><div id="inputPosition" />
</template>

2)用 Teleport 标签包裹输入框的html代码,文本框是否需要显示用变量控制
当然,传输的内容,最大层,需要定位到底部,注意样式层级
这样之后,输入框的输入法键盘弹起,不会对列表产生影响,不会出现兼容性问题。

// html
<Teleport to="#inputPosition"><div v-show="isTextareaFocus" class="textarea-box"><!-- 输入框与发送按钮 --><div><textarea ref="textareaRef" /><button>发送</button></div><!-- 表情 --><div><div v-for="(emoji, index) in emojiList" :key="index">{{ emoji }}</div></div></div>
</Teleport>

3)点击文本div时,显示文本输入框,并且自动聚焦

<script setup lang="ts">import { ref, nextTick } from 'vue'const isTextareaFocus = ref(false) // 文本输入框是否聚焦(即显示)const textareaRef = ref() // 输入框对应的DOMconst emojiList = ['👍', '😀', '😮', '🥰', '😡', '🤣', '😤', '🙏'] // '🫡', '🫰🏻'/** 方法:输入框文本-是否聚焦、显示 */const displayTextarea = (display = false) => {isTextareaFocus.value = display}/** 操作:点击文本div */const handleToFocus = () => {displayTextarea(true)nextTick(() => {textareaRef.value?.focus() // 聚焦})}
</script>

2.键盘按钮的收起,判断输入框是否失去焦点:

1)Android上,键盘按钮的收起,大部分不会触发输入框的blur事件,会触发webview的改变;
2)IOS上,键盘按钮的收起,会触发输入框的blur事件,大部分不会触发webview的改变;
3)点击表情时,也会导致输入框失去焦点。

我的处理思路如下:
1)判断到是Android时,添加全局监听,根据webview的变化比对,判断是键盘弹出还是收起,收起时,隐藏文本输入框及表情;
2)由于touchStart会先于click事件,而click事件会触发输入框的聚焦与失去焦点的事件,所以对dom添加touchStart事件,若是表情或发送按钮触发的,则标记不清除输入框的聚焦,否则标记需要清除;然后在触发输入框的blur事件中,判断该标记状态,是否执行隐藏的逻辑(这是用于IOS);

<textarea ref="textareaRef" @blur="textareaBlur " />import { ref, onMounted, onBeforeUnmount }const isNeedFocus = ref(true) // 是否需要焦点const textareaBlur = () => {if (isNeedFocus.value) {isNeedFocus.value = falsereturn}displayTextarea(false)}/** 操作:键盘弹出时,点击蒙层,关闭输入 */const clickBlur = () => {if (textareaRef.value) {textareaRef.value.blur()}displayTextarea(false)}/** 获取可视高度 */const getClientHeight = () => {return document.documentElement.clientHeight || document.body.clientHeight}let origin = getClientHeight()const compatibilityFn = () => {const resize = getClientHeight()if (origin > resize) {const focusEl = textareaRef.valueif (focusEl) {focusEl.scrollIntoView({ behavior: 'smooth', block: 'center' })}} else {// 和ios保持一致: 键盘收起之后去掉聚焦clickBlur()}origin = resize}const getUserTerminalType = (): UserTerminalEnum => {const u = navigator.userAgentconst isAndroid = u.indexOf('Android') > -1 || u.indexOf('Adr') > -1 // 判断是否是 android终端const isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/) // 判断是否是 iOS终端if (isAndroid) {return UserTerminalEnum.ANDROID}if (isIOS) {return UserTerminalEnum.IOS}return UserTerminalEnum.WEB}const isNotIOS = getUserTerminalType() !== UserTerminalEnum.IOSconst compatibilityOfTextarea = () => {if (!isNotIOS) {return}// 挂载事件到全局window.addEventListener('resize', compatibilityFn)}onMounted(() => {compatibilityOfTextarea()})onBeforeUnmount(() => {if (isNotIOS) {// 移除挂载到全局的事件window.removeEventListener('resize', compatibilityFn)}})

3.表情的插入

整个列表、文本输入框盒子添加touchstart事件,最先执行的是touchstart,根据当前touch事件的触发dom的id,判断是否需要保留文本输入框的聚焦;然后执行的表情的点击事件以及文本输入框的失去焦点事件,其中:
1)touchStartEvent
判断触发的dom的id是否是需要保留聚焦的dom,做一个标记;
2)handleInsertEmoji
做表情的插入,以及对文本输入框的聚焦;
3)handleToBlur
做输入框失去焦点的逻辑处理,根据1)中的标记,进行逻辑处理(之所以要重置标记,是为了下次输入框能正常失去焦点)。

// html
<div class="page" @touchstart="touchStartEvent">...<!-- 文本输入框、表情栏 --><Teleport to="#inputPosition"><div v-show="isTextareaFocus" class="textarea-box" @touchstart="touchStartEvent">...<textarea @blur="handleToBlur" />...<!-- 表情 --><div class="emoji-list"><divid="emoji"v-for="(emoji, index) in emojiList":key="index"@click.stop="handleInsertEmoji(emoji)">{{ emoji }}</div></div></div></Teleport>
</div>// ts/** 进行手势操作时的过滤处理:如点击、滑动等 */const touchStartEvent = (e: any) => {// 这里包含textareaBtn,是为了发送按钮的点击事件能正常触发if (e.target.id === 'emoji' || e.target.id === 'textareaBtn') {isNeedFocus.value = true} else {isNeedFocus.value = false}}/** 操作:表情 */const handleInsertEmoji = (emoji: string) => {if (message.value.length >= messageLength) {return}message.value += emojinextTick(() => {handleToFocus()})}/** 文本输入框失去焦点时的逻辑处理 */const handleToBlur = () => {if (isNeedFocus.value) {isNeedFocus.value = falsereturn}displayTextarea(false)}

具体实现

目录结构
/test
/test/utils.ts
/test/index.vue

1.app.vue

<template><router-view /><!-- 这里是输入框聚焦显示时,传送的目标DOM --><div id="inputPosition" />
</template><style lang="less">* {margin: 0;padding: 0;}
</style>

2.utils.ts

enum UserTerminalEnum {ANDROID,IOS,WEB
}/** 获取当前所在客户端的类型 */
const getUserTerminalType = (): UserTerminalEnum => {const u = navigator.userAgentconst isAndroid = u.indexOf('Android') > -1 || u.indexOf('Adr') > -1 // 判断是否是 android终端const isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/) // 判断是否是 iOS终端if (isAndroid) {return UserTerminalEnum.ANDROID}if (isIOS) {return UserTerminalEnum.IOS}return UserTerminalEnum.WEB
}const isNotIOS = getUserTerminalType() !== UserTerminalEnum.IOSexport { UserTerminalEnum, isNotIOS }

3.index.vue

<template><div class="page" @touchstart="touchStartEvent"><!-- 遮罩 --><div v-if="isTextareaFocus" class="mask-box" @touchstart="clickBlur" /><!-- 文字展示栏 --><div class="input-area"><div class="input-text-box"><!-- 文字展示 --><div class="input-text" @click="handleToFocus">{{ message || placeholderText }}</div><!-- 发送图标按钮 --><divclass="btn-input":class="{ 'btn-input-active': message?.length }"@click="handleSend"/></div></div><!-- 文本输入框、表情栏 --><Teleport to="#inputPosition"><div v-show="isTextareaFocus" class="textarea-box" @touchstart="touchStartEvent"><!-- 输入框与发送按钮 --><div class="textarea-row"><textarearef="textareaRef"v-model="message":class="message.length ? 'textarea-none' : 'textarea-active'"class="textarea-normal":placeholder="placeholderText"style="transition-duration: 0.2s;transition-timing-function: ease;-webkit-user-select: text !important;":contenteditable="true"name="textarea"rows="5"cols="50":maxlength="messageLength"@blur="handleToBlur"/><buttonid="textareaBtn":style="{opacity: message.length ? '1' : '0','transition-delay': message.length ? '200ms' : '0ms'}"@click.stop="handleSend">发送</button></div><!-- 表情 --><div class="emoji-list"><divid="emoji"v-for="(emoji, index) in emojiList":key="index"@click.stop="handleInsertEmoji(emoji)">{{ emoji }}</div></div></div></Teleport></div>
</template><script setup lang="ts">import { ref, nextTick, onMounted, onBeforeUnmount } from 'vue'import { isNotIOS } from './utils'const placeholderText = ref('尽情反馈您的建议哦~')const message = ref('') // 输入框内容const isTextareaFocus = ref(false) // 文本输入框是否聚焦(即显示)const textareaRef = ref() // 输入框对应的DOMconst messageLength = 200const emojiList = ['👍', '😀', '😮', '🥰', '😡', '🤣', '😤', '🙏'] // '🫡', '🫰🏻'const isNeedFocus = ref(true) // 是否需要焦点/** 方法:输入框文本-是否聚焦、显示 */const displayTextarea = (display = false) => {isTextareaFocus.value = display}/** 操作:点击文本div */const handleToFocus = () => {displayTextarea(true)nextTick(() => {textareaRef.value?.focus() // 聚焦})}/** 文本输入框失去焦点时的逻辑处理 */const handleToBlur = () => {if (isNeedFocus.value) {isNeedFocus.value = falsereturn}displayTextarea(false)}/** 进行手势操作时的过滤处理:如点击、滑动等 */const touchStartEvent = (e: Event) => {const target = e.target as HTMLElement// 这里包含textareaBtn,是为了发送按钮的点击事件能正常触发if (target.id === 'emoji' || target.id === 'textareaBtn') {isNeedFocus.value = true} else {isNeedFocus.value = false}}/** 操作:键盘弹出时,点击蒙层,关闭输入 */const clickBlur = () => {if (textareaRef.value) {textareaRef.value.blur()}displayTextarea(false)}/** 操作:表情 */const handleInsertEmoji = (emoji: string) => {if (message.value.length >= messageLength) {return}message.value += emojinextTick(() => {handleToFocus()})}/** 操作:点击发送 */const handleSend = () => {console.log('发送消息')message.value = ''}/** 获取可视高度 */const getClientHeight = () => {return document.documentElement.clientHeight || document.body.clientHeight}let origin = getClientHeight()const compatibilityFn = () => {const resize = getClientHeight()if (origin > resize) {const focusEl = textareaRef.valueif (focusEl) {focusEl.scrollIntoView({ behavior: 'smooth', block: 'center' })}} else {// 和ios保持一致: 键盘收起之后去掉聚焦clickBlur()}origin = resize}const compatibilityOfTextarea = () => {if (!isNotIOS) {return}// 挂载事件到全局window.addEventListener('resize', compatibilityFn)}onMounted(() => {compatibilityOfTextarea()})onBeforeUnmount(() => {if (isNotIOS) {// 移除挂载到全局的事件window.removeEventListener('resize', compatibilityFn)}})
</script><style scoped lang="less">.page {width: 100vw;height: 100vh;position: relative;background-color: #141624;.mask-box {position: absolute;top: 0;right: 0;bottom: 0;left: 0;opacity: 0.5;background-color: #000;}.input-area {height: 82px;padding: 10px 12px 0px;position: absolute;right: 0;bottom: 0;left: 0;border-top: 1px solid #272937;background-color: #141624;.input-text-box {height: 40px;padding: 0 15px;border-radius: 20px;background-color: #272937;display: flex;align-items: center;.input-text {flex: 1;line-height: 40px;font-size: 16px;color: #939191;overflow: hidden;white-space: nowrap;text-overflow: ellipsis;}.btn-input {margin-left: 10px;width: 22px;height: 22px;border-radius: 5px;background-color: #939191;}.btn-input-active {background-color: #3994f9;}}}}.textarea-box {position: absolute;right: 0;bottom: 0;left: 0;z-index: 9999;border-top: 1px solid #272937;background-color: #141624;.textarea-row {display: flex;align-items: flex-end;position: relative;padding: 10px;.textarea-normal {padding: 10px;height: 90px;background-color: #272937;color: #fff;border: none;outline: none;inline-size: none;resize: none;border-radius: 8px;font-size: 15px;}.textarea-none {width: calc(100% - 92px);transition-delay: 0ms;}.textarea-active {width: calc(100% - 20px);transition-delay: 200ms;}#textareaBtn {width: 62px;height: 31px;line-height: 31px;text-align: center;position: absolute;right: 10px;bottom: 10px;border-radius: 15px;border: none;background-color: #3994f9;overflow: hidden;white-space: nowrap;color: #fff;font-size: 15px;transition-duration: 0.2s;transition-timing-function: ease;}}.emoji-list {height: 50px;display: flex;align-items: center;#emoji {width: calc(100% / 8);height: 100%;text-align: center;font-size: 30px;}}}
</style>

最后

觉得有用的朋友请用你的金手指点一下赞,或者评论留言一起探讨技术!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/45564.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Verilog中的 条件语句\多路分支语句\循环语句

Verilog中的条件语句\多分支语句\循环语句 文章目录 Verilog中的条件语句\多分支语句\循环语句一、背景二、if-else2.1 标准结构2.2 例子 三、case-endcase3.1 标准结构3.2 例子3.2.1 三路选择器的case部分&#xff0c;如下&#xff1a;3.2.2 casez的四路选择器&#xff0c;如下…

OpenCV实例(九)基于深度学习的运动目标检测(二)YOLOv2概述

基于深度学习的运动目标检测&#xff08;二&#xff09;YOLOv2&YOLOv3概述 1.YOLOv2概述2.YOLOv3概述2.1 新的基础网络结构&#xff1a;2.2 采用多尺度预测机制。2.3 使用简单的逻辑回归进行分类 1.YOLOv2概述 对YOLO存在的不足&#xff0c;业界又推出了YOLOv2。YOLOv2主要…

宝藏级画图工具-drawio

今天推荐一款非常好用的免费开源画图工具drawio. Drawio即可以下载安装到本地&#xff0c;也可以在线编辑&#xff0c;在线编辑网址为 https://app.diagrams.net/。 本地版下载地址为https://github.com/jgraph/drawio-desktop/releases 1、支持各类图形 Drawio可以非常便捷…

java知识-JVM线程四大引用

一、JVM (1) 基本概念&#xff1a; JVM 是可运行 Java 代码的假想计算机 &#xff0c;包括一套字节码指令集、一组寄存器、一个栈、 一个垃圾回收&#xff0c;堆 和 一个存储方法域。JVM 是运行在操作系统之上的&#xff0c;它与硬件没有直接 的交互。 (2) 运行过程&#x…

excel中有哪些通配符、excel配置问题,数学函数篇1之sum系列

学习excel前需要明确的是事&#xff1a;   在学习excel函数之前&#xff0c;大家需要明确一件事&#xff0c;excel现在设计到了一些新函数&#xff0c;这些新函数只能存在于office365、office2019及更 新版本之中&#xff0c;所以建议大家在学习时安装较新的版本&#xff0c;…

华为OD机试关于无输入截止条件的ACM输入逻辑

无输入截止条件的ACM输入 华为OD机试题中有一些题目是没有输入截止条件的,比如 华为OD机试 - 数字游戏(Java & JS & Python)_伏城之外的博客-CSDN博客 从输入描述来看,每组有两行输入,但是并没有告诉我们具体有几组? 那么输入该如何截止呢? 此时,有两种输入…

硬编码基础二(跳转相关)

硬编码基础二&#xff08;跳转相关&#xff09; 今天的指令都是跟eip的变动有关 JCC短跳系列跳转 这一系列是条件跳转指令也都是两字节定长,第一个字节是opcode也是跳转条件后一个字节是有符号的偏移长度&#xff0c;当条件成立时会跳转到当前eip 2 操作数的位置 70~7f是…

Ctfshow web入门 权限维持篇 web670-web679 详细题解 全

CTFshow 权限维持 web670【】 补充一下PHP中单双引号的区别&#xff1a; 单引号和双引号之间最显着的区别在于我们插入字符串和变量时。单引号不插入字符串和变量。**单引号内的内容会按原样打印出来。**在大多数情况下&#xff0c;单引号内没有任何变量或转义序列的编译。 …

JVM中释放内存的三种方法

判断是否需要垃圾回收可以采用分析。 1标记--清除算法 分为两个阶段&#xff0c;标记和清除&#xff0c;先利用可达性分型标记还存活的对象&#xff0c;之后将没有被标记的对象删除&#xff0c;这样容易生成空间碎片&#xff0c;而且效率不稳定 标记阶段&#xff1a; 标记阶段…

Appium 2安装与使用java对Android进行自动化测试

文章目录 1、Appium 2.1安装1.1、系统要求1.2、安装Appium2.1服务1.3、安装UiAutomator2驱动1.4、安装Android SDK platform tools1.5、下载OpenJDK 2、Android自动代码例子2.1、安装Android自动化测试元素定位工具Appium Inspector2.2、编写android app自动化测试代码和使用ex…

飞机打方块(二)游戏界面制作

一、背景 1.新建bg节点 二、飞机节点功能实现 1.移动 1.新建plane节点 2.新建脚本GameController.ts,并绑定Canvas GameControll.ts const { ccclass, property } cc._decorator;ccclass export default class NewClass extends cc.Component {property(cc.Node)canvas:…

uniapp编写微信小程序和H5遇到的坑总结

uniapp编写微信小程序和H5遇到的坑总结 1、阻止事件冒泡2、二维码生成3、H5跨域配置4、H5时&#xff0c;地址栏上添加版本号5、H5时&#xff0c;tabBar遮挡部分内容6、uniapp使用webview通信6.1、uniapp编写的小程序嵌入h5之间的通信6.1.1、小程序向h5发送消息6.1.2、h5向小程序…

前端大屏常用的适配方案

假设我们正在开发一个可视化拖拽的搭建平台&#xff0c;可以拖拽生成工作台或可视化大屏&#xff0c;或者直接就是开发一个大屏&#xff0c;首先必须要考虑的一个问题就是页面如何适应屏幕&#xff0c;因为我们在搭建或开发时一般都会基于一个固定的宽高&#xff0c;但是实际的…

websocket + stomp + sockjs学习

文章目录 学习链接后台代码引入依赖application.ymlWebSocketConfigPrivateControllerWebSocketService WebSocketEventListenerCorsFilter 前端代码Room.vue 学习链接 WebSocket入门教程示例代码&#xff0c;代码地址已fork至本地gitee&#xff0c;原github代码地址&#xff…

STM32--DMA

文章目录 DMA简介DMA特性 DMA框图DMA基本结构DMA请求数据宽度对齐DMA数据转运工程DMAADC多通道 DMA简介 直接存储器存取(DMA)用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须CPU干预&#xff0c;数据可以通过DMA快速地移动&#xff0c;这就节省了CPU的…

PyTorch学习笔记(十七)——完整的模型验证(测试,demo)套路

完整代码&#xff1a; import torch import torchvision from PIL import Image from torch import nnimage_path "../imgs/dog.png" image Image.open(image_path) print(image)# 因为png格式是四个通道&#xff0c;除了RGB三通道外&#xff0c;还有一个透明度通…

字符设备驱动实例(ADC驱动)

四、ADC驱动 ADC是将模拟信号转换为数字信号的转换器&#xff0c;在 Exynos4412 上有一个ADC&#xff0c;其主要的特性如下。 (1)量程为0~1.8V。 (2)精度有 10bit 和 12bit 可选。 (3)采样时钟最高为5MHz&#xff0c;转换速率最高为1MSPS (4)具有四路模拟输入&#xff0c;同一时…

差值结构的复合底部

( A, B )---3*30*2---( 1, 0 )( 0, 1 ) 让网络的输入只有3个节点&#xff0c;AB训练集各由6张二值化的图片组成&#xff0c;让A 中有3个点&#xff0c;B中有1个点&#xff0c;且不重合&#xff0c;统计迭代次数并排序。 其中有20组数据 让迭代次数与排斥能成反比&#xff0c;排…

第 7 章 排序算法(3)(选择排序)

7.6选择排序 7.6.1基本介绍 选择式排序也属于内部排序法&#xff0c;是从欲排序的数据中&#xff0c;按指定的规则选出某一元素&#xff0c;再依规定交换位置后达到排序的目的。 7.6.2选择排序思想: 选择排序&#xff08;select sorting&#xff09;也是一种简单的排序方法…

装饰器读取不到被装饰函数的参数-已解决

def write_case_log(func):def wrapper(*args, **kwargs):logger.info("{}开始执行".format(func.__name__))func(*args,**kwargs)logger.info("{}执行中".format(args))logger.info("{}执行结束",format(func.__name__))return wrapper被装饰函…