實現仿element遠程搜索下拉框實現方式與遇到的坑

背景

由於項目中須要寫一個下拉組件相似於element-ui的遠程搜索下拉框,可是element的組件不支持下拉框和已選中人的自定義,因此本身模擬element的方式實現了此組件(vue+ts)。html

my-demo: vue

my-demo
element-ui:

element-ui
組件的代碼請跳轉另外一篇文章( demo

嵌套結構的設計

首先看到這個結構(選中的結果顯示在輸入框內,後面可繼續輸入),想到兩種實現方式:
git

  1. 外層是一個input輸入框,選中後將結果渲染在div中定位在輸入框內,光標定位在div的後面
    此種方案有兩個很大問題:一、選中結果的長度不肯定因此div的長度不肯定,光標定位的位置就很難肯定; 二、選中多個結果後會出現換行,判斷幾個div會佔滿一行會特別難,而且換行後輸入框要變高,而且光標定位在最後,很難實現。
    顯然此種方案不可行
  2. 外層是一個假的輸入框,內層嵌套一個真正的輸入框,而後將內部輸入框的外邊框隱藏掉,用外部假的輸入框的樣式模擬一個輸入框的各類狀態,element-ui的實現方式也爲此種狀況。
    此方案能夠實現

方案二的實現,element-ui的實現方式爲最外層框爲一個div,而後將兩個input同時定位輸入框位置,真輸入框進行輸入操做,假輸入框顯示placeholder,在選擇結果跟真輸入框展現在同一行並把輸入框位置擠到後面。
我在實現此方案時,感受兩個input有點多餘,假的input只是提供了一個placeholder的功能,徹底可使用裏面input的動態placeholder,在選中內容後將placeholder置爲空,這樣能夠省去一個假的input標籤和定位操做(定位就會涉及到層級問題比較複雜)。選中結果展現是使用了flex佈局,將選中的結果循環渲染在div中,最後面再加上input輸入框,便可實現輸入框一直在選中結果的最後面,而且選中多人後能夠換行(解決了輸入框高度和選中結果寬度的不固定問題)。而後經過對input輸入框focus事件和blur事件的監聽來對外部div進行一些樣式的改變來模擬一個真實輸入框的輸入效果、失焦效果和獲取焦點的效果。github

注:選中結果與輸入框的同行顯示與換行,我使用的是flex佈局實現的,使用span標籤或行內塊元素渲染選中結果也能夠實現此效果,可是須要保證slot傳進來的結構不能使用塊級元素。element-ui

代碼實現

<!--外層div(模擬inout的各類狀態)-->
<div class="demo-outBox-XL" @click="chooseInput" :class="{'demo-is-focus-XL': inputFocus}">
    <!--輸入框後面的清空按鈕-->
    <span @click.stop="clearChecked">
        <img src="./assets/crossIcon.svg" class="demo-InputCloseIcon-XL" >
    </span>
    <div class="demo-chooseContent-XL" :class="{'demo-showInput-XL': !showClearIcon}" v-if="isMultiple">
        <!--選中的結果-->
        <div v-for="(item, i) in checkedArr" :key="'checked' + i" class="demo-checkedTag-XL">
            <div class="demo-outContent-XL" @click="chooseTag(item)" :class="{'demo-deleteStatus-XL': i === checkedArr.length - 1 && deleteStatus}">
                <slot name="tag" v-bind:item="item">{{item[props.label]}}</slot>
            </div>
        </div>
        <!--輸入框input-->
        <input type="text" v-model.trim="searchVal" :placeholder="!checkedArr.length ? placeholder : ''" class="demo-inInput-XL" @focus="handleFocus" @blur="loseFocus" ref="inInput" @keydown.8="deleteOne" v-on="$listeners">
    </div>
</div>
複製代碼

下拉框和選中結果顯示樣式的設計

此功能在element-ui上並無,element的下拉框和選中結果只支持顯示某一個字段,而沒法實現自定義,我在實現此功能時使用了具名插槽,經過組件內v-for循環拿到數據,而後經過v-bind:item="item"暴露給父組件,在父組件中使用 slot-scope="{ item }"接收組件內傳過來的數據,並在父組件中設置顯示樣式,實現顯示樣式的自定義;json

實現此功能時須要注意由組件暴露出來item若是直接使用slot-scope="item"接收會沒法使用,此時是一個json字符串,須要使用{item}才能拿到傳過來的對象加以使用。在使用slot時增長了默認值,能夠保證若是調用組件時沒有傳插槽時不會出問題,默認顯示label字段。插槽的寫法用的是舊版的書寫方式,由於項目緣由並不支持最新版的插槽書寫方式(能夠看vue官網有新寫法和廢棄寫法的區別)。windows

子組件代碼示例

<!--選中的結果-->
        <div v-for="(item, i) in checkedArr" :key="'checked' + i" class="demo-checkedTag-XL">
            <div class="demo-outContent-XL" @click="chooseTag(item)" :class="{'demo-deleteStatus-XL': i === checkedArr.length - 1 && deleteStatus}">
                <!--name爲名稱   v-bind爲要暴露出去的對象 {{item[props.label]}}爲默認值-->
                <slot name="tag" v-bind:item="item">{{item[props.label]}}</slot>
            </div>
        </div>
複製代碼

父組件代碼示例

<!--樣式並不用寫在行內,此處爲了方便寫在行內了(並不建議這樣寫)-->
<template slot="tag" slot-scope="{ item }">
    <img src="./img.jpg" style="width: 16px; height: 16px; border-radius: 50%;">
    <div style="line-height: 14px; font-size: 14px; margin-left:4px; word-wrap: break-word; word-break: break-word;">
        {{item.name}}
    </div>
</template>
複製代碼

將綁定在組件上的方法綁定在目標標籤input上

首先想到的是使用native修飾符,嘗試後發現並不能實現,只有input事件能夠綁定上去,change事件並不會失效,因此否認了這個方法。
而後使用了$emit在input上綁定經常使用方法,並在方法內去使用$emit去調用父組件綁定的對應的方法,此方法存在必定缺陷並不完美,即只能綁定本身提早預設好的一些方法,若是組內內部沒有去調用的方法,則在父組件中綁定方法會無效,因此一樣否認了此種方法。
在使用此方法時遇到了另一個問題,本身在組件上寫的v-model(默認子組件使用value接收父組件傳的值,調用父組件的input方法來改變父組件的值)雙向綁定與本身想要在組件上綁定的input事件衝突,調用input事件時會給v-model的值置空,經過修改model配置更改了v-model默認的傳值與接收,再也不使用input方法更改更改組件的v-model綁定的值。瀏覽器

最後,經過$listeners接收父組件綁定的方法,再經過v-on綁定在組件的input上解決了此問題,而且寫組件時在input綁定的方法並不會與父組件傳過來的相同方法衝突,而是會兩個都執行,完美。bash

代碼示例

<!--父組件-->
<Myinput v-model="arr" :options="options" multiple @input="inputEvent" @change="changeEvent" @keyup.enter="enterEvent">
</Myinput>

<!--子組件-->
<div class="demo-chooseContent-XL" :class="{'demo-showInput-XL': !showClearIcon}" v-if="isMultiple">
    <div v-for="(item, i) in checkedArr" :key="'checked' + i" class="demo-checkedTag-XL">
        <div class="demo-outContent-XL" @click="chooseTag(item)" :class="{'demo-deleteStatus-XL': i === checkedArr.length - 1 && deleteStatus}">
            <slot name="tag" v-bind:item="item">{{item[props.label]}}</slot>
            <span @click.stop="deleteChecked(item)" class="demo-icon-XL">
                <img src="./assets/crossIcon.svg" class="demo-tagIcon-XL" >
            </span>
        </div>
    </div>
    <input type="text" v-model.trim="searchVal" v-on="$listeners" :placeholder="!checkedArr.length ? placeholder : ''" class="demo-inInput-XL" @focus="handleFocus" @blur="loseFocus" ref="inInput" @keydown.8="deleteOne">
</div>
複製代碼

拖拽功能實現

此功能是後來產品加的功能,選中人以後要進行順序的拖拽,首先想到了兩種實現方式:
dom

  1. 原生的drag方法
  2. 引用drag插件

而後就使用了原生的方法實現了此功能,能夠實現,而且沒發現問題,很短期實現了此功能,代碼示例以下:

<div class="demo-chooseContent-XL" :class="{'demo-showInput-XL': !showClearIcon}" v-if="isMultiple">
    <div v-for="(item, i) in checkedArr" :key="'checked' + i" class="demo-checkedTag-XL"
        :draggable="isDraggable && !deleteStatus"
        @dragstart="handleDragStart($event, item)"
        @dragover.prevent="handleDragOver($event, item)"
        @dragenter="handleDragEnter($event, item)"
        @dragend="handleDragEnd($event, item)"
    >
        <div class="demo-outContent-XL" @click="chooseTag(item)" :class="{'demo-deleteStatus-XL': i === checkedArr.length - 1 && deleteStatus}">
            <slot name="tag" v-bind:item="item">{{item[props.label]}}</slot>
            <span @click.stop="deleteChecked(item)" class="demo-icon-XL">
                <img src="./assets/crossIcon.svg" class="demo-tagIcon-XL" >
            </span>
        </div>
    </div>
    <input type="text" v-model.trim="searchVal" :placeholder="!checkedArr.length ? placeholder : ''" class="demo-inInput-XL" @focus="handleFocus" @blur="loseFocus" ref="inInput" @keydown.8="deleteOne" v-on="$listeners">
</div>

// 實現拖拽功能
private dragging: any = null;
private handleDragStart(e: any, item: object): void {
    this.dragging = item;
}
private handleDragEnd(e: any, item: object): void {
    this.dragging = null;
}

private handleDragOver(e: any, item: object): void {
    e.dataTransfer.dropEffect = "move"; // e.dataTransfer.dropEffect="move";//在dragenter中針對放置目標來設置!
}

private handleDragEnter(e: any, item: object): void {
    e.dataTransfer.effectAllowed = "move"; //爲須要移動的元素設置dragstart事件
    if (item === this.dragging) {
    return;
    }
    const newArr = [...this.checkedArr];
    const src = newArr.indexOf(this.dragging);
    const dst = newArr.indexOf(item);
    newArr.splice(dst, 0, ...newArr.splice(src, 1));
    this.checkedArr = newArr;
}
複製代碼

提測以後纔是噩夢的開始,safari瀏覽器在拖拽時沒法實現輸入框的自動滾動(輸入框限制了高度,超出三行出現滾動效果,在其餘瀏覽器把最下面一行內容拉到輸入框頂部則本身會向上滾動頁面,然而safari卻無任何效果),沒辦法只能回來改。首先是在網上查了一下這個問題,並未查到什麼解決方案,而後本身考慮了兩種解決方案:一、改成使用插件解決;二、經過監聽拖拽時鼠標位置來用js控制滾動。
插件能解決的問題,我才懶得手寫監聽還要各類判斷,而後使用了vue-Draggable插件,此時又遇到了此插件不支持ts,個人ts環境中引入後報錯,查了好久經過一些配置才解決了此問題,然而提交後,測試那依舊是不能滾動。(我是windows電腦此問題只在mac的safari上才能復現,我就只能找測試復現,用測試的mac來訪問我本地環境測試,兩天的時間都在本身的位置和測試的位置來回跑,我是多麼渴望此時擁有一臺mac)
問題仍是要解決的,只能經過監聽鼠標位置了(自信滿滿的寫好了監聽和各類狀況判斷),此次總該是解決了問題,然而到了測試那依舊是不動。沒理由啊,監聽鼠標位置怎麼都沒法實現呢,經過打印發現,在拖拽過程當中鼠標mousemove事件並不會觸發,此方案並不能實現滾動效果。
又經過百度+google查找終於看到了一個drag事件(之前沒作過拖拽的功能,根本不知道這個事件),看這名字感受應該能夠用上,終於經過此事件解決了此問題,由於滾動過快又對此功能加了一個節流,這個問題差很少用了一天半的時間才徹底解決掉。代碼示例以下:

<div class="wfc-chooseContent-XRZJ" :class="{'wfc-showInput-XRZJ': !showPlaceholder}" v-if="isMultiple" @drag="divDragging">
    <div v-for="(item, i) in checkedArr" :key="'checked' + i" class="wfc-checkedTag-XRZJ"
        :draggable="isDraggable && !deleteStatus"
        @dragstart="handleDragStart($event, item)"
        @dragover.prevent="handleDragOver($event, item)"
        @dragenter="handleDragEnter($event, item)"
        @dragend="handleDragEnd($event, item)"
    >
        <div class="wfc-outContent-XRZJ" @click="chooseTag(item)" :class="{'wfc-deleteStatus-XRZJ': i === checkedArr.length - 1 && deleteStatus}">
            <slot name="tag" v-bind:item="item">{{item[props.label]}}</slot>
            <span @click.stop="deleteChecked(item)" class="wfc-icon-XRZJ">
                <img src="./assets/crossIcon.svg" class="wfc-tagIcon-XRZJ" >
            </span>
        </div>
    </div>
    <input type="text" v-model.trim="searchVal" class="wfc-inInput-XRZJ" :placeholder="checkedArr.length ? '' : placeholder" @focus="handleFocus" @blur="loseFocus" @keydown.8="deleteOne" ref="inInput" v-on="$listeners">
</div>

dragInterVal: boolean = false
private divDragging(e: any) {
    if (this.dragInterVal) {
        return
    }
    if (e.x === 0 && e.y === 0) {
        return
    }
    if (e.layerY < 26 && this.domTemp.scrollTop > 0) {
        this.domTemp.scrollTop = this.domTemp.scrollTop - 24
        this.dragInterVal = true
        setTimeout(() => {
            this.dragInterVal = false
        }, 400);
    } else if (94 - e.layerY < 30 && this.domTemp.scrollHeight - this.domTemp.scrollTop - 94 > 0) {
        this.domTemp.scrollTop = this.domTemp.scrollTop + 24
        this.dragInterVal = true
        setTimeout(() => {
            this.dragInterVal = false
        }, 400);
    }
}
複製代碼

搜索結果變化

此功能爲根據搜索結果將下拉框內容變色,即模糊查詢時搜索結果中被查詢字段變色,雖然此功能不屬於下拉框組件內的內容,可是想拿出來講一下,提供一些思路。

單獨拿出v-html這個指令來講,應該不少人都知道有什麼做用,可是卻不多會使用到,此功能有了這個指令就變得異常簡單。
首先咱們能拿到輸入框內的內容(暫時命名爲searchVal),也能夠拿到根據此內容模糊查詢到的結果,此時咱們只需對此結果進行一個循環,用標籤的形式<span style="color: red;">searchVal<span>替換調結果中含有的searchVal的字段(經過replace方法和正則),並使用v-html綁定到頁面上便可實現此效果。代碼示例以下:

<template slot="option" slot-scope="{ item }">
    <div class="wfc-optionStyleXRZJrules">
        <img class="wfc-headPortraitXRZJrules" :src="item.photoBig">
        <div class="wfc-rightXRZJrules">
            <!-- 變色的內容 -->
            <div class="wfc-nameXRZJrules" v-html="item.newname"></div>
            <div class="wfc-emailXRZJrules">{{item.email}}</div>
            <div class="wfc-bottomXRZJrules">{{isEN ? item.org_name_mult_lang.en : item.org_name_mult_lang.zh}}</div>
        </div>
    </div>
</template>

this.options = searchUser.map((item: any) => {
    item.newname = item.name.replace(new RegExp(val, 'g'), `<span style="color: #3C8CFF;">${val}</span>`)
    return item
})
複製代碼

###其餘的內容暫時沒有想到須要拿出來講的問題,後面想到會進行補充,哪些寫的很差的地方請留言,感謝撥冗翻閱拙做,敬請斧正。

相關文章
相關標籤/搜索