Skip to content

Commit 5f76612

Browse files
committed
WIP feat: add fuzzy search to popup menu and search pad
1 parent 99c1ad7 commit 5f76612

File tree

6 files changed

+188
-1
lines changed

6 files changed

+188
-1
lines changed

lib/base/Modeler.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {
1515
BpmnPropertiesProviderModule as bpmnPropertiesProviderModule
1616
} from 'bpmn-js-properties-panel';
1717

18+
import fuzzySearchModule from './features/fuzzy-search';
19+
1820
/**
1921
* @typedef {import('bpmn-js/lib/BaseViewer').BaseViewerOptions} BaseViewerOptions
2022
*
@@ -57,7 +59,8 @@ Modeler.prototype._extensionModules = [
5759
minimapModule,
5860
executableFixModule,
5961
propertiesPanelModule,
60-
bpmnPropertiesProviderModule
62+
bpmnPropertiesProviderModule,
63+
fuzzySearchModule
6164
];
6265

6366
Modeler.prototype._modules = [].concat(
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import Fuse from 'fuse.js/basic';
2+
3+
const options = {
4+
includeScore: true,
5+
ignoreLocation: true,
6+
includeMatches: true,
7+
threshold: 0.25,
8+
keys: [
9+
{
10+
name: 'label',
11+
weight: 3
12+
},
13+
{
14+
name: 'description',
15+
weight: 2
16+
},
17+
'search'
18+
]
19+
};
20+
21+
export default class FuzzySearchPopupMenuProvider {
22+
constructor(popupMenu) {
23+
popupMenu.registerProvider('bpmn-append', this);
24+
popupMenu.registerProvider('bpmn-create', this);
25+
popupMenu.registerProvider('bpmn-replace', this);
26+
}
27+
28+
getPopupMenuEntries() {
29+
return {};
30+
}
31+
32+
findPopupMenuEntries(entries, pattern) {
33+
const fuse = new Fuse(entries, options);
34+
35+
const result = fuse.search(pattern);
36+
37+
return result.map(({ item }) => item);
38+
}
39+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import Fuse from 'fuse.js/basic';
2+
3+
import {
4+
getLabel,
5+
isLabel
6+
} from 'bpmn-js/lib/util/LabelUtil';
7+
8+
const options = {
9+
includeScore: true,
10+
ignoreLocation: true,
11+
includeMatches: true,
12+
threshold: 0.25,
13+
keys: [
14+
{
15+
name: 'label',
16+
weight: 2
17+
},
18+
'id'
19+
]
20+
};
21+
22+
export default function BpmnSearchProvider(canvas, elementRegistry, searchPad) {
23+
this._canvas = canvas;
24+
this._elementRegistry = elementRegistry;
25+
26+
searchPad.registerProvider(this);
27+
}
28+
29+
BpmnSearchProvider.$inject = [
30+
'canvas',
31+
'elementRegistry',
32+
'searchPad'
33+
];
34+
35+
/**
36+
* @param {string} pattern
37+
*
38+
* @return {SearchResult[]}
39+
*/
40+
BpmnSearchProvider.prototype.find = function(pattern) {
41+
var rootElements = this._canvas.getRootElements();
42+
43+
var elements = this._elementRegistry
44+
.filter(function(element) {
45+
return !isLabel(element) && !rootElements.includes(element);
46+
})
47+
.map(function(element) {
48+
return {
49+
element,
50+
id: element.id,
51+
label: getLabel(element)
52+
};
53+
});
54+
55+
const fuse = new Fuse(elements, options);
56+
57+
const result = fuse.search(pattern);
58+
59+
return result.map(({ item }) => {
60+
const { element } = item;
61+
62+
return {
63+
element,
64+
primaryTokens: highlightSubstring(getLabel(element), pattern),
65+
secondaryTokens: highlightSubstring(element.id, pattern)
66+
};
67+
});
68+
};
69+
70+
function highlightSubstring(string, substring) {
71+
if (!substring.length) return [ { normal: string } ];
72+
73+
const occurances = findAllSubstringOccurrences(string, substring);
74+
75+
if (!occurances.length) return [ { normal: string } ];
76+
77+
let lastIndexEnd = 0;
78+
79+
const tokens = [];
80+
81+
occurances.forEach((start, index) => {
82+
const end = start + substring.length;
83+
84+
if (start !== 0) {
85+
tokens.push({
86+
normal: string.slice(lastIndexEnd, start)
87+
});
88+
}
89+
90+
tokens.push({
91+
matched: string.slice(start, end)
92+
});
93+
94+
if (index === occurances.length - 1 && end !== string.length - 1) {
95+
tokens.push({
96+
normal: string.slice(end)
97+
});
98+
}
99+
100+
lastIndexEnd = end;
101+
});
102+
103+
return tokens;
104+
}
105+
106+
function findAllSubstringOccurrences(string, subString) {
107+
let indices = [];
108+
let startIndex = 0;
109+
let index;
110+
111+
while (
112+
(index = string
113+
.toLowerCase()
114+
.indexOf(subString.toLowerCase(), startIndex)) > -1
115+
) {
116+
indices.push(index);
117+
118+
startIndex = index + 1;
119+
}
120+
121+
return indices;
122+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import FuzzySearchProvider from './FuzzySearchProvider';
2+
import FuzzySearchPopupMenuProvider from './FuzzySearchPopupMenuProvider';
3+
4+
export default {
5+
__init__: [ 'bpmnSearch', 'fuzzySearchPopupMenuProvider' ],
6+
bpmnSearch: [ 'type', FuzzySearchProvider ],
7+
fuzzySearchPopupMenuProvider: [ 'type', FuzzySearchPopupMenuProvider ]
8+
};

package-lock.json

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"diagram-js-grid": "^1.1.0",
7171
"diagram-js-minimap": "^5.2.0",
7272
"diagram-js-origin": "^1.4.0",
73+
"fuse.js": "^7.0.0",
7374
"inherits-browser": "^0.1.0",
7475
"min-dash": "^4.2.2",
7576
"zeebe-bpmn-moddle": "^1.7.0"

0 commit comments

Comments
 (0)