仿 ElementPlus 组件库(十)—— Select 组件实现

在仿 ElementPlus 组件库的开发过程中,我们已经成功打造了多个实用组件,如 Input 组件、Switch 组件等,接下来,我们将深入探索 Select 组件的实现细节。

一、什么是 Select 组件

Select 组件,也被称为下拉选择框组件,是用户界面中广泛使用的交互元素之一。它的核心功能是为用户提供一个选项列表,允许用户从多个预设的选项中选择一个,从而实现数据的输入或功能的设置。

从基本构成来看,Select 组件通常由一个可点击的触发区域(一般显示当前选中的选项)和一个下拉菜单组成。下拉菜单中包含了所有可供选择的选项,当用户点击触发区域时,下拉菜单会展开显示,用户可以通过点击菜单项来选择相应的选项。

Select 组件的工作原理是基于事件监听和状态管理,当用户进行选择操作时,组件会捕获该事件,并更新内部的选中状态,同时可以将选中的值传递给其他相关组件或进行相应的业务逻辑处理。

二、实现 Select 组件

(一)组件目录

目录 复制代码
components
├── Select
    ├── Select.vue
    ├── types.ts
    ├── style.css   

(二)实现 Select 组件基本功能

  • SelectOption 接口 :定义了选择器选项的结构,包含 label(显示文本)、value(选项值)和可选的 disabled(是否禁用)属性。
  • SelectProps 接口 :定义了组件的属性,包括 modelValue(用于 v-model 双向绑定)、options(选项数组)、placeholder(占位文本)和 disabled(是否禁用组件)。
  • SelectEmits 接口 :定义了组件触发的事件,包括 change(选项改变时触发)、update:modelValue(用于 v-model 更新)和 visible-change(下拉菜单显示状态改变时触发)。
  • types.ts
typescript 复制代码
export interface SelectOption {
  label: string;
  value: string;
  disabled?: boolean;
}

export interface SelectProps {
  // v-model
  modelValue: string;
  // 选项
  options: SelectOption[];
  // 基本表单属性
  placeholder: string;
  disabled: boolean;
}

export interface SelectEmits {
  (e:'change', value: string) : void;
  (e:'update:modelValue', value: string) : void;
  (e: 'visible-change', value:boolean): void;

}
  • 模板部分

    • 创建了一个带有 yl-select 类名的 div 容器,根据 disabled 属性添加 is-disabled 类。
    • 使用 Tooltip 组件包裹 Input 组件,实现下拉菜单的显示。
    • Input 组件通过 v-model 绑定 innerValue,并根据 disabledplaceholder 属性进行配置。
    • Tooltipcontent 插槽中,使用 ulli 元素渲染选项列表,每个选项根据 disabled 属性添加 is-disabled 类。
  • 脚本部分

    • 引入所需的 Vue 功能和组件,定义组件名称为 YlSelect
    • 使用 definePropsdefineEmits 定义组件的属性和事件。
    • 使用 ref 定义响应式变量,包括 tooltipRefinnerValueisDropdownShow
    • 实现 controlDropdown 函数,用于控制下拉菜单的显示和隐藏,并触发 visible-change 事件。
    • 实现 toggleDropdown 函数,用于切换下拉菜单的显示状态,当组件禁用时不执行操作。
  • Select.vue

typescript 复制代码
<template>
  <div class="yl-select" :class="{ 'is-disabled': disabled }" @click="toggleDropdown">
    <Tooltip placement="bottom-start" ref="tooltipRef" manual>
      <Input v-model="innerValue" :disabled="disabled" :placeholder="placeholder" />

      <template #content>
        <ul class="yl-select__menu">
          <template v-for="(item, index) in options" :key="index">
            <li
              class="yl-select__menu-item"
              :class="{
                'is-disabled': item.disabled,
              }"
              :id="`select-item-${item.value}`"
            >
              {{ item.label }}
            </li>
          </template>
        </ul>
      </template>
    </Tooltip>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import type { Ref } from 'vue'
import type { SelectProps, SelectEmits } from './types'
import Tooltip from '../Tooltip/Tooltip.vue'
import type { TooltipInstance } from '../Tooltip/types'
import Input from '../Input/Input.vue'
defineOptions({
  name: 'YlSelect',
})
const props = defineProps<SelectProps>()
const emits = defineEmits<SelectEmits>()
const tooltipRef = ref() as Ref<TooltipInstance>
const innerValue = ref('')
const isDropdownShow = ref(false)
const controlDropdown = (show: boolean) => {
  if (show) {
    tooltipRef.value.show()
  } else {
    tooltipRef.value.hide()
  }
  isDropdownShow.value = show
  emits('visible-change', show)
}
const toggleDropdown = () => {
  if (props.disabled) return
  if (isDropdownShow.value) {
    controlDropdown(false)
  } else {
    controlDropdown(true)
  }
}
</script>

(三)实现 Select 组件选择功能

新增 SelectStates 接口,用于定义组件内部状态的结构,包含 inputValue(输入框显示的值)和 selectedOption(当前选中的选项,可为 null)。

  • types.ts
typescript 复制代码
export interface SelectStates {
  inputValue: string;
  selectedOption: null | SelectOption;
}
  • 模板部分

    • li 元素的 class 绑定中,新增了 is-selected 类,用于根据 states.selectedOption 的值来判断当前选项是否被选中。当 states.selectedOptionvalue 与当前选项的 value 相等时,应用该类。
    • li 元素添加了 @click.stop 事件监听器,绑定到 itemSelect 方法,当用户点击选项时会触发该方法,同时阻止事件冒泡。
  • 脚本部分

    • 引入了 reactive 函数,并从 types.ts 中导入新定义的 SelectStates 接口。
    • 新增 findOption 函数,该函数接收一个 value 作为参数,在 props.options 数组中查找 value 匹配的选项。如果找到则返回该选项,否则返回 null
    • 根据 props.modelValue 的值,使用 findOption 函数查找初始选中的选项,并将其赋值给 initialOption
    • 使用 reactive 创建响应式的 states 对象,根据 initialOption 初始化 inputValueselectedOption。如果 initialOption 存在,则 inputValue 为其 labelselectedOption 为该选项;否则 inputValue 为空字符串,selectedOptionnull
    • 实现 itemSelect 方法,该方法接收一个 SelectOption 类型的参数 e。当点击的选项未被禁用时,更新 states 对象的 inputValueselectedOption,并触发 changeupdate:modelValue 事件,将选中选项的 value 作为参数传递。最后调用 controlDropdown 方法关闭下拉菜单。
  • Select.vue

typescript 复制代码
<li  class="yl-select__menu-item"
//...              
              :class="{
                'is-disabled': item.disabled,
                'is-selected': states.selectedOption?.value === item.value,
              }"
              @click.stop="itemSelect(item)"
            >
            
import { ref, reactive } from 'vue'
import type { SelectProps, SelectEmits, SelectOption, SelectStates} from './types'

const findOption = (value: string) => {
  const option = props.options.find((option) => option.value === value)
  return option ? option : null
}

const initialOption = findOption(props.modelValue)
const states = reactive<SelectStates>({
  inputValue: initialOption ? initialOption.label : '',
  selectedOption: initialOption,
})

const itemSelect = (e: SelectOption) => {
  if (e.disabled) return
  states.inputValue = e.label
  states.selectedOption = e
  emits('change', e.value)
  emits('update:modelValue', e.value)
  controlDropdown(false)
}

(四)为 Select 组件添加部分样式和focus

  • Tooltip/types.ts :新增 TooltipEmits 接口,定义了 visible-changeclick-outside 两个事件,用于处理下拉框显示状态变化和点击外部区域的情况。

  • Tooltip/types.ts

typescript 复制代码
export interface TooltipEmits {
  (e: 'visible-change', value: boolean): void
  (e: 'click-outside', value: boolean): void
}
  • Tooltip/Tooltip.vue :使用 useClickOutside 函数监听点击外部区域事件。当触发方式为 click、下拉框处于打开状态且非手动控制时,调用 closeFinal 方法关闭下拉框。同时,若下拉框处于打开状态,触发 click-outside 事件。

  • Tooltip/Tooltip.vue

typescript 复制代码
useClickOutside(popperContainerNode, () => {
  if (props.trigger === 'click' && isOpen.value && !props.manual) {
    closeFinal()
  }
  if (isOpen.value) {
    emits('click-outside', true)
  }
})
  • Input/types.ts :新增 InputInstance 接口,定义了 ref 属性,其类型为 HTMLInputElementHTMLTextAreaElement,用于获取输入框的 DOM 引用。

  • Input/types.ts

typescript 复制代码
export interface InputInstance {
  ref: HTMLInputElement | HTMLTextAreaElement
}
  • 模板部分

    • Tooltip 组件上添加 :popperOptions 绑定 popperOptions 对象,用于配置弹出框的样式和位置。
    • 监听 Tooltipclick-outside 事件,调用 controlDropdown(false) 方法关闭下拉框。
    • Input 组件上添加 ref 绑定 inputRef,并设置 readonly 属性,使输入框只读。
    • Inputsuffix 插槽中添加 Icon 组件,根据 isDropdownShow 状态添加 is-active 类,实现下拉箭头的旋转效果。
  • 脚本部分

    • 引入 Icon 组件和 InputInstance 类型。
    • 定义 inputRef 引用,类型为 InputInstance
    • 定义 popperOptions 对象,配置 offsetsameWidth 修饰符,使弹出框与输入框宽度相同且有一定偏移量。
    • itemSelect 方法中,选中选项后调用 inputRef.value.ref.focus() 方法,将焦点聚焦到输入框上。
  • Select.vue

typescript 复制代码
       <Tooltip
      placement="bottom-start"
      ref="tooltipRef"
      :popperOptions="popperOptions"
      @click-outside="controlDropdown(false)"
      manual
    >
      <Input
        v-model="states.inputValue"
        :disabled="disabled"
        :placeholder="placeholder"
        ref="inputRef"
        readonly
      >
        <template #suffix>
          <Icon
            icon="angle-down"
            class="header-angle"
            :class="{ 'is-active': isDropdownShow }"
          ></Icon>
        </template>
      </Input>
//...
    </Tooltip>

import Icon from '../Icon/Icon.vue'
import type { InputInstance } from '../Input/types'

const inputRef = ref() as Ref<InputInstance>
const popperOptions: any = {
  modifiers: [
    {
      name: 'offset',
      options: {
        offset: [0, 9],
      },
    },
    {
      name: 'sameWidth',
      enabled: true,
      fn: ({ state }: { state: any }) => {
        state.styles.popper.width = `${state.rects.reference.width}px`
      },
      phase: 'beforeWrite',
      requires: ['computeStyles'],
    },
  ],
}

const itemSelect = (e: SelectOption) => {
//...
  inputRef.value.ref.focus()
}
  • 定义了 yl-select 组件的一系列 CSS 变量,用于统一管理组件的颜色、字体大小等样式。

  • yl-select 及其子元素的样式进行详细设置,包括输入框聚焦效果、下拉箭头旋转动画、选项列表样式、选项悬停和选中效果、禁用选项样式等。

  • style.css

css 复制代码
.yl-select {
  --yl-select-item-hover-bg-color: var(--yl-fill-color-light);
  --yl-select-item-font-size: var(--yl-font-size-base);
  --yl-select-item-font-color: var(--yl-text-color-regular);
  --yl-select-item-selected-font-color: var(--yl-color-primary);
  --yl-select-item-disabled-font-color: var(--yl-text-color-placeholder);
  --yl-select-input-focus-border-color: var(--yl-color-primary);
}

.yl-select {
  display: inline-block;
  vertical-align: middle;
  line-height: 32px;
  .yl-tooltip .yl-tooltip__popper {
    padding: 0;
  }
  .yl-input.is-focus .yl-input__wrapper {
    box-shadow: 0 0 0 1px var(--yl-select-input-focus-border-color) inset !important;
  }
  .yl-input {
    .header-angle {
      transition: transform var(--yl-transition-duration);
      &.is-active {
        transform: rotate(180deg);
      }
    }
  }
  .yl-input__inner {
    cursor: pointer;
  }
  .yl-select__menu {
    list-style: none;
    margin: 6px 0;
    padding: 0;
    box-sizing: border-box;
  }
  .yl-select__menu-item {
    margin: 0;
    font-size: var(--yl-select-item-font-size);
    padding: 0 32px 0 20px;
    position: relative;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    color: var(--yl-select-item-font-color);
    height: 34px;
    line-height: 34px;
    box-sizing: border-box;
    cursor: pointer;
    &:hover {
      background-color: var(--yl-select-item-hover-bg-color);
    }
    &.is-selected {
      color: var(--yl-select-item-selected-font-color);
      font-weight: 700;
    }
    &.is-disabled {
      color: var(--yl-select-item-disabled-font-color);
      cursor: not-allowed;
      &:hover {
        background-color: transparent;
      }
    }
  }
}
  • styles/index.css
typescript 复制代码
@import '../components/Select/style.css';

(五)为 Select 组件添加可清空选项功能

  • SelectProps 接口 :新增可选属性 clearable,用于控制 Select 组件是否可清空选项。
  • SelectStates 接口 :新增 mouseHover 属性,用于记录鼠标是否悬停在组件上。
  • SelectEmits 接口 :新增 clear 事件,用于在清空选项时触发。
  • types.ts
typescript 复制代码
export interface SelectProps {
  clearable?: boolean
}

export interface SelectStates {
  mouseHover: boolean
}

export interface SelectEmits {
  (e: 'clear'): void
}
  • 模板部分

    • div 元素上添加 @mouseenter@mouseleave 事件监听器,分别在鼠标进入和离开组件时更新 states.mouseHover 的值。
    • Input 组件的 suffix 插槽中,使用 v-if 指令根据 showClearIcon 的值来决定显示清空图标(circle-mark)还是下拉箭头图标(angle-down)。
    • 为清空图标添加 @mousedown.prevent 事件监听器,调用 NOOP 函数阻止默认的鼠标按下行为,同时添加 @click.stop 事件监听器,调用 onClear 函数处理清空操作。
  • 脚本部分

    • 引入 computed 函数,用于创建计算属性。
    • 使用 reactive 创建响应式的 states 对象,初始化 mouseHoverfalse
    • 创建计算属性 showClearIcon,根据 props.clearablestates.mouseHoverstates.selectedOptionstates.inputValue 的值来判断是否显示清空图标。
    • 实现 onClear 函数,当点击清空图标时,将 states.selectedOption 置为 nullstates.inputValue 置为空字符串,同时触发 clearchangeupdate:modelValue 事件,将空字符串作为参数传递。
    • 定义 NOOP 空函数,用于阻止默认的鼠标按下行为。
  • Select.vue

typescript 复制代码
<template>
  <div
    class="yl-select"
//...
    @mouseenter="states.mouseHover = true"
    @mouseleave="states.mouseHover = false"
  >
        <template #suffix>
          <Icon
            icon="circle-mark"
            v-if="showClearIcon"
            class="yl-input__clear"
            @mousedown.prevent="NOOP"
            @click.stop="onClear"
          ></Icon>
          <Icon
            v-else
            icon="angle-down"
            class="header-angle"
            :class="{ 'is-active': isDropdownShow }"
          ></Icon>
        </template>
//...  
</template>

import { ref, reactive, computed } from 'vue'

const states = reactive<SelectStates>({
  mouseHover: false,
})

const showClearIcon = computed(() => {
  // hover
  // props.clearable 为 true
  // 必须要选择过选项
  // input值不为空
  return (
    props.clearable && states.mouseHover && states.selectedOption && states.inputValue.trim() != ''
  )
})
const onClear = () => {
  states.selectedOption = null
  states.inputValue = ''
  emits('clear')
  emits('change', '')
  emits('update:modelValue', '')
}
const NOOP = () => {}

(六)为 Select 组件添加自定义模板功能

  • types.ts
typescript 复制代码
import type { VNode } from "vue"

export interface SelectProps {
//...
  renderLabel?: RenderLabelFunc
}

export type RenderLabelFunc = (option: SelectOption) => VNode
  • Select.vue
typescript 复制代码
      <template #content>
            <li
              class="yl-select__menu-item"
//...
            >
              <RenderVnode :vNode="renderLabel ? renderLabel(item) : item.label"> </RenderVnode>
//...
      </template>

import RenderVnode from '../Common/RenderVnode'

(七)为 Select 组件添加下拉菜单筛选功能

  • SelectProps 接口 :新增了可选属性 renderLabel,其类型为 RenderLabelFunc。这个属性允许使用者传入一个自定义的渲染函数,用于定制选项标签的渲染方式。
  • RenderLabelFunc 类型 :定义为一个函数类型,接收一个 SelectOption 类型的参数 option,并返回一个 Vue 的虚拟节点 VNode。该函数用于将 SelectOption 渲染为特定的虚拟节点。
  • types.ts
typescript 复制代码
export interface SelectProps {
//...
  filterable?: boolean
  filterMethod: CustomerFilterFunc

  
}

export type CustomerFilterFunc = (value: string) => SelectOption[]
  • 模板部分

    • li 元素内部,使用了 RenderVnode 组件来渲染选项内容。
    • 通过三元表达式判断 renderLabel 是否存在,如果存在则调用 renderLabel 函数并传入当前选项 item 得到虚拟节点,将其作为 RenderVnode 组件的 vNode 属性;如果不存在,则直接使用 item.label 作为渲染内容。
  • 脚本部分 :引入了 RenderVnode 组件,该组件用于将传入的虚拟节点进行渲染。

  • Select.vue

typescript 复制代码
      <Input
//...
        :placeholder="filteredPlaceholder"
        :readonly="!filterable || isDropdownShow"
        @input="onFilter"
      >
<ul class="yl-select__menu">
          <template v-for="(item, index) in filteredOptions" :key="index">
//...
        </ul>
        
import { ref, reactive, computed, watch } from 'vue'
import { isFunction } from 'lodash-es'

const filteredOptions = ref(props.options)
watch(
  () => props.options,
  (newOptions) => {
    filteredOptions.value = newOptions
  },
)
const generateFilterOptions = (searchValue: string) => {
  if (!props.filterable) return
  if (props.filterMethod && isFunction(props.filterMethod)) {
    filteredOptions.value = props.filterMethod(searchValue)
  } else {
    filteredOptions.value = props.options.filter((option) => option.label.includes(searchValue))
  }
}
const onFilter = () => {
  generateFilterOptions(states.inputValue)
}
const filteredPlaceholder = computed(() => {
  return props.filterable && states.selectedOption && isDropdownShow.value
    ? states.selectedOption.label
    : props.placeholder
})
  if (show) {
    // filter 模式
    // 之前选择过对应的值
    if (props.filterable && states.selectedOption) {
      states.inputValue = ''
    }
    // 进行一次默认选项的生成
    if (props.filterable) {
      generateFilterOptions(states.inputValue)
    }
    tooltipRef.value.show()
  } else {
    tooltipRef.value.hide()
    // blur 的时候 将之前的值回灌到 input 中
    if (props.filterable) {
      states.inputValue = states.selectedOption ? states.selectedOption.label : ''
    }
  }

(八)为 Select 组件添加远程搜索功能

  • types.ts
typescript 复制代码
export interface SelectProps {
//...
  options?: SelectOption[]
  remote?: boolean
  remoteMethod: CustomerFilterRemoteFunc

}

export interface SelectStates {
//...
  loading: boolean
}

export type CustomerFilterRemoteFunc = (value: string) => Promise<SelectOption[]>
  • Select.vue
typescript 复制代码
      <Input
//...
        @input="debounceOnFilter"
      >
      <template #content>
        <div class="yl-select__loading" v-if="states.loading">
          <Icon icon="spinner" spin></Icon>
        </div>
        <div class="yl-select__nodata" v-else-if="filterable && filteredOptions.length === 0">
          no matching data
        </div>        
        <ul class="yl-select__menu" v-else>
//...
        </ul>
       
import { isFunction, debounce } from 'lodash-es'       
const props = withDefaults(defineProps<SelectProps>(), {
  options: () => [],
})
const timeout = computed(() => (props.remote ? 300 : 0))
const states = reactive<SelectStates>({
//...
  loading: false,
})
const debounceOnFilter = debounce(() => {
  onFilter()
}, timeout.value)

const generateFilterOptions = async (searchValue: string) => {
  if (!props.filterable) return
  if (props.filterMethod && isFunction(props.filterMethod)) {
    filteredOptions.value = props.filterMethod(searchValue)
  } else if (props.remote && props.remoteMethod && isFunction(props.remoteMethod)) {
    states.loading = true
    try {
      filteredOptions.value = await props.remoteMethod(searchValue)
    } catch (e) {
      console.error(e)
      filteredOptions.value = []
    } finally {
      states.loading = false
    }
  } else {
    filteredOptions.value = props.options.filter((option) => option.label.includes(searchValue))
  }
}
  • style.css
css 复制代码
  .yl-select__nodata, .yl-select__loading {
    padding: 10px 0;
    margin: 0;
    text-align: center;
    color: var(--yl-text-color-secondary);
    font-size: var(--yl-select-font-size);
  }

(九)为 Select 组件添加键盘控制功能

  • SelectProps 接口

    • options 属性变为可选,因为远程搜索时选项可能动态获取。
    • 新增 remote 属性,类型为布尔值,用于标识是否开启远程搜索功能。
    • 新增 remoteMethod 属性,类型为 CustomerFilterRemoteFunc,这是一个函数,接收搜索值作为参数,返回一个 Promise,该 Promise 解析为 SelectOption 数组,用于从远程获取匹配的选项。
  • SelectStates 接口 :新增 loading 属性,类型为布尔值,用于表示远程搜索时的加载状态。

  • CustomerFilterRemoteFunc 类型 :定义了远程搜索方法的函数签名,接收搜索值并返回一个解析为 SelectOption 数组的 Promise

  • types.ts

typescript 复制代码
export interface SelectStates {
//...
  highlightIndex: number
}
  • 模板部分

    • Input 组件上添加 @input 事件监听器,绑定到 debounceOnFilter 方法,实现输入时触发搜索。
    • template #content 部分添加了加载提示和无匹配数据提示:
      • states.loadingtrue 时,显示加载提示,包含一个旋转的图标。
      • filterabletruefilteredOptions 为空时,显示无匹配数据提示。
  • 脚本部分

    • 使用 withDefaultsprops 中的 options 提供默认值为空数组。
    • 创建计算属性 timeout,根据 props.remote 的值决定防抖时间,若开启远程搜索则为 300 毫秒,否则为 0。
    • states 对象中新增 loading 属性并初始化为 false
    • 使用 lodash-esdebounce 函数创建 debounceOnFilter 方法,对 onFilter 方法进行防抖处理,防抖时间由 timeout 决定。
    • 实现 generateFilterOptions 异步方法,根据不同情况生成过滤后的选项:
      • props.filterablefalse,不进行过滤。
      • props.filterMethod 存在且为函数,使用该方法进行过滤。
      • 若开启远程搜索(props.remotetrue)且 props.remoteMethod 存在且为函数,设置 states.loadingtrue,调用 props.remoteMethod 进行远程搜索,搜索完成后更新 filteredOptions,并在 finally 块中将 states.loading 设为 false
      • 若以上条件都不满足,使用本地选项过滤,过滤包含搜索值的选项。
  • Select.vue

typescript 复制代码
      <Input
//...
        @keydown="handleKeyDown"
      >
            <li
              class="yl-select__menu-item"
              :class="{
                'is-disabled': item.disabled,
                'is-selected': states.selectedOption?.value === item.value,
                'is-highlighted': states.highlightIndex === index,
              }"
//...
            </li>      
const states = reactive<SelectStates>({
//...
  highlightIndex: -1,
}) 

const generateFilterOptions = async (searchValue: string) => {
  //...
  states.highlightIndex = -1
}

const controlDropdown = (show: boolean) => {
//...
    if (props.filterable) {
      states.inputValue = states.selectedOption ? states.selectedOption.label : ''
    }
    states.highlightIndex = -1
  }
//...
}

const handleKeyDown = (e: KeyboardEvent) => {
  switch (e.key) {
    case 'Enter':
      toggleDropdown()
      break
    case 'Escape':
      if (isDropdownShow.value) {
        controlDropdown(false)
      }
      break
    case 'ArrowUp':
      e.preventDefault()
      //states.highlightIndex = -1
      if (filteredOptions.value.length > 0) {
        if (states.highlightIndex === -1 || states.highlightIndex === 0) {
          states.highlightIndex = filteredOptions.value.length - 1
        } else {
          // move up
          states.highlightIndex--
        }
      }
      break
    case 'ArrowDown':
      e.preventDefault()
      //states.highlightIndex = -1
      if (filteredOptions.value.length > 0) {
        if (
          states.highlightIndex === -1 ||
          states.highlightIndex === filteredOptions.value.length - 1
        ) {
          states.highlightIndex = 0
          // move up
          states.highlightIndex++
        }
      }
      break

    default:
      break
  }
}
  • style.css
typescript 复制代码
&.is-highlighted {
      background-color: var(--yl-select-item-hover-bg-color);
    }
相关推荐
崔庆才丨静觅2 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60613 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了3 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅3 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅4 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊4 小时前
jwt介绍
前端
爱敲代码的小鱼4 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax