From 439bc4ead0a7992cf0c93031120978037a72dc7b Mon Sep 17 00:00:00 2001 From: Maciej Barelkowski Date: Wed, 26 May 2021 10:58:24 +0200 Subject: [PATCH] feat(modeling): use BPMN in Color for color setting Additionally to custom bpmn.io properties, `modeling#setColor` will use [BPMN in Color properties](https://github.com/bpmn-miwg/bpmn-in-color). --- lib/features/copy-paste/BpmnCopyPaste.js | 7 +- lib/features/modeling/cmd/SetColorHandler.js | 85 ++++++++- lib/features/replace/BpmnReplace.js | 7 +- .../features/copy-paste/BpmnCopyPasteSpec.js | 8 +- test/spec/features/modeling/SetColorSpec.js | 177 +++++++++++++----- test/spec/features/replace/BpmnReplaceSpec.js | 8 +- 6 files changed, 231 insertions(+), 61 deletions(-) diff --git a/lib/features/copy-paste/BpmnCopyPaste.js b/lib/features/copy-paste/BpmnCopyPaste.js index 8c778193..f2c358ab 100644 --- a/lib/features/copy-paste/BpmnCopyPaste.js +++ b/lib/features/copy-paste/BpmnCopyPaste.js @@ -52,10 +52,13 @@ export default function BpmnCopyPaste(bpmnFactory, eventBus, moddleCopy) { descriptor.di = {}; - // fill and stroke will be set to DI + // colors will be set to DI copyProperties(businessObject.di, descriptor.di, [ 'fill', - 'stroke' + 'stroke', + 'background-color', + 'border-color', + 'color' ]); copyProperties(businessObject.di, descriptor, 'isExpanded'); diff --git a/lib/features/modeling/cmd/SetColorHandler.js b/lib/features/modeling/cmd/SetColorHandler.js index 590cb6c8..67c3f9cb 100644 --- a/lib/features/modeling/cmd/SetColorHandler.js +++ b/lib/features/modeling/cmd/SetColorHandler.js @@ -1,6 +1,8 @@ import { assign, - forEach + forEach, + isString, + pick } from 'min-dash'; @@ -12,6 +14,24 @@ var DEFAULT_COLORS = { export default function SetColorHandler(commandStack) { this._commandStack = commandStack; + + this._normalizeColor = function(color) { + + // Remove color for falsy values. + if (!color) { + return undefined; + } + + if (isString(color)) { + var hexColor = colorToHex(color); + + if (hexColor) { + return hexColor; + } + } + + throw new Error('invalid color value: ' + color); + }; } SetColorHandler.$inject = [ @@ -28,21 +48,76 @@ SetColorHandler.prototype.postExecute = function(context) { var di = {}; if ('fill' in colors) { - assign(di, { fill: colors.fill }); + assign(di, { + 'background-color': this._normalizeColor(colors.fill) }); } if ('stroke' in colors) { - assign(di, { stroke: colors.stroke }); + assign(di, { + 'border-color': this._normalizeColor(colors.stroke) }); } forEach(elements, function(element) { + var assignedDi = isConnection(element) ? pick(di, [ 'border-color' ]) : di; + + // TODO @barmac: remove once we drop bpmn.io properties + ensureLegacySupport(assignedDi); self._commandStack.execute('element.updateProperties', { element: element, properties: { - di: di + di: assignedDi } }); }); -}; \ No newline at end of file +}; + +/** + * Convert color from rgb(a)/hsl to hex. Returns `null` for unknown color names and for colors + * with alpha less than 1.0. This depends on `` serialization of the `context.fillStyle`. + * Cf. https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-fillstyle + * + * @example + * ```js + * var color = 'fuchsia'; + * console.log(colorToHex(color)); + * // "#ff00ff" + * color = 'rgba(1,2,3,0.4)'; + * console.log(colorToHex(color)); + * // null + * ``` + * + * @param {string} color + * @returns {string|null} + */ +function colorToHex(color) { + var context = document.createElement('canvas').getContext('2d'); + + // (0) Start with transparent to account for browser default values. + context.fillStyle = 'transparent'; + + // (1) Assign color so that it's serialized. + context.fillStyle = color; + + // (2) Return null for non-hex serialization result. + return /^#[0-9a-fA-F]{6}$/.test(context.fillStyle) ? context.fillStyle : null; +} + +function isConnection(element) { + return !!element.waypoints; +} + +/** + * Add legacy properties if required. + * @param {{ 'border-color': string?, 'background-color': string? }} di + */ +function ensureLegacySupport(di) { + if ('border-color' in di) { + di.stroke = di['border-color']; + } + + if ('background-color' in di) { + di.fill = di['background-color']; + } +} diff --git a/lib/features/replace/BpmnReplace.js b/lib/features/replace/BpmnReplace.js index 448c3a0b..eb1eea36 100644 --- a/lib/features/replace/BpmnReplace.js +++ b/lib/features/replace/BpmnReplace.js @@ -264,10 +264,13 @@ export default function BpmnReplace( newElement.di = {}; - // fill and stroke will be set to DI + // colors will be set to DI copyProperties(oldBusinessObject.di, newElement.di, [ 'fill', - 'stroke' + 'stroke', + 'background-color', + 'border-color', + 'color' ]); newElement = replace.replaceElement(element, newElement, hints); diff --git a/test/spec/features/copy-paste/BpmnCopyPasteSpec.js b/test/spec/features/copy-paste/BpmnCopyPasteSpec.js index e7d5f0ee..63de5cd6 100644 --- a/test/spec/features/copy-paste/BpmnCopyPasteSpec.js +++ b/test/spec/features/copy-paste/BpmnCopyPasteSpec.js @@ -354,8 +354,8 @@ describe('features/copy-paste', function() { // given var task = elementRegistry.get('Task_1'), rootElement = canvas.getRootElement(), - fill = 'red', - stroke = 'green'; + fill = '#ff0000', + stroke = '#00ff00'; // when @@ -378,6 +378,10 @@ describe('features/copy-paste', function() { var taskBo = getBusinessObject(task); + expect(taskBo.di.get('background-color')).to.equal(fill); + expect(taskBo.di.get('border-color')).to.equal(stroke); + + // TODO @barmac: remove when we drop bpmn.io properties expect(taskBo.di.fill).to.equal(fill); expect(taskBo.di.stroke).to.equal(stroke); }) diff --git a/test/spec/features/modeling/SetColorSpec.js b/test/spec/features/modeling/SetColorSpec.js index aa361a22..46ddbd37 100644 --- a/test/spec/features/modeling/SetColorSpec.js +++ b/test/spec/features/modeling/SetColorSpec.js @@ -6,6 +6,8 @@ import { import modelingModule from 'lib/features/modeling'; import coreModule from 'lib/core'; +var FUCHSIA_HEX = '#ff00ff', + YELLOW_HEX = '#ffff00'; describe('features/modeling - set color', function() { @@ -30,7 +32,7 @@ describe('features/modeling - set color', function() { modeling.setColor(taskShape, { fill: 'FUCHSIA' }); // then - expect(taskShape.businessObject.di.fill).to.equal('FUCHSIA'); + expect(taskShape.businessObject.di.get('background-color')).to.equal(FUCHSIA_HEX); })); @@ -44,7 +46,7 @@ describe('features/modeling - set color', function() { modeling.setColor(taskShape); // then - expect(taskShape.businessObject.di.fill).not.to.exist; + expect(taskShape.businessObject.di.get('background-color')).not.to.exist; })); @@ -59,8 +61,8 @@ describe('features/modeling - set color', function() { modeling.setColor(taskShape, { fill: 'YELLOW' }); // then - expect(taskShape.businessObject.di.fill).to.equal('YELLOW'); - expect(taskShape.businessObject.di.stroke).to.equal('YELLOW'); + expect(taskShape.businessObject.di.get('background-color')).to.equal(YELLOW_HEX); + expect(taskShape.businessObject.di.get('border-color')).to.equal(YELLOW_HEX); } )); @@ -76,8 +78,8 @@ describe('features/modeling - set color', function() { modeling.setColor(taskShape, { fill: undefined }); // then - expect(taskShape.businessObject.di.fill).not.to.exist; - expect(taskShape.businessObject.di.stroke).to.equal('YELLOW'); + expect(taskShape.businessObject.di.get('background-color')).not.to.exist; + expect(taskShape.businessObject.di.get('border-color')).to.equal(YELLOW_HEX); } )); @@ -93,8 +95,8 @@ describe('features/modeling - set color', function() { modeling.setColor(taskShape); // then - expect(taskShape.businessObject.di.fill).not.to.exist; - expect(taskShape.businessObject.di.stroke).not.to.exist; + expect(taskShape.businessObject.di.get('background-color')).not.to.exist; + expect(taskShape.businessObject.di.get('border-color')).not.to.exist; } )); @@ -108,8 +110,8 @@ describe('features/modeling - set color', function() { modeling.setColor(taskShape, { stroke: 'FUCHSIA' }); // then - expect(taskShape.businessObject.di.stroke).to.equal('FUCHSIA'); - expect(taskShape.businessObject.di.fill).not.to.exist; + expect(taskShape.businessObject.di.get('border-color')).to.equal(FUCHSIA_HEX); + expect(taskShape.businessObject.di.get('background-color')).not.to.exist; })); @@ -123,8 +125,8 @@ describe('features/modeling - set color', function() { modeling.setColor(taskShape); // then - expect(taskShape.businessObject.di.stroke).not.to.exist; - expect(taskShape.businessObject.di.fill).not.to.exist; + expect(taskShape.businessObject.di.get('border-color')).not.to.exist; + expect(taskShape.businessObject.di.get('background-color')).not.to.exist; })); @@ -139,10 +141,10 @@ describe('features/modeling - set color', function() { modeling.setColor([ taskShape, startEventShape ], { fill: 'FUCHSIA' }); // then - expect(taskShape.businessObject.di.fill).to.equal('FUCHSIA'); - expect(startEventShape.businessObject.di.fill).to.equal('FUCHSIA'); - expect(taskShape.businessObject.di.stroke).not.to.exist; - expect(startEventShape.businessObject.di.stroke).not.to.exist; + expect(taskShape.businessObject.di.get('background-color')).to.equal(FUCHSIA_HEX); + expect(startEventShape.businessObject.di.get('background-color')).to.equal(FUCHSIA_HEX); + expect(taskShape.businessObject.di.get('border-color')).not.to.exist; + expect(startEventShape.businessObject.di.get('border-color')).not.to.exist; } )); @@ -159,8 +161,8 @@ describe('features/modeling - set color', function() { modeling.setColor([ taskShape, startEventShape ]); // then - expect(taskShape.businessObject.di.fill).not.to.exist; - expect(startEventShape.businessObject.di.fill).not.to.exist; + expect(taskShape.businessObject.di.get('background-color')).not.to.exist; + expect(startEventShape.businessObject.di.get('background-color')).not.to.exist; } )); @@ -179,10 +181,10 @@ describe('features/modeling - set color', function() { ], { stroke: 'FUCHSIA' }); // then - expect(taskShape.businessObject.di.stroke).to.equal('FUCHSIA'); - expect(startEventShape.businessObject.di.stroke).to.equal('FUCHSIA'); - expect(taskShape.businessObject.di.fill).not.to.exist; - expect(startEventShape.businessObject.di.fill).not.to.exist; + expect(taskShape.businessObject.di.get('border-color')).to.equal(FUCHSIA_HEX); + expect(startEventShape.businessObject.di.get('border-color')).to.equal(FUCHSIA_HEX); + expect(taskShape.businessObject.di.get('background-color')).not.to.exist; + expect(startEventShape.businessObject.di.get('background-color')).not.to.exist; } )); @@ -202,11 +204,53 @@ describe('features/modeling - set color', function() { modeling.setColor([ taskShape, startEventShape ]); // then - expect(taskShape.businessObject.di.stroke).not.to.exist; - expect(startEventShape.businessObject.di.stroke).not.to.exist; + expect(taskShape.businessObject.di.get('border-color')).not.to.exist; + expect(startEventShape.businessObject.di.get('border-color')).not.to.exist; } )); + + it('should not set background-color on BPMNEdge', inject(function(elementRegistry, modeling) { + + // given + var sequenceFlow = elementRegistry.get('SequenceFlow_1'); + + // when + modeling.setColor(sequenceFlow, { fill: 'FUCHSIA' }); + + // then + expect(sequenceFlow.businessObject.di.get('background-color')).not.to.exist; + })); + + + it('should throw for an invalid color', inject(function(elementRegistry, modeling) { + + // given + var sequenceFlow = elementRegistry.get('SequenceFlow_1'); + + // when + function setColor() { + modeling.setColor(sequenceFlow, { fill: 'INVALID_COLOR' }); + } + + // then + expect(setColor).to.throw(/^invalid color value/); + })); + + + it('should throw for a color with alpha', inject(function(elementRegistry, modeling) { + + // given + var sequenceFlow = elementRegistry.get('SequenceFlow_1'); + + // when + function setColor() { + modeling.setColor(sequenceFlow, { fill: 'rgba(0, 255, 0, 0.5)' }); + } + + // then + expect(setColor).to.throw(/^invalid color value/); + })); }); @@ -223,7 +267,7 @@ describe('features/modeling - set color', function() { commandStack.undo(); // then - expect(taskShape.businessObject.di.fill).not.to.exist; + expect(taskShape.businessObject.di.get('background-color')).not.to.exist; } )); @@ -240,7 +284,7 @@ describe('features/modeling - set color', function() { commandStack.undo(); // then - expect(taskShape.businessObject.di.fill).to.equal('FUCHSIA'); + expect(taskShape.businessObject.di.get('background-color')).to.equal(FUCHSIA_HEX); } )); @@ -256,7 +300,7 @@ describe('features/modeling - set color', function() { commandStack.undo(); // then - expect(taskShape.businessObject.di.stroke).not.to.exist; + expect(taskShape.businessObject.di.get('border-color')).not.to.exist; } )); @@ -273,7 +317,7 @@ describe('features/modeling - set color', function() { commandStack.undo(); // then - expect(taskShape.businessObject.di.stroke).to.equal('FUCHSIA'); + expect(taskShape.businessObject.di.get('border-color')).to.equal(FUCHSIA_HEX); } )); @@ -290,8 +334,8 @@ describe('features/modeling - set color', function() { commandStack.undo(); // then - expect(taskShape.businessObject.di.fill).not.to.exist; - expect(startEventShape.businessObject.di.fill).not.to.exist; + expect(taskShape.businessObject.di.get('background-color')).not.to.exist; + expect(startEventShape.businessObject.di.get('background-color')).not.to.exist; } )); @@ -309,8 +353,8 @@ describe('features/modeling - set color', function() { commandStack.undo(); // then - expect(taskShape.businessObject.di.fill).to.equal('FUCHSIA'); - expect(startEventShape.businessObject.di.fill).to.equal('FUCHSIA'); + expect(taskShape.businessObject.di.get('background-color')).to.equal(FUCHSIA_HEX); + expect(startEventShape.businessObject.di.get('background-color')).to.equal(FUCHSIA_HEX); } )); @@ -330,8 +374,8 @@ describe('features/modeling - set color', function() { commandStack.undo(); // then - expect(taskShape.businessObject.di.stroke).not.to.exist; - expect(startEventShape.businessObject.di.stroke).not.to.exist; + expect(taskShape.businessObject.di.get('border-color')).not.to.exist; + expect(startEventShape.businessObject.di.get('border-color')).not.to.exist; } )); @@ -349,8 +393,8 @@ describe('features/modeling - set color', function() { commandStack.undo(); // then - expect(taskShape.businessObject.di.stroke).to.equal('FUCHSIA'); - expect(startEventShape.businessObject.di.stroke).to.equal('FUCHSIA'); + expect(taskShape.businessObject.di.get('border-color')).to.equal(FUCHSIA_HEX); + expect(startEventShape.businessObject.di.get('border-color')).to.equal(FUCHSIA_HEX); } )); @@ -371,7 +415,7 @@ describe('features/modeling - set color', function() { commandStack.redo(); // then - expect(taskShape.businessObject.di.fill).to.equal('FUCHSIA'); + expect(taskShape.businessObject.di.get('background-color')).to.equal(FUCHSIA_HEX); } )); @@ -389,7 +433,7 @@ describe('features/modeling - set color', function() { commandStack.redo(); // then - expect(taskShape.businessObject.di.fill).not.to.exist; + expect(taskShape.businessObject.di.get('background-color')).not.to.exist; } )); @@ -406,7 +450,7 @@ describe('features/modeling - set color', function() { commandStack.redo(); // then - expect(taskShape.businessObject.di.stroke).to.equal('FUCHSIA'); + expect(taskShape.businessObject.di.get('border-color')).to.equal(FUCHSIA_HEX); } )); @@ -424,7 +468,7 @@ describe('features/modeling - set color', function() { commandStack.redo(); // then - expect(taskShape.businessObject.di.stroke).not.to.exist; + expect(taskShape.businessObject.di.get('border-color')).not.to.exist; } )); @@ -442,8 +486,8 @@ describe('features/modeling - set color', function() { commandStack.redo(); // then - expect(taskShape.businessObject.di.fill).to.equal('FUCHSIA'); - expect(startEventShape.businessObject.di.fill).to.equal('FUCHSIA'); + expect(taskShape.businessObject.di.get('background-color')).to.equal(FUCHSIA_HEX); + expect(startEventShape.businessObject.di.get('background-color')).to.equal(FUCHSIA_HEX); } )); @@ -462,8 +506,8 @@ describe('features/modeling - set color', function() { commandStack.redo(); // then - expect(taskShape.businessObject.di.fill).not.to.exist; - expect(startEventShape.businessObject.di.fill).not.to.exist; + expect(taskShape.businessObject.di.get('background-color')).not.to.exist; + expect(startEventShape.businessObject.di.get('background-color')).not.to.exist; } )); @@ -481,8 +525,8 @@ describe('features/modeling - set color', function() { commandStack.redo(); // then - expect(taskShape.businessObject.di.stroke).to.equal('FUCHSIA'); - expect(startEventShape.businessObject.di.stroke).to.equal('FUCHSIA'); + expect(taskShape.businessObject.di.get('border-color')).to.equal(FUCHSIA_HEX); + expect(startEventShape.businessObject.di.get('border-color')).to.equal(FUCHSIA_HEX); } )); @@ -504,11 +548,48 @@ describe('features/modeling - set color', function() { commandStack.redo(); // then - expect(taskShape.businessObject.di.stroke).not.to.exist; - expect(startEventShape.businessObject.di.stroke).not.to.exist; + expect(taskShape.businessObject.di.get('border-color')).not.to.exist; + expect(startEventShape.businessObject.di.get('border-color')).not.to.exist; } )); }); + + // TODO @barmac: remove once we drop bpmn.io properties + describe('legacy', function() { + + it('should set both BPMN in Color and bpmn.io properties on BPMNShape', inject( + function(elementRegistry, modeling) { + + // given + var taskShape = elementRegistry.get('Task_1'); + + // when + modeling.setColor(taskShape, { stroke: '#abcdef', fill: '#fedcba' }); + + // then + expect(taskShape.businessObject.di.get('border-color')).to.eql('#abcdef'); + expect(taskShape.businessObject.di.get('stroke')).to.eql('#abcdef'); + expect(taskShape.businessObject.di.get('background-color')).to.eql('#fedcba'); + expect(taskShape.businessObject.di.get('fill')).to.eql('#fedcba'); + } + )); + + + it('should set both BPMN in Color and bpmn.io properties on BPMNEdge', inject( + function(elementRegistry, modeling) { + + // given + var sequenceFlow = elementRegistry.get('SequenceFlow_1'); + + // when + modeling.setColor(sequenceFlow, { stroke: '#abcdef' }); + + // then + expect(sequenceFlow.businessObject.di.get('border-color')).to.eql('#abcdef'); + expect(sequenceFlow.businessObject.di.get('stroke')).to.eql('#abcdef'); + } + )); + }); }); diff --git a/test/spec/features/replace/BpmnReplaceSpec.js b/test/spec/features/replace/BpmnReplaceSpec.js index d5d81d3b..ad7c3af5 100644 --- a/test/spec/features/replace/BpmnReplaceSpec.js +++ b/test/spec/features/replace/BpmnReplaceSpec.js @@ -1631,8 +1631,8 @@ describe('features/replace - bpmn replace', function() { var newElementData = { type: 'bpmn:UserTask' }, - fill = 'red', - stroke = 'green'; + fill = '#ff0000', + stroke = '#00ff00'; modeling.setColor(task, { fill: fill, stroke: stroke }); @@ -1642,6 +1642,10 @@ describe('features/replace - bpmn replace', function() { // then var businessObject = newElement.businessObject; + expect(businessObject.di.get('background-color')).to.equal(fill); + expect(businessObject.di.get('border-color')).to.equal(stroke); + + // TODO @barmac: remove when we drop bpmn.io properties expect(businessObject.di.fill).to.equal(fill); expect(businessObject.di.stroke).to.equal(stroke); }));