先讲需求:
我需要实现一个类似chrome浏览器地址栏的补全功能:
功能点
- 输入“jian”, 会自动补全“jianshu.com”, 后面补全部分的“shu.com”蓝底白字显示
- 下拉选项第一个是补全的或正在输入的内容
- 存在补全(蓝底白字),按删除键,会先删除补全文字(蓝底白字)
- 按上下键,选中的选项,内容会补全到输入框
- 按右键,会使用该补全,光标在文字尾部
- 按左键,会使用该补全,光标在当前位置
- 存在补全时(存在蓝底白字),光标隐藏
实现
1、输入框补全文字样式
这是有两部分,即正在输入的文字,和 提示部分(蓝底白字), 这个我是用两个部分实现,一个是输入框(正常输入),一个是背景层(透明文字占位 + 蓝底白字块)
- 代码:
<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>
注意个细节,就是输入多个空格的时候,实际渲染只有一个空格,这样作为背景现实,就会错位。所以需要使用样式
white-space: pre;
。
2、提示文字
假设补全的文字是“jianshu.com”, 会发现,每输入一个字字符,提示的部分就减少一个字符,这里就可以拆成两个部分:输入文字 + 提示字符 = “jianshu.com”。我们可以借用compute来实现
- 代码:
<script>
data() {
return {
completeWord: "jianshu.com"; // 假设补全的文字,暂时放在这
}
}
computed: {
/**
* 补全提示的字符
* 没有输入文字,就不需要提示字符
*/
remainStr() {
if (
!this.completeWord ||
!this.keyword
) {
return ''
}
// 提示字符 = 补全文字 - 已经输入的文字
return this.completeWord.slice(this.keyword.length)
},
}
</script>
3、按下删除键
按删除键不是删除已输入的文字,而是先删除提示块,再按一次之后才是删除已输入的文字
- 代码
<template>
<input @keydown="handleKeydown" />
</template>
<script>
methods: {
/*
* 键盘点击事件
* @param {Event} event 事件
*/
handleKeydown(event) {
const { keyCode } = event
// 删除键码: 8
if (keyCode === 8 && this.remainStr) {
this.completeWord = this.keyword
event.preventDefault()
return
}
},
}
</script>
4、按左右键逻辑
左右键是应用提示补全的字符,不同的是,右键是光标在末尾,左键光标在原来位置
- 代码
<template>
<input
ref="input"
@keydown="handleKeydown"
/>
</template>
<script>
methods: {
handleKeydown(event) {
// 按左,按右
if (this.remainStr && (keyCode === 37 || keyCode === 39)) {
this.keyword = this.completeWord // 应用补全
// 按右键
if (keyCode === 37) {
const inputElm = this.$refs.input
let caretPosition = null // 光标位置
if (inputElm) {
caretPosition = inputElm.selectionStart
}
setTimeout(() => {
// 还原光标位置
inputElm.setSelectionRange(caretPosition, caretPosition)
}, 20)
}
event.preventDefault()
}
}
}
</script>
5、按下“上下键”逻辑
按下“上下键”,下拉选项会被选中,但会发现,光标还是聚焦在输入框。而且选中的项的值会填充到输入框
- 代码:
<template>
<!-- 选项 -->
<ul>
<li
v-for="(item, index) in list"
:key="item"
:class="{
active: focusIndex === index,
}"
>
{{ item }}
</li>
</ul>
</template>
<script>
data() {
return {
// 实际中,数据从localstorage 中获取用户曾经输入过的数据
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
// 选中的项,赋值给keyword 和 补全词
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、第一个选项为补全或输入框输入的
输入值是,第一选项默认选中,并且值为提示补全后的值,或者是正在输入的值(没有补全时)
- 代码
<template>
<input
@input="handleChange"
/>
</template>
<script>
// 原始数据,实际应用中,从缓存获取
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 开头的第一个符合项
const word = originList.find((w) => w.startsWith(value))
if (word) {
this.completeWord = word
// 补全的单词挪到第一个
this.list = [word, ...originList.filter((w) => w !== word)]
} else {
// 没有补全词,输入的放在第一个
this.list = [value, ...originList]
}
// 默认选中第一个
this.focusIndex = 0
}
}
}
</script>
一个细节,按下删除键的时候,删除的是提示字符,第一选项也是非补全词;可以加个判断字段
canComplete
, 在按下删除键的时候,该值为false, 上面代码就加个判断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>
// 原始数据
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: '', // 补全的文本
focusIndex: -1, // 选中
canComplete: false, // 是否可以补全
}
},
methods: {
/**
* 补全
* */
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
// 计算补全
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]
}
},
/*
* 键盘点击事件
* @param {Event} event 事件
*/
handleKeydown(event) {
const { keyCode } = event
// 删除单词
if (keyCode === 8) {
this.canComplete = false // 不需要补全
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 // 应用补全
// 按右键
if (keyCode === 37) {
// 获取光标的位置
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: {
/**
* 补全提示的字符
*/
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>
总结
这只是个简单的示例,需要结合实际项目去实现。部分细节不必过于纠结