You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Plugins/features/fuzzySearch/dataTables.fuzzySearch.js

450 lines
17 KiB
JavaScript

/*!
* Fuzzy Search for DataTables
* 2021 SpryMedia Ltd - datatables.net/license MIT license
*
* Damerau-Levenshtein function courtesy of https://github.com/tad-lispy/node-damerau-levenshtein
* BSD 2-Clause License
* Copyright (c) 2018, Tadeusz Łazurski
* All rights reserved.
*/
(function() {
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 (! initial) {
return true;
}
// 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();
var initialFuzzy = initial.fuzzySearch;
// If this is not set then fuzzy searching is not enabled on the table so return.
if(!initialFuzzy) {
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(initialFuzzy.toggleSmart) {
toggle =$('<button class="toggleSearch">Abc</button>')
.insertAfter(input)
.css({
'border': 'none',
'background': 'none',
'position': 'relative',
'right': '33px',
'top': '0px',
'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'
});
$(input.parent()).css('right', '-33px').css('position', 'relative');
label = $('<div>Search Type<div>').css({'padding-bottom': '0.5em', 'font-size': '0.8em'})
tooltip = $('<div class="fuzzyToolTip"></div>')
.css({
'position': 'absolute',
'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');
var fuzzySearchVal = '';
var searchVal = '';
// 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.search === undefined || !initial.search.return)) || event.key === "Enter" || event.type === 'click') {
// 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;
})
searchVal = input.val();
fuzzySearchVal = searchVal;
fromPlugin = true;
api.search(searchVal);
fromPlugin = false;
searchVal = "";
}
// Otherwise perform a fuzzy search
else {
// Get the value from the input element and convert to lower case
fuzzySearchVal = input.val();
searchVal = "";
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)
});
fromPlugin = true;
// Empty the datatables search and replace it with our own
api.search("");
input.val(fuzzySearchVal);
fromPlugin = false;
}
fromPlugin = true;
api.draw();
fromPlugin = false;
}
}
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)
});
// 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);
tooltip.css('left', input.position().left + 3)
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);
tooltip.css('left', input.position().left + 3)
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) {
input.val(state._fuzzySearch.val);
if (state._fuzzySearch.active === 'true') {
toggle.click();
api.page(state.start/state.length).draw('page');
}
}
}
api.on('search', function(){
if(!fromPlugin) {
input.val(api.search() !== searchVal ? api.search() : fuzzySearchVal);
}
})
// Always add this event no matter if toggling is enabled
input.on('input keydown', triggerSearchFunction);
})
}());