diff --git a/lib/base/Modeler.js b/lib/base/Modeler.js index b0121221..25d94940 100644 --- a/lib/base/Modeler.js +++ b/lib/base/Modeler.js @@ -15,6 +15,8 @@ import { BpmnPropertiesProviderModule as bpmnPropertiesProviderModule } from 'bpmn-js-properties-panel'; +import fuzzySearchModule from './features/fuzzy-search'; + /** * @typedef {import('bpmn-js/lib/BaseViewer').BaseViewerOptions} BaseViewerOptions * @@ -57,7 +59,8 @@ Modeler.prototype._extensionModules = [ minimapModule, executableFixModule, propertiesPanelModule, - bpmnPropertiesProviderModule + bpmnPropertiesProviderModule, + fuzzySearchModule ]; Modeler.prototype._modules = [].concat( diff --git a/lib/base/features/fuzzy-search/FuzzySearchPopupMenuProvider.js b/lib/base/features/fuzzy-search/FuzzySearchPopupMenuProvider.js new file mode 100644 index 00000000..59a5d636 --- /dev/null +++ b/lib/base/features/fuzzy-search/FuzzySearchPopupMenuProvider.js @@ -0,0 +1,39 @@ +import Fuse from 'fuse.js/basic'; + +const options = { + includeScore: true, + ignoreLocation: true, + includeMatches: true, + threshold: 0.25, + keys: [ + { + name: 'label', + weight: 3 + }, + { + name: 'description', + weight: 2 + }, + 'search' + ] +}; + +export default class FuzzySearchPopupMenuProvider { + constructor(popupMenu) { + popupMenu.registerProvider('bpmn-append', this); + popupMenu.registerProvider('bpmn-create', this); + popupMenu.registerProvider('bpmn-replace', this); + } + + getPopupMenuEntries() { + return {}; + } + + findPopupMenuEntries(entries, pattern) { + const fuse = new Fuse(entries, options); + + const result = fuse.search(pattern); + + return result.map(({ item }) => item); + } +} \ No newline at end of file diff --git a/lib/base/features/fuzzy-search/FuzzySearchProvider.js b/lib/base/features/fuzzy-search/FuzzySearchProvider.js new file mode 100644 index 00000000..ca0d55e8 --- /dev/null +++ b/lib/base/features/fuzzy-search/FuzzySearchProvider.js @@ -0,0 +1,122 @@ +import Fuse from 'fuse.js/basic'; + +import { + getLabel, + isLabel +} from 'bpmn-js/lib/util/LabelUtil'; + +const options = { + includeScore: true, + ignoreLocation: true, + includeMatches: true, + threshold: 0.25, + keys: [ + { + name: 'label', + weight: 2 + }, + 'id' + ] +}; + +export default function BpmnSearchProvider(canvas, elementRegistry, searchPad) { + this._canvas = canvas; + this._elementRegistry = elementRegistry; + + searchPad.registerProvider(this); +} + +BpmnSearchProvider.$inject = [ + 'canvas', + 'elementRegistry', + 'searchPad' +]; + +/** + * @param {string} pattern + * + * @return {SearchResult[]} + */ +BpmnSearchProvider.prototype.find = function(pattern) { + var rootElements = this._canvas.getRootElements(); + + var elements = this._elementRegistry + .filter(function(element) { + return !isLabel(element) && !rootElements.includes(element); + }) + .map(function(element) { + return { + element, + id: element.id, + label: getLabel(element) + }; + }); + + const fuse = new Fuse(elements, options); + + const result = fuse.search(pattern); + + return result.map(({ item }) => { + const { element } = item; + + return { + element, + primaryTokens: highlightSubstring(getLabel(element), pattern), + secondaryTokens: highlightSubstring(element.id, pattern) + }; + }); +}; + +function highlightSubstring(string, substring) { + if (!substring.length) return [ { normal: string } ]; + + const occurances = findAllSubstringOccurrences(string, substring); + + if (!occurances.length) return [ { normal: string } ]; + + let lastIndexEnd = 0; + + const tokens = []; + + occurances.forEach((start, index) => { + const end = start + substring.length; + + if (start !== 0) { + tokens.push({ + normal: string.slice(lastIndexEnd, start) + }); + } + + tokens.push({ + matched: string.slice(start, end) + }); + + if (index === occurances.length - 1 && end !== string.length - 1) { + tokens.push({ + normal: string.slice(end) + }); + } + + lastIndexEnd = end; + }); + + return tokens; +} + +function findAllSubstringOccurrences(string, subString) { + let indices = []; + let startIndex = 0; + let index; + + while ( + (index = string + .toLowerCase() + .indexOf(subString.toLowerCase(), startIndex)) > -1 + ) { + indices.push(index); + + startIndex = index + 1; + } + + return indices; +} \ No newline at end of file diff --git a/lib/base/features/fuzzy-search/index.js b/lib/base/features/fuzzy-search/index.js new file mode 100644 index 00000000..7f555141 --- /dev/null +++ b/lib/base/features/fuzzy-search/index.js @@ -0,0 +1,8 @@ +import FuzzySearchProvider from './FuzzySearchProvider'; +import FuzzySearchPopupMenuProvider from './FuzzySearchPopupMenuProvider'; + +export default { + __init__: [ 'bpmnSearch', 'fuzzySearchPopupMenuProvider' ], + bpmnSearch: [ 'type', FuzzySearchProvider ], + fuzzySearchPopupMenuProvider: [ 'type', FuzzySearchPopupMenuProvider ] +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a56eada3..48657c3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "diagram-js-grid": "^1.0.0", "diagram-js-minimap": "^5.1.0", "diagram-js-origin": "^1.4.0", + "fuse.js": "^7.0.0", "inherits-browser": "^0.1.0", "min-dash": "^4.2.1", "zeebe-bpmn-moddle": "^1.1.0" @@ -5632,6 +5633,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fuse.js": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.0.0.tgz", + "integrity": "sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==", + "engines": { + "node": ">=10" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -20089,6 +20098,11 @@ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true }, + "fuse.js": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.0.0.tgz", + "integrity": "sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==" + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", diff --git a/package.json b/package.json index 14470688..e9968e78 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "diagram-js-grid": "^1.0.0", "diagram-js-minimap": "^5.1.0", "diagram-js-origin": "^1.4.0", + "fuse.js": "^7.0.0", "inherits-browser": "^0.1.0", "min-dash": "^4.2.1", "zeebe-bpmn-moddle": "^1.1.0"