Vue實(shí)現(xiàn)類似chrome瀏覽器地址欄補(bǔ)全功能

先講需求:

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


image.png

功能點(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ǔ)全文字樣式

image.png

這是有兩部分,即正在輸入的文字,和 提示部分(藍(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é)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容