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 上使用模糊搜索插件,您可以通过在页面上的 script 标签中包含以下 javascript 来实现

JS

最后,(是的,它就是这么简单!)您需要将fuzzySearch初始化选项设置为true - 例如:

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

从这里您将发现模糊搜索已启用,并且拼写错误或错字不会强制从表格中删除记录。在将此选项初始化为布尔值时,视觉上不会发生任何变化,但通过使用其他选项,可以扩展更多功能。

选项

类型 选项 描述
booleanobject fuzzySearch 在表格上启用模糊搜索。
布尔值 fuzzySearch.toggleSmart 允许切换搜索模式 - 只需将鼠标悬停在输入元素上,然后从工具提示中选择所需的搜索模式。
列选择器 fuzzySearch.rankColumn 定义一列用于显示搜索词和匹配值之间的相似度。
数字 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()),它接受两个字符串参数并返回一个包含三个值的 Object,如下所示。

  • steps - 两个字符串之间的 Damerau-Levenshtein 距离
  • relative - 步数除以最长字符串的长度
  • similarity - 1 - relative的值

创建此插件的规范

在创建插件之前,仔细考虑我们想要提供的功能非常重要。

第一个显然是模糊搜索功能。鉴于 DataTables 已经有一个搜索框,此插件在搜索表格时应该重用它。这意味着最终用户需要更改的 UI 较少,并且可以保持界面简洁美观。

用户可能还需要能够在精确搜索和模糊搜索之间切换。为此,应将一个图标附加到搜索框,该图标能够指示搜索模式。将鼠标悬停在搜索框上时,应显示一个工具提示。此工具提示应包含两个按钮,用于适当地切换搜索模式。这不应该默认为启用状态,但用户应该能够通过使用fuzzySearch.toggleSmart初始化选项来启用它。

另一个很酷的功能是在表格中添加一列,显示输入字符串与该行中的数据的相似程度。这不应该默认为启用状态,但用户应该能够通过使用fuzzySearch.rankColumn初始化选项来指定要使用哪一列。

回车搜索是在论坛中经常出现的问题。在 DataTables 1.11 中,我们添加了search.return初始化选项。鉴于最初可能没有任何匹配项,因此插件还应与该初始化选项集成,并在按下 Enter 键后延迟搜索。这不会是默认行为。

搜索函数将使用从levenshtein()函数返回的similarity属性来决定是否显示行。此比较的阈值应该可以通过使用fuzzySearch.threshold初始化选项来设置。

另一个有用的功能是添加一个 API 方法,该方法可以获取和设置模糊搜索的搜索值。

最后,当stateSave初始化选项启用时,搜索模式应保存并在重置时恢复。

创建模糊搜索代码

拥有 npm 模块很棒,但鉴于其中的代码相当简单且简短(66 行),我们要做的第一件事就是将其提取出来并放置在我们自己的文件中。这将节省我们捆绑代码以在插件中发布。

现在我们可以开始编写我们自己的代码了。让我们首先编写一个fuzzySearch()函数,该函数将为给定行返回一个布尔值pass和一个scorepass值指示该行是否应包含在搜索结果中。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,并从中获取模糊搜索的初始化选项和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属性,则根据其中的pass值进行搜索。如果已定义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。请在论坛中给我们留言,告诉我们您对我们软件的使用情况,或者如果您遇到任何问题,或者对未来的增强功能有任何想法。我们很想了解人们是否能够将模糊搜索集成到他们的项目中以及您的客户反馈。