在Angular中实现一个级联效果的下拉框

实现一个具备级联效果的下拉搜索框,实现的结果以下图所示 html

咱们主要经过这个组件,来学习一些细微的逻辑,好比: 如何计算input框内文字的长度; 如何获取光标的位置;如何实现滚动条随着上下键盘的按动进行移动......

具体需求以下

  • 级联搜索最多不超过三级,以”.“做为级联搜索的链接符
  • 搜索框跟着文本框中的”.“进行向后移动,向右移动的最大距离不能超过文本框的宽度
  • 当用户修改以前的级联内容,则不进行搜索,并隐藏搜索框;若用户在以前输入的是”.“, 则将此”.“以后的内容所有删除并搜索当前的相关内容

接下来咱们根据需求,来写咱们的逻辑

首先咱们搭建html页面
<input
       #targetInput
        autocomplete="off"
        nz-input
        [(ngModel)]="searchValue"
        (keydown)="handlePress($event)"
        (input)="handleSearchList()"/>
      
      <div #searchList class="search-popup" [hidden]="!visible" (keyDown)="onKeydown($event)">
          <nz-spin [nzSpinning]="searchLoading" [class.spinning-height]="searchLoading">
            <div class="data-box"  *ngIf="searchData && searchData.length !== 0">
              <ul>
              // 这里在上篇文章中已经讲解过,如何实现让匹配的文字高亮显示~
                <li
                  id="item"
                  *ngFor="let item of searchData;let i = index;"
                  [class.item-selected]="curIndex === i"
                  (mouseover)='hoverDataItem(i)'
                  (click)="onSelectClick(item)">
                  <span [innerHTML]="item | highlightSearchResult:searchValue | safe: 'html'"></span>
                </li>
              </ul>
            </div>
          </nz-spin>
     </div>
复制代码
.search-popup {
  height: 376px;
  width: 246px;
  overflow-y: auto;
  box-shadow: 0 2px 8px rgba(0,0,0,.15);
  border-radius: 4px;
  position: absolute;
  background-color: #fff;
  z-index: 999;
  top: 92px;
  right: 61px;

  .data-box {
    margin: 0 10px;

    &:not(:last-child) {
      border-bottom: 1px solid #E4E5E7;
    }

    .no-search-data {
      display: inline-block;
      width: 100%;
      text-align: center;
      color: #C3C9D3;
      line-height: 40px;
    }
  }

  & ul {
    margin: 0 -10px;
    margin-bottom: 0;
    text-align: left;
  }

  & li {
    padding: 3px 10px;
    position: relative;
    list-style: none;
    height: 32px;
    line-height: 26px;
    &:hover {
      cursor: pointer;
      background-color: #e6f7ff;
    }
    &.item-selected {
      background-color: #E6F7FF;
    }
  }

  &.item-selected {
    background-color: #E6F7FF;

  }

.hidden-box {
  display: inline-block;
  border: 1px solid #ddd;
  visibility: hidden;
}
复制代码
实现相关的逻辑

根据前两个需求,咱们须要根据文本框中的”.“进行向后移动,向右移动的最大距离不能超过文本框的宽度。数组

思路: 咱们须要将文本框中的字符串根据”.“来转换成数组,而且要想办法获取文本框中文字的长度。
如何获取文本框中文字的长度呢?
咱们能够将文字的内容,从新放到一个display: inline-block的div容器中,而后获取容器的宽度,以下代码所示~bash

// html
  <!-- 用于测量input框的文字宽度 -->
  <div class="hidden-box" #firstLevel></div> // 以”.“转化的数组,下标为0的内容的宽度
  <div class="hidden-box" #secondLevel></div> // 以”.“转化的数组,下标为1的内容的宽度
  <div class="hidden-box" #allLevel></div> // 整个文本框的文字的宽度
 
 // ts
 import { ElementRef, Renderer2 } from '@angular/core';
 
 export class SearchListComponent {
    @ViewChild('searchList', { static: true }) public searchList: ElementRef;
    @ViewChild('firstLevel', { static: true }) public firstLevel: ElementRef;
    @ViewChild('secondLevel', { static: true }) public secondLevel: ElementRef;
    @ViewChild('allLevel', { static: true }) public allLevel: ElementRef;
    constructor(private _renderer: Renderer2) {}
         
    public setSearchPosition(rightValue: string): void {
        this._renderer.setStyle(
          this.searchList.nativeElement,
          'right',
          rightValue);
      } 
      
    public setSearchListPosition(targetValue: string): void {
    const inputWidth = 217;
    const defaultRightPosition = 60;
    const maxRightPosition = -148;
    const firstLevel = this.firstLevel.nativeElement;
    const secondLevel = this.secondLevel.nativeElement;
    const allLevel = this.allLevel.nativeElement;
    const targetValueArr = targetValue ? targetValue.split('.') : [];

    // 将input中的内容,根据”.“转换成数组以后,将相关的内容赋值到新的div容器中,为了便于获取文字的宽度
    allLevel.innerHTML = targetValue;
    firstLevel.innerHTML =  targetValueArr && targetValueArr[0];
    secondLevel.innerHTML = targetValueArr && targetValueArr.length > 1 ? targetValueArr[1] : '';

    // 获得相关宽度以后,实现下拉框移动的逻辑
    if (firstLevel.offsetWidth >= inputWidth
      ||  (firstLevel.offsetWidth + secondLevel.offsetWidth) >= inputWidth
      || allLevel.offsetWidth >= inputWidth) {
        this.setSearchPosition(this._renderer, this.searchList, `${maxRightPosition}px`);
     } else if (targetValueArr.length <= 1) {
      this.setSearchPosition(this.renderer, this.searchList, '61px');
    } else if (targetValueArr.length <= 2) {
      this.setSearchPosition(this.renderer, this.searchList, `${defaultRightPosition - firstLevel.offsetWidth}px`);
    } else if (targetValueArr.length <= 3) {
      this.setSearchPosition(renderer,
                              this.searchList,
                              `${defaultRightPosition - firstLevel.offsetWidth - secondLevel.offsetWidth}px`);
    }
  }
 }
复制代码

到这里,咱们能够完成第一和第二个需求,咱们再来看看第三个需求: 主要是根据用户输入的位置以及修改的内容,来决定是否显示搜索和显示下拉框,若是用户输入的不是”.“咱们则不显示,若是用户在以前的级联中输入”.“咱们就须要进行再次帮用户搜索结果。ide

思路: 要想完成需求三,咱们须要知道用户究竟是在哪里操做,即咱们要是能够知道光标的位置就更完美了~学习

// 获取光标的位置
  public getCursorPosition(element: HTMLInputElement): number {
    let cursorPosition = 0;
    if (element.selectionStart || element.selectionStart === 0) {
      cursorPosition = element.selectionStart;
    }
    return cursorPosition;
  }
  
  // 用来获取用户输入的内容是什么
  public handlePress(event: KeyboardEvent): void {
     this.curPressKey = event.key;
   }

  // 用户input的时候调用的核心方法
  public handleSearchList(value: string): void {
    this.curIndex = 0;
    const cursorPosition = this.getCursorPosition(this.targetInput.nativeElement); // 获取光标位置
    let targetValue = value;
    const targetValueArr = targetValue ? targetValue.split('.') : [];
    const valueArrLength = targetValueArr.length;
    this.setSearchListPosition(targetValue); // 调整位置
    // 判断那些状况下应该搜索并显示下拉框
    if (valueArrLength === 1
      || valueArrLength === 2 && cursorPosition >= targetValueArr[0].length + 1
      || valueArrLength === 3 && cursorPosition >= targetValueArr[0].length + targetValueArr[1].length + 2) {
        this.searchLoading = true;
        this.visible = true;
        ...获取下拉框中的数据
    } else {
      this.hidePopup();
    }
  }
复制代码
最后为了更好的提升用的体验,咱们还须要让下拉框支持键盘事件哦~方法也很简单,以下所示
public onKeydown(keyDownInfo: {index: number, code: number, e: KeyboardEvent}): void {
    const { code, e } = keyDownInfo;
    e.stopPropagation();
    if (code === 38) { // 键盘上
      e.preventDefault(); // 防止光标由最后边移动到前边,只是在开发中遇到的一点体验上小问题
      if (this.curIndex > 0) {
        this.curIndex--;
      }
    } else if (code === 40) { // 键盘下
      if (this.curIndex < this.searchData.length - 1) {
        this.curIndex++;
      }
    } else if (code === 13) {   // 回车,即至关于用户点击
      this.ruleModal.showModal();
      const curData = this.searchData[this.curIndex];
      if (curData) {
       this.onSelectClick(curData);
      }
    }
    // 实现下拉框的滚动条随着键盘的上下键按动时一块儿移动
    const lis = document.querySelectorAll('#item');
    const curLiEle = lis[this.curIndex] as HTMLElement;
    const searchList = this.searchList.nativeElement;
    const liEleHeight = 32;
    //(当前选中li标签的offsetTop + li标签自身的高度 - 下拉框的高度)
    searchList.scrollTop = curLiEle.offsetTop + liEleHeight -  searchList.clientHeight;
  }
复制代码

总结

其实这个级联搜索的组件,他的通用性可能并非很强,可是在实现的过程当中,一些细节逻辑的通用性仍是比较强的~ 但愿这些细节能够给同在开发中的你带来一些帮助~❤ui

相关文章
相关标签/搜索