highlight-search
TypeScript icon, indicating that this package has built-in type declarations

1.1.3 • Public • Published

Highlight search

搜索建议组件。

开始

安装组件:

 pnpm install highlight-search 

创建实例

import { createMEditor, parseFetchRes2FieldMapping, MappingItem, Suggest } from "highlight-search";

// for vue2
this.suggest=new Suggest({
 mapping: mappings,
 wordElement: () => {
  return document.querySelectorAll('.word')
 },
 async fetchQuery(params, options) {
 	const { field, operator } = params
	const queryParams = {
    	field: field.value,
        operator: operator,
        start_time: new Date("2022-12-01").getTime(),
        module: MODULE_NAME,
        end_time: new Date("2023-12-02").getTime()
    }
    try {
          // use fetch or axios to got mappingField List
        const res = await fetch(`/api/v1/sop/utils/xqlite/suggest/`, {
          signal: options?.canalSignal,
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(queryParams)
        })
        const info = await res.json() as {
          data: string[]
        }
        return [{
          title: "搜索建议",
          mappings: info.data.map((item) => {
            return {
              name: item,
              value: `${item}`,
              match: `${item}`
            }
          }),
          render(item: MappingItem) {
            return item.name
          },
        }]
      } catch (e) {
        console.warn("[meditor],fetchQuery error", e)
        return []
      }
    },    
})

// for vue3 
const suggest = ref(new Suggest({
    // .. same as vue2 config
}))

// 然后创建editor实例
const editor = createMeditor({
	// 要挂载到的dom元素,可以直接设置dom元素,或者通过函数返回
	target:HTMLElement | ()=>HTMLElement
    //将上面的suggest传递过来,保存响应式
    // vue2
    suggest:this.suggest;
    //vue3
	suggest:suggest.value,
    
	onSubmit:(value:string)=>{
		// 编辑器触发搜索时的回调,value是纯文本字符串,等同于editor.getValue()
      // 检索完毕之后,调用suggest.removeCache() 清空检索缓存
	}
})

createMeditor会返回editor实例,在原有TextBus的editor实例的基础上,还挂载了辅助函数。

suggest:搜索建议相关的所有东西都在这里处理。

Suggest对象上可能会用到的一些属性和函数:

名称 类型 说明
loading boolean(只读) 是否正在loading,目前仅在查询接口时会调用。
showList FieldMapping[] (只读) 应该展示的候选词,已经过滤并且按原分组分割
queryTypeByText (text:string)=>MappingType 根据传入的单词判断该字符串的类型
queryNextType (text:string)=>MappingType 根据传入的单词推断下一个字符串的类型
parseToFormatter (text:string)=>Formatter[] 根据传入的字符串返回解析格式之后的类型
removeCache ()=>void 清除缓存数据,建议每次提交检索之后都调用一次

editor实例上挂载的其他函数

名称 参数类型 说明
choose (item:MappingItem) 用于鼠标手动选中候选词的插入
setValue (str:string) 手动设置编辑器的内容,会自动解析格式并格式化渲染到编辑器中
getValue - 获取当前编辑器的内容,返回纯文本字符串

使用示例

Vue2

<template>
  <div class="editor-box">
    <h3>Highlight-search In Vue2.x</h3>
    <div class="tool-line">
      <button @click="handleSetValue()">设置默认值</button>
      <button @click="handleSearch()">获取输入的语句 </button>
      <span>{{ userInputValue }}</span>
      <span>loading:{{ suggest?.loading }}</span>
    </div>
    <div ref="editorRef" id="editor">
    </div>
    <!-- 以下内容可以任意写,搜索建议的数组是meditor?.suggest.showList,同时,loading和一些辅助函数也已经暴露出来了 -->
    <TransitionGroup tag="div" name="fade" class="suggest-list">
      <div v-if="suggest?.loading" :key="123">
        loading...
      </div>
      <template v-else-if="suggest?.showList?.length">
        <div class="word-box" v-for="(item, index) in suggest?.showList" :key="index">
          <div class="word-title">{{ item.title }} </div>
          <TransitionGroup tag="div" name="fade" class="words">
            <div class="word" v-for="(word, index) in item.mappings" :key="index" :class="{
              active: suggest.activeItem?.value === word.value
            }" @click="handelChoose(word)">
              {{ item?.render?.(word) ?? word.name }}
            </div>
          </TransitionGroup>
        </div>
      </template>
      <div :key="222" v-else>
        暂无搜索建议
      </div>
    </TransitionGroup>
  </div>
</template>

<script>
import { createMEditor, parseFetchRes2FieldMapping, Suggest } from 'highlight-search'
import "highlight-search/index.css"
const MODULE_NAME = "alarm"
export default {
  name: 'App',
  data() {
    return {
      userInputValue: '',
      meditor: {},
      suggest: {}
    }
  },
  mounted() {
    this.initData()
  },
  methods: {
    async initData() {
      const res = await fetch("/api/v1/sop/utils/xqlite/mappings/", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
      })
      const info = await res.json()
      const mappings = parseFetchRes2FieldMapping(info.data, MODULE_NAME)
      this.suggest = new Suggest({
        mapping: mappings,
        wordElement: () => {
          return document.querySelectorAll('.word')
        },
        async fetchQuery(params, options) {
          const { field, operator } = params
          const queryParams = {
            field: field.value,
            operator: operator,
            start_time: new Date("2022-12-01").getTime(),
            module: MODULE_NAME,
            end_time: new Date("2023-12-02").getTime()
          }
          try {
            const res = await fetch(`/api/v1/sop/utils/xqlite/suggest/`, {
              signal: options?.canalSignal,
              method: 'POST',
              headers: {
                'Content-Type': 'application/json'
              },
              body: JSON.stringify(queryParams)
            })
            const info = await res.json()
            return [{
              title: "搜索建议",
              mappings: info.data.map((item) => {
                return {
                  name: item,
                  value: `${item}`,
                  match: `${item}`
                }
              }),
              render(item) {
                return item.name
              },
            }]
          } catch (e) {
            console.warn("[meditor],fetchQuery error", e)
            return []
          }
        }
      })
      this.meditor = await createMEditor(
        {
          target: this.$refs['editorRef'],
          suggest: this.suggest,
          onSubmit: (value) => {
            console.log('[MEditor]:onSubmit,str is"', value + '"')
            alert(value)
          }
        })
    },
    handelChoose(word) {
      this.meditor?.choose(word)
    },
    handleSetValue() {
      const info = "source_ip = 123 and destination_ip_province = 123.2.2.2 or log_name exist"
      this.meditor.setValue(info)
    },
    handleSearch() {
      const str = this.meditor?.getValue()
      console.log('meditor.value.suggest.showList', this.meditor.suggest.showList)
      this.userInputValue = str ?? ''
      this.meditor.suggest.removeCache()
    }
  }
}
</script>

<style lang="scss">
.editor-box {
  display: flex;
  flex-direction: column;
  height: 100vh;
  width: 100vw;
  overflow: hidden;
}

#editor {
  flex-shrink: 0;
  text-align: left;
  width: 100%;
}

.tool-line {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  margin-bottom: 20px;

  button {
    margin: 0 20px;
  }
}

.suggest-list {
  flex: 1 0 0;
  overflow-y: auto;
  margin-top: 10px;
  width: 100%;
  display: flex;
  flex-direction: column;
  text-align: left;
  position: relative;
  align-items: flex-start;

  .word-box {
    width: calc(100% - 40px);
    padding: 20px;
    background-color: #eeeeee;

    .word-title {
      text-align: left;
      font-size: 20px;
      font-weight: bold;
      flex-shrink: 0;
      margin-left: 10px;
    }

    .words {
      display: flex;
      flex-wrap: wrap;

      .word {
        margin: 10px;
        padding: 6px 10px;
        background-color: #fff;
        border-radius: 2px;
        cursor: pointer;

        &:hover {
          background-color: #cccccc;
          color: #ffffff;
        }

        &.active {
          background-color: #000000;
          color: #ffffff;
        }
      }
    }
  }
}

/* 1. 声明过渡效果 */
.fade-move,
.fade-enter-active,
.fade-leave-active {
  transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
}

/* 2. 声明进入和离开的状态 */
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
  transform: scaleY(0.01) translate(30px, 0);
}

/* 3. 确保离开的项目被移除出了布局流
      以便正确地计算移动时的动画效果。 */
.fade-leave-active {
  position: absolute;
}
</style>

Vue3

<template>
  <div class="editor-box">
    <h3>highlight-search in Vue3 </h3>
    <div class="tool-line">
      <button @click="handleSetValue()">设置默认值</button>
      <button @click="handleSearch()">获取输入的语句 </button>
      <span>{{ userInputValue }}</span>
      <span>loading:{{ meditor?.suggest?.loading }}</span>
    </div>
    <div ref="editorRef" id="editor">
    </div>
    <!-- 以下内容可以任意写,搜索建议的数组是meditor?.suggest.showList,同时,loading和一些辅助函数也已经暴露出来了 -->
    <TransitionGroup tag="div" name="fade" class="suggest-list">
      <div v-if="suggest?.loading" :key="123">
        loading...
      </div>
      <div class="word-box" v-else-if="suggest?.showList?.length" v-for="(item, index) in suggest?.showList" :key="index">
        <div class="word-title">{{ item.title }} </div>
        <TransitionGroup tag="div" name="fade" class="words">
          <div class="word" v-for="(word, index) in item.mappings" :key="index" :class="{
            active: suggest?.activeItem?.value === word.value
          }" @click="handelChoose(word)">
            {{ item?.render?.(word) ?? word.name }}
          </div>
        </TransitionGroup>
      </div>
      <div :key="222" v-else>
        暂无搜索建议
      </div>
    </TransitionGroup>
  </div>
</template>

<script setup lang="ts" name="App">
import { onMounted, ref, watch } from "vue";
import { createMEditor, parseFetchRes2FieldMapping, MappingItem, Suggest } from "highlight-search";
import "highlight-search/index.css";
const editorRef = ref<HTMLElement>()
const meditor = ref()
const suggest = ref()
const userInputValue = ref('')
const MODULE_NAME = "alarm"
const initEditor = async () => {
  const res = await fetch("/api/v1/sop/utils/xqlite/mappings/", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
  })
  const info = await res.json()
  const mappings = parseFetchRes2FieldMapping(info.data, MODULE_NAME)
  suggest.value = new Suggest({
    mapping: mappings,
    wordElement: () => {
      return document.querySelectorAll('.word')
    },
    async fetchQuery(params, options) {
      const { field, operator } = params
      const queryParams = {
        field: field.value,
        operator: operator,
        start_time: new Date("2022-12-01").getTime(),
        module: MODULE_NAME,
        end_time: new Date("2023-12-02").getTime()
      }
      try {
        const res = await fetch(`/api/v1/sop/utils/xqlite/suggest/`, {
          signal: options?.canalSignal,
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(queryParams)
        })
        const info = await res.json() as {
          data: string[]
        }
        return [{
          title: "搜索建议",
          mappings: info.data.map((item) => {
            return {
              name: item,
              value: `${item}`,
              match: `${item}`
            }
          }),
          render(item: MappingItem) {
            return item.name
          },
        }]
      } catch (e) {
        console.warn("[meditor],fetchQuery error", e)
        return []
      }
    },
  })
  const editor = await createMEditor(
    {
      target: editorRef.value!,
      suggest: suggest.value,
      onSubmit: (value: any) => {
        alert(value)
      }
    })
  meditor.value = editor
}

watch(() => suggest, (val) => {
  console.log('val', val)
}, {
  deep: true
})

onMounted(() => {
  initEditor()
})
const handelChoose = (word: MappingItem) => {
  meditor.value?.choose(word)
}
const handleSetValue = () => {
  const info = "source_ip = 123 and destination_ip_province = 123.2.2.2 or log_name exist"
  meditor.value?.setValue(info)
}
const handleSearch = () => {
  const str = meditor.value?.getValue()
  console.log('meditor.value.suggest.showList', meditor.value?.suggest?.showList)
  userInputValue.value = str ?? ''
  suggest.value.removeCache()
}

</script>

<style scoped lang="scss">
.editor-box {
  display: flex;
  flex-direction: column;
  height: 100vh;
  width: 100%;
  overflow: hidden;
}

#editor {
  flex-shrink: 0;
  text-align: left;
  width: 100%;
}

.tool-line {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  margin-bottom: 20px;

  button {
    margin: 0 20px;
  }
}

.suggest-list {
  flex: 1 0 0;
  overflow-y: auto;
  margin-top: 10px;
  width: 100%;
  display: flex;
  flex-direction: column;
  text-align: left;
  position: relative;
  align-items: flex-start;

  .word-box {
    width: calc(100% - 40px);
    padding: 20px;
    background-color: #eeeeee;

    .word-title {
      text-align: left;
      font-size: 20px;
      font-weight: bold;
      flex-shrink: 0;
      margin-left: 10px;
    }

    .words {
      display: flex;
      flex-wrap: wrap;

      .word {
        margin: 10px;
        padding: 6px 10px;
        background-color: #fff;
        border-radius: 2px;
        cursor: pointer;

        &:hover {
          background-color: #cccccc;
          color: #ffffff;
        }

        &.active {
          background-color: #000000;
          color: #ffffff;
        }
      }
    }
  }
}

/* 1. 声明过渡效果 */
.fade-move,
.fade-enter-active,
.fade-leave-active {
  transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
}

/* 2. 声明进入和离开的状态 */
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
  transform: scaleY(0.01) translate(30px, 0);
}

/* 3. 确保离开的项目被移除出了布局流
      以便正确地计算移动时的动画效果。 */
.fade-leave-active {
  position: absolute;
}
</style>

自定义样式

插件并不会自带任何样式,但是预设了一套基本的样式。

你可以引用查看简单的示例

import "highlight-search/index.css"

实际渲染的dom结构是这样的(最理想的情况下,实际上会存在各种随机的嵌套)

<div data-component="root" class="meditor-root">
    <span class="meditor-comp-mappingField">source_ip</span>
    <span class="meditor-comp-comparator">&nbsp;=</span>
    <span class="meditor-comp-inputValue">&nbsp;123</span>
    <span class="meditor-comp-logical">&nbsp;and</span>
    <span class="meditor-comp-mappingField">&nbsp;dest_port</span>
    <span class="meditor-comp-comparator">&nbsp;=</span>
    <span class="meditor-comp-inputValue">&nbsp;80</span>
</div>

涉及到的class类名

  • meditor-root : root元素,所有的内容都在改dom元素下。
  • meditor-comp-mappingField :MappingField 对应的class类名。( 其余的同理)

深入原理

需求理解

需求拆分

设计实现

兼容处理

/highlight-search/

    Package Sidebar

    Install

    npm i highlight-search

    Weekly Downloads

    8

    Version

    1.1.3

    License

    none

    Unpacked Size

    4.91 MB

    Total Files

    25

    Last publish

    Collaborators

    • wankong