Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions packages/core/__tests__/view/style/StyleSheet.test.ts
Original file line number Diff line number Diff line change
@@ -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',
});
});
});
34 changes: 21 additions & 13 deletions packages/core/src/view/style/Stylesheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
Expand All @@ -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;
}
}
Expand Down
98 changes: 63 additions & 35 deletions packages/html/stories/Stylesheet.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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);

Expand All @@ -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));

Expand All @@ -68,52 +65,83 @@ 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).
const parent = graph.getDefaultParent();

// 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;
Expand Down