Skip to content

Commit

Permalink
feat(popup-menu): sort matches by location
Browse files Browse the repository at this point in the history
  • Loading branch information
philippfromme committed Jun 5, 2024
1 parent aeb84c6 commit 4e4f201
Show file tree
Hide file tree
Showing 3 changed files with 239 additions and 14 deletions.
62 changes: 48 additions & 14 deletions lib/features/popup-menu/PopupMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ import {
isDefined
} from 'min-dash';

import {
compareStrings,
compareTokens,
findMatches,
hasMatch
} from '../../util/SearchUtil';

import PopupMenuComponent from './PopupMenuComponent';

/**
Expand Down Expand Up @@ -639,18 +646,45 @@ PopupMenu.prototype._getEntry = function(entryId) {
return entry;
};

function defaultFindEntries(entries, value) {
return entries.filter(entry => {
const searchableFields = [
entry.description || '',
entry.label || '',
entry.search || ''
].map(string => string.toLowerCase());

// every word of `value` should be included in one of the searchable fields
return value
.toLowerCase()
.split(/\s/g)
.every(word => searchableFields.some(field => field.includes(word)));
});
function defaultFindEntries(entries, pattern) {
return entries
.reduce(function(results, entry) {
var label = entry.label || '',
description = entry.description || '',
search = entry.search || '';

var primaryTokens = findMatches(label, pattern),
secondaryTokens = findMatches(description, pattern),
tertiaryTokens = findMatches(search, pattern);

if (hasMatch(primaryTokens) || hasMatch(secondaryTokens) || hasMatch(tertiaryTokens)) {
return [
...results,
{
primaryTokens,
secondaryTokens,
tertiaryTokens,
entry
}
];
}

return results;
}, [])
.sort(function(a, b) {
var aLabel = a.entry.label || '',
aDescription = a.entry.description || '';

var bLabel = b.entry.label || '',
bDescription = b.entry.description || '';

return compareTokens(a.primaryTokens, b.primaryTokens)
|| compareTokens(a.secondaryTokens, b.secondaryTokens)
|| compareTokens(a.tertiaryTokens, b.tertiaryTokens)
|| compareStrings(aLabel, bLabel)
|| compareStrings(aDescription, bDescription);
})
.map(function(result) {
return result.entry;
});
}
122 changes: 122 additions & 0 deletions lib/util/SearchUtil.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* @typedef { {
* normal?: string;
* matched?: string;
* } } Token
*/

/**
* @param {Token} token
*
* @return {boolean}
*/
export function isMatch(token) {
return 'matched' in token;
}

/**
* @param {Token[]} tokens
*
* @return {boolean}
*/
export function hasMatch(tokens) {
return tokens.find(isMatch);
}

/**
* Compares two token arrays.
*
* @param {Token[]} tokensA
* @param {Token[]} tokensB
*
* @returns {number}
*/
export function compareTokens(tokensA, tokensB) {
const tokensAHasMatch = hasMatch(tokensA),
tokensBHasMatch = hasMatch(tokensB);

if (tokensAHasMatch && !tokensBHasMatch) {
return -1;
}

if (!tokensAHasMatch && tokensBHasMatch) {
return 1;
}

if (!tokensAHasMatch && !tokensBHasMatch) {
return 0;
}

const tokensAFirstMatch = tokensA.find(isMatch),
tokensBFirstMatch = tokensB.find(isMatch);

if (tokensAFirstMatch.index < tokensBFirstMatch.index) {
return -1;
}

if (tokensAFirstMatch.index > tokensBFirstMatch.index) {
return 1;
}

return 0;
}

/**
* Compares two strings.
*
* @param {string} a
* @param {string} b
*
* @returns {number}
*/
export function compareStrings(a, b) {
return a.localeCompare(b);
}

/**
* @param {string} text
* @param {string} pattern
*
* @return {Token[]}
*/
export function findMatches(text, pattern) {
var tokens = [],
originalText = text;

if (!text) {
return tokens;
}

text = text.toLowerCase();
pattern = pattern.toLowerCase();

var index = text.indexOf(pattern);

if (index > -1) {
if (index !== 0) {
tokens.push({
normal: originalText.slice(0, index),
index: 0
});
}

tokens.push({
matched: originalText.slice(index, index + pattern.length),
index: index
});

if (pattern.length + index < text.length) {
tokens.push({
normal: originalText.slice(index + pattern.length),
index: index + pattern.length
});
}
} else {
tokens.push({
normal: originalText,
index: 0
});
}

return tokens;
}
69 changes: 69 additions & 0 deletions test/spec/features/popup-menu/PopupMenuSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1674,6 +1674,75 @@ describe('features/popup-menu', function() {
expect(shownEntries[0].querySelector('.djs-popup-label').textContent).to.eql('Alpha');
}));


describe('sorting', function() {

it('should order entries', inject(async function(popupMenu) {

// given
var testMenuProvider = {
getPopupMenuEntries: function() {
return {
a: {
id: 'a',
label: 'bar foo'
},
b: {
id: 'b',
label: 'foo baz'
},
c: {
id: 'c',
label: 'bar',
description: 'bar foo'
},
d: {
id: 'd',
label: 'bar',
description: 'foo baz'
},
e: {
id: 'e',
label: 'bar',
description: 'foo bar'
},
f: {
id: 'f',
label: 'foo bar'
},
g: {
id: 'g',
label: 'bar'
}
};
}
};

popupMenu.registerProvider('test-menu', testMenuProvider);
popupMenu.open({}, 'test-menu', { x: 100, y: 100 }, { search: true });

// when
await triggerSearch('foo');

// then
var shownEntries;

await waitFor(() => {
shownEntries = queryPopupAll('.entry');

expect(shownEntries).to.have.length(6);
});

expect(shownEntries[0].getAttribute('data-id')).to.eql('f');
expect(shownEntries[1].getAttribute('data-id')).to.eql('b');
expect(shownEntries[2].getAttribute('data-id')).to.eql('a');
expect(shownEntries[3].getAttribute('data-id')).to.eql('e');
expect(shownEntries[4].getAttribute('data-id')).to.eql('d');
expect(shownEntries[5].getAttribute('data-id')).to.eql('c');
}));

});

});


Expand Down

0 comments on commit 4e4f201

Please sign in to comment.