fix: consider default style when computing effective style properties (#152)

`StyleSheet.getCellStyle` didn't keep the properties of the default
style when `baseStyleNames` was set in the `cellStyle` parameter.

The JSDoc was incorrect (it came from mxGraph) about how the style is
computed, in particular about the default style. It probably leads to
the erroneous implementation during migration. It is now fixed and
clearly describe the rules followed to merge style properties.

The 'Stylesheet' story has been updated to correctly use the maxGraph
API. It also includes more examples involving `baseStyleNames` to
show the 'properties merge' in action.
development
Thomas Bouffard 2022-12-17 09:16:26 +01:00 committed by GitHub
parent b7a322b36f
commit 5a346079b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 241 additions and 48 deletions

View File

@ -0,0 +1,157 @@
/*
Copyright 2022-present The maxGraph project Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { describe, expect, test } from '@jest/globals';
import { CellStateStyle, CellStyle, Stylesheet } from '../../../src';
/**
* Additional properties to test extension points by extending `CellStyle` and `CustomCellStateStyle`.
*/
type CustomStyleAdditions = {
customProp1: number;
customProp2: string;
};
type CustomCellStyle = CellStyle & CustomStyleAdditions;
type CustomCellStateStyle = CellStateStyle & CustomStyleAdditions;
// Here we just check that the default styles are initialized, and some properties set
// We don't test all properties on purpose
describe('Default styles', () => {
test('Default edge style is set', () => {
expect(new Stylesheet().getDefaultEdgeStyle()).toEqual(
expect.objectContaining(<CellStyle>{
align: 'center',
endArrow: 'classic',
shape: 'connector',
})
);
});
test('Default vertex style is set', () => {
expect(new Stylesheet().getDefaultVertexStyle()).toEqual(
expect.objectContaining(<CellStyle>{
align: 'center',
fillColor: '#C3D9FF',
shape: 'rectangle',
})
);
});
});
describe('putCellStyle', () => {
test('a vertex style', () => {
const style: CellStyle = { shape: 'rectangle', fillColor: 'red' };
const stylesheet = new Stylesheet();
stylesheet.putCellStyle('aVertexStyle', style);
expect(stylesheet.styles.get('aVertexStyle')).toBe(style);
});
});
describe('getCellStyle', () => {
test.each([undefined, []])('baseStyleNames=%s', (baseStyleNames) => {
const stylesheet = new Stylesheet();
const cellStyle = stylesheet.getCellStyle(
{
baseStyleNames,
fillColor: 'red',
shape: 'triangle',
strokeColor: 'yellow',
},
{ align: 'center', strokeColor: 'green' }
);
expect(cellStyle).toStrictEqual(<CellStateStyle>{
align: 'center', // from default
fillColor: 'red',
shape: 'triangle',
strokeColor: 'yellow', // from style
});
});
test('baseStyleNames set and related styles are registered', () => {
const stylesheet = new Stylesheet();
stylesheet.putCellStyle('style-1', { shape: 'triangle', fillColor: 'blue' });
const cellStyle = stylesheet.getCellStyle(
{
baseStyleNames: ['style-1'],
shape: 'cloud',
strokeColor: 'yellow',
},
{ strokeColor: 'green', dashed: true }
);
expect(cellStyle).toStrictEqual(<CellStateStyle>{
dashed: true, // from default
fillColor: 'blue', // from style-1
shape: 'cloud', // from style (override default and style-1)
strokeColor: 'yellow',
});
});
test('baseStyleNames set and related styles are registered or not', () => {
const stylesheet = new Stylesheet();
stylesheet.putCellStyle('style-1', {
shape: 'triangle',
fillColor: 'blue',
fillOpacity: 80,
});
stylesheet.putCellStyle('style2', {
arcSize: 6,
fillColor: 'black',
fillOpacity: 75,
});
stylesheet.putCellStyle('style3', { fillColor: 'chartreuse' });
const cellStyle = stylesheet.getCellStyle(
{
baseStyleNames: ['style-1', 'unknown', 'style2', 'style3'],
shape: 'cloud',
strokeColor: 'yellow',
},
{ strokeColor: 'green', dashed: true }
);
expect(cellStyle).toStrictEqual(<CellStateStyle>{
arcSize: 6, // from style2
dashed: true, // from default
fillColor: 'chartreuse', // from style3 (latest in baseStyleNames)
fillOpacity: 75, // from style2 (latest in baseStyleNames having this property)
shape: 'cloud', // from style (override default and style-1)
strokeColor: 'yellow',
});
});
test('Custom CellStyle type - baseStyleNames set and related styles are registered', () => {
const stylesheet = new Stylesheet();
stylesheet.putCellStyle('style-1', <CustomCellStyle>{
customProp1: 100,
shape: 'triangle',
});
const cellStyle = stylesheet.getCellStyle(
{
baseStyleNames: ['style-1'],
shape: 'cloud',
strokeColor: 'yellow',
},
<CustomCellStyle>{ strokeColor: 'green', customProp1: 10, customProp2: 'value' }
);
expect(cellStyle).toStrictEqual(<CustomCellStateStyle>{
customProp1: 100, // from style-1
customProp2: 'value', // from default
shape: 'cloud',
strokeColor: 'yellow',
});
});
});

View File

@ -192,26 +192,31 @@ export class Stylesheet {
}
/**
* Returns the cell style for the specified baseStyleNames or the given
* defaultStyle if no style can be found for the given baseStyleNames.
* Returns a {@link CellStateStyle} computed by merging the default style, styles referenced in the specified `baseStyleNames`
* and the properties of the `cellStyle` parameter.
*
* The properties are merged by taken the properties from various styles in the following order:
* - default style
* - registered styles referenced in `baseStyleNames`, in the order of the array
* - `cellStyle` parameter
*
* @param cellStyle An object that represents the style.
* @param defaultStyle Default style to be returned if no style can be found.
* @param defaultStyle Default style used as reference to compute the returned style.
*/
getCellStyle(cellStyle: CellStyle, defaultStyle: CellStateStyle) {
let style: CellStateStyle;
if (cellStyle.baseStyleNames && cellStyle.baseStyleNames.length > 0) {
if (cellStyle.baseStyleNames) {
// creates style with the given baseStyleNames. (merges from left to right)
style = cellStyle.baseStyleNames.reduce((acc, styleName) => {
return (acc = {
...acc,
...this.styles.get(styleName),
});
}, {});
} else if (cellStyle.baseStyleNames && cellStyle.baseStyleNames.length === 0) {
// baseStyleNames is explicitly an empty array, so don't use any default styles.
style = {};
style = cellStyle.baseStyleNames.reduce(
(acc, styleName) => {
return (acc = {
...acc,
...this.styles.get(styleName),
});
},
{ ...defaultStyle }
);
} else {
style = { ...defaultStyle };
}
@ -222,6 +227,9 @@ export class Stylesheet {
...cellStyle,
};
// Remove the 'baseStyleNames' that may have been copied from the cellStyle parameter to match the method signature
'baseStyleNames' in style && delete style.baseStyleNames;
return style;
}
}

View File

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { Graph, Perimeter, constants, EdgeStyle } from '@maxgraph/core';
import { Graph, Perimeter, Point } from '@maxgraph/core';
import { globalTypes } from '../.storybook/preview';
@ -41,8 +41,7 @@ const Template = ({ label, ...args }) => {
// Disables basic selection and cell handling
graph.setEnabled(false);
// Returns a special label for edges. Note: This does
// a supercall to use the default implementation.
// Returns a special label for edges. Note: This does a super call to use the default implementation.
graph.getLabel = function (cell) {
const label = Graph.prototype.getLabel.apply(this, arguments);
@ -56,9 +55,7 @@ const Template = ({ label, ...args }) => {
graph.setTooltips(true);
graph.getTooltip = function (state) {
const { cell } = state;
const model = this.getDataModel();
if (modcellel.isEdge()) {
if (cell.isEdge()) {
const source = this.getLabel(cell.getTerminal(true));
const target = this.getLabel(cell.getTerminal(false));
@ -68,30 +65,49 @@ const Template = ({ label, ...args }) => {
};
// Creates the default style for vertices
let style = [];
style.shape = constants.SHAPE.RECTANGLE;
style.perimiter = Perimeter.RectanglePerimeter;
style.strokeColor = 'gray';
style.rounded = true;
style.fillColor = '#EEEEEE';
style.gradientColor = 'white';
style.fontColor = '#774400';
style.align = constants.ALIGN.CENTER;
style.verticalAlign = constants.ALIGN.MIDDLE;
style.fontSize = '12';
style.fontStyle = 1;
graph.getStylesheet().putDefaultVertexStyle(style);
/** @type {import('@maxgraph/core').CellStyle} */
const defaultVertexStyle = {
align: 'center',
fillColor: '#EEEEEE',
fontColor: '#774400',
fontSize: 12,
fontStyle: 1,
gradientColor: 'white',
perimeter: Perimeter.RectanglePerimeter,
rounded: true,
shape: 'rectangle',
strokeColor: 'gray',
verticalAlign: 'middle',
};
graph.getStylesheet().putDefaultVertexStyle(defaultVertexStyle);
// Creates the default style for edges
style = [];
style.shape = constants.SHAPE.CONNECTOR;
style.strokeColor = '#6482B9';
style.align = constants.ALIGN.CENTER;
style.verticalAlign = constants.ALIGN.MIDDLE;
style.edge = EdgeStyle.ElbowConnector;
style.endArrow = constants.ARROW.CLASSIC;
style.fontSize = '10';
graph.getStylesheet().putDefaultEdgeStyle(style);
/** @type {import('@maxgraph/core').CellStyle} */
const defaultEdgeStyle = {
align: 'center',
edgeStyle: 'elbowEdgeStyle',
endArrow: 'classic',
fontSize: 10,
shape: 'connector',
strokeColor: '#6482B9',
verticalAlign: 'middle',
};
graph.getStylesheet().putDefaultEdgeStyle(defaultEdgeStyle);
// Additional styles
const redColor = '#f10d0d';
/** @type {import('@maxgraph/core').CellStyle} */
const edgeImportantStyle = {
fontColor: redColor,
fontSize: 14,
fontStyle: 3,
strokeColor: redColor,
};
graph.getStylesheet().putCellStyle('importantEdge', edgeImportantStyle);
/** @type {import('@maxgraph/core').CellStyle} */
const shapeImportantStyle = { strokeColor: redColor };
graph.getStylesheet().putCellStyle('importantShape', shapeImportantStyle);
// Gets the default parent for inserting new cells. This
// is normally the first child of the root (ie. layer 0).
@ -99,21 +115,33 @@ const Template = ({ label, ...args }) => {
// Adds cells to the model in a single step
graph.batchUpdate(() => {
const v1 = graph.insertVertex(parent, null, 'Interval 1', 20, 20, 180, 30);
const v1 = graph.insertVertex({
parent,
value: 'Interval 1',
position: [20, 20],
size: [180, 30],
style: { baseStyleNames: ['importantShape'] },
});
const v2 = graph.insertVertex(parent, null, 'Interval 2', 140, 80, 280, 30);
const v3 = graph.insertVertex(parent, null, 'Interval 3', 200, 140, 360, 30);
const v4 = graph.insertVertex(parent, null, 'Interval 4', 480, 200, 120, 30);
const v5 = graph.insertVertex(parent, null, 'Interval 5', 60, 260, 400, 30);
const e1 = graph.insertEdge(parent, null, '1', v1, v2);
e1.getGeometry().points = [{ x: 160, y: 60 }];
const e2 = graph.insertEdge(parent, null, '2', v1, v5);
e2.getGeometry().points = [{ x: 80, y: 60 }];
e1.getGeometry().points = [new Point(160, 60)];
const e2 = graph.insertEdge({
parent,
value: '2',
source: v1,
target: v5,
style: { baseStyleNames: ['importantEdge'] },
});
e2.getGeometry().points = [new Point(80, 60)];
const e3 = graph.insertEdge(parent, null, '3', v2, v3);
e3.getGeometry().points = [{ x: 280, y: 120 }];
e3.getGeometry().points = [new Point(280, 120)];
const e4 = graph.insertEdge(parent, null, '4', v3, v4);
e4.getGeometry().points = [{ x: 500, y: 180 }];
e4.getGeometry().points = [new Point(500, 180)];
const e5 = graph.insertEdge(parent, null, '5', v3, v5);
e5.getGeometry().points = [{ x: 380, y: 180 }];
e5.getGeometry().points = [new Point(380, 180)];
});
return container;