diff --git a/lib/features/popup-menu/PopupMenu.js b/lib/features/popup-menu/PopupMenu.js index d75bc7dde..892c20177 100644 --- a/lib/features/popup-menu/PopupMenu.js +++ b/lib/features/popup-menu/PopupMenu.js @@ -104,6 +104,7 @@ PopupMenu.prototype._render = function() { className, entries, headerEntries, + noSearchResultsCallback, options } = this._current; @@ -133,6 +134,7 @@ PopupMenu.prototype._render = function() { className=${ className } entries=${ entriesArray } headerEntries=${ headerEntriesArray } + noSearchResultsCallback=${ noSearchResultsCallback } scale=${ scale } onOpened=${ this._onOpened.bind(this) } onClosed=${ this._onClosed.bind(this) } @@ -171,7 +173,8 @@ PopupMenu.prototype.open = function(target, providerId, position, options) { const { entries, - headerEntries + headerEntries, + noSearchResultsCallback } = this._getContext(target, providerId); this._current = { @@ -180,6 +183,7 @@ PopupMenu.prototype.open = function(target, providerId, position, options) { target, entries, headerEntries, + noSearchResultsCallback, container: this._createContainer({ provider: providerId }), options }; @@ -204,9 +208,12 @@ PopupMenu.prototype._getContext = function(target, provider) { const headerEntries = this._getHeaderEntries(target, providers); + const noSearchResultsCallback = this._getNoSearchResultsCallback(providers); + return { entries, headerEntries, + noSearchResultsCallback, empty: !( Object.keys(entries).length || Object.keys(headerEntries).length @@ -509,6 +516,19 @@ PopupMenu.prototype._getHeaderEntries = function(target, providers) { }; +PopupMenu.prototype._getNoSearchResultsCallback = function(providers) { + var noSearchResultsCallback; + + forEach(providers, function(provider) { + if (isFunction(provider.getNoSearchResultsCallback)) { + noSearchResultsCallback = provider.getNoSearchResultsCallback(); + } + }); + + return noSearchResultsCallback; +}; + + /** * Check if the popup menu is open. * diff --git a/lib/features/popup-menu/PopupMenuComponent.js b/lib/features/popup-menu/PopupMenuComponent.js index 3773d0244..eaa2abfd8 100644 --- a/lib/features/popup-menu/PopupMenuComponent.js +++ b/lib/features/popup-menu/PopupMenuComponent.js @@ -20,6 +20,7 @@ import { isDefined } from 'min-dash'; /** * @typedef {import('./PopupMenuProvider').PopupMenuEntry} PopupMenuEntry * @typedef {import('./PopupMenuProvider').PopupMenuHeaderEntry} PopupMenuHeaderEntry + * @typedef {import('./PopupMenuProvider').PopupMenuNoSearchResultsCallback} PopupMenuNoSearchResultsCallback * * @typedef {import('../../util/Types').Point} Point */ @@ -36,6 +37,7 @@ import { isDefined } from 'min-dash'; * @param {number} props.scale * @param {string} [props.title] * @param {boolean} [props.search] + * @param {PopupMenuNoSearchResultsCallback} [props.noSearchResultsCallback] * @param {number} [props.width] */ export default function PopupMenuComponent(props) { @@ -49,6 +51,7 @@ export default function PopupMenuComponent(props) { width, scale, search, + noSearchResultsCallback, entries: originalEntries, onOpened, onClosed @@ -246,7 +249,7 @@ export default function PopupMenuComponent(props) { /> ${ entries.length === 0 && html` -
No matching entries found.
+
${ noSearchResultsCallback ? noSearchResultsCallback(value) : 'No matching entries found.' }
` } ` } diff --git a/lib/features/popup-menu/PopupMenuProvider.ts b/lib/features/popup-menu/PopupMenuProvider.ts index cbc7eeb44..c48b9f13a 100644 --- a/lib/features/popup-menu/PopupMenuProvider.ts +++ b/lib/features/popup-menu/PopupMenuProvider.ts @@ -1,3 +1,5 @@ +import { VNode } from '@bpmn-io/diagram-js-ui'; + import { PopupMenuTarget } from './PopupMenu'; export type PopupMenuEntryAction = (event: Event, entry: PopupMenuEntry, action?: string) => any; @@ -31,6 +33,8 @@ export type PopupMenuHeaderEntries = PopupMenuHeaderEntry[]; export type PopupMenuProviderHeaderEntriesCallback = (entries: PopupMenuHeaderEntries) => PopupMenuHeaderEntries; +export type PopupMenuNoSearchResultsCallback = (value: string) => VNode; + /** * An interface to be implemented by a popup menu provider. */ @@ -97,4 +101,17 @@ export default interface PopupMenuProvider { * @param target */ getHeaderEntries?(target: PopupMenuTarget): PopupMenuProviderHeaderEntriesCallback | PopupMenuHeaderEntries; + + /** + * Returns a callback that returns a VNode to be rendered when there are no search results. + * + * @example + * + * ```javascript + * getNoSearchResultsCallback() { + * return (value) => No results for { value }; + * } + * ``` + */ + getNoSearchResultsCallback?(): PopupMenuNoSearchResultsCallback; } \ No newline at end of file diff --git a/test/spec/features/popup-menu/PopupMenuSpec.js b/test/spec/features/popup-menu/PopupMenuSpec.js index df88d7474..d2af4e2b4 100755 --- a/test/spec/features/popup-menu/PopupMenuSpec.js +++ b/test/spec/features/popup-menu/PopupMenuSpec.js @@ -22,6 +22,8 @@ import { createEvent as globalEvent } from '../../../util/MockEvents'; import popupMenuModule from 'lib/features/popup-menu'; import modelingModule from 'lib/features/modeling'; +import { html } from 'lib/ui'; + describe('features/popup-menu', function() { @@ -1218,44 +1220,51 @@ describe('features/popup-menu', function() { expect(popupMenu.isOpen()).to.be.true; })); + }); - describe('search rank', function() { - var testMenuProvider = { - getEntries: function() { - return [ - { - id: 'A', - label: 'A' - }, - { - id: 'B', - label: 'B' - }, - { - id: 'C', - label: 'C' - }, - { - id: 'D', - label: 'D', - rank: 1 - }, - { - id: 'E', - label: 'E', - rank: 0 - }, - { - id: 'F', - label: 'F (hide initially)', - rank: -1 - } - ]; - } - }; + describe('search', function() { + + var testMenuProvider = { + getEntries: function() { + return [ + { + id: 'A', + label: 'A' + }, + { + id: 'B', + label: 'B' + }, + { + id: 'C', + label: 'C' + }, + { + id: 'D', + label: 'D', + rank: 1 + }, + { + id: 'E', + label: 'E', + rank: 0 + }, + { + id: 'F', + label: 'F (hide initially)', + rank: -1 + } + ]; + }, + getNoSearchResultsCallback: () => { + return value => html`

${ value }

`; + } + }; + describe('ranking', function() { + it('should hide rank < 0 items', inject(async function(popupMenu) { // given @@ -1288,6 +1297,23 @@ describe('features/popup-menu', function() { }); + + it('should render custom entry if no search results', inject(async function(popupMenu) { + + // given + popupMenu.registerProvider('test-menu', testMenuProvider); + popupMenu.open({}, 'test-menu', { x: 100, y: 100 }, { search: true }); + + // when + await triggerSearch('foobar'); + + // then + var node = queryPopup('#no-search-results-foobar'); + + expect(node).to.exist; + expect(node.textContent).to.eql('foobar'); + })); + });