diff --git a/assets/bpmn-js.css b/assets/bpmn-js.css
index ff933e144d..0b6ba32d7a 100644
--- a/assets/bpmn-js.css
+++ b/assets/bpmn-js.css
@@ -8,7 +8,7 @@
--color-grey-225-10-80: hsl(225, 10%, 80%);
--color-grey-225-10-85: hsl(225, 10%, 85%);
--color-grey-225-10-90: hsl(225, 10%, 90%);
- --color-grey-225-10-95: hsl(225, 10%, 95%);
+ --color-grey-225-10-95: hsl(225, 10%, 95%);
--color-grey-225-10-97: hsl(225, 10%, 97%);
--color-blue-205-100-45: hsl(205, 100%, 45%);
@@ -24,8 +24,8 @@
--color-red-360-100-97: hsl(360, 100%, 97%);
--color-white: hsl(0, 0%, 100%);
- --color-black: hsl(0, 0%, 0%);
- --color-black-opacity-05: hsla(0, 0%, 0%, 5%);
+ --color-black: hsl(0, 0%, 0%);
+ --color-black-opacity-05: hsla(0, 0%, 0%, 5%);
--color-black-opacity-10: hsla(0, 0%, 0%, 10%);
--breadcrumbs-font-family: var(--bjs-font-family);
@@ -113,4 +113,33 @@
.selected .bjs-drilldown-empty {
display: inherit;
-}
\ No newline at end of file
+}
+
+[data-popup="align-elements"] .djs-popup-body {
+ display: flex;
+}
+
+[data-popup="align-elements"] .djs-popup-body [data-group] + [data-group] {
+ border-left: 1px solid var(--popup-border-color);
+}
+
+[data-popup="align-elements"] [data-group="align"] {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+}
+
+[data-popup="align-elements"] .djs-popup-body .entry {
+ height: 20px;
+ width: 20px;
+
+ padding: 6px 8px;
+}
+
+[data-popup="align-elements"] .djs-popup-body .entry img {
+ height: 100%;
+ width: 100%;
+}
+
+[data-popup="align-elements"] .bjs-align-elements-menu-entry {
+ display: inline-block;
+}
diff --git a/lib/Modeler.js b/lib/Modeler.js
index 717232ba98..7aedb9de40 100644
--- a/lib/Modeler.js
+++ b/lib/Modeler.js
@@ -10,7 +10,7 @@ import MoveCanvasModule from 'diagram-js/lib/navigation/movecanvas';
import TouchModule from 'diagram-js/lib/navigation/touch';
import ZoomScrollModule from 'diagram-js/lib/navigation/zoomscroll';
-import AlignElementsModule from 'diagram-js/lib/features/align-elements';
+import AlignElementsModule from './features/align-elements';
import AutoPlaceModule from './features/auto-place';
import AutoResizeModule from './features/auto-resize';
import AutoScrollModule from 'diagram-js/lib/features/auto-scroll';
diff --git a/lib/features/align-elements/AlignElementsContextPadProvider.js b/lib/features/align-elements/AlignElementsContextPadProvider.js
new file mode 100644
index 0000000000..235d053c16
--- /dev/null
+++ b/lib/features/align-elements/AlignElementsContextPadProvider.js
@@ -0,0 +1,87 @@
+import {
+ assign
+} from 'min-dash';
+
+import ICONS from './AlignElementsIcons';
+
+var LOW_PRIORITY = 900;
+
+/**
+ * A provider for align elements context pad button
+ */
+export default function AlignElementsContextPadProvider(contextPad, popupMenu, translate, canvas) {
+
+ contextPad.registerProvider(LOW_PRIORITY, this);
+
+ this._contextPad = contextPad;
+ this._popupMenu = popupMenu;
+ this._translate = translate;
+ this._canvas = canvas;
+}
+
+AlignElementsContextPadProvider.$inject = [
+ 'contextPad',
+ 'popupMenu',
+ 'translate',
+ 'canvas'
+];
+
+AlignElementsContextPadProvider.prototype.getMultiElementContextPadEntries = function(elements) {
+ var actions = {};
+
+ if (this._isAllowed(elements)) {
+ assign(actions, this._getEntries(elements));
+ }
+
+ return actions;
+};
+
+AlignElementsContextPadProvider.prototype._isAllowed = function(elements) {
+ return !this._popupMenu.isEmpty(elements, 'align-elements');
+};
+
+AlignElementsContextPadProvider.prototype._getEntries = function(elements) {
+ var self = this;
+
+ return {
+ 'align-elements': {
+ group: 'align-elements',
+ title: self._translate('Align elements'),
+ imageUrl: ICONS['align'],
+ action: {
+ click: function(event, elements) {
+ var position = self._getMenuPosition(elements);
+
+ assign(position, {
+ cursor: {
+ x: event.x,
+ y: event.y
+ }
+ });
+
+ self._popupMenu.open(elements, 'align-elements', position);
+ }
+ }
+ }
+ };
+};
+
+AlignElementsContextPadProvider.prototype._getMenuPosition = function(elements) {
+ var Y_OFFSET = 5;
+
+ var diagramContainer = this._canvas.getContainer(),
+ pad = this._contextPad.getPad(elements).html;
+
+ var diagramRect = diagramContainer.getBoundingClientRect(),
+ padRect = pad.getBoundingClientRect();
+
+ var top = padRect.top - diagramRect.top;
+ var left = padRect.left - diagramRect.left;
+
+ var pos = {
+ x: left,
+ y: top + padRect.height + Y_OFFSET
+ };
+
+ return pos;
+};
diff --git a/lib/features/align-elements/AlignElementsIcons.js b/lib/features/align-elements/AlignElementsIcons.js
new file mode 100644
index 0000000000..86aeb6a5d4
--- /dev/null
+++ b/lib/features/align-elements/AlignElementsIcons.js
@@ -0,0 +1,15 @@
+/**
+ * To change the icons, modify the SVGs in `./resources`, execute `npx svgo -f resources --datauri enc -o dist`,
+ * and then replace respective icons with the optimized data URIs in `./dist`.
+ */
+var icons = {
+ align: 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%202000%202000%22%3E%3Cpath%20style%3D%22stroke%3AcurrentColor%3Bstroke-width%3A100%3Bstroke-linecap%3Around%22%20d%3D%22M200%20150v1700%22%2F%3E%3Crect%20x%3D%22500%22%20y%3D%22150%22%20width%3D%221300%22%20height%3D%22700%22%20rx%3D%221%22%20style%3D%22fill%3Anone%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%22%2F%3E%3Crect%20x%3D%22500%22%20y%3D%221150%22%20width%3D%22700%22%20height%3D%22700%22%20rx%3D%221%22%20style%3D%22fill%3AcurrentColor%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%3Bopacity%3A.5%22%2F%3E%3C%2Fsvg%3E',
+ bottom: 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201800%201800%22%3E%3Cpath%20style%3D%22stroke%3AcurrentColor%3Bstroke-width%3A100%3Bstroke-linecap%3Around%22%20d%3D%22M150%201650h1500%22%2F%3E%3Crect%20x%3D%22150%22%20y%3D%22350%22%20width%3D%22600%22%20height%3D%221300%22%20rx%3D%221%22%20style%3D%22fill%3Anone%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%22%2F%3E%3Crect%20x%3D%221050%22%20y%3D%22850%22%20width%3D%22600%22%20height%3D%22800%22%20rx%3D%221%22%20style%3D%22fill%3AcurrentColor%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%3Bopacity%3A.5%22%2F%3E%3C%2Fsvg%3E',
+ center: 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201800%201800%22%3E%3Cpath%20style%3D%22stroke%3AcurrentColor%3Bstroke-width%3A100%3Bstroke-linecap%3Around%22%20d%3D%22M900%20150v1500%22%2F%3E%3Crect%20x%3D%22250%22%20y%3D%22150%22%20width%3D%221300%22%20height%3D%22600%22%20rx%3D%221%22%20style%3D%22fill%3Anone%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%22%2F%3E%3Crect%20x%3D%22500%22%20y%3D%221050%22%20width%3D%22800%22%20height%3D%22600%22%20rx%3D%221%22%20style%3D%22fill%3AcurrentColor%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%3Bopacity%3A.5%22%2F%3E%3C%2Fsvg%3E',
+ left: 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201800%201800%22%3E%3Cpath%20style%3D%22stroke%3AcurrentColor%3Bstroke-width%3A100%3Bstroke-linecap%3Around%22%20d%3D%22M100%20150v1500%22%2F%3E%3Crect%20x%3D%22100%22%20y%3D%22150%22%20width%3D%221300%22%20height%3D%22600%22%20rx%3D%221%22%20style%3D%22fill%3Anone%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%22%2F%3E%3Crect%20x%3D%22100%22%20y%3D%221050%22%20width%3D%22800%22%20height%3D%22600%22%20rx%3D%221%22%20style%3D%22fill%3AcurrentColor%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%3Bopacity%3A.5%22%2F%3E%3C%2Fsvg%3E',
+ right: 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201800%201800%22%3E%3Cpath%20style%3D%22stroke%3AcurrentColor%3Bstroke-width%3A100%3Bstroke-linecap%3Around%22%20d%3D%22M1650%20150v1500%22%2F%3E%3Crect%20x%3D%22350%22%20y%3D%22150%22%20width%3D%221300%22%20height%3D%22600%22%20rx%3D%221%22%20style%3D%22fill%3Anone%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%22%2F%3E%3Crect%20x%3D%22850%22%20y%3D%221050%22%20width%3D%22800%22%20height%3D%22600%22%20rx%3D%221%22%20style%3D%22fill%3AcurrentColor%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%3Bopacity%3A.5%22%2F%3E%3C%2Fsvg%3E',
+ top: 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201800%201800%22%3E%3Cpath%20style%3D%22stroke%3AcurrentColor%3Bstroke-width%3A100%3Bstroke-linecap%3Around%22%20d%3D%22M150%20150h1500%22%2F%3E%3Crect%20x%3D%22150%22%20y%3D%22150%22%20width%3D%22600%22%20height%3D%221300%22%20rx%3D%221%22%20style%3D%22fill%3Anone%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%22%2F%3E%3Crect%20x%3D%221050%22%20y%3D%22150%22%20width%3D%22600%22%20height%3D%22800%22%20rx%3D%221%22%20style%3D%22fill%3AcurrentColor%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%3Bopacity%3A.5%22%2F%3E%3C%2Fsvg%3E',
+ middle: 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201800%201800%22%3E%3Cpath%20style%3D%22stroke%3AcurrentColor%3Bstroke-width%3A100%3Bstroke-linecap%3Around%22%20d%3D%22M150%20900h1500%22%2F%3E%3Crect%20x%3D%22150%22%20y%3D%22250%22%20width%3D%22600%22%20height%3D%221300%22%20rx%3D%221%22%20style%3D%22fill%3Anone%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%22%2F%3E%3Crect%20x%3D%221050%22%20y%3D%22500%22%20width%3D%22600%22%20height%3D%22800%22%20rx%3D%221%22%20style%3D%22fill%3AcurrentColor%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%3Bopacity%3A.5%22%2F%3E%3C%2Fsvg%3E'
+};
+
+export default icons;
diff --git a/lib/features/align-elements/AlignElementsMenuProvider.js b/lib/features/align-elements/AlignElementsMenuProvider.js
new file mode 100644
index 0000000000..61805a75bf
--- /dev/null
+++ b/lib/features/align-elements/AlignElementsMenuProvider.js
@@ -0,0 +1,72 @@
+import ICONS from './AlignElementsIcons';
+
+import {
+ assign,
+ forEach,
+} from 'min-dash';
+
+var ALIGNMENT_OPTIONS = [
+ 'left',
+ 'center',
+ 'right',
+ 'top',
+ 'middle',
+ 'bottom'
+];
+
+/**
+ * A provider for align elements popup menu.
+ */
+export default function AlignElementsMenuProvider(popupMenu, alignElements, translate, rules) {
+
+ this._alignElements = alignElements;
+ this._translate = translate;
+ this._popupMenu = popupMenu;
+ this._rules = rules;
+
+ popupMenu.registerProvider('align-elements', this);
+}
+
+AlignElementsMenuProvider.$inject = [
+ 'popupMenu',
+ 'alignElements',
+ 'translate',
+ 'rules'
+];
+
+AlignElementsMenuProvider.prototype.getPopupMenuEntries = function(elements) {
+ var entries = {};
+
+ if (this._isAllowed(elements)) {
+ assign(entries, this._getEntries(elements));
+ }
+
+ return entries;
+};
+
+AlignElementsMenuProvider.prototype._isAllowed = function(elements) {
+ return this._rules.allowed('elements.align', { elements: elements });
+};
+
+AlignElementsMenuProvider.prototype._getEntries = function(elements) {
+ var alignElements = this._alignElements,
+ translate = this._translate,
+ popupMenu = this._popupMenu;
+
+ var entries = {};
+
+ forEach(ALIGNMENT_OPTIONS, function(alignment) {
+ entries[ 'align-elements-' + alignment ] = {
+ group: 'align',
+ title: translate('Align elements ' + alignment),
+ className: 'bjs-align-elements-menu-entry',
+ imageUrl: ICONS[alignment],
+ action: function(event, entry) {
+ alignElements.trigger(elements, alignment);
+ popupMenu.close();
+ }
+ };
+ });
+
+ return entries;
+};
diff --git a/lib/features/align-elements/BpmnAlignElements.js b/lib/features/align-elements/BpmnAlignElements.js
new file mode 100644
index 0000000000..451b8068fb
--- /dev/null
+++ b/lib/features/align-elements/BpmnAlignElements.js
@@ -0,0 +1,39 @@
+import inherits from 'inherits-browser';
+
+import RuleProvider from 'diagram-js/lib/features/rules/RuleProvider';
+import { getParents } from 'diagram-js/lib/util/Elements';
+
+import {
+ filter
+} from 'min-dash';
+
+/**
+ * Rule provider for alignment of BPMN elements.
+ */
+export default function BpmnAlignElements(eventBus) {
+ RuleProvider.call(this, eventBus);
+}
+
+BpmnAlignElements.$inject = [ 'eventBus' ];
+
+inherits(BpmnAlignElements, RuleProvider);
+
+BpmnAlignElements.prototype.init = function() {
+ this.addRule('elements.align', function(context) {
+ var elements = context.elements;
+
+ // filter out elements which cannot be aligned
+ var filteredElements = filter(elements, function(element) {
+ return !(element.waypoints || element.host || element.labelTarget);
+ });
+
+ // filter out elements which are children of any of the selected elements
+ filteredElements = getParents(filteredElements);
+
+ if (filteredElements.length < 2) {
+ return false;
+ }
+
+ return filteredElements;
+ });
+};
diff --git a/lib/features/align-elements/index.js b/lib/features/align-elements/index.js
new file mode 100644
index 0000000000..2ce329e43e
--- /dev/null
+++ b/lib/features/align-elements/index.js
@@ -0,0 +1,23 @@
+import AlignElementsModule from 'diagram-js/lib/features/align-elements';
+import ContextPadModule from 'diagram-js/lib/features/context-pad';
+import PopupMenuModule from 'diagram-js/lib/features/popup-menu';
+
+import AlignElementsContextPadProvider from './AlignElementsContextPadProvider';
+import AlignElementsMenuProvider from './AlignElementsMenuProvider';
+import BpmnAlignElements from './BpmnAlignElements';
+
+export default {
+ __depends__: [
+ AlignElementsModule,
+ ContextPadModule,
+ PopupMenuModule
+ ],
+ __init__: [
+ 'alignElementsContextPadProvider',
+ 'alignElementsMenuProvider',
+ 'bpmnAlignElements'
+ ],
+ alignElementsContextPadProvider: [ 'type', AlignElementsContextPadProvider ],
+ alignElementsMenuProvider: [ 'type', AlignElementsMenuProvider ],
+ bpmnAlignElements: [ 'type', BpmnAlignElements]
+};
diff --git a/lib/features/align-elements/resources/align-bottom-tool.svg b/lib/features/align-elements/resources/align-bottom-tool.svg
new file mode 100644
index 0000000000..ff578d93e9
--- /dev/null
+++ b/lib/features/align-elements/resources/align-bottom-tool.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/lib/features/align-elements/resources/align-horizontal-center-tool.svg b/lib/features/align-elements/resources/align-horizontal-center-tool.svg
new file mode 100644
index 0000000000..699e1a72ce
--- /dev/null
+++ b/lib/features/align-elements/resources/align-horizontal-center-tool.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/lib/features/align-elements/resources/align-left-tool.svg b/lib/features/align-elements/resources/align-left-tool.svg
new file mode 100644
index 0000000000..236f92acaf
--- /dev/null
+++ b/lib/features/align-elements/resources/align-left-tool.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/lib/features/align-elements/resources/align-right-tool.svg b/lib/features/align-elements/resources/align-right-tool.svg
new file mode 100644
index 0000000000..ac040e7087
--- /dev/null
+++ b/lib/features/align-elements/resources/align-right-tool.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/lib/features/align-elements/resources/align-tool.svg b/lib/features/align-elements/resources/align-tool.svg
new file mode 100644
index 0000000000..7dfa51e48c
--- /dev/null
+++ b/lib/features/align-elements/resources/align-tool.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/lib/features/align-elements/resources/align-top-tool.svg b/lib/features/align-elements/resources/align-top-tool.svg
new file mode 100644
index 0000000000..2cdc23de3f
--- /dev/null
+++ b/lib/features/align-elements/resources/align-top-tool.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/lib/features/align-elements/resources/align-vertical-center-tool.svg b/lib/features/align-elements/resources/align-vertical-center-tool.svg
new file mode 100644
index 0000000000..3b56063d43
--- /dev/null
+++ b/lib/features/align-elements/resources/align-vertical-center-tool.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/lib/features/distribute-elements/BpmnDistributeElements.js b/lib/features/distribute-elements/BpmnDistributeElements.js
index ce63a419a1..4e641c477c 100644
--- a/lib/features/distribute-elements/BpmnDistributeElements.js
+++ b/lib/features/distribute-elements/BpmnDistributeElements.js
@@ -1,3 +1,8 @@
+import inherits from 'inherits-browser';
+
+import RuleProvider from 'diagram-js/lib/features/rules/RuleProvider';
+import { getParents } from 'diagram-js/lib/util/Elements';
+
import {
filter
} from 'min-dash';
@@ -11,10 +16,19 @@ import {
* Registers element exclude filters for elements that
* currently do not support distribution.
*/
-export default function BpmnDistributeElements(distributeElements) {
+export default function BpmnDistributeElements(distributeElements, eventBus, rules) {
+ RuleProvider.call(this, eventBus);
+}
+
+BpmnDistributeElements.$inject = [ 'distributeElements', 'eventBus', 'rules' ];
+
+inherits(BpmnDistributeElements, RuleProvider);
+
+BpmnDistributeElements.prototype.init = function() {
+ this.addRule('elements.distribute', function(context) {
+ var elements = context.elements;
- distributeElements.registerFilter(function(elements) {
- return filter(elements, function(element) {
+ elements = filter(elements, function(element) {
var cannotDistribute = isAny(element, [
'bpmn:Association',
'bpmn:BoundaryEvent',
@@ -22,14 +36,20 @@ export default function BpmnDistributeElements(distributeElements) {
'bpmn:DataOutputAssociation',
'bpmn:Lane',
'bpmn:MessageFlow',
- 'bpmn:Participant',
'bpmn:SequenceFlow',
'bpmn:TextAnnotation'
]);
return !(element.labelTarget || cannotDistribute);
});
- });
-}
-BpmnDistributeElements.$inject = [ 'distributeElements' ];
\ No newline at end of file
+ // filter out elements which are children of any of the selected elements
+ elements = getParents(elements);
+
+ if (elements.length < 3) {
+ return false;
+ }
+
+ return elements;
+ });
+};
diff --git a/lib/features/distribute-elements/DistributeElementsIcons.js b/lib/features/distribute-elements/DistributeElementsIcons.js
new file mode 100644
index 0000000000..b1621a992a
--- /dev/null
+++ b/lib/features/distribute-elements/DistributeElementsIcons.js
@@ -0,0 +1,10 @@
+/**
+ * To change the icons, modify the SVGs in `./resources`, execute `npx svgo -f resources --datauri enc -o dist`,
+ * and then replace respective icons with the optimized data URIs in `./dist`.
+ */
+var icons = {
+ horizontal: 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201800%201800%22%3E%3Cpath%20style%3D%22fill%3Anone%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%3Bstroke-linejoin%3Around%22%20d%3D%22M450%20400V150h900v250%22%2F%3E%3Crect%20x%3D%22150%22%20y%3D%22450%22%20width%3D%22600%22%20height%3D%221200%22%20rx%3D%221%22%20style%3D%22fill%3Anone%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%22%2F%3E%3Crect%20x%3D%221050%22%20y%3D%22450%22%20width%3D%22600%22%20height%3D%22800%22%20rx%3D%221%22%20style%3D%22fill%3AcurrentColor%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%3Bopacity%3A.5%22%2F%3E%3C%2Fsvg%3E',
+ vertical: 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201800%201800%22%3E%3Cpath%20style%3D%22fill%3Anone%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%3Bstroke-linejoin%3Around%22%20d%3D%22M400%201350H150V450h250%22%2F%3E%3Crect%20x%3D%22450%22%20y%3D%22150%22%20width%3D%221200%22%20height%3D%22600%22%20rx%3D%221%22%20style%3D%22fill%3Anone%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%22%2F%3E%3Crect%20x%3D%22450%22%20y%3D%221050%22%20width%3D%22800%22%20height%3D%22600%22%20rx%3D%221%22%20style%3D%22fill%3AcurrentColor%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%3Bopacity%3A.5%22%2F%3E%3C%2Fsvg%3E',
+};
+
+export default icons;
diff --git a/lib/features/distribute-elements/DistributeElementsMenuProvider.js b/lib/features/distribute-elements/DistributeElementsMenuProvider.js
new file mode 100644
index 0000000000..7fbefd1e7e
--- /dev/null
+++ b/lib/features/distribute-elements/DistributeElementsMenuProvider.js
@@ -0,0 +1,69 @@
+import ICONS from './DistributeElementsIcons';
+
+import { assign } from 'min-dash';
+
+var LOW_PRIORITY = 900;
+
+/**
+ * A provider for distribute elements popup menu.
+ */
+export default function DistributeElementsMenuProvider(
+ popupMenu, distributeElements, translate, rules) {
+ this._distributeElements = distributeElements;
+ this._translate = translate;
+ this._popupMenu = popupMenu;
+ this._rules = rules;
+
+ popupMenu.registerProvider('align-elements', LOW_PRIORITY, this);
+}
+
+DistributeElementsMenuProvider.$inject = [
+ 'popupMenu',
+ 'distributeElements',
+ 'translate',
+ 'rules'
+];
+
+DistributeElementsMenuProvider.prototype.getPopupMenuEntries = function(elements) {
+ var entries = {};
+
+ if (this._isAllowed(elements)) {
+ assign(entries, this._getEntries(elements));
+ }
+
+ return entries;
+};
+
+DistributeElementsMenuProvider.prototype._isAllowed = function(elements) {
+ return this._rules.allowed('elements.distribute', { elements: elements });
+};
+
+DistributeElementsMenuProvider.prototype._getEntries = function(elements) {
+ var distributeElements = this._distributeElements,
+ translate = this._translate,
+ popupMenu = this._popupMenu;
+
+ var entries = {
+ 'distribute-elements-horizontal': {
+ group: 'distribute',
+ title: translate('Distribute elements horizontally'),
+ className: 'bjs-align-elements-menu-entry',
+ imageUrl: ICONS['horizontal'],
+ action: function(event, entry) {
+ distributeElements.trigger(elements, 'horizontal');
+ popupMenu.close();
+ }
+ },
+ 'distribute-elements-vertical': {
+ group: 'distribute',
+ title: translate('Distribute elements vertically'),
+ imageUrl: ICONS['vertical'],
+ action: function(event, entry) {
+ distributeElements.trigger(elements, 'vertical');
+ popupMenu.close();
+ }
+ },
+ };
+
+ return entries;
+};
diff --git a/lib/features/distribute-elements/index.js b/lib/features/distribute-elements/index.js
index d6ec22c8dc..15ea2f0ff6 100644
--- a/lib/features/distribute-elements/index.js
+++ b/lib/features/distribute-elements/index.js
@@ -1,12 +1,19 @@
import DistributeElementsModule from 'diagram-js/lib/features/distribute-elements';
+import PopupMenuModule from 'diagram-js/lib/features/popup-menu';
import BpmnDistributeElements from './BpmnDistributeElements';
+import DistributeElementsMenuProvider from './DistributeElementsMenuProvider';
export default {
__depends__: [
+ PopupMenuModule,
DistributeElementsModule
],
- __init__: [ 'bpmnDistributeElements' ],
- bpmnDistributeElements: [ 'type', BpmnDistributeElements ]
+ __init__: [
+ 'bpmnDistributeElements',
+ 'distributeElementsMenuProvider'
+ ],
+ bpmnDistributeElements: [ 'type', BpmnDistributeElements ],
+ distributeElementsMenuProvider: [ 'type', DistributeElementsMenuProvider ]
};
diff --git a/lib/features/distribute-elements/resources/distribute-horizontally-tool.svg b/lib/features/distribute-elements/resources/distribute-horizontally-tool.svg
new file mode 100644
index 0000000000..0c1e2c4c43
--- /dev/null
+++ b/lib/features/distribute-elements/resources/distribute-horizontally-tool.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/lib/features/distribute-elements/resources/distribute-vertically-tool.svg b/lib/features/distribute-elements/resources/distribute-vertically-tool.svg
new file mode 100644
index 0000000000..446c99aa24
--- /dev/null
+++ b/lib/features/distribute-elements/resources/distribute-vertically-tool.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/test/fixtures/bpmn/align-elements.bpmn b/test/fixtures/bpmn/align-elements.bpmn
index 9d530b4ec9..2c91d9ccf9 100644
--- a/test/fixtures/bpmn/align-elements.bpmn
+++ b/test/fixtures/bpmn/align-elements.bpmn
@@ -1,17 +1,17 @@
-
+
-
-
SequenceFlow_08zyuyv
SequenceFlow_08zyuyv
+
+
@@ -21,14 +21,14 @@
-
- Task_lane
+
+ EndEvent_lane
SubProcess_lane
-
- EndEvent_lane
+
+ Task_lane
@@ -41,92 +41,92 @@
SequenceFlow_1nrce3c
SequenceFlow_0qa7db7
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
-
+
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
diff --git a/test/fixtures/bpmn/distribute-elements-filtering.bpmn b/test/fixtures/bpmn/distribute-elements-filtering.bpmn
index 111671e2b6..9a30fba1fa 100644
--- a/test/fixtures/bpmn/distribute-elements-filtering.bpmn
+++ b/test/fixtures/bpmn/distribute-elements-filtering.bpmn
@@ -1,5 +1,5 @@
-
+
SequenceFlow_0vrvkcp
@@ -15,44 +15,53 @@
SequenceFlow_1jet52k
+
+
+
-
-
+
+
+
-
+
-
-
-
-
+
-
-
+
+
-
-
+
+
-
+
+
+
+
-
+
-
-
-
+
+
+
+
+
+
+
+
-
+
-
+
diff --git a/test/fixtures/bpmn/distribute-elements-filtering.collaboration.bpmn b/test/fixtures/bpmn/distribute-elements-filtering.collaboration.bpmn
new file mode 100644
index 0000000000..82b45073ab
--- /dev/null
+++ b/test/fixtures/bpmn/distribute-elements-filtering.collaboration.bpmn
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/spec/features/align-elements/AlignElementsContextPadProviderSpec.js b/test/spec/features/align-elements/AlignElementsContextPadProviderSpec.js
new file mode 100644
index 0000000000..943408e4c5
--- /dev/null
+++ b/test/spec/features/align-elements/AlignElementsContextPadProviderSpec.js
@@ -0,0 +1,100 @@
+import {
+ bootstrapModeler,
+ getBpmnJS,
+ inject
+} from 'test/TestHelper';
+
+import {
+ query as domQuery
+} from 'min-dom';
+
+import alignElementsModule from 'lib/features/align-elements';
+import modelingModule from 'lib/features/modeling';
+import coreModule from 'lib/core';
+
+
+
+describe('features/align-elements - context pad', function() {
+
+ var testModules = [ alignElementsModule, modelingModule, coreModule ];
+
+ var basicXML = require('../../../fixtures/bpmn/align-elements.bpmn');
+
+ beforeEach(bootstrapModeler(basicXML, { modules: testModules }));
+
+
+ it('should provide button to open menu', inject(function(elementRegistry, contextPad) {
+
+ // given
+ var elements = [
+ elementRegistry.get('EndEvent_lane'),
+ elementRegistry.get('Task_lane'),
+ elementRegistry.get('SubProcess_lane')
+ ];
+
+ // when
+ contextPad.open(elements);
+
+ // then
+ expect(getEntry(elements, 'align-elements')).to.exist;
+ }));
+
+
+ it('should NOT provide button if no actions are available', inject(
+ function(elementRegistry, contextPad, popupMenu) {
+
+ // given
+ var elements = [
+ elementRegistry.get('EndEvent_lane'),
+ elementRegistry.get('Task_lane'),
+ elementRegistry.get('SubProcess_lane')
+ ];
+ popupMenu.registerProvider('align-elements', 0, {
+ getPopupMenuEntries: function() {
+ return function() {
+ return {};
+ };
+ }
+ });
+
+ // when
+ contextPad.open(elements);
+
+ // then
+ expect(getEntry(elements, 'align-elements')).not.to.exist;
+ })
+ );
+
+
+ it('should open popup menu when item is clicked', inject(
+ function(elementRegistry, contextPad, popupMenu) {
+
+ // given
+ var elements = [
+ elementRegistry.get('EndEvent_lane'),
+ elementRegistry.get('Task_lane'),
+ elementRegistry.get('SubProcess_lane')
+ ];
+ contextPad.open(elements);
+
+ // when
+ var entry = getEntry(elements, 'align-elements');
+ entry.click();
+
+ // then
+ expect(popupMenu.isOpen()).to.be.true;
+ })
+ );
+});
+
+
+// helper //////////////////////////////////////////////////////////////////////
+function getEntry(target, actionName) {
+ return padEntry(getBpmnJS().invoke(function(contextPad) {
+ return contextPad.getPad(target).html;
+ }), actionName);
+}
+
+function padEntry(element, name) {
+ return domQuery('[data-action="' + name + '"]', element);
+}
diff --git a/test/spec/features/align-elements/AlignElementsMenuProviderSpec.js b/test/spec/features/align-elements/AlignElementsMenuProviderSpec.js
new file mode 100644
index 0000000000..d03a37682b
--- /dev/null
+++ b/test/spec/features/align-elements/AlignElementsMenuProviderSpec.js
@@ -0,0 +1,93 @@
+import {
+ bootstrapModeler,
+ getBpmnJS,
+ inject
+} from 'test/TestHelper';
+
+import {
+ query as domQuery
+} from 'min-dom';
+
+import {
+ forEach
+} from 'min-dash';
+
+import alignElementsModule from 'lib/features/align-elements';
+import modelingModule from 'lib/features/modeling';
+import coreModule from 'lib/core';
+
+
+
+describe('features/align-elements - popup menu', function() {
+
+ var testModules = [ alignElementsModule, modelingModule, coreModule ];
+
+ var basicXML = require('../../../fixtures/bpmn/align-elements.bpmn');
+
+ beforeEach(bootstrapModeler(basicXML, { modules: testModules }));
+
+
+ it('should provide alignment buttons', inject(function(elementRegistry, popupMenu) {
+
+ // given
+ var elements = [
+ elementRegistry.get('EndEvent_lane'),
+ elementRegistry.get('Task_lane'),
+ elementRegistry.get('SubProcess_lane')
+ ];
+
+ // when
+ popupMenu.open(elements, 'align-elements', {
+ x: 0,
+ y: 0
+ });
+
+ // then
+ forEach([
+ 'left',
+ 'center',
+ 'right',
+ 'top',
+ 'middle',
+ 'bottom'
+ ], function(alignment) {
+ expect(getEntry('align-elements-' + alignment)).to.exist;
+ });
+ }));
+
+
+ it('should close popup menu when button is clicked', inject(
+ function(elementRegistry, popupMenu) {
+
+ // given
+ var elements = [
+ elementRegistry.get('EndEvent_lane'),
+ elementRegistry.get('Task_lane'),
+ elementRegistry.get('SubProcess_lane')
+ ];
+ popupMenu.open(elements, 'align-elements', {
+ x: 0,
+ y: 0
+ });
+ var entry = getEntry('align-elements-center');
+
+ // when
+ entry.click();
+
+ // then
+ expect(popupMenu.isOpen()).to.be.false;
+ })
+ );
+});
+
+
+// helper //////////////////////////////////////////////////////////////////////
+function getEntry(actionName) {
+ return padEntry(getBpmnJS().invoke(function(popupMenu) {
+ return popupMenu._current.container;
+ }), actionName);
+}
+
+function padEntry(element, name) {
+ return domQuery('[data-id="' + name + '"]', element);
+}
diff --git a/test/spec/features/align-elements/BpmnAlignElementsSpec.js b/test/spec/features/align-elements/BpmnAlignElementsSpec.js
index d903003a83..104889da68 100644
--- a/test/spec/features/align-elements/BpmnAlignElementsSpec.js
+++ b/test/spec/features/align-elements/BpmnAlignElementsSpec.js
@@ -3,7 +3,7 @@ import {
inject
} from 'test/TestHelper';
-import alignElementsModule from 'diagram-js/lib/features/align-elements';
+import alignElementsModule from 'lib/features/align-elements';
import modelingModule from 'lib/features/modeling';
import coreModule from 'lib/core';
@@ -31,10 +31,10 @@ describe('features/align-elements', function() {
alignElements.trigger(elements, 'left');
// then
- expect(taskBoundEvt.x).to.equal(136);
- expect(task.x).to.equal(136);
- expect(subProcess.x).to.equal(136);
- expect(endEvent.x).to.equal(136);
+ expect(taskBoundEvt.x).to.equal(276);
+ expect(task.x).to.equal(276);
+ expect(subProcess.x).to.equal(276);
+ expect(endEvent.x).to.equal(276);
}));
@@ -51,10 +51,10 @@ describe('features/align-elements', function() {
alignElements.trigger(elements, 'right');
// then
- expect(task.x).to.equal(720);
- expect(taskHello.x).to.equal(720);
- expect(subProcess.x).to.equal(470);
- expect(endEvent.x).to.equal(784);
+ expect(task.x).to.equal(860);
+ expect(taskHello.x).to.equal(860);
+ expect(subProcess.x).to.equal(610);
+ expect(endEvent.x).to.equal(924);
}));
@@ -71,10 +71,10 @@ describe('features/align-elements', function() {
alignElements.trigger(elements, 'center');
// then
- expect(task.x).to.equal(428);
- expect(taskHello.x).to.equal(428);
- expect(subProcess.x).to.equal(303);
- expect(endEvent.x).to.equal(460);
+ expect(task.x).to.equal(568);
+ expect(taskHello.x).to.equal(568);
+ expect(subProcess.x).to.equal(443);
+ expect(endEvent.x).to.equal(600);
}));
@@ -90,9 +90,9 @@ describe('features/align-elements', function() {
alignElements.trigger(elements, 'top');
// then
- expect(task.y).to.equal(375);
- expect(subProcess.y).to.equal(375);
- expect(endEvent.y).to.equal(375);
+ expect(task.y).to.equal(445);
+ expect(subProcess.y).to.equal(445);
+ expect(endEvent.y).to.equal(445);
}));
@@ -108,9 +108,9 @@ describe('features/align-elements', function() {
alignElements.trigger(elements, 'bottom');
// then
- expect(task.y).to.equal(761);
- expect(subProcess.y).to.equal(641);
- expect(endEvent.y).to.equal(805);
+ expect(task.y).to.equal(831);
+ expect(subProcess.y).to.equal(711);
+ expect(endEvent.y).to.equal(875);
}));
@@ -126,11 +126,59 @@ describe('features/align-elements', function() {
alignElements.trigger(elements, 'middle');
// then
- expect(task.y).to.equal(568);
- expect(subProcess.y).to.equal(508);
- expect(endEvent.y).to.equal(590);
+ expect(task.y).to.equal(638);
+ expect(subProcess.y).to.equal(578);
+ expect(endEvent.y).to.equal(660);
}));
});
+
+ describe('rules', function() {
+
+ it('should not align boundary event', inject(function(alignElements, elementRegistry) {
+
+ // given
+ var boundaryEvent = elementRegistry.get('BoundaryEvent_1'),
+ host = elementRegistry.get('Task_boundary_evt');
+ var elements = [
+ host,
+ elementRegistry.get('Task_hello'),
+ boundaryEvent
+ ];
+ var initialRelativePosition = {
+ x: boundaryEvent.x - host.x,
+ y: boundaryEvent.y - host.y
+ };
+
+ // when
+ alignElements.trigger(elements, 'middle');
+
+ // then
+ expect(boundaryEvent.x).to.equal(initialRelativePosition.x + host.x);
+ expect(boundaryEvent.y).to.equal(initialRelativePosition.y + host.y);
+ }));
+
+
+ it('should not align container children', inject(
+ function(alignElements, elementRegistry) {
+
+ // given
+ var elements = elementRegistry.getAll('SubProcessChild').slice(1),
+ child = elementRegistry.get('Task_hello');
+ var initialRelativePosition = {
+ x: child.x - child.parent.x,
+ y: child.y - child.parent.y
+ };
+
+ // when
+ alignElements.trigger(elements, 'middle');
+
+ // then
+ expect(child.x).to.equal(initialRelativePosition.x + child.parent.x);
+ expect(child.y).to.equal(initialRelativePosition.y + child.parent.y);
+ })
+ );
+ });
+
});
diff --git a/test/spec/features/distribute-elements/BpmnDistributeElementsSpec.js b/test/spec/features/distribute-elements/BpmnDistributeElementsSpec.js
index d09a332dd3..8e5509ec46 100644
--- a/test/spec/features/distribute-elements/BpmnDistributeElementsSpec.js
+++ b/test/spec/features/distribute-elements/BpmnDistributeElementsSpec.js
@@ -1,3 +1,5 @@
+import { forEach } from 'min-dash';
+
import {
bootstrapModeler,
inject
@@ -7,6 +9,8 @@ import bpmnDistributeElements from 'lib/features/distribute-elements';
import modelingModule from 'lib/features/modeling';
import coreModule from 'lib/core';
+import { is } from 'lib/util/ModelUtil';
+
function last(arr) {
return arr[arr.length - 1];
}
@@ -27,7 +31,7 @@ describe('features/distribute-elements', function() {
beforeEach(inject(function(elementRegistry, canvas) {
elements = elementRegistry.filter(function(element) {
- return element.parent;
+ return element.parent && !is(element, 'bpmn:Participant');
});
}));
@@ -79,34 +83,96 @@ describe('features/distribute-elements', function() {
describe('filtering elements', function() {
- var xml = require('../../../fixtures/bpmn/distribute-elements-filtering.bpmn');
+ describe('process', function() {
- beforeEach(bootstrapModeler(xml, { modules: testModules }));
+ var xml = require('../../../fixtures/bpmn/distribute-elements-filtering.bpmn'),
+ elements;
- var elements;
+ beforeEach(bootstrapModeler(xml, { modules: testModules }));
- beforeEach(inject(function(elementRegistry, canvas) {
- elements = elementRegistry.filter(function(element) {
- return element.parent;
- });
- }));
+ beforeEach(inject(function(elementRegistry) {
+ elements = elementRegistry.filter(function(element) {
+ return element.parent;
+ });
+ }));
- it('should not distribute boundary events', inject(function(distributeElements, elementRegistry) {
- // given
- var boundaryEvent = elementRegistry.get('BoundaryEvent_1');
+ it('should not distribute boundary events', inject(function(distributeElements, elementRegistry) {
- // when
- var rangeGroups = distributeElements.trigger(elements, 'horizontal');
+ // given
+ var boundaryEvent = elementRegistry.get('BoundaryEvent_1');
- // then
- expect(rangeGroups).to.have.length(3);
+ // when
+ var rangeGroups = distributeElements.trigger(elements, 'horizontal');
- expect(rangeGroups[1].elements).not.to.include(boundaryEvent);
+ // then
+ expect(rangeGroups).to.have.length(3);
- }));
+ forEach(rangeGroups, function(rangeGroup) {
+ expect(rangeGroup.elements).not.to.include(boundaryEvent);
+ });
+ }));
+
+
+ it('should not distribute sub process children', inject(
+ function(distributeElements, elementRegistry) {
+
+ // given
+ var childElement = elementRegistry.get('SubProcessChild');
+
+ // when
+ var rangeGroups = distributeElements.trigger(elements, 'horizontal');
+
+ // then
+ expect(rangeGroups).to.have.length(3);
+
+ forEach(rangeGroups, function(rangeGroup) {
+ expect(rangeGroup.elements).not.to.include(childElement);
+ });
+ })
+ );
+ });
+
+
+ describe('collaboration', function() {
+
+ var xml = require('../../../fixtures/bpmn/distribute-elements-filtering.collaboration.bpmn'),
+ elements;
+
+ beforeEach(bootstrapModeler(xml, { modules: testModules }));
+
+
+ beforeEach(inject(function(elementRegistry) {
+ elements = elementRegistry.filter(function(element) {
+ return element.parent;
+ });
+ }));
+
+
+ it('should distribute participants', inject(function(distributeElements, elementRegistry) {
+
+ // given
+ var participants = elementRegistry.filter(function(element) {
+ return is(element, 'bpmn:Participant');
+ });
+
+ // when
+ var rangeGroups = distributeElements.trigger(elements, 'vertical');
+
+ // then
+ expect(rangeGroups).to.have.length(3);
+
+ var distributedElements = [];
+
+ forEach(rangeGroups, function(rangeGroup) {
+ distributedElements = distributedElements.concat(rangeGroup.elements);
+ });
+ expect(distributedElements).to.have.length(3);
+ expect(distributedElements).to.have.members(participants);
+ }));
+ });
});
});
diff --git a/test/spec/features/distribute-elements/DistributeElementsMenuProviderSpec.js b/test/spec/features/distribute-elements/DistributeElementsMenuProviderSpec.js
new file mode 100644
index 0000000000..421d908570
--- /dev/null
+++ b/test/spec/features/distribute-elements/DistributeElementsMenuProviderSpec.js
@@ -0,0 +1,95 @@
+import {
+ bootstrapModeler,
+ getBpmnJS,
+ inject
+} from 'test/TestHelper';
+
+import {
+ query as domQuery
+} from 'min-dom';
+
+import {
+ forEach
+} from 'min-dash';
+
+import distributeElementsModule from 'lib/features/distribute-elements';
+import modelingModule from 'lib/features/modeling';
+import coreModule from 'lib/core';
+
+
+describe('features/distribute-elements - popup menu', function() {
+
+ var testModules = [ distributeElementsModule, modelingModule, coreModule ];
+
+ var basicXML = require('../../../fixtures/bpmn/distribute-elements.bpmn');
+
+ beforeEach(bootstrapModeler(basicXML, { modules: testModules }));
+
+
+ it('should provide distribution buttons', inject(function(elementRegistry, popupMenu) {
+
+ // given
+ var elements = [
+ elementRegistry.get('ExclusiveGateway_10cec0a'),
+ elementRegistry.get('Task_08pns8h'),
+ elementRegistry.get('Task_0511uak'),
+ elementRegistry.get('EndEvent_0c9irey')
+ ];
+
+ // when
+ popupMenu.open(elements, 'align-elements', {
+ x: 0,
+ y: 0
+ });
+
+ // then
+ forEach([
+ 'horizontal',
+ 'vertical'
+ ], function(distribution) {
+ expect(getEntry('distribute-elements-' + distribution)).to.exist;
+ });
+ }));
+
+ forEach([
+ 'horizontal',
+ 'vertical'
+ ], function(distribution) {
+ it('should close popup menu when button is clicked', inject(
+ function(elementRegistry, popupMenu) {
+
+ // given
+ var elements = [
+ elementRegistry.get('ExclusiveGateway_10cec0a'),
+ elementRegistry.get('Task_08pns8h'),
+ elementRegistry.get('Task_0511uak'),
+ elementRegistry.get('EndEvent_0c9irey')
+ ];
+ popupMenu.open(elements, 'align-elements', {
+ x: 0,
+ y: 0
+ });
+ var entry = getEntry('distribute-elements-' + distribution);
+
+ // when
+ entry.click();
+
+ // then
+ expect(popupMenu.isOpen()).to.be.false;
+ })
+ );
+ });
+
+});
+
+
+// helper //////////////////////////////////////////////////////////////////////
+function getEntry(actionName) {
+ return padEntry(getBpmnJS().invoke(function(popupMenu) {
+ return popupMenu._current.container;
+ }), actionName);
+}
+
+function padEntry(element, name) {
+ return domQuery('[data-id="' + name + '"]', element);
+}