diff --git a/packages/dmn-js-drd/src/NavigatedViewer.js b/packages/dmn-js-drd/src/NavigatedViewer.js index 49501422f..2cf5e1fbf 100644 --- a/packages/dmn-js-drd/src/NavigatedViewer.js +++ b/packages/dmn-js-drd/src/NavigatedViewer.js @@ -2,6 +2,11 @@ import inherits from 'inherits-browser'; import Viewer from './Viewer'; +import ZoomScroll from 'diagram-js/lib/navigation/zoomscroll'; +import MoveCanvas from 'diagram-js/lib/navigation/movecanvas'; +import TouchModule from 'diagram-js/lib/navigation/touch'; + +import DmnSearchModule from './features/search'; /** * A viewer that includes mouse navigation facilities @@ -14,14 +19,12 @@ export default function NavigatedViewer(options) { inherits(NavigatedViewer, Viewer); -import ZoomScroll from 'diagram-js/lib/navigation/zoomscroll'; -import MoveCanvas from 'diagram-js/lib/navigation/movecanvas'; -import TouchModule from 'diagram-js/lib/navigation/touch'; NavigatedViewer.prototype._navigationModules = [ ZoomScroll, MoveCanvas, - TouchModule + TouchModule, + DmnSearchModule ]; NavigatedViewer.prototype._modules = [].concat( diff --git a/packages/dmn-js-drd/src/features/editor-actions/DrdEditorActions.js b/packages/dmn-js-drd/src/features/editor-actions/DrdEditorActions.js index 91e78e237..031c97218 100644 --- a/packages/dmn-js-drd/src/features/editor-actions/DrdEditorActions.js +++ b/packages/dmn-js-drd/src/features/editor-actions/DrdEditorActions.js @@ -26,14 +26,15 @@ DrdEditorActions.prototype._registerDefaultActions = function(injector) { // (1) retrieve optional components to integrate with - var canvas = injector.get('canvas', false); - var elementRegistry = injector.get('elementRegistry', false); - var selection = injector.get('selection', false); - var lassoTool = injector.get('lassoTool', false); - var handTool = injector.get('handTool', false); - var directEditing = injector.get('directEditing', false); - var distributeElements = injector.get('distributeElements', false); - var alignElements = injector.get('alignElements', false); + const canvas = injector.get('canvas', false), + elementRegistry = injector.get('elementRegistry', false), + selection = injector.get('selection', false), + lassoTool = injector.get('lassoTool', false), + handTool = injector.get('handTool', false), + directEditing = injector.get('directEditing', false), + distributeElements = injector.get('distributeElements', false), + alignElements = injector.get('alignElements', false), + searchPad = injector.get('searchPad', false); // (2) check components and register actions @@ -97,4 +98,10 @@ DrdEditorActions.prototype._registerDefaultActions = function(injector) { } }); } + + if (selection && searchPad) { + this._registerAction('find', function() { + searchPad.toggle(); + }); + } }; diff --git a/packages/dmn-js-drd/src/features/keyboard/DrdKeyboardBindings.js b/packages/dmn-js-drd/src/features/keyboard/DrdKeyboardBindings.js index 8b314d9fd..41be7650b 100644 --- a/packages/dmn-js-drd/src/features/keyboard/DrdKeyboardBindings.js +++ b/packages/dmn-js-drd/src/features/keyboard/DrdKeyboardBindings.js @@ -109,4 +109,16 @@ DrdKeyboardBindings.prototype.registerBindings = function(keyboard, editorAction } }); + // search labels + // CTRL + F + addListener('find', function(context) { + + var event = context.keyEvent; + + if (keyboard.isKey([ 'f', 'F' ], event) && keyboard.isCmd(event)) { + editorActions.trigger('find'); + + return true; + } + }); }; \ No newline at end of file diff --git a/packages/dmn-js-drd/src/features/search/DmnSearchProvider.js b/packages/dmn-js-drd/src/features/search/DmnSearchProvider.js new file mode 100644 index 000000000..8d7a81858 --- /dev/null +++ b/packages/dmn-js-drd/src/features/search/DmnSearchProvider.js @@ -0,0 +1,139 @@ +import { + map, + filter, + sortBy +} from 'min-dash'; + +import { + getLabel +} from '../label-editing/LabelUtil'; + +/** + * @typedef {import('diagram-js/lib/core/Canvas').default} Canvas + * @typedef {import('diagram-js/lib/core/ElementRegistry').default} ElementRegistry + * @typedef {import('diagram-js/lib/features/search-pad/SearchPad').default} SearchPad + * + * @typedef {import('diagram-js/lib/features/search-pad/SearchPadProvider').default + * } SearchPadProvider + * @typedef {import('diagram-js/lib/features/search-pad/SearchPadProvider').SearchResult + * } SearchResult + */ + +/** + * Provides ability to search for DMN elements. + * + * @implements {SearchPadProvider} + * + * @param {ElementRegistry} elementRegistry + * @param {SearchPad} searchPad + * @param {Canvas} canvas + */ +export default function DmnSearchProvider(elementRegistry, searchPad, canvas) { + this._elementRegistry = elementRegistry; + this._canvas = canvas; + + searchPad.registerProvider(this); +} + +DmnSearchProvider.$inject = [ + 'elementRegistry', + 'searchPad', + 'canvas' +]; + +/** + * @param {string} pattern + * + * @return {SearchResult[]} + */ +DmnSearchProvider.prototype.find = function(pattern) { + const rootElement = this._canvas.getRootElement(); + + let elements = this._elementRegistry.filter(function(element) { + if (element.labelTarget) { + return false; + } + return true; + }); + + // do not include root element + elements = filter(elements, function(element) { + return element !== rootElement; + }); + + elements = map(elements, function(element) { + return { + primaryTokens: matchAndSplit(getLabel(element), pattern), + secondaryTokens: matchAndSplit(element.id, pattern), + element: element + }; + }); + + // exclude non-matched elements + elements = filter(elements, function(element) { + return hasMatched(element.primaryTokens) || hasMatched(element.secondaryTokens); + }); + + elements = sortBy(elements, function(element) { + return getLabel(element.element) + element.element.id; + }); + + return elements; +}; + +/** + * @param {Token[]} tokens + * + * @return {boolean} + */ +function hasMatched(tokens) { + const matched = filter(tokens, function(token) { + return !!token.matched; + }); + + return matched.length > 0; +} + +/** + * @param {string} text + * @param {string} pattern + * + * @return {Token[]} + */ +function matchAndSplit(text, pattern) { + const tokens = [], + originalText = text; + + if (!text) { + return tokens; + } + + text = text.toLowerCase(); + pattern = pattern.toLowerCase(); + + const i = text.indexOf(pattern); + + if (i > -1) { + if (i !== 0) { + tokens.push({ + normal: originalText.substr(0, i) + }); + } + + tokens.push({ + matched: originalText.substr(i, pattern.length) + }); + + if (pattern.length + i < text.length) { + tokens.push({ + normal: originalText.substr(pattern.length + i, text.length) + }); + } + } else { + tokens.push({ + normal: originalText + }); + } + + return tokens; +} \ No newline at end of file diff --git a/packages/dmn-js-drd/src/features/search/index.js b/packages/dmn-js-drd/src/features/search/index.js new file mode 100644 index 000000000..7edaa08f9 --- /dev/null +++ b/packages/dmn-js-drd/src/features/search/index.js @@ -0,0 +1,12 @@ +import SearchPadModule from 'diagram-js/lib/features/search-pad'; + +import DmnSearchProvider from './DmnSearchProvider'; + + +export default { + __depends__: [ + SearchPadModule + ], + __init__: [ 'dmnSearch' ], + dmnSearch: [ 'type', DmnSearchProvider ] +}; diff --git a/packages/dmn-js-drd/test/spec/ModelerSpec.js b/packages/dmn-js-drd/test/spec/ModelerSpec.js index 4352968cd..9d49e5e89 100644 --- a/packages/dmn-js-drd/test/spec/ModelerSpec.js +++ b/packages/dmn-js-drd/test/spec/ModelerSpec.js @@ -163,7 +163,8 @@ describe('Modeler', function() { 'alignElements', 'lassoTool', 'handTool', - 'directEditing' + 'directEditing', + 'find' ]; // when diff --git a/packages/dmn-js-drd/test/spec/features/editor-actions/DrdEditorActionsSpec.js b/packages/dmn-js-drd/test/spec/features/editor-actions/DrdEditorActionsSpec.js index b680b93a0..1c7c1fbd4 100644 --- a/packages/dmn-js-drd/test/spec/features/editor-actions/DrdEditorActionsSpec.js +++ b/packages/dmn-js-drd/test/spec/features/editor-actions/DrdEditorActionsSpec.js @@ -12,6 +12,7 @@ import handToolModule from 'diagram-js/lib/features/hand-tool'; import distributeElementsModule from 'src/features/distribute-elements'; import coreModule from 'src/core'; import lassoTool from 'diagram-js/lib/features/lasso-tool'; +import searchModule from 'src/features/search'; var diagramXML = require('./DrdEditorActions.dmn'); @@ -27,7 +28,8 @@ describe('features/editor-actions', function() { distributeElementsModule, lassoToolModule, handToolModule, - lassoTool + lassoTool, + searchModule ] })); @@ -116,4 +118,24 @@ describe('features/editor-actions', function() { }); + + describe('lassoTool', function() { + + it('should toggle', inject(function(editorActions, searchPad) { + + // given + editorActions.trigger('find'); + + // assume + expect(searchPad.isOpen()).to.be.true; + + // when + editorActions.trigger('find'); + + // then + expect(!!searchPad.isOpen()).to.be.false; + })); + + }); + }); diff --git a/packages/dmn-js-drd/test/spec/features/keyboard/DrdKeyboardBindingsSpec.js b/packages/dmn-js-drd/test/spec/features/keyboard/DrdKeyboardBindingsSpec.js index 73baf1ebf..ccb06eef5 100644 --- a/packages/dmn-js-drd/test/spec/features/keyboard/DrdKeyboardBindingsSpec.js +++ b/packages/dmn-js-drd/test/spec/features/keyboard/DrdKeyboardBindingsSpec.js @@ -14,6 +14,7 @@ import lassoToolModule from 'diagram-js/lib/features/lasso-tool'; import handToolModule from 'diagram-js/lib/features/hand-tool'; import keyboardModule from 'src/features/keyboard'; import modelingModule from 'src/features/modeling'; +import searchModule from 'src/features/search'; import { createKeyEvent @@ -34,7 +35,8 @@ describe('features - keyboard', function() { handToolModule, keyboardModule, editorActionsModule, - modelingModule + modelingModule, + searchModule ] })); @@ -52,7 +54,8 @@ describe('features - keyboard', function() { 'selectElements', 'lassoTool', 'handTool', - 'directEditing' + 'directEditing', + 'find' ]; // then @@ -151,6 +154,25 @@ describe('features - keyboard', function() { }); + + forEach([ 'f', 'F' ], function(key) { + + it('should open search', + inject(function(keyboard, searchPad) { + + // given + const e = createKeyEvent(key, { ctrlKey: true }); + + // when + keyboard._keyHandler(e); + + // then + expect(searchPad.isOpen()).to.be.true; + }) + ); + + }); + }); }); \ No newline at end of file diff --git a/packages/dmn-js-drd/test/spec/features/search/DmnSearchProviderSpec.js b/packages/dmn-js-drd/test/spec/features/search/DmnSearchProviderSpec.js new file mode 100644 index 000000000..904b48273 --- /dev/null +++ b/packages/dmn-js-drd/test/spec/features/search/DmnSearchProviderSpec.js @@ -0,0 +1,143 @@ +import { + bootstrapViewer, + inject +} from 'test/TestHelper'; + +import coreModule from 'src/core'; +import modelingModule from 'src/features/modeling'; +import dmnSearchModule from 'src/features/search'; + + +describe('features - DMN search provider', function() { + + const testModules = [ + coreModule, + modelingModule, + dmnSearchModule + ]; + + + const diagramXML = require('./dmn-search.dmn'); + + beforeEach(bootstrapViewer(diagramXML, { modules: testModules })); + + + it('find should return all elements that match label or ID', inject( + function(dmnSearch) { + + // given + const pattern = 'Decision'; + + // when + const elements = dmnSearch.find(pattern); + + // then + expect(elements).length(2); + elements.forEach(function(e) { + expect(e).to.have.property('element'); + expect(e).to.have.property('primaryTokens'); + expect(e).to.have.property('secondaryTokens'); + }); + }) + ); + + + it('matches IDs', inject(function(dmnSearch) { + + // given + const pattern = 'Decision_id'; + + // when + const elements = dmnSearch.find(pattern); + + // then + expect(elements[0].primaryTokens).to.eql([ + { normal: 'Decision 1' } + ]); + expect(elements[0].secondaryTokens).to.eql([ + { matched: 'Decision_id' }, + { normal: '_1' } + ]); + })); + + + it('should not return root element (definitions)', inject(function(dmnSearch) { + + // given + const pattern = 'Definitions'; + + // when + const elements = dmnSearch.find(pattern); + + // then + expect(elements).to.have.length(0); + })); + + + describe('should split result into matched and non matched tokens', function() { + + it('matched all', inject(function(dmnSearch) { + + // given + const pattern = 'Start Middle End'; + + // when + const elements = dmnSearch.find(pattern); + + // then + expect(elements[0].primaryTokens).to.eql([ + { matched: 'Start Middle End' } + ]); + })); + + + it('matched start', inject(function(dmnSearch) { + + // given + const pattern = 'Start'; + + // when + const elements = dmnSearch.find(pattern); + + // then + expect(elements[0].primaryTokens).to.eql([ + { matched: 'Start' }, + { normal: ' Middle End' } + ]); + })); + + + it('matched middle', inject(function(dmnSearch) { + + // given + const pattern = 'Middle'; + + // when + const elements = dmnSearch.find(pattern); + + // then + expect(elements[0].primaryTokens).to.eql([ + { normal: 'Start ' }, + { matched: 'Middle' }, + { normal: ' End' } + ]); + })); + + + it('matched end', inject(function(dmnSearch) { + + // given + const pattern = 'End'; + + // when + const elements = dmnSearch.find(pattern); + + // then + expect(elements[0].primaryTokens).to.eql([ + { normal: 'Start Middle ' }, + { matched: 'End' } + ]); + })); + + }); +}); diff --git a/packages/dmn-js-drd/test/spec/features/search/dmn-search.dmn b/packages/dmn-js-drd/test/spec/features/search/dmn-search.dmn new file mode 100644 index 000000000..ca4e7d697 --- /dev/null +++ b/packages/dmn-js-drd/test/spec/features/search/dmn-search.dmn @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +