当前位置: 首页 > news >正文

日常学习开发记录-input组件

实现

  • 1.实现
  • 2.inline-table和table-cell实现
    • 2.1 表格布局的特性
    • 2.2 示例
  • 3.clear清除事件未生效
    • 3.1 原因
    • 3.2 解决
  • 4. 增加type为text和textarea
    • 4.1 rows,autosize的实现
  • 5.拓展-composition事件

1.实现

<template>
  <div 
    class="my-input"
    :class="{
      'is-disabled': disabled,
      'is-focus': focused,
      'my-input--suffix': showSuffix,
      'my-input--prefix': showPrefix,
      'my-input-group': $slots.prepend || $slots.append,
      'my-input-group--prepend': $slots.prepend,
      'my-input-group--append': $slots.append
    }"
  >
    <!-- 前置元素 -->
     <span class="my-input-group__prepend" v-if="$slots.prepend">
      <slot name="prepend"></slot>
     </span>
    <!-- 前缀图标-->
    <span class="my-input__prefix" v-if="showPrefix">
      <i :class="prefixIcon" v-if="prefixIcon"></i>
      <slot name="prefix"></slot>
    </span>
    
    <!-- 输入框 -->
    <input
      ref="input"
      class="my-input__inner"
      :type="type"
      :placeholder="placeholder"
      :disabled="disabled"
      :readonly="readonly"
      :value="value"
      @input="handleInput"
      @focus="handleFocus"
      @blur="handleBlur"
      @change="handleChange"
    >
    
    <!-- 后缀图标,包括清空按钮 -->
    <span class="my-input__suffix" v-if="showSuffix">
      <span class="my-input__suffix-inner">
        <i
          class="my-input__clear my-icon-circle-close"
          v-if="clearable && value && !disabled && !readonly"
          @click="clear"
        ></i>
        <i :class="suffixIcon" v-if="suffixIcon"></i>
        <slot name="suffix"></slot>
      </span>
    </span>
    <!-- 后置元素 -->
    <span class="my-input-group__append" v-if="$slots.append">
      <slot name="append"></slot>
    </span>
  </div>
</template>

<script>
export default {
  name: 'MyInput',
  props: {
    // v-model 绑定值
    value: {
      type: [String, Number],
      default: ''
    },
    // 输入框类型
    type: {
      type: String,
      default: 'text'
    },
    // 占位文本
    placeholder: String,
    // 是否禁用
    disabled: Boolean,
    // 是否只读
    readonly: Boolean,
    // 是否可清空
    clearable: Boolean,
    // 前缀图标
    prefixIcon: String,
    // 后缀图标
    suffixIcon: String
  },
  
  data() {
    return {
      focused: false,
      // 用于存储内部值,支持可控和非可控模式
      currentValue: this.value
    };
  },
  
  computed: {
    // 是否显示前缀
    showPrefix() {
      return this.prefixIcon || this.$slots.prefix;
    },
    
    // 是否显示后缀
    showSuffix() {
      return this.suffixIcon || this.clearable || this.$slots.suffix;
    }
  },
  
  watch: {
    value(val) {
      this.currentValue = val;
    }
  },
  
  methods: {
    /**
     * 处理输入事件
     */
    handleInput(event) {
      const value = event.target.value;
      this.$emit('input', value);
      this.currentValue = value;
    },
    
    /**
     * 处理聚焦事件
     */
    handleFocus(event) {
      this.focused = true;
      this.$emit('focus', event);
    },
    
    /**
     * 处理失焦事件
     */
    handleBlur(event) {
      this.focused = false;
      this.$emit('blur', event);
    },
    
    /**
     * 处理变更事件
     */
    handleChange(event) {
      this.$emit('change', event.target.value);
    },
    
    /**
     * 清空输入框
     */
    clear() {
      this.$emit('input', '');
      this.$emit('change', '');
      this.$emit('clear');
      this.currentValue = '';
    },
    
    /**
     * 聚焦输入框
     */
    focus() {
      this.$refs.input.focus();
    },
    
    /**
     * 失焦输入框
     */
    blur() {
      this.$refs.input.blur();
    }
  }
};
</script>



2.inline-table和table-cell实现

使用prefix和suffix插槽会有问题:
在这里插入图片描述
能看到在定宽的组件中重叠错位了。
Element UI的input组件中使用的display:inline-table和display:table-cell布局技术确实很巧妙。这种布局方式允许父元素有一个固定宽度,而子元素却可以根据内容自适应并且可以超出父元素的宽度限制。

2.1 表格布局的特性

CSS表格布局有一个独特的特性:表格单元格(table cells)会根据其内容自动调整大小,并且可以超出表格本身的设定宽度。这与普通的块级元素完全不同,块级元素默认会受到父元素宽度的限制。
display: inline-table:使元素像内联元素一样在行内显示,但内部使用表格布局规则
display: table-cell:使元素表现为表格单元格

为什么可以超出父元素宽度?
表格布局有一个特殊的宽度计算算法:
先考虑内容的实际宽度
然后根据可用空间和其他单元格调整。

2.2 示例

关键点是:表格单元格会优先满足内容需求,即使这意味着需要超出表格的指定宽度

<style>
.input-wrapper {
  /* 普通块级元素布局 */
  width: 300px;
  border: 1px solid #ccc;
}

.table-input-wrapper {
  /* 表格布局 */
  display: inline-table;
  width: 300px;
  border: 1px solid #ccc;
}

.addon {
  display: table-cell;
  background: #f5f7fa;
  padding: 0 10px;
  white-space: nowrap;
}

.input {
  display: table-cell;
  width: 100%;
  border: none;
  padding: 8px;
}
</style>

<!-- 普通布局 - 会被截断或换行 -->
<div class="input-wrapper">
  <span>这是一个非常长的前置文本内容可能会超出父容器</span>
  <input type="text" placeholder="输入内容">
</div>

<!-- 表格布局 - 会完整显示 -->
<div class="table-input-wrapper">
  <span class="addon">这是一个非常长的前置文本内容可能会超出父容器</span>
  <input class="input" type="text" placeholder="输入内容">
</div>

结果:
在这里插入图片描述

3.clear清除事件未生效

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

点击清除,clear没执行。

3.1 原因

点击清空按钮时,事件顺序如下:
mousedown 事件首先触发
由于清空按钮在输入框内部,输入框会失去焦点(blur)
输入框触发 blur 事件
如果值有变化,输入框还会触发 change 事件
然后才是清空按钮的 click 事件,此时才执行 clear 方法
问题在于:change事件在clear方法之前触发了,而且由于输入框已经失去焦点,您的清空操作可能不会生效。

3.2 解决

事件处加上 @mousedown.prevent,

1.阻止了mousedown的默认行为
2.阻止了输入框失去焦点
3.因此不会触发blur和change事件
4.让清空按钮的click事件和clear方法能够正常执行

4. 增加type为text和textarea

4.1 rows,autosize的实现

直接将input标签换成一个textarea标签是可以进行双向绑定的。但是还是差了点味。
思路(学习借鉴的elementui源码):创建一个隐藏的 textarea,放到 body 标签下,将 Textarea 组件的 value 值赋值给隐藏的 textarea,通过获取这个隐藏的 textarea 的 scrollHeight(scrollHeight 会返回该元素在不使用滚动条时的高度),来设置组件上面的 textarea 的高度。主要通过calcTextareaHeight 方法用于计算 textarea 的动态高度。

/**
 * 用于动态计算文本域(textarea)高度的模块
 * 通过创建隐藏的textarea元素复制样式和内容,计算理想高度
 */

// 全局隐藏textarea引用,避免重复创建
let hiddenTextarea

/**
 * 应用于隐藏textarea的CSS样式
 * 确保元素完全不可见且不影响页面布局
 */
const HIDDEN_STYLE = `
  height:0 !important;
  visibility:hidden !important;
  overflow:hidden !important;
  position:absolute !important;
  z-index:-1000 !important;
  top:0 !important;
  right:0 !important
`

/**
 * 需要从目标textarea复制的CSS属性列表
 * 这些属性会影响文本渲染和尺寸计算
 */
const CONTEXT_STYLE = [
  'letter-spacing', // 字符间距
  'line-height', // 行高
  'padding-top', // 上内边距
  'padding-bottom', // 下内边距
  'font-family', // 字体族
  'font-weight', // 字体粗细
  'font-size', // 字体大小
  'text-rendering', // 文本渲染方式
  'text-transform', // 文本转换
  'width', // 宽度
  'text-indent', // 文本缩进
  'padding-left', // 左内边距
  'padding-right', // 右内边距
  'border-width', // 边框宽度
  'box-sizing', // 盒模型类型
]

/**
 * 计算目标元素的样式参数
 * @param {HTMLElement} targetElement - 目标textarea元素
 * @returns {Object} 包含上下文样式和关键尺寸参数的对象
 */
function calculateNodeStyling(targetElement) {
  // 获取目标元素的计算样式
  const style = window.getComputedStyle(targetElement)

  // 获取盒模型类型(border-box或content-box)
  const boxSizing = style.getPropertyValue('box-sizing')

  // 计算上下padding总和
  const paddingSize =
    parseFloat(style.getPropertyValue('padding-bottom')) +
    parseFloat(style.getPropertyValue('padding-top'))

  // 计算上下border总和
  const borderSize =
    parseFloat(style.getPropertyValue('border-bottom-width')) +
    parseFloat(style.getPropertyValue('border-top-width'))

  // 构建CSS样式字符串,包含所有CONTEXT_STYLE中定义的属性
  const contextStyle = CONTEXT_STYLE.map(name => `${name}:${style.getPropertyValue(name)}`).join(
    ';'
  )
  console.log({ contextStyle, paddingSize, borderSize, boxSizing })
  return { contextStyle, paddingSize, borderSize, boxSizing }
}

/**
 * 计算textarea的理想高度
 * @param {HTMLElement} targetElement - 目标textarea元素
 * @param {number} minRows - 最小行数,默认为1
 * @param {number|null} maxRows - 最大行数,默认为null(无限制)
 * @returns {Object} 包含计算得到的高度信息,格式为{height: 'XXpx', minHeight: 'XXpx'}
 */
export default function calcTextareaHeight(targetElement, minRows = 1, maxRows = null) {
  // 创建隐藏文本域(如果尚未创建)
  if (!hiddenTextarea) {
    hiddenTextarea = document.createElement('textarea')
    document.body.appendChild(hiddenTextarea)
  }

  // 从目标元素获取样式参数
  let { paddingSize, borderSize, boxSizing, contextStyle } = calculateNodeStyling(targetElement)

  // 设置隐藏文本域的样式,确保渲染特性与目标元素一致
  hiddenTextarea.setAttribute('style', `${contextStyle};${HIDDEN_STYLE}`)

  // 复制目标元素的内容或占位符到隐藏文本域
  hiddenTextarea.value = targetElement.value || targetElement.placeholder || ''

  // 获取隐藏文本域的内容滚动高度
  let height = hiddenTextarea.scrollHeight
  const result = {}

  // 根据盒模型调整高度计算
  if (boxSizing === 'border-box') {
    // border-box模型:滚动高度不包含border,需要加上
    height = height + borderSize
  } else if (boxSizing === 'content-box') {
    // content-box模型:滚动高度包含padding,需要减去
    height = height - paddingSize
  }

  // 计算单行高度(用于行数限制计算)
  hiddenTextarea.value = '' // 清空内容
  let singleRowHeight = hiddenTextarea.scrollHeight - paddingSize // 空文本域的高度减去padding
  // 应用最小行数限制
  if (minRows !== null) {
    let minHeight = singleRowHeight * minRows // 计算最小高度
    if (boxSizing === 'border-box') {
      // border-box模型下需加上padding和border
      minHeight = minHeight + paddingSize + borderSize
    }
    // 取计算高度和最小高度的较大值
    height = Math.max(minHeight, height)
    result.minHeight = `${minHeight}px`
  }

  // 应用最大行数限制
  if (maxRows !== null) {
    let maxHeight = singleRowHeight * maxRows // 计算最大高度
    if (boxSizing === 'border-box') {
      // border-box模型下需加上padding和border
      maxHeight = maxHeight + paddingSize + borderSize
    }
    // 取计算高度和最大高度的较小值
    height = Math.min(maxHeight, height)
  }

  // 设置最终计算结果
  result.height = `${height}px`

  // 清理:移除隐藏文本域以释放资源
  hiddenTextarea.parentNode && hiddenTextarea.parentNode.removeChild(hiddenTextarea)
  hiddenTextarea = null

  return result
}

实现效果:
在这里插入图片描述

5.拓展-composition事件

现在的input搭配输入法有个问题还
在这里插入图片描述

我输入一个“啊”字,事件分发出去了两次。

composition相关事件是HTML
DOM的标准事件,专门用于处理输入法编辑器(IME)输入过程。在使用中文、日文、韩文等需要组合多个按键输入的语言时特别重要。
这三个事件的作用是: compositionstart: 当用户开始使用输入法输入时触发 代码中将isComposing标记为true
防止在组合输入过程中触发正常的input事件处理 compositionupdate: 当输入法正在组合字符时触发
代码中检查最后输入的字符是否是韩文(isKorean函数) 根据检测结果更新isComposing标志 compositionend:
当输入法完成组合输入,确认文字时触发 代码中重置isComposing为false 然后手动触发handleInput事件
这种设计解决了一个重要问题:避免在使用输入法时,未完成的组合文字过早触发input事件导致的问题。

完善后,结果:
在这里插入图片描述

相关文章:

  • 数据库设计实验(3)—— 分离与附加、还原与备份
  • 【Java面试场景题搜集总结】
  • Redis和MongoDB的区别
  • 【数学建模】熵权法
  • maven插件不能正确解析
  • 八、JavaScript函数
  • NAT技术-初级总结
  • MySQL :参数修改
  • springboot请求响应
  • 设计一个高性能的分布式限流系统
  • Redis的消息队列是怎么实现的
  • HarmonyOS开发,深拷贝、浅拷贝的封装和调用
  • Spring Boot 核心知识点:依赖注入 (Dependency Injection)
  • 智慧社区2.0
  • C# 中常见 JSON 处理库的优缺点对比
  • 【设计模式】3W 学习法深入剖析创建型模式:原理、实战与开源框架应用(含 Java 代码)
  • 条款43:学习处理模板化基类内的名称
  • 提示deepseek生成完整的json用于对接外部API
  • 【Film】MovieAgent:自动化电影生成通过多智能体CoT规划
  • Linux上的`i2c-tools`工具集的详细介绍;并利用它操作IMX6ULL的I2C控制器进而控制芯片AP3216C读取光照值和距离值
  • 霍步刚任辽宁沈阳市委书记
  • 我使馆就中国公民和企业遭不公正待遇向菲方持续提出严正交涉
  • 商务部召开全国离境退税工作推进会:提高退税商店覆盖面,扩大入境消费
  • 云南德宏州盈江县发生4.5级地震,震源深度10千米
  • “家国万里时光故事会” 举行,多家庭共话家风与家国情怀
  • 第一集|好饭不怕晚,折腰若如初见