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/fuzzySearch.js b/lib/base/features/fuzzy-search/fuzzySearch.js new file mode 100644 index 00000000..f3d7d93c --- /dev/null +++ b/lib/base/features/fuzzy-search/fuzzySearch.js @@ -0,0 +1,120 @@ +import Fuse from 'fuse.js/basic'; + +/** + * @typedef { { +* index: number; +* match: boolean; +* value: string; +* } } Token +* +* @typedef {Token[]} Tokens +*/ + +/** +* @template R +* +* @typedef { { +* item: R, +* tokens: Record +* } } SearchResult +*/ + +/** + * Search items using fuzzy search. + * + * @template T + * + * @param {T[]} items + * @param {string} pattern + * @param { { +* keys: string[]; +* } } options +* +* @returns {SearchResult[]} +*/ +export default function fuzzySearch(items, pattern, options) { + const fuse = new Fuse(items, { + includeScore: true, + ignoreLocation: true, + includeMatches: true, + threshold: 0.25, + keys: options.keys + }); + + const result = fuse.search(pattern); + + console.log(result); + + return result.map(({ item, matches }) => { + let tokens = {}; + + for (const key of options.keys) { + + if (item[key] && item[key].length) { + tokens = { + ...tokens, + [key]: matchesToTokens(matches, key, item[key]) + }; + } else { + tokens = { + ...tokens, + [key]: [] + }; + } + } + + return { + item, + tokens + }; + }); +}; + +function matchesToTokens(matches, key, value) { + const match = matches.find((match) => match.key === key); + + if (!match) { + return [ + { + index: 0, + value + } + ]; + } + + const { indices } = match; + + const tokensMatch = indices.map(([ start, end ]) => { + return { + index: start, + match: true, + value: match.value.slice(start, end + 1) + }; + }); + + const tokens = []; + + let lastIndex = 0; + + tokensMatch.forEach((token, index) => { + if (token.index !== lastIndex) { + tokens.push({ + index: lastIndex, + value: match.value.slice(lastIndex, token.index) + }); + } + + tokens.push(token); + + lastIndex = token.index + token.value.length; + + if (index === tokensMatch.length - 1 && lastIndex !== match.value.length) { + tokens.push({ + index: lastIndex, + value: match.value.slice(lastIndex) + }); + } + }); + + return tokens; +} \ 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..8f232b92 --- /dev/null +++ b/lib/base/features/fuzzy-search/index.js @@ -0,0 +1,5 @@ +import fuzzySearch from './fuzzySearch'; + +export default { + search: [ 'value', fuzzySearch ] +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3ac793d5..99b83f71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "diagram-js-grid": "^1.1.0", "diagram-js-minimap": "^5.2.0", "diagram-js-origin": "^1.4.0", + "fuse.js": "^7.0.0", "inherits-browser": "^0.1.0", "min-dash": "^4.2.2", "zeebe-bpmn-moddle": "^1.7.0" @@ -5821,6 +5822,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", @@ -18731,6 +18740,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 3d0c978b..3ab35b9f 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "diagram-js-grid": "^1.1.0", "diagram-js-minimap": "^5.2.0", "diagram-js-origin": "^1.4.0", + "fuse.js": "^7.0.0", "inherits-browser": "^0.1.0", "min-dash": "^4.2.2", "zeebe-bpmn-moddle": "^1.7.0"