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

news2024/9/27 5:56:25

简介

本文是基于 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() // 输入框对应的DOM
  const 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 = false
      return
    }
    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.value
      if (focusEl) {
        focusEl.scrollIntoView({ behavior: 'smooth', block: 'center' })
      }
    } else {
      // 和ios保持一致: 键盘收起之后去掉聚焦
      clickBlur()
    }
    origin = resize
  }
  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

  const 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">
        <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

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.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

export { 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>
        <!-- 发送图标按钮 -->
        <div
          class="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">
          <textarea
            ref="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"
          />
          <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>
    </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() // 输入框对应的DOM
  const messageLength = 200
  const 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 = 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 = ''
  }

  /** 获取可视高度 */
  const getClientHeight = () => {
    return document.documentElement.clientHeight || document.body.clientHeight
  }

  let origin = getClientHeight()

  const compatibilityFn = () => {
    const resize = getClientHeight()
    if (origin > resize) {
      const focusEl = textareaRef.value
      if (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.coloradmin.cn/o/889693.html

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

相关文章

QT多屏显示程序

多屏显示的原理其实很好理解&#xff0c;就拿横向扩展来说&#xff1a; 计算机把桌面的 宽度扩展成了 w1&#xff08;屏幕1的宽度&#xff09; w2(屏幕2的宽度) 。 当一个窗口的起始横坐标 > w1&#xff0c;则 他就被显示在第二个屏幕上了。 多屏虚拟成一个桌面 qt的说明…

React+Typescript使用接口泛型处理props

好 刚讲完组件 那么 这次 我们来看一下 数据传递的 props 还是上文的案例 例如 我们想将 title 传给Hello组件 之前我们可以直接这样 以一个标签属性的形式传过去 而我们在子组件中 这样去使用 但现在 我们从编辑器中都可以看出 这种写法已经不行了 然后 我们将 hello 组件…

【Swagger】只需要3步搭建Swagger环境,就可以让你的项目实现Swagger在线文档,实时浏览,修改展示

目录 1. pom.xml文件中添加Swagger的jar包 2. 配置Swagger 3. 项目启动中加入Swagger注解的开关&#xff0c;启动Swagger功能 4. 启动项目&#xff0c;查看效果 Swagger 的功能这里就不多说明了&#xff0c;相信大家都懂的&#xff0c;好奇多问一句&#xff0c;大家有知道其…

Python文件操作教程,Python文件操作笔记

文件的打开与关闭 想一想&#xff1a; 如果想用word编写一份简历&#xff0c;应该有哪些流程呢&#xff1f; 打开word软件&#xff0c;新建一个word文件写入个人简历信息保存文件关闭word软件 同样&#xff0c;在操作文件的整体过程与使用word编写一份简历的过程是很相似的…

数据科学家需要掌握的Docker要点

大家好&#xff0c;Python以及pandas和scikit-learn等Python数据分析和机器学习库套件可以帮助你轻松开发数据科学应用程序。然而Python中的依赖性管理是一项挑战&#xff0c;在进行数据科学项目时&#xff0c;需要花费大量时间安装各种库&#xff0c;并跟踪正在使用的库的版本…

linux系统服务学习(八)DNS域名系统配置

文章目录 DNS域名管理系统一、DNS概述1、DNS系统概述☆ DNS的正向解析☆ DNS的反向解析☆ 根域&#xff08;.&#xff09;☆ 一级域名<顶级域|国家域>☆ 二级域名(自己购买管理)☆ 域名机构 2、DNS工作原理3、dig工具使用 二、DNS服务器的搭建1、DNS服务器端软件2、DNS服…

运行软件mfc140u.dll丢失怎么办?mfc140u.dll的三个修复方法

最近我在使用一款软件时遇到了一个问题&#xff0c;提示缺少mfc140u.dll文件。。这个文件是我在使用某个应用程序时所需要的&#xff0c;但是由于某种原因&#xff0c;它变得无法正常使用了。经过一番搜索和了解&#xff0c;我了解到mfc140u.dll是Microsoft Visual Studio 2015…

关于openfeign调用时content-type的问题

问题1描述&#xff1a; 今天在A服务使用openfeign调用B服务的时候&#xff0c;发现经常会偶发性报错。错误如下&#xff1a; 情况为偶发&#xff0c;很让人头疼。 两个接口如下&#xff1a; A服务接口&#xff1a; delayReasonApi.test(student);就是使用openfeign调用B服务的…

计组 | DMA

前言 记录一些计组相关联的题集与知识点&#xff0c;方便记忆与理解。 DMA 采用DMA方式传送数据时&#xff0c;每传送一个数据就要用一个&#xff08; C&#xff09;时间。 A 指令周期 B 机器周期 C 存储周期 D 总线周期发…

macOS(m1/m2)破解Sublime Text和Navicat16

破解Sublime Text 说明&#xff1a;全程使用的是终端操作 1. 下载Sublime Text&#xff0c;建议使用brew下载 2. 进入到下载的app的文件夹 cd "/Applications/Sublime Text.app/Contents/MacOS/"3. 执行以下操作以确认版本是否匹配 md5 -q sublime_text | grep -i…

分析区域产业发展现状,谋划产业发展路径,提升产业竞争力

随着经济全球化的深入发展&#xff0c;产业与区域经济发展有着不可分割的关系&#xff0c;产业是区域经济发展的基础&#xff0c;产业链的形成可以促进区域经济的协调发展&#xff0c;产业竞争力的提升能够带动区域经济的增长。那么该如何打造区域产业链闭环&#xff0c;提升产…

如何将labelImg打包成exe

最近整理一下数据标注这块的内容&#xff0c;在目标检测和目标分割里面用的最多的标注工具labelimg&#xff0c;labelme labelimg主要用于目标检测领域制作自己的数据集&#xff0c;如&#xff1a;YOLO系列目标检测模型 labelme主要用于图像分割领域制作自己的数据集&#xf…

静态代码测试工具HelixQAC新版对MISRA C规则提供100%覆盖率

Helix QAC 2023.2中的新增功能 Helix QAC 2023.2对 MISRA C:2012 和 MISRA C:2023 规则提供了100% 的覆盖率&#xff0c;并更新了相应的合规性模块以适用于MISRA C:2023。此外&#xff0c;此版本还包括改进的 C23 语言支持、对 Validate 平台的改进和 Helix QAC 和 Validate 的…

什么是客户自助服务?

自助服务是指通过自动化技术和系统&#xff0c;使顾客或用户能够自主完成某些服务或操作&#xff0c;而无需直接依赖人工的帮助。它提供了一种方便、快捷和高效的方式&#xff0c;让用户可以自行完成特定任务或获取所需的信息。 自助服务可以在各种场景中应用&#xff0c;例如…

unity拓展 unity自带的类(Tranform为例)

因为我们使用了ILRuntime热更&#xff0c;unity 打出的WebGL包&#xff0c;运行就会报找不到DoTween里面的方法&#xff0c;所以吧DoTween拓展到tranform类里面&#xff0c;这样就不会报错了&#xff0c;下面是示例 using DG.Tweening; using System.Collections; using Syste…

Python搭建http文件服务器实现手机电脑文件传输功能

第一种代码的界面如下&#xff1a;&#xff08;有缺点&#xff0c;中文乱码&#xff09; # !/usr/bin/env python3 # -*- coding:utf-8 _*-"""Simple HTTP Server With Upload. python -V3.6 This module builds on http.server by implementing the standard G…

java 工程管理系统源码+项目说明+功能描述+前后端分离 + 二次开发 em

Java版工程项目管理系统 Spring CloudSpring BootMybatisVueElementUI前后端分离 功能清单如下&#xff1a; 首页 工作台&#xff1a;待办工作、消息通知、预警信息&#xff0c;点击可进入相应的列表 项目进度图表&#xff1a;选择&#xff08;总体或单个&#xff09;项目显…

适合使用CRM系统的行业有哪些?

激烈的竞争环境下&#xff0c;企业急需一款工具来管理客户关系。CRM正是这样一款软件&#xff0c;可以帮助企业管理客户&#xff0c;提高客户满意度&#xff0c;从而实现业绩增长。那么&#xff0c;哪些行业适合使用CRM系统&#xff1f;为什么&#xff1f; 一、零售行业 CRM系…

redis-基础

1、redis简述 redis 是一门C语音开发的&#xff0c;redis开发者&#xff0c;一开始的本意是作用消息队列&#xff0c;后面随着IT圈的迅速发展&#xff0c;redis不满足诉求&#xff1b;最后开发成k/v形式的内存存储的工具 特性&#xff1a;速度快、单进程单线程、支持集群、持…

32.Netty源码之服务端如何处理客户端新建连接

highlight: arduino-light 服务端如何处理客户端新建连接 Netty 服务端完全启动后&#xff0c;就可以对外工作了。接下来 Netty 服务端是如何处理客户端新建连接的呢&#xff1f; 主要分为四步&#xff1a; md Boss NioEventLoop 线程轮询客户端新连接 OP_ACCEPT 事件&#xff…