简介
本文是基于 VUE3+TS
的代码说明。
记录自己遇到的 div 与 textarea 输入框交互的聚焦、失去焦点、键盘收起、表情插入不失去焦点的需求实现。
需求分析
1.固定在页面底部;
2.默认显示纯文字与发送图标按钮,文字超出的省略显示;
3.点击文字后,显示文本输入框、表情、半透明遮罩层,自动聚焦;
4.有输入内容时,文本输入框右侧显示发送按钮;
5.点击表情,将表情加入到输入框最后,并且输入法键盘不收起;
6.输入框失去焦点、点击键盘上的收起或完成时,隐藏文本输入框和表情,显示默认的纯文字样式。
注意
------以下代码是伪代码------
1.输入框聚焦后,可能存在输入框位置不正确的问题
如输入框被遮挡、输入框没有挨着键盘等类似的问题。
这些问题在网上的解决方案较多,可自行查阅。
我的处理思路如下:
// 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>
点击文本div时,显示文本输入框,并且自动聚焦
<script setup lang="ts">
import { ref, nextTick } from 'vue'
const isTextareaFocus = ref(false) // 文本输入框是否聚焦(即显示)
const textareaRef = ref() // 输入框对应的DOM
const emojiList = ['👍', '😀', '😮', '🥰', '😡', '🤣', '😤', '🙏'] // '🫡', '🫰🏻'
/** 方法:输入框文本-是否聚焦、显示 */
const displayTextarea = (display = false) => {
isTextareaFocus.value = display
}
/** 操作:点击文本div */
const handleToFocus = () => {
displayTextarea(true)
nextTick(() => {
textareaRef.value?.focus() // 聚焦
// 部分ios上添加表情后,光标不在最后位置的问题处理
const length = message.value.length
textareaRef.value?.setSelectionRange(length, length)
})
}
</script>
2.键盘按钮的收起,判断输入框是否失去焦点:
1)Android
上,键盘按钮的收起,大部分不会触发输入框的blur事件,会触发webview的改变;
2)IOS
上,键盘按钮的收起,会触发输入框的blur事件,大部分不会触发webview的改变;
3)点击表情时,也会导致输入框失去焦点。
我的处理思路如下:
1)默认都有的处理逻辑
/** 进行手势操作时的过滤处理:如点击、滑动等 */
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
}
}
2)ios的特殊处理逻辑
if (val) {
// 键盘弹起
const focusEl = textareaRef.value
if (focusEl) {
focusEl.scrollIntoView({ block: 'center' })
}
} else {
// 键盘收起
clickBlur()
}
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">
<div
id="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 += emoji
nextTick(() => {
handleToFocus()
})
}
/** 文本输入框失去焦点时的逻辑处理 */
const handleToBlur = () => {
if (isNeedFocus.value) {
isNeedFocus.value = false
return
}
displayTextarea(false)
}
具体实现
目录结构
/test
/test/utils.ts
/test/index.vue文章来源:https://www.toymoban.com/news/detail-661569.html
1.utils.ts
import { ref } from 'vue'
enum UserTerminalEnum {
ANDROID,
IOS,
WEB
}
/** 获取当前所在客户端的类型 */
const getUserTerminalType = (): UserTerminalEnum => {
const u = navigator.userAgent
const 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.IOS
/**
* 防抖函数
* @param fn 回调函数
* @param wait 等待时间
* @param immediate 是否立即出发
*/
function debounce<T extends any[], U>(
fn: (...args: T) => U,
wait: number = 300,
immediate: boolean = false
) {
if (typeof fn !== 'function') throw new Error('must have a callback fn')
if (typeof wait === 'boolean') immediate = wait
let timer: NodeJS.Timer | null = null // 注意需要在.eslintrc.cjs中开启 globals: { NodeJS: true }
return function proxy(_this: any, ...args: T) {
const _self = _this
const _immediate = immediate && !timer
timer && clearTimeout(timer)
timer = setTimeout(() => {
!immediate && fn.apply(_self, args)
timer = null
}, wait)
_immediate && fn.apply(_self, args)
}
}
const width = ref(0) // 页面可视宽度
const height = ref(0) // 页面可视高度
// const outerWidth = ref(0)
// const outerHeight = ref(0)
// const screenWidth = ref(0)
// const screenHeight = ref(0)
// const screenRatio = ref(0)
// const isLandScape = ref(false)
const INPUT_EL_TAG_NAMES = ['INPUT', 'TEXTAREA']
const isKeyboardVisible = ref(false) // 键盘是否弹起
let isBind = false
const getKeyboardVisible = (newHeight: number): boolean => {
const { activeElement } = document
if (!activeElement) return false
const activeElTagName = activeElement.tagName
return newHeight <= height.value && INPUT_EL_TAG_NAMES.includes(activeElTagName)
}
// const getIsLandScape = () => {
// const match = window.matchMedia('(orientation: landscape)')
// return !!match.matches
// }
const getSize = () => {
const { innerHeight, innerWidth, outerWidth: ow, outerHeight: oh, screen } = window
const _isKeyboardVisible = getKeyboardVisible(innerHeight)
isKeyboardVisible.value = _isKeyboardVisible
height.value = innerHeight
width.value = innerWidth
// outerWidth.value = ow
// outerHeight.value = oh
// screenWidth.value = screen.availWidth
// screenHeight.value = screen.availHeight
// isLandScape.value = _isKeyboardVisible ? false : getIsLandScape()
// if (!_isKeyboardVisible) {
// screenRatio.value = height.value / width.value
// }
}
const useClientWindowInfo = () => {
if (!isBind) {
getSize()
window.addEventListener('resize', debounce(getSize, 200))
window.addEventListener('fullscreenchange', () => {
setTimeout(getSize, 500)
})
isBind = true
}
return {
width,
height,
isKeyboardVisible
}
}
export { UserTerminalEnum, isNotIOS, debounce, useClientWindowInfo }
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>
<!-- 发送图标按钮 -->
<div
class="btn-input"
:class="{ 'btn-input-active': message?.length }"
@click="handleSend"
/>
</div>
</div>
<!-- 文本输入框、表情栏 -->
<div v-show="isTextareaFocus" class="textarea-box" @touchstart="touchStartEvent">
<!-- 输入框与发送按钮 -->
<div class="textarea-row">
<textarea
ref="textareaRef"
v-model="message"
:class="message.length ? 'textarea-none' : 'textarea-active'"
class="textarea-normal"
:placeholder="placeholderText"
:contenteditable="true"
name="textarea"
rows="5"
cols="50"
:maxlength="messageLength"
@blur="handleToBlur"
/>
<button
id="textareaBtn"
:style="{
opacity: message.length ? '1' : '0',
'transition-delay': message.length ? '200ms' : '0ms'
}"
@click.stop="handleSend"
>发送</button
>
</div>
<!-- 表情 -->
<div class="emoji-list">
<div
id="emoji"
v-for="(emoji, index) in emojiList"
:key="index"
@click.stop="handleInsertEmoji(emoji)"
>{{ emoji }}</div
>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick, watch } from 'vue'
import { isNotIOS, useClientWindowInfo } from './utils'
const { isKeyboardVisible } = useClientWindowInfo()
const placeholderText = ref('尽情反馈您的建议哦~')
const message = ref('') // 输入框内容
const isTextareaFocus = ref(false) // 文本输入框是否聚焦(即显示)
const textareaRef = ref() // 输入框对应的DOM
const messageLength = 200
const emojiList = ['👍', '😀', '😮', '🥰', '😡', '🤣', '😤', '🙏'] // '🫡', '🫰🏻'
const isNeedFocus = ref(true) // 是否需要焦点
/** 方法:输入框文本-是否聚焦、显示 */
const displayTextarea = (display = false) => {
isTextareaFocus.value = display
}
/** 方法:输入框聚焦 */
const handleToFocus = () => {
displayTextarea(true)
nextTick(() => {
textareaRef.value?.focus() // 聚焦
if (isNotIOS) {
// 部分ios上添加表情后,光标不在最后位置的问题处理
const length = message.value.length
textareaRef.value?.setSelectionRange(length, length)
// 部分ios上添加表情后,没有自动滑动到底部
textareaRef.value.scrollTop = textareaRef.value.scrollHeight
}
})
}
/** 文本输入框失去焦点时的逻辑处理 */
const handleToBlur = () => {
if (isNeedFocus.value) {
isNeedFocus.value = false
return
}
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 += emoji
nextTick(() => {
handleToFocus()
})
}
/** 操作:点击发送 */
const handleSend = () => {
console.log('发送消息', message.value)
message.value = ''
}
/** 监听:键盘弹起/收起时 */
watch(
() => isKeyboardVisible.value,
(val: boolean) => {
if (!isNotIOS) {
return
}
if (val) {
// 键盘弹起
const focusEl = textareaRef.value
if (focusEl) {
focusEl.scrollIntoView({ block: 'center' })
}
} else {
// 键盘收起
clickBlur()
}
}
)
</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;
transition-duration: 0.2s;
transition-timing-function: ease;
-webkit-user-select: text !important;
}
.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>
最后
觉得有用的朋友请用你的金手指点一下赞,或者评论留言一起探讨技术!文章来源地址https://www.toymoban.com/news/detail-661569.html
到了这里,关于H5: div与textarea输入框的交互(聚焦、失去焦点、键盘收起)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!