跳转到内容

原生 Input 组件封装输入建议

点击查看代码
vue
<!-- InputSuggestions.vue -->
<template>
  <el-popover
    placement="bottom-start"
    trigger="manual"
    v-model="visible"
    popper-class="input-select-popover"
    transition="el-zoom-in-top"
  >
    <input
      type="text"
      size="18"
      v-model="innerValue"
      slot="reference"
      ref="input"
      @focus="handleFocus"
      @blur="handleBlur"
      @input="handleInput"
      @click="handleClick"
      @keyup.enter="handleEnter"
      @keydown.up="handleUp"
      @keydown.down="handleDown"
    />
    <div
      class="content"
      :style="{ minWidth: inputWidth + 'px' }"
      ref="content"
      @mousedown.prevent
    >
      <ul class="list">
        <li
          class="item"
          v-for="(item, index) in innerOptions"
          :key="item"
          :class="{ active: index === activeIndex }"
          ref="items"
          @click.prevent.stop="handleItemClick(item)"
        >
          <span>{{ item }}</span>
        </li>
        <li class="item empty" v-if="innerOptions.length === 0">
          <span>暂无数据</span>
        </li>
      </ul>
    </div>
  </el-popover>
</template>

<script>
export default {
  name: "InputSelect",
  props: {
    value: {
      type: String,
      default: "",
    },
    options: {
      type: Array,
      default: () => [],
    },
    searchable: {
      type: Boolean,
      default: true,
    },
  },
  data() {
    return {
      visible: false,
      innerValue: this.value,
      activeIndex: -1,
      inputWidth: 0,
      selected: !!this.value,
    };
  },
  computed: {
    innerOptions() {
      if (this.searchable && !this.selected) {
        const regex = new RegExp(this.innerValue, "i");
        return this.options.filter((item) => regex.test(item));
      } else {
        return this.options;
      }
    },
  },
  watch: {
    value(val) {
      this.innerValue = val;
    },
    innerValue(val, old) {
      this.$emit("input", val);
      this.activeIndex = -1;
    },
    activeIndex(val) {
      this.$nextTick(() => {
        if (val !== -1 && this.$refs.items[val]) {
          // 判断是否在可视区域
          const top = this.$refs.items[val].offsetTop;
          const bottom =
            this.$refs.items[val].offsetTop +
            this.$refs.items[val].offsetHeight;
          const containerTop = this.$refs.content.scrollTop;
          const containerBottom =
            containerTop + this.$refs.content.offsetHeight;
          if (top < containerTop) {
            this.$refs.content.scrollTop = top;
          } else if (bottom > containerBottom) {
            this.$refs.content.scrollTop =
              bottom - this.$refs.content.offsetHeight;
          }
        }
      });
    },
  },
  methods: {
    show() {
      this.visible = true;
    },
    hide() {
      this.visible = false;
    },
    handleFocus() {
      if (this.searchable && this.options.length > 0) {
        this.show();
      }
    },
    handleBlur() {
      this.hide();
    },
    handleInput() {
      if (this.selected) {
        this.visible = true;
      }
      this.selected = false;
    },
    handleClick() {
      this.handleFocus();
    },
    handleEnter() {
      if (this.activeIndex === -1) return;
      const item = this.innerOptions[this.activeIndex];
      if (item === void 0) return;
      this.innerValue = item;
      this.hide();
      this.$nextTick(() => {
        this.selected = true;
      });
    },
    handleUp() {
      if (this.activeIndex === -1 || this.activeIndex === 0) {
        this.activeIndex = this.innerOptions.length - 1;
      } else {
        this.activeIndex--;
      }
    },
    handleDown() {
      if (
        this.activeIndex === -1 ||
        this.activeIndex === this.innerOptions.length - 1
      ) {
        this.activeIndex = 0;
      } else {
        this.activeIndex++;
      }
    },
    handleItemClick(item) {
      this.innerValue = item;
      this.hide();
      this.$nextTick(() => {
        this.selected = true;
      });
    },
  },
  mounted() {
    if (this.$refs.input) {
      this.inputWidth = this.$refs.input.offsetWidth;
    }
  },
};
</script>

<style lang="scss" scoped>
.content {
  max-height: 260px;
  overflow-y: auto;
  .list {
    box-sizing: border-box;
    list-style: none;
    padding: 6px 0px;
    margin: 0px;
    .item {
      font-size: 14px;
      position: relative;
      text-overflow: ellipsis;
      color: rgb(96, 98, 102);
      height: 34px;
      line-height: 34px;
      box-sizing: border-box;
      cursor: pointer;
      padding: 0px 20px;
      white-space: nowrap;
      overflow: hidden;
      &.active,
      &:hover {
        background-color: rgb(245, 247, 250);
        color: rgb(64, 158, 255);
      }
      &.empty {
        display: flex;
        justify-content: center;
        align-items: center;
        color: rgb(144, 147, 153);
      }
    }
  }
}
</style>

<style lang="scss">
.input-select-popover {
  padding: 0px;
}
</style>

最后更新于: