实现一个具有级联效果的下拉搜索框,实现的结果如下图所示我们主要通过这个组件,来学习一些细微的逻辑,比如: 如何计算input框内文字的长度; 如何获取光标的位置;如何实现滚动条随着上下键盘的按动进行移动......
具体需求如下

级联搜索最多不超过三级,以”.“作为级联搜索的连接符

搜索框跟着文本框中的”.“进行向后移动,向右移动的最大距离不能超过文本框的宽度

当用户修改之前的级联内容,则不进行搜索,并隐藏搜索框;若用户在之前输入的是”.“, 则将此”.“之后的内容全部删除并搜索当前的相关内容
级联搜索最多不超过三级,以”.“作为级联搜索的连接符搜索框跟着文本框中的”.“进行向后移动,向右移动的最大距离不能超过文本框的宽度当用户修改之前的级联内容,则不进行搜索,并隐藏搜索框;若用户在之前输入的是”.“, 则将此”.“之后的内容全部删除并搜索当前的相关内容接下来我们根据需求,来写我们的逻辑
接下来我们根据需求,来写我们的逻辑
首先我们搭建html页面



#targetInput

autocomplete="off"

nz-input

[(ngModel)]="searchValue"

(keydown)="handlePress($event)"

(input)="handleSearchList()"/>











    // 这里在上篇文章中已经讲解过,如何实现让匹配的文字高亮显示~


    id="item"

    *ngFor="let item of searchData;let i = index;"

    [class.item-selected]="curIndex === i"

    (mouseover)='hoverDataItem(i)'

    (click)="onSelectClick(item)">














#targetInput

autocomplete="off"

nz-input

[(ngModel)]="searchValue"

(keydown)="handlePress($event)"

(input)="handleSearchList()"/>











    // 这里在上篇文章中已经讲解过,如何实现让匹配的文字高亮显示~


    id="item"

    *ngFor="let item of searchData;let i = index;"

    [class.item-selected]="curIndex === i"

    (mouseover)='hoverDataItem(i)'

    (click)="onSelectClick(item)">












.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;
}
.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容器中,然后获取容器的宽度,如下代码所示~
// html

// 以”.“转化的数组,下标为0的内容的宽度
// 以”.“转化的数组,下标为1的内容的宽度
// 整个文本框的文字的宽度

// 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`);

}
}
}
// html

// 以”.“转化的数组,下标为0的内容的宽度
// 以”.“转化的数组,下标为1的内容的宽度
// 整个文本框的文字的宽度

// 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`);

}
}
}到这里,我们可以完成第一和第二个需求,我们再来看看第三个需求: 主要是根据用户输入的位置以及修改的内容,来决定是否显示搜索和显示下拉框,如果用户输入的不是”.“我们则不显示,如果用户在之前的级联中输入”.“我们就需要进行再次帮用户搜索结果。思路: 要想完成需求三,我们需要知道用户到底是在哪里操作,即我们要是可以知道光标的位置就更完美了~
// 获取光标的位置
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 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;
}
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;
}总结
总结
总结
其实这个级联搜索的组件,他的通用性可能并不是很强,但是在实现的过程中,一些细节逻辑的通用性还是比较强的~ 希望这些细节可以给同在开发中的你带来一些帮助~❤