(function() { /** * Levenshtein function courtesy of https://github.com/tad-lispy/node-damerau-levenshtein / https://www.npmjs.com/package/damerau-levenshtein */ function levenshtein(__this, that, limit) { var thisLength = __this.length, thatLength = that.length, matrix = []; // If the limit is not defined it will be calculate from this and that args. limit = (limit || ((thatLength > thisLength ? thatLength : thisLength)))+1; for (var i = 0; i < limit; i++) { matrix[i] = [i]; matrix[i].length = limit; } for (i = 0; i < limit; i++) { matrix[0][i] = i; } if (Math.abs(thisLength - thatLength) > (limit || 100)){ return prepare (limit || 100); } if (thisLength === 0){ return prepare (thatLength); } if (thatLength === 0){ return prepare (thisLength); } // Calculate matrix. var j, this_i, that_j, cost, min, t; for (i = 1; i <= thisLength; ++i) { this_i = __this[i-1]; // Step 4 for (j = 1; j <= thatLength; ++j) { // Check the jagged ld total so far if (i === j && matrix[i][j] > 4) return prepare (thisLength); that_j = that[j-1]; cost = (this_i === that_j) ? 0 : 1; // Step 5 // Calculate the minimum (much faster than Math.min(...)). min = matrix[i - 1][j ] + 1; // Devarion. if ((t = matrix[i ][j - 1] + 1 ) < min) min = t; // Insertion. if ((t = matrix[i - 1][j - 1] + cost) < min) min = t; // Substitution. // Update matrix. matrix[i][j] = (i > 1 && j > 1 && this_i === that[j-2] && __this[i-2] === that_j && (t = matrix[i-2][j-2]+cost) < min) ? t : min; // Transposition. } } return prepare (matrix[thisLength][thatLength]); /** * */ function prepare(steps) { var length = Math.max(thisLength, thatLength) var relative = length === 0 ? 0 : (steps / length); var similarity = 1 - relative return { steps: steps, relative: relative, similarity: similarity }; } } 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: '' } } var threshold = initial.threshold !== undefined ? initial.threshold : 0.5; // Split the searchVal into individual words. var splitSearch = searchVal.split(/ /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(/ /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--; } } // 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 var'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)) + "%" }; } $.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; if (initial.rankColumn !== undefined) { 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; } else if (initial.rankColumn !== undefined) { settings.aoData[dataIndex].anCells[initial.rankColumn].innerHTML = ''; settings.aoData[dataIndex]._aSortData[initial.rankColumn] = ''; } return true; } ); $(document).on('init.dt', function(e, settings) { var api = new $.fn.dataTable.Api(settings); var initial = api.init().fuzzySearch; // If this is not set then fuzzy searching is not enabled on the table so return. if(!initial) { return; } var fromPlugin = false; // Find the input element var input = $('div.dataTables_filter input', api.table().container()) var fontBold = { 'font-weight': '600', 'background-color': 'rgba(255,255,255,0.1)' }; var fontNormal = { 'font-weight': '500', 'background-color': 'transparent' }; var toggleCSS = { '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(initial.toggleSmart) { toggle =$('') .insertAfter(input) .css({ 'border': 'none', 'background': 'none', 'position': 'absolute', 'right': '0px', 'top': '4px', 'cursor': 'pointer', 'color': '#3b5e99', 'margin-top': '1px' }); exact =$('') .insertAfter(input) .css(toggleCSS) .css(fontBold) .attr('highlighted', true); fuzzy =$('') .insertAfter(input) .css(toggleCSS); input.css({ 'padding-right': '30px' }); label = $('
Search Type
').css({'padding-bottom': '0.5em', 'font-size': '0.8em'}) tooltip = $('
') .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); } function toggleFuzzy(event) { 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(event); } // Turn off the default datatables searching events $(settings.nTable).off('search.dt.DT'); // 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 ((event.type === 'input' && !initial.returnSearch) || 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; }) fromPlugin = true; api.search(input.val()); fromPlugin = false; } // 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, initial) }); fromPlugin = true; // Empty the datatables search and replace it with our own api.search(""); input.val(searchVal); fromPlugin = false; } fromPlugin = true; api.draw(); fromPlugin = false; } } var apiRegister = $.fn.dataTable.Api.register; apiRegister('search.fuzzy()', function(value) { if(value === undefined) { return input.val(); } else { input.val(value); triggerSearchFunction({key: 'Enter'}); return this; } }) input.off(); // Set listeners to occur on toggle and typing if(toggle) { // Highlights one of the buttons in the tooltip and un-highlights the other function highlightButton(toHighlight, event) { if(!toHighlight.attr('highlighted')){ toggleFuzzy(event) } } // Removes the tooltip element function removeToolTip() { tooltip.remove(); } // Actions for the toggle button toggle .on('click', toggleFuzzy) .on('mouseenter', function() { tooltip .insertAfter(toggle) .on('mouseleave', removeToolTip); exact.on('click', (event) => highlightButton(exact, event)); fuzzy.on('click', (event) => highlightButton(fuzzy, event)); }) .on('mouseleave', removeToolTip); // Actions for the input element input .on('mouseenter', function() { tooltip .insertAfter(toggle) .on('mouseleave', removeToolTip); exact.on('click', (event) => highlightButton(exact, event)) fuzzy.on('click', (event) => highlightButton(fuzzy, event)) }) .on('mouseleave', function() { var inToolTip = false; tooltip.on('mouseenter', () => inToolTip = true); toggle.on('mouseenter', () => inToolTip = true); setTimeout(function(){ if(!inToolTip) { removeToolTip(); } }, 250); }); var state = api.state.loaded(); api.on('stateSaveParams', function(e, settings, data) { data._fuzzySearch = { active: toggle.attr('blurred'), val: input.val() } }) if(state !== null && state._fuzzySearch !== undefined && state._fuzzySearch.active === 'true') { input.val(state._fuzzySearch.val); toggle.click(); } } api.on('search', function(){ if(!fromPlugin) { input.val(api.search()); } }) // Always add this event no matter if toggling is enabled input.on('input keydown', triggerSearchFunction); }) }());