Vue实现输入框悬浮放大

1,156 阅读4分钟

Vue实现输入框悬浮放大

普通.png

背景

最近在做一体机的混合开发H5,类似于pad端。项目需要用户手动输入的地方很多,在每次点击输入框获取焦点的时候都会被由下而上的输入法给挡住输入框,这样用户看不到输入内容用户体验非常不好,在网上找了一轮没有符合的轮子组件。随即有了优化用户体验的组件开发动作。

正文

需求

流程.png

技术选型

因为主体项目是用vue3 + vite 开发的,所以本次组件使用setup进行开发。

关键字:vue3 setupcomponentspromisedirective

实现逻辑

components

先写一个用于交互的功能组件,需要实现以下几点:

  1. 实现需求中的样式要求
  2. 回显原始输入框的内容
  3. 确认时把输入信息带出
  4. 对输入内容进行校验、裁剪
  5. 原始输入框是密码类型需要做兼容

srccomponents文件夹里新建组件

<!--
 * @Author: 蔡霸霸
 * @Date: 2022-06-17 16:23:15
 * @LastEditTime: 2022-06-29 17:00:46
 * @LastEditors: 蔡霸霸
 * @Description: 
 * @FilePath: \prod\src\components\BigInput\index.vue
-->
<script setup>
import { ref, computed, onMounted, watch, nextTick } from 'vue'const props = defineProps({
  submitCallback: {
    //确认回调
    type: Function,
    default: () => {},
  },
  val: {
    //原始数据 用于回显
    type: String,
  },
  maxlength: {
    //输入长度限制
    type: String || Number,
    default: 99,
  },
  filterInputData: {
    //输入过滤 以及是否为密码输入框
    type: Object,
    default: () => {
      return {
        isPassWord: false,
      }
    },
  },
})
​
const inputRef = ref(null) //组件输入框的实例
const inputVal = ref('') //组件输入框绑定的值
//监听回显数据变动 并赋值
watch(
  props.val,
  (newV) => {
    inputVal.value = props.val
  },
  {
    immediate: true,
  }
)
​
//密码输入框逻辑
const inputTypeVal = ref('text')
if (props.filterInputData.isPassWord) {
  inputTypeVal.value = 'password'
}
//密码输入框icon样式
const elIcon = computed(() => {
  return inputTypeVal.value === 'text' ? 'icon-yanjing-' : 'icon-biyanjing'
})
//点击密码输入框icon切换样式
const Iconclick = async () => {
  if (inputTypeVal.value === 'text') {
    inputTypeVal.value = 'password'
  } else {
    inputTypeVal.value = 'text'
  }
  await nextTick()
  //获取输入焦点
  inputRef.value.focus()
}
onMounted(() => {
  //组件加载完成后自动获取输入框焦点
  inputRef.value.focus()
})
​
//输入变动
const inputChange = () => {
  const {
    max = 999,
    isNum = false,
    isID = false,
    isToUpper = false,
    isSafe = false,
  } = props.filterInputData
  let elVal = inputVal.value
  let reg = '' //除去大部分特殊字符 只允许输入小部分特殊字符 全部英文 全部中文 全部数字
  if (isSafe) {
    reg = /[^-_.::#a-zA-Z0-9\u4e00-\u9fa5]/gi
  } else if (isNum) {
    //是否为纯数字
    reg = /[^0-9]/gi
  } else if (isID) {
    //只允许身份证号内允许的字段
    reg = /[^#0-9]/gi
  } else if (isToUpper) {
    //只允许输入英文和数字
    reg = /[^a-zA-Z0-9]/gi
  }
  if (reg) {
    elVal = elVal.replace(reg, '')
  }
  if (max) {
    //最大长度
    elVal = elVal.slice(0, max)
  }
  if (isID || isToUpper) {
    elVal = elVal.toUpperCase()
  }
  inputVal.value = elVal
}
</script><template>
  <div class="biginput">
    <!-- 输入模态框主体 -->
    <form action="javascript:return false;" class="biginput-form">
      <input
        ref="inputRef"
        v-model="inputVal"
        :type="inputTypeVal"
        autocomplete="off"
        :maxlength="maxlength"
        @keyup.enter="submitCallback(inputVal)"
        @input="inputChange"
      />
      <!-- 如果为密码输入类型则显示显隐icon -->
      <i
        v-if="props.filterInputData.isPassWord"
        :class="['iconfont ', elIcon]"
        @click="Iconclick()"
      >
      </i>
    </form>
    <!--浮层 -->
    <div class="biginput-mask" @click="submitCallback(inputVal)"></div>
  </div>
</template><style lang="less" scoped>
.biginput {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  z-index: 9999;
  &-form {
    width: 55%;
    height: 10%;
    position: absolute;
    top: 10%;
    left: 50%;
    transform: translateX(-50%);
    z-index: 2;
    input {
      width: 100%;
      height: 100%;
      border: none;
      border-radius: 8px;
      font-size: 36px;
      color: #333;
      letter-spacing: 2px;
      padding: 0 60px 0 32px;
      font-weight: 600;
      outline: none;
    }
    i {
      position: absolute;
      right: 8px;
      top: 26px;
      font-size: 50px;
      color: #000;
    }
  }
​
  &-mask {
    position: absolute;
    z-index: 1;
    width: 100vw;
    height: 100vh;
    background: rgba(0, 0, 0, 0.8);
  }
}
</style>
promise

输入完毕点击输入法回车或点击浮层后会调用传入组件的回调函数并把输入的值传递到回调函数内,这样我们就封装一层promise,把当前的普通组件封装成异步组件通过render方式渲染,这样就能在Js内直接传参调用。

在组件旁新建register.js文件

/*
 *Author: 蔡霸霸
 *Date: 2022-03-11 17:15:27
 *LastEditTime: 2022-06-29 15:50:07
 *LastEditors: 蔡霸霸
 *Description:
 *FilePath: \prod\src\components\BigInput\register.js
 */
import { createVNode, render } from 'vue'
import BigInput from './index.vue' //引入组件//初始化dom容器
const div = document.createElement('div')
div.setAttribute('class', 'my-BigInput_' + Date.now())
document.body.appendChild(div)
​
export default (props) => {
  //新建promise
  return new Promise((resolve) => {
    // 确认的回调
    const submitCallback = (val) => {
      render(null, div)
      //确认后返回传参
      resolve(val)
    }
    const vnode = createVNode(BigInput, {
      ...props,
      submitCallback,
    })
    //渲染
    render(vnode, div)
  })
}

这样,该组件就变成了promise组件,以异步方式调用,例如:

import BigInput from '../components/BigInput/register'//引入注册方法const doSomething = async()=>{
    try{
        //调用组件渲染
       const inputVal =   await BigInput({
          val: '阿巴阿巴',
          maxlength: 20,
        })
        console.log(inputVal)//返回输入的值
    }catch(error){
        console.log(error)
    }
}

到此为止已经可以成功调用了,但是我们的目标不限于此,为了方便复用,我们可以再次封装,以自定义指令directive形式绑定调用

directive

将上面封装的异步组件二次封装成通过自定义指令组件

/*
 *Author: 蔡霸霸
 *Date: 2022-06-22 16:21:33
 *LastEditTime: 2022-06-22 17:04:58
 *LastEditors: 蔡霸霸
 *Description:
 *FilePath: \prod\src\utils\directive.js
 */
import BigInput from '../components/BigInput/register'
import { nextTick } from 'vue'const sharpEnter = {
  flag: false, //用于二次调用focus处理防抖开关
  async mounted(el, { value }) {
    //等待实例dom加载完成
    await nextTick()
    //获取输入框dom
    const inputDom = ['INPUT', 'TEXTAREA'].includes(el.nodeName)
      ? el
      : el.querySelector('input') || el.querySelector('textarea')
    if (!inputDom) {
      throw new Error('放大输入框:当前绑定DOM中无input或textarea类型')
    }
    //输入等待开关
    let isInputZh = false
    const hackFunction = async () => {
      if (sharpEnter.flag) return false//如果刚结束的focus事件开关为打开则不继续下一步
      if (isInputZh) return false //如果输入框还在输入等待中则不进行过滤
      let res = ''
      try {
        //调用组件传值进去 拿值回来
        res = await BigInput({
          filterInputData: value,
          val: inputDom.value,
          maxlength: inputDom.getAttribute('maxlength'),//获取原始输入框的maxlength
        })
      } catch (error) {
        console.log(error)
      } finally {
        //打开开关 防止二次弹起
        sharpEnter.flag = true
        inputDom.value = res
        //因为是通过js直接改变input的值,没有触发vue双向绑定,所以需要手动触发input事件
        inputDom.dispatchEvent(new Event('input'))
        inputDom.dispatchEvent(new Event('change'))
        //在实际流程中发现单纯的input事件无法触发element的输入校验,还需要再次触发focus事件
        inputDom.dispatchEvent(new Event('focus'))
        inputDom.dispatchEvent(new Event('blur'))
        //防抖
        setTimeout(() => {
          sharpEnter.flag = false
        }, 200)
      }
    }
    inputDom.addEventListener(
      'compositionstart',
      (e) => {
        //中文输入等待start
        isInputZh = true
      },
      false
    )
    inputDom.addEventListener(
      'compositionend',
      (e) => {
        //中文输入等待end
        isInputZh = false
        /**
         * @hackFunction
         *  sometime oninput会在compositionend结束前调用 这样的oninput会直接被return
         */
         hackFunction()
      },
      false
    )
    //原始输入框被点击获取焦点后调用封装组件
    inputDom.onfocus = () => {
      hackFunction()
    }
  },
  beforeUnmount(el) {
    //实例销毁操作
    console.log('beforeUnmount')
    const inputDom = ['INPUT', 'TEXTAREA'].includes(el.nodeName)
      ? el
      : el.querySelector('input') || el.querySelector('textarea')
    //解绑
    inputDom.removeEventListener('compositionstart', null, false)
    inputDom.removeEventListener('compositionend', null, false)
    inputDom.onfocus = null
  },
}
export { sharpEnter }
​

自定义指令文件封装完成后引入到main.js中进行注册

/*
 *Author: 蔡霸霸
 *Date: 2022-06-22 16:04:59
 *LastEditTime: 2022-06-22 16:38:28
 *LastEditors: 蔡霸霸
 *Description:
 *FilePath: \prod\src\main.js
 */
import { createApp } from 'vue'
import App from './App.vue'import './assets/icons/font_wgbr11oez/iconfont.css'const app = createApp(App)
​
//引入注册自定义指令
import { sharpEnter } from '@/utils/directive'
app.directive('sharpEnter', sharpEnter)//v-sharpEnter
app.mount('#app')
​

到此为止,整个组件的封装就完成了,接下来只需要在页面中的input绑定上自定义指令v-sharpEnter即可

<template><!-- 默认输入框输入 -->
    <input type="text" v-model="defaultInput" v-sharpEnter /><!-- 地址输入框输入 -->
    <textarea
        v-model="textareaInput"
        v-sharpEnter
        cols="30"
        rows="10"
        maxlength="50"
      ></textarea>
    
    <!-- 密码输入框输入 -->
    <input
        type="password"
        v-model="pwInput"
        v-sharpEnter="{
          isPassWord: true,
        }"
      /></template>

效果展示

密码.png

普通.png

演示_密码.gif

演示_普通.gif

总结

流程看似复杂,但是在项目实现过程中环环相扣,最后实现的效果在后期开发中非常方便。

项目中的小组件,希望大家都多多踊跃分享下,让这个世界少一些重复造轮人。

最后附上项目仓库地址。

项目仓库地址

akon_3585/vue-model-input - 码云 - 开源中国 (gitee.com)