diff --git a/lib/Modeler.js b/lib/Modeler.js index f0919238..96efaeca 100644 --- a/lib/Modeler.js +++ b/lib/Modeler.js @@ -169,6 +169,7 @@ Modeler.prototype._interactionModules = [ // non-modeling components require('./features/label-editing'), require('./features/auto-resize'), + require('./features/search'), require('diagram-js/lib/navigation/zoomscroll'), require('diagram-js/lib/navigation/movecanvas'), require('diagram-js/lib/navigation/touch') diff --git a/lib/features/keyboard/BpmnKeyBindings.js b/lib/features/keyboard/BpmnKeyBindings.js index 01aabf5c..f53e0e99 100644 --- a/lib/features/keyboard/BpmnKeyBindings.js +++ b/lib/features/keyboard/BpmnKeyBindings.js @@ -1,7 +1,7 @@ 'use strict'; function BpmnKeyBindings(keyboard, spaceTool, lassoTool, handTool, directEditing, - selection, canvas, elementRegistry, editorActions) { + searchPad, selection, canvas, elementRegistry, editorActions) { var actions = { selectElements: function() { @@ -30,6 +30,9 @@ function BpmnKeyBindings(keyboard, spaceTool, lassoTool, handTool, directEditing if (currentSelection.length) { directEditing.activate(currentSelection[0]); } + }, + find: function() { + searchPad.toggle(); } }; @@ -44,6 +47,13 @@ function BpmnKeyBindings(keyboard, spaceTool, lassoTool, handTool, directEditing return true; } + // ctrl + f -> search labels + if (key === 70 && keyboard.isCmd(modifiers)) { + editorActions.trigger('find'); + + return true; + } + if (keyboard.hasModifier(modifiers)) { return; } @@ -84,6 +94,7 @@ BpmnKeyBindings.$inject = [ 'lassoTool', 'handTool', 'directEditing', + 'searchPad', 'selection', 'canvas', 'elementRegistry', diff --git a/lib/features/search/BpmnSearchProvider.js b/lib/features/search/BpmnSearchProvider.js new file mode 100644 index 00000000..a7c3a836 --- /dev/null +++ b/lib/features/search/BpmnSearchProvider.js @@ -0,0 +1,120 @@ +'use strict'; + +var map = require('lodash/collection/map'), + filter = require('lodash/collection/filter'), + sortBy = require('lodash/collection/sortBy'); + +var labelUtil = require('../label-editing/LabelUtil'); + + +/** + * Provides ability to search through BPMN elements + */ +function Search(elementRegistry, searchPad) { + + this._elementRegistry = elementRegistry; + + searchPad.registerProvider(this); +} + +module.exports = Search; + +Search._inject = [ + 'elementRegistry', + 'searchPad' +]; + + +/** + * Finds all elements that match given pattern + * + * : + * { + * primaryTokens: >, + * secondaryTokens: >, + * element: + * } + * + * : + * { + * normal|matched: + * } + * + * @param {String} pattern + * @return {Array} + */ +Search.prototype.find = function(pattern) { + var elements = this._elementRegistry.filter(function(element) { + if (element.labelTarget) { + return false; + } + return true; + }); + + elements = map(elements, function(element) { + return { + primaryTokens: matchAndSplit(labelUtil.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 labelUtil.getLabel(element.element) + element.element.id; + }); + + return elements; +}; + + +function hasMatched(tokens) { + var matched = filter(tokens, function(t){ + return !!t.matched; + }); + + return matched.length > 0; +} + + +function matchAndSplit(text, pattern) { + var tokens = [], + originalText = text; + + if (!text) { + return tokens; + } + + text = text.toLowerCase(); + pattern = pattern.toLowerCase(); + + var 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/lib/features/search/index.js b/lib/features/search/index.js new file mode 100644 index 00000000..4e61c3b9 --- /dev/null +++ b/lib/features/search/index.js @@ -0,0 +1,7 @@ +module.exports = { + __depends__: [ + require('diagram-js/lib/features/search-pad') + ], + __init__: [ 'bpmnSearch'], + bpmnSearch: [ 'type', require('./BpmnSearchProvider') ] +}; diff --git a/test/spec/features/keyboard/BpmnKeyBindingsSpec.js b/test/spec/features/keyboard/BpmnKeyBindingsSpec.js index 7ce10f5f..513e3941 100644 --- a/test/spec/features/keyboard/BpmnKeyBindingsSpec.js +++ b/test/spec/features/keyboard/BpmnKeyBindingsSpec.js @@ -1,12 +1,11 @@ 'use strict'; -var TestHelper = require('../../../TestHelper'); - var TestContainer = require('mocha-test-container-support'); var coreModule = require('../../../../lib/core'), modelingModule = require('../../../../lib/features/modeling'), keyboardModule = require('../../../../lib/features/keyboard'), + bpmnSearchModule = require('../../../../lib/features/search'), selectionModule = require('diagram-js/lib/features/selection'), spaceToolModule = require('diagram-js/lib/features/space-tool'), lassoToolModule = require('diagram-js/lib/features/lasso-tool'), @@ -27,6 +26,7 @@ describe('features - keyboard', function() { modelingModule, selectionModule, spaceToolModule, + bpmnSearchModule, lassoToolModule, handToolModule, keyboardModule, @@ -46,7 +46,7 @@ describe('features - keyboard', function() { it('should include triggers inside editorActions', inject(function(editorActions) { // then - expect(editorActions.length()).to.equal(11); + expect(editorActions.length()).to.equal(12); })); @@ -117,6 +117,21 @@ describe('features - keyboard', function() { expect(selectedElements).not.to.contain(rootElement); })); + + it('should trigger search for labels', inject(function(canvas, keyboard, searchPad, elementRegistry) { + + sinon.spy(searchPad, 'toggle'); + + // given + var e = createKeyEvent(container, 70, true); + + // when + keyboard._keyHandler(e); + + // then + expect(searchPad.toggle).calledOnce; + })); + }); }); diff --git a/test/spec/features/search/BpmnSearchProviderSpec.js b/test/spec/features/search/BpmnSearchProviderSpec.js new file mode 100644 index 00000000..b7225544 --- /dev/null +++ b/test/spec/features/search/BpmnSearchProviderSpec.js @@ -0,0 +1,121 @@ +'use strict'; + +var coreModule = require('../../../../lib/core'), + modelingModule = require('../../../../lib/features/modeling'), + bpmnSearchModule = require('../../../../lib/features/search'); + +/* global bootstrapViewer, inject */ + +describe('features - BPMN search provider', function() { + + var diagramXML = require('./bpmn-search.bpmn'); + + var testModules = [ + coreModule, + modelingModule, + bpmnSearchModule + ]; + + beforeEach(bootstrapViewer(diagramXML, { modules: testModules })); + + + it('find should return all elements that match label or ID', inject(function(bpmnSearch) { + // given + var pattern = '123456'; + + // when + var elements = bpmnSearch.find(pattern); + + // then + expect(elements).length(3); + 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(bpmnSearch) { + // given + var pattern = 'datastore'; + + // when + var elements = bpmnSearch.find(pattern); + + // then + expect(elements[0].primaryTokens).to.eql([ + { normal: 'has matched ID'} + ]); + expect(elements[0].secondaryTokens).to.eql([ + { normal: 'some_'}, + { matched: 'DataStore'}, + { normal: '_123456_id'}, + ]); + })); + + + describe('should split result into matched and non matched tokens', function() { + + it('matched all', inject(function(bpmnSearch) { + // given + var pattern = 'all matched'; + + // when + var elements = bpmnSearch.find(pattern); + + // then + expect(elements[0].primaryTokens).to.eql([ + { matched: 'all matched'} + ]); + })); + + + it('matched start', inject(function(bpmnSearch) { + // given + var pattern = 'before'; + + // when + var elements = bpmnSearch.find(pattern); + + // then + expect(elements[0].primaryTokens).to.eql([ + { matched: 'before'}, + { normal: ' 321'} + ]); + })); + + + it('matched middle', inject(function(bpmnSearch) { + // given + var pattern = 'middle'; + + // when + var elements = bpmnSearch.find(pattern); + + // then + expect(elements[0].primaryTokens).to.eql([ + { normal: '123 '}, + { matched: 'middle'}, + { normal: ' 321'} + ]); + })); + + + it('matched end', inject(function(bpmnSearch) { + // given + var pattern = 'after'; + + // when + var elements = bpmnSearch.find(pattern); + + // then + expect(elements[0].primaryTokens).to.eql([ + { normal: '123 '}, + { matched: 'after'} + ]); + })); + + }); + +}); diff --git a/test/spec/features/search/bpmn-search.bpmn b/test/spec/features/search/bpmn-search.bpmn new file mode 100644 index 00000000..68142334 --- /dev/null +++ b/test/spec/features/search/bpmn-search.bpmn @@ -0,0 +1,100 @@ + + + + + SequenceFlow_0wgiusn + + some_DataStore_123456_id + + + + SequenceFlow_0wgiusn + + + + + + SequenceFlow_1bhe9h2 + SequenceFlow_02ymelh + + + SequenceFlow_02ymelh + SequenceFlow_0ugwp0d + + + + SequenceFlow_0ugwp0d + + + + SequenceFlow_1bhe9h2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +