先講需求:
我需要實(shí)現(xiàn)一個(gè)類似chrome瀏覽器地址欄的補(bǔ)全功能:

功能點(diǎn)
- 輸入“jian”, 會(huì)自動(dòng)補(bǔ)全“jianshu.com”, 后面補(bǔ)全部分的“shu.com”藍(lán)底白字顯示
- 下拉選項(xiàng)第一個(gè)是補(bǔ)全的或正在輸入的內(nèi)容
- 存在補(bǔ)全(藍(lán)底白字),按刪除鍵,會(huì)先刪除補(bǔ)全文字(藍(lán)底白字)
- 按上下鍵,選中的選項(xiàng),內(nèi)容會(huì)補(bǔ)全到輸入框
- 按右鍵,會(huì)使用該補(bǔ)全,光標(biāo)在文字尾部
- 按左鍵,會(huì)使用該補(bǔ)全,光標(biāo)在當(dāng)前位置
- 存在補(bǔ)全時(shí)(存在藍(lán)底白字),光標(biāo)隱藏
實(shí)現(xiàn)
1、輸入框補(bǔ)全文字樣式

這是有兩部分,即正在輸入的文字,和 提示部分(藍(lán)底白字), 這個(gè)我是用兩個(gè)部分實(shí)現(xiàn),一個(gè)是輸入框(正常輸入),一個(gè)是背景層(透明文字占位 + 藍(lán)底白字塊)
- 代碼:
<div class="input__content">
<input
class="input__input"
type="text"
v-model="keyword"
/>
<!-- 背景 -->
<div class="input__bg">
<span class="input__bg--1">{{ keyword }}</span>
<span class="input__bg--2">{{ remainStr }}</span>
</div>
</div>
<script>
data() {
return {
keyword: '',
remainStr: 'shu.com', // 提示的字符,先寫在這
}
}
</script>
<style lang="scss" scoped>
.input {
&__content {
background: #fff;
position: relative;
border: 1px solid #ccc;
height: 32px;
}
&__input {
height: 100%;
width: 100%;
border: 0;
background: none;
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
&__bg {
color: #fff;
position: absolute;
top: 0;
left: -2px;
z-index: 0;
line-height: 30px;
&--1 {
color: #fff;
opacity: 0;
white-space: pre;
}
&--2 {
color: #fff;
background-color: #40638a;
}
}
}
</style>
注意個(gè)細(xì)節(jié),就是輸入多個(gè)空格的時(shí)候,實(shí)際渲染只有一個(gè)空格,這樣作為背景現(xiàn)實(shí),就會(huì)錯(cuò)位。所以需要使用樣式
white-space: pre;。
2、提示文字
假設(shè)補(bǔ)全的文字是“jianshu.com”, 會(huì)發(fā)現(xiàn),每輸入一個(gè)字字符,提示的部分就減少一個(gè)字符,這里就可以拆成兩個(gè)部分:輸入文字 + 提示字符 = “jianshu.com”。我們可以借用compute來實(shí)現(xiàn)
- 代碼:
<script>
data() {
return {
completeWord: "jianshu.com"; // 假設(shè)補(bǔ)全的文字,暫時(shí)放在這
}
}
computed: {
/**
* 補(bǔ)全提示的字符
* 沒有輸入文字,就不需要提示字符
*/
remainStr() {
if (
!this.completeWord ||
!this.keyword
) {
return ''
}
// 提示字符 = 補(bǔ)全文字 - 已經(jīng)輸入的文字
return this.completeWord.slice(this.keyword.length)
},
}
</script>
3、按下刪除鍵
按刪除鍵不是刪除已輸入的文字,而是先刪除提示塊,再按一次之后才是刪除已輸入的文字
- 代碼
<template>
<input @keydown="handleKeydown" />
</template>
<script>
methods: {
/*
* 鍵盤點(diǎn)擊事件
* @param {Event} event 事件
*/
handleKeydown(event) {
const { keyCode } = event
// 刪除鍵碼: 8
if (keyCode === 8 && this.remainStr) {
this.completeWord = this.keyword
event.preventDefault()
return
}
},
}
</script>
4、按左右鍵邏輯
左右鍵是應(yīng)用提示補(bǔ)全的字符,不同的是,右鍵是光標(biāo)在末尾,左鍵光標(biāo)在原來位置
- 代碼
<template>
<input
ref="input"
@keydown="handleKeydown"
/>
</template>
<script>
methods: {
handleKeydown(event) {
// 按左,按右
if (this.remainStr && (keyCode === 37 || keyCode === 39)) {
this.keyword = this.completeWord // 應(yīng)用補(bǔ)全
// 按右鍵
if (keyCode === 37) {
const inputElm = this.$refs.input
let caretPosition = null // 光標(biāo)位置
if (inputElm) {
caretPosition = inputElm.selectionStart
}
setTimeout(() => {
// 還原光標(biāo)位置
inputElm.setSelectionRange(caretPosition, caretPosition)
}, 20)
}
event.preventDefault()
}
}
}
</script>
5、按下“上下鍵”邏輯
按下“上下鍵”,下拉選項(xiàng)會(huì)被選中,但會(huì)發(fā)現(xiàn),光標(biāo)還是聚焦在輸入框。而且選中的項(xiàng)的值會(huì)填充到輸入框
- 代碼:
<template>
<!-- 選項(xiàng) -->
<ul>
<li
v-for="(item, index) in list"
:key="item"
:class="{
active: focusIndex === index,
}"
>
{{ item }}
</li>
</ul>
</template>
<script>
data() {
return {
// 實(shí)際中,數(shù)據(jù)從localstorage 中獲取用戶曾經(jīng)輸入過的數(shù)據(jù)
list: [
'jianhsu.com',
'jianshu.com/p/1000001',
'jianshu.com/p/1000002',
'jianshu.com/p/1000003'
],
focusIndex: -1
}
},
methods: {
/* 選擇列表 */
selectList(moveNumber) {
let index = this.focusIndex
if (this.focusIndex < 0) {
index = -1
}
index += moveNumber
if (index >= 0 && index < this.list.length) {
this.focusIndex = index
// 選中的項(xiàng),賦值給keyword 和 補(bǔ)全詞
this.keyword = this.completeWord = this.list[index]
}
},
handleKeydown(event) {
// 向上按鍵
if (keyCode === 38) {
this.selectList(-1)
event.preventDefault()
}
// 向下按鍵
if (keyCode === 40) {
this.selectList(+1)
event.preventDefault()
}
}
}
</script>
6、第一個(gè)選項(xiàng)為補(bǔ)全或輸入框輸入的
輸入值是,第一選項(xiàng)默認(rèn)選中,并且值為提示補(bǔ)全后的值,或者是正在輸入的值(沒有補(bǔ)全時(shí))
- 代碼
<template>
<input
@input="handleChange"
/>
</template>
<script>
// 原始數(shù)據(jù),實(shí)際應(yīng)用中,從緩存獲取
const originList = [
'jianhsu.com',
'jianshu.com/p/1000001',
'jianshu.com/p/1000002',
'jianshu.com/p/1000003'
]
export default {
methods: {
handleChange(e) {
const { value } = v.target
this.completeWord = value
// 輸入為空
if (!value) {
this.focusIndex = -1
this.list = originList
return
}
// 獲取value 開頭的第一個(gè)符合項(xiàng)
const word = originList.find((w) => w.startsWith(value))
if (word) {
this.completeWord = word
// 補(bǔ)全的單詞挪到第一個(gè)
this.list = [word, ...originList.filter((w) => w !== word)]
} else {
// 沒有補(bǔ)全詞,輸入的放在第一個(gè)
this.list = [value, ...originList]
}
// 默認(rèn)選中第一個(gè)
this.focusIndex = 0
}
}
}
</script>
一個(gè)細(xì)節(jié),按下刪除鍵的時(shí)候,刪除的是提示字符,第一選項(xiàng)也是非補(bǔ)全詞;可以加個(gè)判斷字段
canComplete, 在按下刪除鍵的時(shí)候,該值為false, 上面代碼就加個(gè)判斷if (word) { if (this.canComplete) { this.completeWord = word } }
最后全部代碼
<template>
<div class="input">
input:
<div class="input__content">
<input
ref="input"
class="input__input"
type="text"
v-model="keyword"
@input="handleChange"
@keydown="handleKeydown"
:class="{
'hide-caret': remainStr,
}"
/>
<!-- 背景 -->
<div class="input__bg">
<span class="input__bg--1">{{ keyword }}</span>
<span class="input__bg--2">{{ remainStr }}</span>
</div>
</div>
<ul class="list">
<li
v-for="(item, index) in list"
:key="item"
:class="{
active: focusIndex === index,
}"
>
{{ item }}
</li>
</ul>
</div>
</template>
<script>
// 原始數(shù)據(jù)
const originList = [
'jianhsu.com',
'jianshu.com/p/1000001',
'jianshu.com/p/1000002',
'jianshu.com/p/1000003',
'bilibilii.com',
'你好,世界'
]
export default {
name: 'Input',
data() {
return {
keyword: '',
list: originList,
completeWord: '', // 補(bǔ)全的文本
focusIndex: -1, // 選中
canComplete: false, // 是否可以補(bǔ)全
}
},
methods: {
/**
* 補(bǔ)全
* */
complete(value) {
if (!value) {
this.focusIndex = -1
this.list = originList
return
}
const word = originList.find((w) => w.startsWith(value))
if (word) {
if (this.canComplete) {
this.completeWord = word
}
this.list = [word, ...originList.filter((w) => w !== word)]
} else {
this.list = [value, ...originList]
}
this.focusIndex = 0
},
/**
* 輸入框輸入事件
* @param {Event} v 事件
*/
handleChange(v) {
const { value } = v.target
this.completeWord = value
// 計(jì)算補(bǔ)全
this.complete(value)
},
/* 選擇列表 */
selectList(moveNumber) {
let index = this.focusIndex
if (this.focusIndex < 0) {
index = -1
}
index += moveNumber
if (index >= 0 && index < this.list.length) {
this.focusIndex = index
this.keyword = this.completeWord = this.list[index]
}
},
/*
* 鍵盤點(diǎn)擊事件
* @param {Event} event 事件
*/
handleKeydown(event) {
const { keyCode } = event
// 刪除單詞
if (keyCode === 8) {
this.canComplete = false // 不需要補(bǔ)全
if (this.remainStr) {
this.completeWord = this.keyword
event.preventDefault()
return
}
} else {
this.canComplete = true
}
// 按左,按右
if (this.remainStr && (keyCode === 37 || keyCode === 39)) {
this.keyword = this.completeWord // 應(yīng)用補(bǔ)全
// 按右鍵
if (keyCode === 37) {
// 獲取光標(biāo)的位置
const inputElm = this.$refs.input
let caretPosition = null
if (inputElm) {
caretPosition = inputElm.selectionStart
}
// 需要異步
setTimeout(() => {
inputElm.setSelectionRange(caretPosition, caretPosition)
}, 20)
}
event.preventDefault()
}
// 向上按鍵
if (keyCode === 38) {
this.selectList(-1)
event.preventDefault()
}
// 向下按鍵
if (keyCode === 40) {
this.selectList(+1)
event.preventDefault()
}
},
},
computed: {
/**
* 補(bǔ)全提示的字符
*/
remainStr() {
if (
!this.completeWord ||
!this.keyword ||
this.keyword.length >= this.completeWord.length
) {
return ''
}
return this.completeWord.slice(this.keyword.length)
},
},
}
</script>
<style lang="scss" scoped>
.input {
&__content {
background: #fff;
position: relative;
border: 1px solid #ccc;
height: 32px;
}
&__input {
height: 100%;
width: 100%;
border: 0;
background: none;
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
&__bg {
color: #fff;
position: absolute;
top: 0;
left: -2px;
z-index: 0;
line-height: 30px;
&--1 {
color: #fff;
opacity: 0;
white-space: pre;
}
&--2 {
color: #fff;
background-color: #40638a;
}
}
.hide-caret {
caret-color: transparent;
}
.list {
padding: 0;
border: 1px solid #cecece;
li {
border-left: 2px solid transparent;
padding-left: 6px;
list-style: none;
line-height: 32px;
&:hover {
background: #dedede;
}
&.active {
border-color: #679df3;
background-color: #dedede;
}
}
}
}
</style>
總結(jié)
這只是個(gè)簡(jiǎn)單的示例,需要結(jié)合實(shí)際項(xiàng)目去實(shí)現(xiàn)。部分細(xì)節(jié)不必過于糾結(jié)