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.
385 lines
16 KiB
JavaScript
385 lines
16 KiB
JavaScript
3 years ago
|
(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; // Deletion.
|
||
|
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
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
fuzzySearch = function(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(/[^(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--;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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){
|
||
|
highest = {
|
||
|
pass: true,
|
||
|
score: splitSearch[x].length / splitData[y].length
|
||
|
};
|
||
|
}
|
||
|
|
||
|
// 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 rows have a score greater than 0.5
|
||
|
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;
|
||
|
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 {
|
||
|
settings.aoData[dataIndex].anCells[initial.rankColumn].innerHTML = '';
|
||
|
settings.aoData[dataIndex]._aSortData[initial.rankColumn] = '';
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
);
|
||
|
|
||
|
$(document).on('init.dt', function(e, settings) {
|
||
|
let api = new $.fn.dataTable.Api(settings);
|
||
|
var initial = api.init().fuzzySearch;
|
||
|
|
||
|
// If this is set then fuzzy searching is enabled on the table.
|
||
|
if (initial) {
|
||
|
// Find the input element
|
||
|
let input = $('div.dataTables_filter input', api.table().container())
|
||
|
|
||
|
let fontBold = {'font-weight': '600', 'background-color': 'rgba(255,255,255,0.1)'};
|
||
|
let fontNormal = {'font-weight': '500', 'background-color': 'transparent',};
|
||
|
let 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
|
||
|
let toggle, tooltip, exact, fuzzy, label;
|
||
|
if(initial.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);
|
||
|
}
|
||
|
|
||
|
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);
|
||
|
}
|
||
|
triggerSearchFunction();
|
||
|
}
|
||
|
|
||
|
// 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 (!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;
|
||
|
})
|
||
|
api.search(input.val())
|
||
|
api.draw();
|
||
|
}
|
||
|
// 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)
|
||
|
});
|
||
|
|
||
|
// Empty the datatables search and replace it with our own
|
||
|
api.search("");
|
||
|
input.val(searchVal);
|
||
|
api.draw();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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, noHighlight) {
|
||
|
if(!toHighlight.attr('highlighted')){
|
||
|
toHighlight.attr('highlighted', true);
|
||
|
toHighlight.css(fontBold);
|
||
|
noHighlight.css(fontNormal);
|
||
|
noHighlight.removeAttr('highlighted');
|
||
|
toggleFuzzy()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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', () => highlightButton(exact, fuzzy));
|
||
|
fuzzy.on('click', () => highlightButton(fuzzy, exact));
|
||
|
})
|
||
|
.on('mouseleave', removeToolTip);
|
||
|
|
||
|
// Actions for the input element
|
||
|
input
|
||
|
.on('mouseenter', function() {
|
||
|
tooltip
|
||
|
.insertAfter(toggle)
|
||
|
.on('mouseleave', removeToolTip);
|
||
|
exact.on('click', () => highlightButton(exact, fuzzy))
|
||
|
fuzzy.on('click', () => highlightButton(fuzzy, exact))
|
||
|
})
|
||
|
.on('mouseleave', function() {
|
||
|
var inToolTip = false;
|
||
|
tooltip.on('mouseenter', () => inToolTip = true);
|
||
|
toggle.on('mouseenter', () => inToolTip = true);
|
||
|
setTimeout(function(){
|
||
|
if(!inToolTip) {
|
||
|
removeToolTip();
|
||
|
}
|
||
|
}, 50)
|
||
|
});
|
||
|
|
||
|
var state = api.state.loaded();
|
||
|
|
||
|
|
||
|
if(state !== null && state._fuzzySearch === 'true') {
|
||
|
toggle.click();
|
||
|
}
|
||
|
|
||
|
api.on('stateSaveParams', function(e, settings, data) {
|
||
|
data._fuzzySearch = toggle.attr('blurred');
|
||
|
})
|
||
|
|
||
|
api.state.save();
|
||
|
}
|
||
|
|
||
|
// Always add this event no matter if toggling is enabled
|
||
|
input.on('input keydown', triggerSearchFunction);
|
||
|
}
|
||
|
})
|
||
|
}());
|
||
|
|