2021 年 9 月 17 日星期五
作者:Sandy Galloway

模糊搜尋外掛

模糊搜尋用於搜尋引擎和資料庫中,執行與搜尋詞彙相似但不一定完全相同的結果的搜尋。這樣可以將拼寫錯誤和輸入錯誤考慮在內。它還可以讓方言的細微差異不會影響搜尋結果。一個常見的例子是用於搜尋姓氏;「Smith」和「Smythe」的發音相同,但是當使用精確比對搜尋時,輸入「Smith」將不會傳回「Smythe」。

此外掛為 DataTables 新增模糊搜尋功能。它透過精確比對和 Damerau-Levenshtein 演算法 的組合來完成此操作。

在我們深入研究之前,先預覽一下您可以從此外掛獲得什麼。以下範例以最簡單的形式初始化外掛 - 以新的模糊搜尋演算法取代 DataTables 標準的精確搜尋。

姓名職位辦公室薪資
Tiger Nixon系統架構師愛丁堡$320,800
Garrett Winters會計師東京$170,750
Ashton Cox初級技術作者舊金山$86,000
Cedric Kelly資深 Javascript 開發人員愛丁堡$433,060
Airi Satou會計師東京$162,700
Brielle Williamson整合專家紐約$372,000
Herrod Chandler銷售助理舊金山$137,500
Rhona Davidson整合專家東京$327,900
Colleen HurstJavascript 開發人員舊金山$205,500
Sonya Frost軟體工程師愛丁堡$103,600
Jena Gaines辦公室經理倫敦$90,560
Quinn Flynn支援負責人愛丁堡$342,000
Charde Marshall區域總監舊金山$470,600
Haley Kennedy資深行銷設計師倫敦$313,500
Tatyana Fitzpatrick區域總監倫敦$385,750
Michael Silva行銷設計師倫敦$198,500
Paul Byrd財務長 (CFO)紐約$725,000
Gloria Little系統管理員紐約$237,500
Bradley Greer軟體工程師倫敦$132,000
Dai Rios人事主管愛丁堡$217,500
Jenette Caldwell開發主管紐約$345,000
Yuri Berry行銷長 (CMO)紐約$675,000
Caesar Vance售前支援紐約$106,450
Doris Wilder銷售助理雪梨$85,600
Angelica Ramos執行長 (CEO)倫敦$1,200,000
Gavin Joyce開發人員愛丁堡$92,575
Jennifer Chang區域總監新加坡$357,650
Brenden Wagner軟體工程師舊金山$206,850
Fiona Green營運長 (COO)舊金山$850,000
Shou Itou區域行銷東京$163,000
Michelle House整合專家雪梨$95,400
Suki Burks開發人員倫敦$114,500
Prescott Bartlett技術作者倫敦$145,000
Gavin Cortez團隊負責人舊金山$235,500
Martena Mccray售後支援愛丁堡$324,050
Unity Butler行銷設計師舊金山$85,675
Howard Hatfield辦公室經理舊金山$164,500
Hope Fuentes秘書舊金山$109,850
Vivian Harrell財務總監舊金山$452,500
Timothy Mooney辦公室經理倫敦$136,200
Jackson Bradshaw總監紐約$645,750
Olivia Liang支援工程師新加坡$234,500
Bruno Nash軟體工程師倫敦$163,500
Sakura Yamamoto支援工程師東京$139,575
Thor Walton開發人員紐約$98,540
Finn Camacho支援工程師舊金山$87,500
Serge Baldwin資料協調員新加坡$138,575
Zenaida Frank軟體工程師紐約$125,250
Zorita Serrano軟體工程師舊金山$115,000
Jennifer Acosta初級 Javascript 開發人員愛丁堡$75,650
Cara Stevens銷售助理紐約$145,600
Hermione Butler區域總監倫敦$356,250
Lael Greer系統管理員倫敦$103,500
Jonas Alexander開發人員舊金山$86,500
Shad Decker區域總監愛丁堡$183,000
Michael BruceJavascript 開發人員新加坡$183,000
Donna Snider客戶支援紐約$112,000

快速入門

如果您想在您的 DataTable 上使用模糊搜尋外掛,您可以透過在您的頁面上包含以下 Javascript 程式碼 (使用 script 標籤) 來完成。

JS

最後(是的,就這麼簡單!),您需要將 fuzzySearch 初始化選項設定為 true - 例如。

$('#myTable').DataTable({
    fuzzySearch: true
})

從這裡,您會發現模糊搜尋已啟用,並且拼寫錯誤或輸入錯誤不會強制從表格中移除記錄。以布林值初始化此選項時,視覺上不會有任何變更,但是透過使用其他選項,還有額外功能的空間。

選項

類型 選項 描述
booleanobject fuzzySearch 在表格上啟用模糊搜尋。
boolean fuzzySearch.toggleSmart 允許切換搜尋模式 - 只需將滑鼠游標停留在輸入元素上方,並從工具提示中選取您想要的搜尋模式。
column-selector fuzzySearch.rankColumn 定義一個欄位,用於顯示搜尋詞彙與比對值之間的相似度。
number fuzzySearch.threshold 設定來自 Damerau-Levenshtein 演算法的比對閾值。值介於 0 和 1 之間。較低的數字表示不太精確的比對。預設值為 0.5。

範例

啟用 fuzzySearch.toggleSmart 選項後,終端使用者可以在 DataTables 的正常智慧搜尋和模糊搜尋之間切換,並透過指示器顯示他們所在的搜尋模式。

$('#fuzzy-toggle').DataTable({
    fuzzySearch: {
        toggleSmart: true
    }
});

姓名職位辦公室薪資
Tiger Nixon系統架構師愛丁堡$320,800
Garrett Winters會計師東京$170,750
Ashton Cox初級技術作者舊金山$86,000
Cedric Kelly資深 Javascript 開發人員愛丁堡$433,060
Airi Satou會計師東京$162,700
Brielle Williamson整合專家紐約$372,000
Herrod Chandler銷售助理舊金山$137,500
Rhona Davidson整合專家東京$327,900
Colleen HurstJavascript 開發人員舊金山$205,500
Sonya Frost軟體工程師愛丁堡$103,600
Jena Gaines辦公室經理倫敦$90,560
Quinn Flynn支援負責人愛丁堡$342,000
Charde Marshall區域總監舊金山$470,600
Haley Kennedy資深行銷設計師倫敦$313,500
Tatyana Fitzpatrick區域總監倫敦$385,750
Michael Silva行銷設計師倫敦$198,500
Paul Byrd財務長 (CFO)紐約$725,000
Gloria Little系統管理員紐約$237,500
Bradley Greer軟體工程師倫敦$132,000
Dai Rios人事主管愛丁堡$217,500
Jenette Caldwell開發主管紐約$345,000
Yuri Berry行銷長 (CMO)紐約$675,000
Caesar Vance售前支援紐約$106,450
Doris Wilder銷售助理雪梨$85,600
Angelica Ramos執行長 (CEO)倫敦$1,200,000
Gavin Joyce開發人員愛丁堡$92,575
Jennifer Chang區域總監新加坡$357,650
Brenden Wagner軟體工程師舊金山$206,850
Fiona Green營運長 (COO)舊金山$850,000
Shou Itou區域行銷東京$163,000
Michelle House整合專家雪梨$95,400
Suki Burks開發人員倫敦$114,500
Prescott Bartlett技術作者倫敦$145,000
Gavin Cortez團隊負責人舊金山$235,500
Martena Mccray售後支援愛丁堡$324,050
Unity Butler行銷設計師舊金山$85,675
Howard Hatfield辦公室經理舊金山$164,500
Hope Fuentes秘書舊金山$109,850
Vivian Harrell財務總監舊金山$452,500
Timothy Mooney辦公室經理倫敦$136,200
Jackson Bradshaw總監紐約$645,750
Olivia Liang支援工程師新加坡$234,500
Bruno Nash軟體工程師倫敦$163,500
Sakura Yamamoto支援工程師東京$139,575
Thor Walton開發人員紐約$98,540
Finn Camacho支援工程師舊金山$87,500
Serge Baldwin資料協調員新加坡$138,575
Zenaida Frank軟體工程師紐約$125,250
Zorita Serrano軟體工程師舊金山$115,000
Jennifer Acosta初級 Javascript 開發人員愛丁堡$75,650
Cara Stevens銷售助理紐約$145,600
Hermione Butler區域總監倫敦$356,250
Lael Greer系統管理員倫敦$103,500
Jonas Alexander開發人員舊金山$86,500
Shad Decker區域總監愛丁堡$183,000
Michael BruceJavascript 開發人員新加坡$183,000
Donna Snider客戶支援紐約$112,000

下一個範例會加入一個欄位,用於顯示透過使用 fuzzySearch.rankColumn 選項初始化所顯示的相似度,表格會依此排序,以提供搜尋引擎可能預期的輸出。

var fsrco = $('#fuzzy-ranking').DataTable({
    fuzzySearch: {
        rankColumn: 3
    },
    sort: [[3, 'desc']]
});

fsrco.on('draw', function(){
    fsrco.order([3, 'desc']);
});

姓名職位辦公室薪資
Tiger Nixon系統架構師愛丁堡$320,800
Garrett Winters會計師東京$170,750
Ashton Cox初級技術作者舊金山$86,000
Cedric Kelly資深 Javascript 開發人員愛丁堡$433,060
Airi Satou會計師東京$162,700
Brielle Williamson整合專家紐約$372,000
Herrod Chandler銷售助理舊金山$137,500
Rhona Davidson整合專家東京$327,900
Colleen HurstJavascript 開發人員舊金山$205,500
Sonya Frost軟體工程師愛丁堡$103,600
Jena Gaines辦公室經理倫敦$90,560
Quinn Flynn支援負責人愛丁堡$342,000
Charde Marshall區域總監舊金山$470,600
Haley Kennedy資深行銷設計師倫敦$313,500
Tatyana Fitzpatrick區域總監倫敦$385,750
Michael Silva行銷設計師倫敦$198,500
Paul Byrd財務長 (CFO)紐約$725,000
Gloria Little系統管理員紐約$237,500
Bradley Greer軟體工程師倫敦$132,000
Dai Rios人事主管愛丁堡$217,500
Jenette Caldwell開發主管紐約$345,000
Yuri Berry行銷長 (CMO)紐約$675,000
Caesar Vance售前支援紐約$106,450
Doris Wilder銷售助理雪梨$85,600
Angelica Ramos執行長 (CEO)倫敦$1,200,000
Gavin Joyce開發人員愛丁堡$92,575
Jennifer Chang區域總監新加坡$357,650
Brenden Wagner軟體工程師舊金山$206,850
Fiona Green營運長 (COO)舊金山$850,000
Shou Itou區域行銷東京$163,000
Michelle House整合專家雪梨$95,400
Suki Burks開發人員倫敦$114,500
Prescott Bartlett技術作者倫敦$145,000
Gavin Cortez團隊負責人舊金山$235,500
Martena Mccray售後支援愛丁堡$324,050
Unity Butler行銷設計師舊金山$85,675
Howard Hatfield辦公室經理舊金山$164,500
Hope Fuentes秘書舊金山$109,850
Vivian Harrell財務總監舊金山$452,500
Timothy Mooney辦公室經理倫敦$136,200
Jackson Bradshaw總監紐約$645,750
Olivia Liang支援工程師新加坡$234,500
Bruno Nash軟體工程師倫敦$163,500
Sakura Yamamoto支援工程師東京$139,575
Thor Walton開發人員紐約$98,540
Finn Camacho支援工程師舊金山$87,500
Serge Baldwin資料協調員新加坡$138,575
Zenaida Frank軟體工程師紐約$125,250
Zorita Serrano軟體工程師舊金山$115,000
Jennifer Acosta初級 Javascript 開發人員愛丁堡$75,650
Cara Stevens銷售助理紐約$145,600
Hermione Butler區域總監倫敦$356,250
Lael Greer系統管理員倫敦$103,500
Jonas Alexander開發人員舊金山$86,500
Shad Decker區域總監愛丁堡$183,000
Michael BruceJavascript 開發人員新加坡$183,000
Donna Snider客戶支援紐約$112,000

深入探討 - 建構外掛

使用我們的 FuzzySearch 外掛非常簡單,因此如果您對實作細節感興趣,讓我們深入探討它的運作方式,並且我們可以研究如何建立 自訂的列式篩選外掛

Damerau-Levenshtein 演算法

Damerau-Levenshtein 演算法 用於測量兩個序列之間的編輯距離。此演算法通常用於搜尋引擎、資料庫和拼字檢查器中,以更好地提高他們識別輸入中潛在錯誤的能力。我們不會在這裡深入探討這個演算法,我們需要知道的是它在我們的應用程式之前,已在許多應用程式中進行了嘗試和測試!

另一個很大的優點是它可在 npm 上取得,使其非常適合我們的使用案例。

npm 模組提供一個函式 (levenstein()),該函式採用兩個字串引數並傳回具有三個值的物件,如下所示。

  • steps - 兩個字串之間的 Damerau-Levenshtein 距離
  • relative - 步數除以最長字串的長度
  • similarity - 1 - relative 的值

建立此外掛的規格

在建立此外掛之前,仔細考量我們想要提供的功能非常重要。

第一個顯然是模糊搜尋功能。鑑於 DataTables 已經有一個搜尋方塊,此外掛應在搜尋表格時重複使用該方塊。這表示終端使用者的 UI 變更較少,並讓介面保持精簡。

使用者也可以在精確搜尋和模糊搜尋之間切換,這可能對使用者很有用。為此,應該在搜尋方塊附加一個圖示,該圖示能夠指示搜尋模式。當將滑鼠游標停留在搜尋方塊上方時,應顯示工具提示。此工具提示應包含兩個按鈕,以適當方式切換搜尋模式。這不應該是預設值,但使用者應該能夠使用 fuzzySearch.toggleSmart 初始化選項來啟用它。

另一個很酷的功能是表格中可以有一個欄位,顯示輸入字串與該列資料的相似程度。這不應該是預設值,但使用者應該能夠使用 fuzzySearch.rankColumn 初始化選項,指向要用於此功能的欄位。

在論壇中經常會出現按 Enter 鍵進行搜尋的需求。在 DataTables 1.11 中,我們加入了 search.return 初始化選項。鑑於一開始可能沒有任何比對,此外掛也應該與此初始化選項整合,並延遲搜尋直到按下 Enter 鍵。這不會是預設行為。

搜尋函式將使用從 levenshtein() 函式傳回的 similarity 屬性,以決定是否顯示列。此比較的閾值應該能夠使用 fuzzySearch.threshold 初始化選項來設定。

另一個有用的功能是新增一個 API 方法,該方法可以取得和設定模糊搜尋的搜尋值。

最後,當啟用 stateSave 初始化選項時,應儲存並重設搜尋模式。

建立模糊搜尋程式碼

擁有 npm 模組非常棒,但是鑑於其中的程式碼相當簡單且簡短(66 行),我們要做的第一件事是將它取出並放置在我們自己的檔案中。這將使我們省去在 plguins 中發佈的程式碼。

現在我們可以開始編寫自己的程式碼。首先,讓我們編寫一個 fuzzySearch() 函式,該函式將針對給定的列傳回布林值 passscorepass 值表示該列是否應包含在搜尋結果中。score 值是應該顯示在由 rankColumn 指示的相似度欄位中的值(如果已啟用)。

此函式採用 3 個參數。

  • searchVal 已輸入到搜尋方塊中的值
  • data 正在處理的列的資料
  • initial 使用的 fuzzySearch 初始化選項

要執行的第一個檢查是是否已定義 searchVal。如果沒有,我們希望顯示所有列,因此我們傳回 true 和空白分數。

function fuzzySearch(searchVal, data, initial) {
    // If no searchVal has been defined then return all rows.
    if(searchVal === undefined || searchVal.length === 0) {
        return {
            pass: true,
            score: ''
        }
    }
    ...
}

我們的搜尋演算法會將搜尋詞彙中的每個字詞與列資料中的每個字詞進行比較。如果每個搜尋字詞的至少一個組合都高於閾值,則應顯示該列。為此,我們分割搜尋詞彙並宣告和填入一個陣列,該陣列包含分數以及每個字詞是否通過。最初,這些值為 {pass: false, score: 0}。如果在分割後有任何空白字詞,我們不希望考量它們,因此會從陣列中移除這些字詞。

    ...
    // Split the searchVal into individual words.
    var splitSearch = searchVal.split(/[^(a-z|A-Z|0-9)]/g);

    // Array to keep scores in
    var highestCollated = [];

    // Remove any empty words or spaces
    for(var x = 0; x < splitSearch.length; x++) {
        if (splitSearch[x].length === 0 || splitSearch[x] === ' ') {
            splitSearch.splice(x, 1);
            x--;
        }
        // Aside - Add to the score collection if not done so yet for this search word
        else if (highestCollated.length < splitSearch.length) {
            highestCollated.push({pass: false, score: 0});
        }
    }
    ...

接下來,我們要對列資料執行一些非常類似的操作,逐一處理每個儲存格。

    ...
    // Going to check each cell for potential matches
    for(var i = 0; i < data.length; i++) {
        // Convert all data points to lower case fo insensitive sorting
        data[i] = data[i].toLowerCase();

        // Split the data into individual words
        var splitData = data[i].split(/[^(a-z|A-Z|0-9)]/g);

        // Remove any empty words or spaces
        for (var y = 0; y < splitData.length; y++){
            if(splitData[y].length === 0 || splitData[y] === ' ') {
                splitData.splice(y, 1);
                x--;
            }
        }
        ...

在上面顯示的相同 for 迴圈中,我們將在搜尋方塊中的字詞與我們剛才為此儲存格識別的字詞之間進行一些比較。以下顯示了進行比較的程式碼。

        ...
        // Check each search term word
        for(var x = 0; x < splitSearch.length; x++) {
            // Reset highest score
            var highest = {
                pass: undefined,
                score: 0
            };

            // Against each word in the cell
            for (var y = 0; y < splitData.length; y++){
                // If this search Term word is the beginning of the word in
                //  the cell we want to pass this word
                if(splitData[y].indexOf(splitSearch[x]) === 0){
                    var newScore = 
                        splitSearch[x].length / splitData[y].length;
                    highest = {
                        pass: true,
                        score: highest.score < newScore ?
                            newScore :
                            highest.score
                    };
                }

                // Get the levenshtein similarity score for the two words
                var steps =
                    levenshtein(splitSearch[x], splitData[y]).similarity;
                
                // If the levenshtein similarity score is better than a
                // previous one for the search word then let's store it
                if(steps > highest.score) {
                    highest.score = steps;
                }
            }

            // If this cell has a higher scoring word than previously found
            // to the search term in the row, store it
            if(highestCollated[x].score < highest.score || highest.pass) {
                highestCollated[x] = {
                    pass: highest.pass || highestCollated.pass ?
                        true :
                        highest.score > threshold,
                    score: highest.score
                };
            }
        }
    }
    ...

最後,我們檢查搜尋字詞是否在整個列的某個位置通過。

    // Check that all of the search words have passed
    for(var i = 0; i < highestCollated.length; i++) {
        if(!highestCollated[i].pass) {
            return {
                pass: false,
                score: Math.round(((highestCollated.reduce((a,b) => a+b.score, 0) / highestCollated.length) * 100)) + "%"
            };
        }
    }

    // If we get to here, all scores greater than 0.5 so display the row
    return {
        pass: true,
        score: Math.round(((highestCollated.reduce((a,b) => a+b.score, 0) / highestCollated.length) * 100)) + "%"
    };
}

由於 rankColumn 選項會在表格的其中一個欄位中顯示分數,因此由於 DataTables 執行其操作的順序,無法在搜尋函式中填入欄位。相反地,我們必須為 init 事件設定監聽器。

在這個事件的監聽器中,我們會建立一個 API,並從該 API 取得模糊搜尋的初始化選項以及 DataTables 的初始化物件。如果初始化選項中沒有定義 fuzzySearch,我們就可以在此處中止。否則,我們將繼續並識別此表格的輸入元素。

$(document).on('init.dt', function(e, settings) {
    var api = new $.fn.dataTable.Api(settings);
    var initial = api.init();
    var initialFuzzy = initial.fuzzySearch;

    // If this is not set then fuzzy searching is not enabled on the table so return.
    if(!initialFuzzy) {
        return;
    }

    // Find the input element
    var input = $('div.dataTables_filter input', api.table().container())
    ...

接下來,我們要移除 DataTables 的預設搜尋事件,並關閉針對此表格識別出的輸入元素的監聽器。然後,我們會定義自己的函式,該函式應在輸入或按下按鍵時觸發。

    // Turn off the default DataTables searching events
    $(settings.nTable).off('search.dt.DT');

    var fuzzySearchVal = ''; // Storage for the most recent fuzzy search value - ui or api set
    var searchVal = ''; // Storage for the most recent exact search value - ui or api set

    // The function that we want to run on search
    var triggerSearchFunction = function(event){
        ...
    }

    input.off();

    // Always add this event no matter if toggling is enabled
    input.on('input keydown', triggerSearchFunction);

triggerSearchFunction() 函式會針對每一列執行 fuzzySearch() 函式,並將結果儲存在該列的 DataTables 內部屬性中。我們必須在此強調,當您建立自己的搜尋外掛程式時,建議這樣做。然後,會呼叫 draw() 函式以觸發搜尋。

    // Get the value from the input element and convert to lower case
    fuzzySearchVal = input.val();
    searchVal = fuzzySearchVal; // Overwrite the value for search as ui interaction
    
    if (fuzzySearchVal !== undefined && fuzzySearchVal.length === 0) {
        fuzzySearchVal = fuzzySearchVal.toLowerCase();
    }
    
    // For each row call the fuzzy search function to get result
    api.rows().iterator('row', function(settings, rowIdx) {
        settings.aoData[rowIdx]._fuzzySearch = fuzzySearch(fuzzySearchVal, settings.aoData[rowIdx]._aFilterData, initialFuzzy)
    });

    // Empty the DataTables search and replace it with our own
    api.search("");
    input.val(fuzzySearchVal);
    api.draw();

現在,我們可以編寫在繪製時呼叫的函式。這與其他搜尋外掛程式非常相似。

如果已定義內部 _fuzzySearch 屬性,則搜尋會根據其中的傳遞值進行。如果已定義 rankColumn,則會填入該列的分數。如果未設定 _fuzzySearch 的內部屬性,則不會設定 HTML,並且所有列都傳回 true。

$.fn.dataTable.ext.search.push(
    function( settings, data, dataIndex ) {
        var initial = settings.oInit.fuzzySearch;
        // If fuzzy searching has not been implemented then pass all rows for this function
        if (settings.aoData[dataIndex]._fuzzySearch !== undefined) {
            // Read score to set the cell content and sort data
            var score = settings.aoData[dataIndex]._fuzzySearch.score;
            settings.aoData[dataIndex].anCells[initial.rankColumn].innerHTML = score;

            // Remove '%' from the end of the score so can sort on a number
            settings.aoData[dataIndex]._aSortData[initial.rankColumn] = +score.substring(0, score.length - 1);

            // Return the value for the pass as decided by the fuzzySearch function
            return settings.aoData[dataIndex]._fuzzySearch.pass;
        }

        settings.aoData[dataIndex].anCells[initial.rankColumn].innerHTML = '';
        settings.aoData[dataIndex]._aSortData[initial.rankColumn] = '';
        return true;
    }
);

接下來,我們要整合可以切換開啟和關閉模糊搜尋的功能。這需要一些 DOM 操作,這些操作在我們稍早設定的 init 監聽器中處理,緊接在識別輸入元素之後。

    var fontBold = {
        'font-weight': '600',
        'background-color': 'rgba(255,255,255,0.1)'
    };
    var fontNormal = {
        'font-weight': '500',
        'background-color': 'transparent'
    };
    var toggleDataTables = {
        'border': 'none',
        'background': 'none',
        'font-size': '100%',
        'width': '50%',
        'display': 'inline-block',
        'color': 'white',
        'cursor': 'pointer',
        'padding': '0.5em'
    }

    // Only going to set the toggle if it is enabled
    var toggle, tooltip, exact, fuzzy, label;
    if(initialFuzzy.toggleSmart) {
        toggle =$('<button class="toggleSearch">Abc</button>')
            .insertAfter(input)
            .css({
                'border': 'none',
                'background': 'none',
                'position': 'absolute',
                'right': '0px',
                'top': '4px',
                'cursor': 'pointer',
                'color': '#3b5e99',
                'margin-top': '1px'
            });
        exact =$('<button class="toggleSearch">Exact</button>')
            .insertAfter(input)
            .css(toggleCSS)
            .css(fontBold)
            .attr('highlighted', true);
        fuzzy =$('<button class="toggleSearch">Fuzzy</button>')
            .insertAfter(input)
            .css(toggleCSS);
        input.css({
            'padding-right': '30px'
        });
        label = $('<div>Search Type<div>').css({'padding-bottom': '0.5em', 'font-size': '0.8em'})
        tooltip = $('<div class="fuzzyToolTip"></div>')
            .css({
                'position': 'absolute',
                'right': '0px',
                'top': '2em',
                'background': 'white',
                'border-radius': '4px',
                'text-align': 'center',
                'padding': '0.5em',
                'background-color': '#16232a',
                'box-shadow': '4px 4px 4px rgba(0, 0, 0, 0.5)',
                'color': 'white',
                'transition': 'opacity 0.25s',                  
                'z-index': '30001'
            })
            .width(input.outerWidth() - 3)
            .append(label).append(exact).append(fuzzy);
    }

這會將圖示、按鈕和標籤插入到正確的位置,以便使用。CSS 在外掛程式中定義,因此不需要單獨的 CSS 檔案。

接下來,會定義一個函式,該函式會切換工具提示中哪個按鈕被反白顯示。這是透過新增自訂的「highlighted」屬性,以及一些上面宣告的額外 CSS 來完成的。圖示也會模糊化,以指示搜尋處於模糊模式。在函式結束時,會使用我們的 triggerSearchFunction() 呼叫來觸發搜尋。我們希望在發生切換時執行此操作,因為它通常會導致顯示不同的資料。

我們現在可以將程式碼新增至 triggerSearchFunction() 函式,以便在執行搜尋之前檢查搜尋模式。

var searchVal = '';
// If the toggle is set and isn't checkd then perform a normal search
if(toggle && !toggle.attr('blurred')) {
    api.rows().iterator('row', function(settings, rowIdx) {
        settings.aoData[rowIdx]._fuzzySearch = undefined;
    })
    api.search(input.val())
}
// Otherwise perform a fuzzy search
else {
    // Get the value from the input element and convert to lower case
    searchVal = input.val();
    
    if (searchVal !== undefined && searchVal.length === 0) {
        searchVal = searchVal.toLowerCase();
    }
    
    // For each row call the fuzzy search function to get result
    api.rows().iterator('row', function(settings, rowIdx) {
        settings.aoData[rowIdx]._fuzzySearch = fuzzySearch(searchVal, settings.aoData[rowIdx]._aFilterData, initialFuzzy)
    });

    // Empty the DataTables search and replace it with our own
    api.search("");
    input.val(searchVal);
}

api.draw();

現在,我們要將一些事件監聽器新增至我們新的 DOM 元素。為了盡量減少程式碼,我們先定義三個函式。

第一個函式 toggleFuzzy(),透過切換按鈕的狀態並觸發搜尋函式,來變更搜尋模式是模糊還是精確。

function toggleFuzzy() {
    if(toggle.attr('blurred')) {
        toggle.css({'filter': 'blur(0px)'}).removeAttr('blurred');
        fuzzy.removeAttr('highlighted').css(fontNormal);
        exact.attr('highlighted', true).css(fontBold);
    }
    else {
        toggle.css({'filter': 'blur(1px)'}).attr('blurred', true);
        exact.removeAttr('highlighted').css(fontNormal);
        fuzzy.attr('highlighted', true).css(fontBold);
    }

    // Whenever the search mode is changed we need to re-search
    triggerSearchFunction();
}

第二個函式 highlightButton(),接受一個參數,即要反白顯示的按鈕。如果未反白顯示,則會呼叫 toggleFuzzy 函式。

// Highlights one of the buttons in the tooltip and un-highlights the other
function highlightButton(toHighlight) {
    if(!toHighlight.attr('highlighted')){
        toggleFuzzy()
    }
}

第三個函式 removeToolTip() 會從頁面移除工具提示。

// Removes the tooltip element
function removeToolTip() {
    tooltip.remove();
}

切換圖示有三個事件監聽器。第一個監聽器位於 click 事件上,且只會呼叫 toggleFuzzy。這表示當點擊切換圖示時,搜尋模式將會變更,並且會更新結果。第二個是 mouseenter 事件。當此事件發生時,會呼叫以下函式。

function() {
    tooltip
        .insertAfter(toggle)
        .on('mouseleave', removeToolTip);
    exact.on('click',  () => highlightButton(exact, fuzzy));
    fuzzy.on('click', () => highlightButton(fuzzy, exact));
}

這會插入工具提示,並設定一個事件監聽器,以便在滑鼠離開時移除自身。然後,它也會設定 highlightButton 函式,以便在點擊其中一個切換按鈕時執行。

切換圖示上的最後一個事件監聽器是針對 mouseleave,當此事件發生時,工具提示將會移除。

搜尋方塊有兩個事件監聽器。第一個監聽器用於 mouseenter 事件,且與切換圖示相同。第二個監聽器用於 mouseleave,這與之前略有不同。

function() {
    var inToolTip = false;
    tooltip.on('mouseenter', () => inToolTip = true);
    toggle.on('mouseenter', () => inToolTip = true);
    setTimeout(function(){
        if(!inToolTip) {
            removeToolTip();
        }
    }, 50)
}

此函式會為切換圖示和工具提示設定 mouseenter 的事件監聽器。如果滑鼠在 50 毫秒內進入其中任何一個,則不會移除工具提示。否則,工具提示確實會隱藏。

這裡最後新增的是處理 stateSave。首先,使用 state.loaded() 擷取載入的狀態。然後,針對 stateSaveParams 設定一個監聽器,以便未來可以儲存搜尋模式的目前狀態。然後,檢查目前的狀態,以查看是否將 _fuzzySearch 屬性設定為 true。如果是,則會點擊切換按鈕以變更為模糊搜尋。

var state = api.state.loaded();

api.on('stateSaveParams', function(e, settings, data) {
    data._fuzzySearch = toggle.attr('blurred');
})

if(state !== null && state._fuzzySearch === 'true') {
    toggle.click();
}

接下來,我們可以新增 search.return 初始化選項的功能。這涉及對 triggerSearchFunction 的最後一個變更,以檢查按下了哪個按鍵。這是一個小的變更,導致產生以下函式。

// The function that we want to run on search
var triggerSearchFunction = function(event){
    // If the search is only to be triggered on return wait for that
    if (!initial.search.return || event.key === "Enter") {
        var searchVal = '';
        // If the toggle is set and isn't checkd then perform a normal search
        if(toggle && !toggle.attr('blurred')) {
            api.rows().iterator('row', function(settings, rowIdx) {
                settings.aoData[rowIdx]._fuzzySearch = undefined;
            })
            api.search(input.val())
        }
        // Otherwise perform a fuzzy search
        else {
            // Get the value from the input element and convert to lower case
            searchVal = input.val();
            
            if (searchVal !== undefined && searchVal.length === 0) {
                searchVal = searchVal.toLowerCase();
            }
            
            // For each row call the fuzzy search function to get result
            api.rows().iterator('row', function(settings, rowIdx) {
                settings.aoData[rowIdx]._fuzzySearch = fuzzySearch(searchVal, settings.aoData[rowIdx]._aFilterData, initialFuzzy)
            });

            // Empty the DataTables search and replace it with our own
            api.search("");
            input.val(searchVal);
        }

        api.draw();
    }
}

最後一步是實作將取得或設定模糊搜尋值的 API 方法。同樣地,我們將在 init 監聽器內執行此操作。我們透過存取 API 註冊函式來完成此操作。此函式接受兩個引數。第一個引數是在 API 執行個體內應採取的路徑,以存取 API 方法。第二個引數是呼叫 API 方法時應執行的動作。

var apiRegister = $.fn.dataTable.Api.register;
apiRegister('search.fuzzy()', function(value) {
    ...
})

然後,我們要新增用於擷取模糊搜尋值的行為。如果傳入的參數未定義,則這是應採取的路徑。

var apiRegister = $.fn.dataTable.Api.register;
apiRegister('search.fuzzy()', function(value) {
    if(value === undefined) {
        return fuzzySearchVal;
    }
    ...
})

否則,會設定值,因此會遵循稍微不同的路徑。模糊搜尋值會輸入到輸入框中並記錄下來,同時記錄目前的搜尋值。然後,使用迭代器根據新值搜尋所有模糊搜尋詳細資訊。

var apiRegister = $.fn.dataTable.Api.register;
apiRegister('search.fuzzy()', function(value) {
    if(value === undefined) {
        return fuzzySearchVal;
    }
    else {
        fuzzySearchVal = value.toLowerCase();
        searchVal = api.search();
        input.val(fuzzySearchVal);
        
        // For each row call the fuzzy search function to get result
        api.rows().iterator('row', function(settings, rowIdx) {
            settings.aoData[rowIdx]._fuzzySearch = fuzzySearch(fuzzySearchVal, settings.aoData[rowIdx]._aFilterData, initialFuzzy)
        });

        return this;
    }
})

最後一個環節是將最新的搜尋值新增至輸入元素。這是透過設定 search 的監聽器來完成的。

api.on('search', function(){
    if(!fromPlugin) {
        input.val(api.search() !== searchVal ? api.search() : fuzzySearchVal);
    }
})

布林標誌 fromPlugin 用於防止當外掛程式導致搜尋時發生無限迴圈。此標誌在 triggerSearchFunction() 函式中設定,只需在每次 search/ draw 之前將值設定為 true,之後設定為 false 即可。然後,根據 DataTables 儲存的目前搜尋值、發生模糊搜尋時的最後搜尋值以及最後的模糊搜尋值來設定輸入值。

如果 DataTables 內儲存的目前搜尋值與我們看到的最後搜尋值不符,則表示自上次更新以來,該值已更新,因此更新的搜尋值會比較新。如果兩者相等,則模糊搜尋值會比較新,因此應顯示該值。

就是這樣。建立複雜的基於列的搜尋外掛程式所需的一切。完整檔案可在 [cdn] 上取得,因此您可以查看完整的流程以及整合在一起的所有部分。

限制

由於 FuzzySearch 執行的篩選全部在用戶端完成,因此此外掛程式支援伺服器端處理。

意見反應

一如既往,我們很樂意聽到您如何使用 DataTable。請在論壇中給我們留言,說明您使用我們軟體的情況,或者您是否遇到任何問題,或對未來的增強功能有任何想法。我們很想知道是否有人能夠將模糊搜尋整合到他們的專案中,以及您的客戶意見反應。