0


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

简介

***本文是基于

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()// 输入框对应的DOMconst emojiList =['👍','😀','😮','🥰','😡','🤣','😤','🙏']// '🫡', '🫰🏻'/** 方法:输入框文本-是否聚焦、显示 */constdisplayTextarea=(display =false)=>{
    isTextareaFocus.value = display
  }/** 操作:点击文本div */consthandleToFocus=()=>{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)默认都有的处理逻辑

/** 进行手势操作时的过滤处理:如点击、滑动等 */consttouchStartEvent=(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/** 进行手势操作时的过滤处理:如点击、滑动等 */consttouchStartEvent=(e:any)=>{// 这里包含textareaBtn,是为了发送按钮的点击事件能正常触发if(e.target.id ==='emoji'|| e.target.id ==='textareaBtn'){
      isNeedFocus.value =true}else{
      isNeedFocus.value =false}}/** 操作:表情 */consthandleInsertEmoji=(emoji:string)=>{if(message.value.length >= messageLength){return}
    message.value += emoji
    nextTick(()=>{handleToFocus()})}/** 文本输入框失去焦点时的逻辑处理 */consthandleToBlur=()=>{if(isNeedFocus.value){
      isNeedFocus.value =falsereturn}displayTextarea(false)}

具体实现

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

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 是否立即出发
 */functiondebounce<Textendsany[],U>(fn:(...args:T)=>U,
  wait:number=300,
  immediate:boolean=false){if(typeof fn !=='function')thrownewError('must have a callback fn')if(typeof wait ==='boolean') immediate = wait
  let timer: NodeJS.Timer |null=null// 注意需要在.eslintrc.cjs中开启 globals: { NodeJS: true }returnfunctionproxy(_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)constINPUT_EL_TAG_NAMES=['INPUT','TEXTAREA']const isKeyboardVisible =ref(false)// 键盘是否弹起let isBind =falseconst getKeyboardVisible =(newHeight:number):boolean=>{const{ activeElement }= document
  if(!activeElement)returnfalseconst activeElTagName = activeElement.tagName
  return newHeight <= height.value &&INPUT_EL_TAG_NAMES.includes(activeElTagName)}// const getIsLandScape = () => {//   const match = window.matchMedia('(orientation: landscape)')//   return !!match.matches// }constgetSize=()=>{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// }}constuseClientWindowInfo=()=>{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()// 输入框对应的DOMconst messageLength =200const emojiList =['👍','😀','😮','🥰','😡','🤣','😤','🙏']// '🫡', '🫰🏻'const isNeedFocus =ref(true)// 是否需要焦点/** 方法:输入框文本-是否聚焦、显示 */constdisplayTextarea=(display =false)=>{
    isTextareaFocus.value = display
  }/** 方法:输入框聚焦 */consthandleToFocus=()=>{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
      }})}/** 文本输入框失去焦点时的逻辑处理 */consthandleToBlur=()=>{if(isNeedFocus.value){
      isNeedFocus.value =falsereturn}displayTextarea(false)}/** 进行手势操作时的过滤处理:如点击、滑动等 */consttouchStartEvent=(e: Event)=>{const target = e.target as HTMLElement
    // 这里包含textareaBtn,是为了发送按钮的点击事件能正常触发if(target.id ==='emoji'|| target.id ==='textareaBtn'){
      isNeedFocus.value =true}else{
      isNeedFocus.value =false}}/** 操作:键盘弹出时,点击蒙层,关闭输入 */constclickBlur=()=>{if(textareaRef.value){
      textareaRef.value.blur()}displayTextarea(false)}/** 操作:添加表情 */consthandleInsertEmoji=(emoji:string)=>{if(message.value.length >= messageLength){return}
    message.value += emoji
    nextTick(()=>{handleToFocus()})}/** 操作:点击发送 */consthandleSend=()=>{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>

最后

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

标签: vue.js 前端 html5

本文转载自: https://blog.csdn.net/weixin_44136505/article/details/132193862
版权归原作者 前端-如此如此。 所有, 如有侵权,请联系我们删除。

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

还没有评论