1.1.3 • Published 1 year ago
highlight-search v1.1.3
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"> =</span>
    <span class="meditor-comp-inputValue"> 123</span>
    <span class="meditor-comp-logical"> and</span>
    <span class="meditor-comp-mappingField"> dest_port</span>
    <span class="meditor-comp-comparator"> =</span>
    <span class="meditor-comp-inputValue"> 80</span>
</div>涉及到的class类名
- meditor-root : root元素,所有的内容都在改dom元素下。
- meditor-comp-mappingField :MappingField 对应的class类名。( 其余的同理)
深入原理
需求理解
需求拆分
设计实现
兼容处理
1.1.3
1 year ago
1.1.2
2 years ago
1.1.1
2 years ago
1.1.0
2 years ago
1.0.9
2 years ago
1.0.8
2 years ago
1.0.7
2 years ago
1.0.6
2 years ago
1.0.5
2 years ago
1.0.2
2 years ago
1.0.4
2 years ago
1.0.3
2 years ago
1.0.1-beta0.7
2 years ago
1.0.1-beta0.6
2 years ago
1.0.1-beat0.3
2 years ago
1.0.1-beat0.2
2 years ago
1.0.1-beat.1
2 years ago
1.0.0
2 years ago