From a85103bd267e3130f68fafef09684c737f0d09f3 Mon Sep 17 00:00:00 2001 From: Maciej Barelkowski Date: Fri, 10 Jun 2022 08:54:57 +0200 Subject: [PATCH] feat: add alignment buttons for multi-element context pad Closes #1680 --- assets/bpmn-js.css | 33 +++++- lib/Modeler.js | 2 +- .../AlignElementsContextPadProvider.js | 87 +++++++++++++++ .../align-elements/AlignElementsIcons.js | 15 +++ .../AlignElementsMenuProvider.js | 72 +++++++++++++ lib/features/align-elements/index.js | 20 ++++ .../resources/align-bottom-tool.svg | 1 + .../align-horizontal-center-tool.svg | 1 + .../resources/align-left-tool.svg | 1 + .../resources/align-right-tool.svg | 1 + .../align-elements/resources/align-tool.svg | 1 + .../resources/align-top-tool.svg | 1 + .../resources/align-vertical-center-tool.svg | 1 + .../DistributeElementsIcons.js | 10 ++ .../DistributeElementsMenuProvider.js | 69 ++++++++++++ lib/features/distribute-elements/index.js | 11 +- .../distribute-horizontally-tool.svg | 1 + .../resources/distribute-vertically-tool.svg | 1 + .../AlignElementsContextPadProviderSpec.js | 100 ++++++++++++++++++ .../AlignElementsMenuProviderSpec.js | 93 ++++++++++++++++ .../DistributeElementsMenuProviderSpec.js | 90 ++++++++++++++++ 21 files changed, 604 insertions(+), 7 deletions(-) create mode 100644 lib/features/align-elements/AlignElementsContextPadProvider.js create mode 100644 lib/features/align-elements/AlignElementsIcons.js create mode 100644 lib/features/align-elements/AlignElementsMenuProvider.js create mode 100644 lib/features/align-elements/index.js create mode 100644 lib/features/align-elements/resources/align-bottom-tool.svg create mode 100644 lib/features/align-elements/resources/align-horizontal-center-tool.svg create mode 100644 lib/features/align-elements/resources/align-left-tool.svg create mode 100644 lib/features/align-elements/resources/align-right-tool.svg create mode 100644 lib/features/align-elements/resources/align-tool.svg create mode 100644 lib/features/align-elements/resources/align-top-tool.svg create mode 100644 lib/features/align-elements/resources/align-vertical-center-tool.svg create mode 100644 lib/features/distribute-elements/DistributeElementsIcons.js create mode 100644 lib/features/distribute-elements/DistributeElementsMenuProvider.js create mode 100644 lib/features/distribute-elements/resources/distribute-horizontally-tool.svg create mode 100644 lib/features/distribute-elements/resources/distribute-vertically-tool.svg create mode 100644 test/spec/features/align-elements/AlignElementsContextPadProviderSpec.js create mode 100644 test/spec/features/align-elements/AlignElementsMenuProviderSpec.js create mode 100644 test/spec/features/distribute-elements/DistributeElementsMenuProviderSpec.js diff --git a/assets/bpmn-js.css b/assets/bpmn-js.css index ff933e144d..132f25100a 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,29 @@ .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: 22px; + width: 22px; + + padding: 8px 10px; +} + +[data-popup="align-elements"] .djs-popup-body .entry img { + height: 100%; + width: 100%; +} 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..899eca359e --- /dev/null +++ b/lib/features/align-elements/AlignElementsIcons.js @@ -0,0 +1,15 @@ +/** + * To change the icons, call `encodeURIComponent` with modified SVG, + * and add `data:image/svg+xml;utf8,` in the beginning of the string. + */ +var icons = { + align: 'data:image/svg+xml;utf8,%3Csvg%20height%3D%222048%22%20width%3D%222048%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20style%3D%22fill%3Anone%3Bfill-rule%3Aevenodd%3Bstroke%3A%23282828%3Bstroke-width%3A146%3Bstroke-linecap%3Abutt%3Bstroke-linejoin%3Amiter%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-dashoffset%3A0%3Bstroke-opacity%3A1%22%20d%3D%22M292%204v2044%22%2F%3E%3Cpath%20style%3D%22fill%3A%23fff%3Bfill-opacity%3A1%3Bstroke%3A%23282828%3Bstroke-width%3A146%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-opacity%3A1%22%20d%3D%22M1752%20515H584V77h1168zm-438%20730H584V807h730zm438%20730H584v-438h1168z%22%2F%3E%3C%2Fsvg%3E', + bottom: 'data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%222048%22%20height%3D%222048%22%3E%3Cpath%20d%3D%22M0%20442v1314h2044V442h-584v1168h-146V880H730v730H584V442H0zm146%20146h292v1022H146V588zm1460%200h292v1022h-292V588zm-730%20438h292v584H876v-584z%22%20style%3D%22color%3A%23000%3Bfont-style%3Anormal%3Bfont-variant%3Anormal%3Bfont-weight%3A400%3Bfont-stretch%3Anormal%3Bfont-size%3Amedium%3Bline-height%3Anormal%3Bfont-family%3Asans-serif%3Btext-indent%3A0%3Btext-align%3Astart%3Btext-decoration%3Anone%3Btext-decoration-line%3Anone%3Btext-decoration-style%3Asolid%3Btext-decoration-color%3A%23000%3Bletter-spacing%3Anormal%3Bword-spacing%3Anormal%3Btext-transform%3Anone%3Bdirection%3Altr%3Bblock-progression%3Atb%3Bwriting-mode%3Alr-tb%3Bbaseline-shift%3Abaseline%3Btext-anchor%3Astart%3Bwhite-space%3Anormal%3Bclip-rule%3Anonzero%3Bdisplay%3Ainline%3Boverflow%3Avisible%3Bvisibility%3Avisible%3Bopacity%3A1%3Bisolation%3Aauto%3Bmix-blend-mode%3Anormal%3Bcolor-interpolation%3AsRGB%3Bcolor-interpolation-filters%3AlinearRGB%3Bsolid-color%3A%23000%3Bsolid-opacity%3A1%3Bfill%3A%23282828%3Bfill-opacity%3A1%3Bfill-rule%3Aevenodd%3Bstroke%3Anone%3Bstroke-width%3A146.00001526%3Bstroke-linecap%3Abutt%3Bstroke-linejoin%3Amiter%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-dashoffset%3A0%3Bstroke-opacity%3A1%3Bcolor-rendering%3Aauto%3Bimage-rendering%3Aauto%3Bshape-rendering%3Aauto%3Btext-rendering%3Aauto%3Benable-background%3Aaccumulate%22%2F%3E%3C%2Fsvg%3E', + center: 'data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%222048%22%20width%3D%222048%22%3E%3Cpath%20d%3D%22M1027.973%201318v146%22%20style%3D%22fill%3Anone%3Bfill-rule%3Aevenodd%3Bstroke%3A%23282828%3Bstroke-width%3A146%3Bstroke-linecap%3Abutt%3Bstroke-linejoin%3Amiter%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-dashoffset%3A0%3Bstroke-opacity%3A1%22%2F%3E%3Cpath%20d%3D%22M365%20515h1314V77H365v438zM657%201245h730V807H657v438z%22%20style%3D%22fill%3A%23fff%3Bfill-opacity%3A1%3Bstroke%3A%23282828%3Bstroke-width%3A146%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-opacity%3A1%22%2F%3E%3Cpath%20d%3D%22M1022%20588v146%22%20style%3D%22fill%3Anone%3Bfill-rule%3Aevenodd%3Bstroke%3A%23282828%3Bstroke-width%3A146%3Bstroke-linecap%3Abutt%3Bstroke-linejoin%3Amiter%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-dashoffset%3A0%3Bstroke-opacity%3A1%22%2F%3E%3Cpath%20d%3D%22M365%201975h1314v-438H365v438z%22%20style%3D%22fill%3A%23fff%3Bfill-opacity%3A1%3Bstroke%3A%23282828%3Bstroke-width%3A146%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-opacity%3A1%22%2F%3E%3C%2Fsvg%3E', + left: 'data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%222048%22%20width%3D%222048%22%3E%3Cpath%20style%3D%22fill%3Anone%3Bfill-rule%3Aevenodd%3Bstroke%3A%23282828%3Bstroke-width%3A146.00001526%3Bstroke-linecap%3Abutt%3Bstroke-linejoin%3Amiter%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-dashoffset%3A0%3Bstroke-opacity%3A1%22%20d%3D%22M0%201537h2044%22%20transform%3D%22rotate(90%20949%20953)%22%2F%3E%3Cpath%20style%3D%22fill%3A%23fff%3Bfill-opacity%3A1%3Bstroke%3A%23282828%3Bstroke-width%3A146%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-opacity%3A1%22%20d%3D%22M511%20369v1168H73V369h438zM1241%20807v730H803V807h438zM1971%20369v1168h-438V369h438z%22%20transform%3D%22rotate(90%20949%20953)%22%2F%3E%3C%2Fsvg%3E', + right: 'data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%222048%22%20width%3D%222048%22%3E%3Cpath%20d%3D%22M1679%204v2044%22%20style%3D%22fill%3Anone%3Bfill-rule%3Aevenodd%3Bstroke%3A%23282828%3Bstroke-width%3A146.00001526%3Bstroke-linecap%3Abutt%3Bstroke-linejoin%3Amiter%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-dashoffset%3A0%3Bstroke-opacity%3A1%22%2F%3E%3Cpath%20d%3D%22M511%20515h1168V77H511v438zM949%201245h730V807H949v438zM511%201975h1168v-438H511v438z%22%20style%3D%22fill%3A%23fff%3Bfill-opacity%3A1%3Bstroke%3A%23282828%3Bstroke-width%3A146%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-opacity%3A1%22%2F%3E%3C%2Fsvg%3E', + top: 'data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%222048%22%20width%3D%222048%22%3E%3Cpath%20style%3D%22fill%3Anone%3Bfill-rule%3Aevenodd%3Bstroke%3A%23282828%3Bstroke-width%3A146.00001526%3Bstroke-linecap%3Abutt%3Bstroke-linejoin%3Amiter%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-dashoffset%3A0%3Bstroke-opacity%3A1%22%20d%3D%22M0%201537h2044%22%20transform%3D%22matrix(1%200%200%20-1%200%201906)%22%2F%3E%3Cpath%20style%3D%22fill%3A%23fff%3Bfill-opacity%3A1%3Bstroke%3A%23282828%3Bstroke-width%3A146%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-opacity%3A1%22%20d%3D%22M511%20369v1168H73V369h438zM1241%20807v730H803V807h438zM1971%20369v1168h-438V369h438z%22%20transform%3D%22matrix(1%200%200%20-1%200%201906)%22%2F%3E%3C%2Fsvg%3E', + middle: 'data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%222048%22%20width%3D%222048%22%3E%3Cpath%20style%3D%22fill%3Anone%3Bfill-rule%3Aevenodd%3Bstroke%3A%23282828%3Bstroke-width%3A146%3Bstroke-linecap%3Abutt%3Bstroke-linejoin%3Amiter%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-dashoffset%3A0%3Bstroke-opacity%3A1%22%20d%3D%22M1027.973%201318v146%22%20transform%3D%22rotate(90%201022%201026)%22%2F%3E%3Cpath%20style%3D%22fill%3A%23fff%3Bfill-opacity%3A1%3Bstroke%3A%23282828%3Bstroke-width%3A146%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-opacity%3A1%22%20d%3D%22M365%20515h1314V77H365v438zM657%201245h730V807H657v438z%22%20transform%3D%22rotate(90%201022%201026)%22%2F%3E%3Cpath%20style%3D%22fill%3Anone%3Bfill-rule%3Aevenodd%3Bstroke%3A%23282828%3Bstroke-width%3A146%3Bstroke-linecap%3Abutt%3Bstroke-linejoin%3Amiter%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-dashoffset%3A0%3Bstroke-opacity%3A1%22%20d%3D%22M1022%20588v146%22%20transform%3D%22rotate(90%201022%201026)%22%2F%3E%3Cpath%20style%3D%22fill%3A%23fff%3Bfill-opacity%3A1%3Bstroke%3A%23282828%3Bstroke-width%3A146%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-opacity%3A1%22%20d%3D%22M365%201975h1314v-438H365v438z%22%20transform%3D%22rotate(90%201022%201026)%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..144a3e0d11 --- /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) { + + this._alignElements = alignElements; + this._translate = translate; + this._popupMenu = popupMenu; + + popupMenu.registerProvider('align-elements', this); +} + +AlignElementsMenuProvider.$inject = [ + 'popupMenu', + 'alignElements', + 'translate' +]; + +AlignElementsMenuProvider.prototype.getPopupMenuEntries = function(elements) { + var entries = {}; + + if (this._isAllowed(elements)) { + assign(entries, this._getEntries(elements)); + } + + return entries; +}; + +AlignElementsMenuProvider.prototype._isAllowed = function(elements) { + + // TODO(barmac): implement + return true; +}; + +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/index.js b/lib/features/align-elements/index.js new file mode 100644 index 0000000000..e822f3eea1 --- /dev/null +++ b/lib/features/align-elements/index.js @@ -0,0 +1,20 @@ +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'; + +export default { + __depends__: [ + AlignElementsModule, + ContextPadModule, + PopupMenuModule + ], + __init__: [ + 'alignElementsContextPadProvider', + 'alignElementsMenuProvider' + ], + alignElementsContextPadProvider: [ 'type', AlignElementsContextPadProvider ], + alignElementsMenuProvider: [ 'type', AlignElementsMenuProvider ] +}; 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..d313c50b90 --- /dev/null +++ b/lib/features/align-elements/resources/align-bottom-tool.svg @@ -0,0 +1 @@ + \ 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..7b8aed2a92 --- /dev/null +++ b/lib/features/align-elements/resources/align-horizontal-center-tool.svg @@ -0,0 +1 @@ + \ 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..6b81bb7bfa --- /dev/null +++ b/lib/features/align-elements/resources/align-left-tool.svg @@ -0,0 +1 @@ + \ 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..256faad3f4 --- /dev/null +++ b/lib/features/align-elements/resources/align-right-tool.svg @@ -0,0 +1 @@ + \ 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..808af80768 --- /dev/null +++ b/lib/features/align-elements/resources/align-tool.svg @@ -0,0 +1 @@ + \ 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..310883dcfb --- /dev/null +++ b/lib/features/align-elements/resources/align-top-tool.svg @@ -0,0 +1 @@ + \ 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..f499030038 --- /dev/null +++ b/lib/features/align-elements/resources/align-vertical-center-tool.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/features/distribute-elements/DistributeElementsIcons.js b/lib/features/distribute-elements/DistributeElementsIcons.js new file mode 100644 index 0000000000..774d9ebbf2 --- /dev/null +++ b/lib/features/distribute-elements/DistributeElementsIcons.js @@ -0,0 +1,10 @@ +/** + * To change the icons, call `encodeURIComponent` with modified SVG, + * and add `data:image/svg+xml;utf8,` in the beginning of the string. + */ +var icons = { + horizontal: 'data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%222048%22%20width%3D%222048%22%3E%3Cpath%20d%3D%22M73%20661v1022h438V661H73z%22%20style%3D%22fill%3A%23fff%3Bfill-opacity%3A1%3Bstroke%3A%23282828%3Bstroke-width%3A146%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-opacity%3A1%22%2F%3E%3Cpath%20d%3D%22M438%20369h1168%22%20style%3D%22fill%3Anone%3Bfill-rule%3Aevenodd%3Bstroke%3A%23282828%3Bstroke-width%3A146%3Bstroke-linecap%3Abutt%3Bstroke-linejoin%3Amiter%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-dashoffset%3A0%3Bstroke-opacity%3A1%22%2F%3E%3Cpath%20d%3D%22M803%20661v730h438V661H803zM1533%20661v1022h438V661h-438z%22%20style%3D%22fill%3A%23fff%3Bfill-opacity%3A1%3Bstroke%3A%23282828%3Bstroke-width%3A146%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-opacity%3A1%22%2F%3E%3Cpath%20d%3D%22M511%20442v146M1022%20442v146M1533%20442v146%22%20style%3D%22fill%3Anone%3Bfill-rule%3Aevenodd%3Bstroke%3A%23282828%3Bstroke-width%3A146%3Bstroke-linecap%3Abutt%3Bstroke-linejoin%3Amiter%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-dashoffset%3A0%3Bstroke-opacity%3A1%22%2F%3E%3C%2Fsvg%3E', + vertical: 'data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%222048%22%20width%3D%222048%22%3E%3Cpath%20style%3D%22fill%3A%23fff%3Bfill-opacity%3A1%3Bstroke%3A%23282828%3Bstroke-width%3A146%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-opacity%3A1%22%20d%3D%22M73%20661v1022h438V661H73z%22%20transform%3D%22rotate(-90%201022%201026)%22%2F%3E%3Cpath%20style%3D%22fill%3Anone%3Bfill-rule%3Aevenodd%3Bstroke%3A%23282828%3Bstroke-width%3A146%3Bstroke-linecap%3Abutt%3Bstroke-linejoin%3Amiter%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-dashoffset%3A0%3Bstroke-opacity%3A1%22%20d%3D%22M438%20369h1168%22%20transform%3D%22rotate(-90%201022%201026)%22%2F%3E%3Cpath%20style%3D%22fill%3A%23fff%3Bfill-opacity%3A1%3Bstroke%3A%23282828%3Bstroke-width%3A146%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-opacity%3A1%22%20d%3D%22M803%20661v730h438V661H803zM1533%20661v1022h438V661h-438z%22%20transform%3D%22rotate(-90%201022%201026)%22%2F%3E%3Cpath%20style%3D%22fill%3Anone%3Bfill-rule%3Aevenodd%3Bstroke%3A%23282828%3Bstroke-width%3A146%3Bstroke-linecap%3Abutt%3Bstroke-linejoin%3Amiter%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-dashoffset%3A0%3Bstroke-opacity%3A1%22%20d%3D%22M511%20442v146M1022%20442v146M1533%20442v146%22%20transform%3D%22rotate(-90%201022%201026)%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..410d45462e --- /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) { + this._distributeElements = distributeElements; + this._translate = translate; + this._popupMenu = popupMenu; + + popupMenu.registerProvider('align-elements', LOW_PRIORITY, this); +} + +DistributeElementsMenuProvider.$inject = [ + 'popupMenu', + 'distributeElements', + 'translate' +]; + +DistributeElementsMenuProvider.prototype.getPopupMenuEntries = function(elements) { + var entries = {}; + + if (this._isAllowed(elements)) { + assign(entries, this._getEntries(elements)); + } + + return entries; +}; + +DistributeElementsMenuProvider.prototype._isAllowed = function(elements) { + + // TODO(barmac): implement + return true; +}; + +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'), + className: 'bjs-align-elements-menu-entry', + 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..6ded9bfa04 --- /dev/null +++ b/lib/features/distribute-elements/resources/distribute-horizontally-tool.svg @@ -0,0 +1 @@ + \ 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..f2c8c5ff73 --- /dev/null +++ b/lib/features/distribute-elements/resources/distribute-vertically-tool.svg @@ -0,0 +1 @@ + \ No newline at end of file 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/distribute-elements/DistributeElementsMenuProviderSpec.js b/test/spec/features/distribute-elements/DistributeElementsMenuProviderSpec.js new file mode 100644 index 0000000000..8be9ab8990 --- /dev/null +++ b/test/spec/features/distribute-elements/DistributeElementsMenuProviderSpec.js @@ -0,0 +1,90 @@ +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; + }); + })); + + + 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-horizontal'); + + // 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); +}