mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-08-30 21:56:43 +02:00
1064- Add click functionality to class diagrams
modified interaction functionality from flowcharts to work with class diagrams
This commit is contained in:
@@ -228,4 +228,48 @@ describe('Class diagram', () => {
|
|||||||
);
|
);
|
||||||
cy.get('svg');
|
cy.get('svg');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('9: should render a simple class diagram with clickable link', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`
|
||||||
|
classDiagram
|
||||||
|
Class01~T~ <|-- AveryLongClass : Cool
|
||||||
|
Class03~T~ *-- Class04~T~
|
||||||
|
Class01 : size()
|
||||||
|
Class01 : int chimp
|
||||||
|
Class01 : int gorilla
|
||||||
|
Class08 <--> C2: Cool label
|
||||||
|
class Class10~T~ {
|
||||||
|
<<service>>
|
||||||
|
int id
|
||||||
|
test()
|
||||||
|
}
|
||||||
|
link class01 "google.com" "A Tooltip"
|
||||||
|
`,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
cy.get('svg');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('10: should render a simple class diagram with clickable callback', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`
|
||||||
|
classDiagram
|
||||||
|
Class01~T~ <|-- AveryLongClass : Cool
|
||||||
|
Class03~T~ *-- Class04~T~
|
||||||
|
Class01 : size()
|
||||||
|
Class01 : int chimp
|
||||||
|
Class01 : int gorilla
|
||||||
|
Class08 <--> C2: Cool label
|
||||||
|
class Class10~T~ {
|
||||||
|
<<service>>
|
||||||
|
int id
|
||||||
|
test()
|
||||||
|
}
|
||||||
|
callback class01 "functionCall" "A Tooltip"
|
||||||
|
`,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
cy.get('svg');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,8 +1,17 @@
|
|||||||
|
import * as d3 from 'd3';
|
||||||
|
import { sanitizeUrl } from '@braintree/sanitize-url';
|
||||||
import { logger } from '../../logger';
|
import { logger } from '../../logger';
|
||||||
|
import { getConfig } from '../../config';
|
||||||
|
|
||||||
|
const MERMAID_DOM_ID_PREFIX = '';
|
||||||
|
|
||||||
|
const config = getConfig();
|
||||||
|
|
||||||
let relations = [];
|
let relations = [];
|
||||||
let classes = {};
|
let classes = {};
|
||||||
|
|
||||||
|
let funs = [];
|
||||||
|
|
||||||
const splitClassNameAndType = function(id) {
|
const splitClassNameAndType = function(id) {
|
||||||
let genericType = '';
|
let genericType = '';
|
||||||
let className = id;
|
let className = id;
|
||||||
@@ -29,6 +38,7 @@ export const addClass = function(id) {
|
|||||||
classes[classId.className] = {
|
classes[classId.className] = {
|
||||||
id: classId.className,
|
id: classId.className,
|
||||||
type: classId.type,
|
type: classId.type,
|
||||||
|
cssClasses: [],
|
||||||
methods: [],
|
methods: [],
|
||||||
members: [],
|
members: [],
|
||||||
annotations: []
|
annotations: []
|
||||||
@@ -38,6 +48,8 @@ export const addClass = function(id) {
|
|||||||
export const clear = function() {
|
export const clear = function() {
|
||||||
relations = [];
|
relations = [];
|
||||||
classes = {};
|
classes = {};
|
||||||
|
funs = [];
|
||||||
|
funs.push(setupToolTips);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getClass = function(id) {
|
export const getClass = function(id) {
|
||||||
@@ -117,6 +129,91 @@ export const cleanupLabel = function(label) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by parser when a special node is found, e.g. a clickable element.
|
||||||
|
* @param ids Comma separated list of ids
|
||||||
|
* @param className Class to add
|
||||||
|
*/
|
||||||
|
export const setCssClass = function(ids, className) {
|
||||||
|
ids.split(',').forEach(function(_id) {
|
||||||
|
let id = _id;
|
||||||
|
if (_id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id;
|
||||||
|
if (typeof classes[id] !== 'undefined') {
|
||||||
|
classes[id].cssClasses.push(className);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by parser when a link is found. Adds the URL to the vertex data.
|
||||||
|
* @param ids Comma separated list of ids
|
||||||
|
* @param linkStr URL to create a link for
|
||||||
|
* @param tooltip Tooltip for the clickable element
|
||||||
|
*/
|
||||||
|
export const setLink = function(ids, linkStr, tooltip) {
|
||||||
|
ids.split(',').forEach(function(_id) {
|
||||||
|
let id = _id;
|
||||||
|
if (_id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id;
|
||||||
|
if (typeof classes[id] !== 'undefined') {
|
||||||
|
if (config.securityLevel !== 'loose') {
|
||||||
|
classes[id].link = sanitizeUrl(linkStr);
|
||||||
|
} else {
|
||||||
|
classes[id].link = linkStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tooltip) {
|
||||||
|
classes[id].tooltip = tooltip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setCssClass(ids, 'clickable');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by parser when a click definition is found. Registers an event handler.
|
||||||
|
* @param ids Comma separated list of ids
|
||||||
|
* @param functionName Function to be called on click
|
||||||
|
* @param tooltip Tooltip for the clickable element
|
||||||
|
*/
|
||||||
|
export const setClickEvent = function(ids, functionName, tooltip) {
|
||||||
|
ids.split(',').forEach(function(id) {
|
||||||
|
setClickFunc(id, functionName, tooltip);
|
||||||
|
});
|
||||||
|
setCssClass(ids, 'clickable');
|
||||||
|
};
|
||||||
|
|
||||||
|
const setClickFunc = function(_id, functionName) {
|
||||||
|
let id = _id;
|
||||||
|
if (_id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id;
|
||||||
|
if (config.securityLevel !== 'loose') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof functionName === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof classes[id] !== 'undefined') {
|
||||||
|
funs.push(function() {
|
||||||
|
const elem = document.querySelector(`[id="${id}"]`);
|
||||||
|
if (elem !== null) {
|
||||||
|
elem.setAttribute('title', classes[id].tooltip);
|
||||||
|
elem.addEventListener(
|
||||||
|
'click',
|
||||||
|
function() {
|
||||||
|
window[functionName](id);
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const bindFunctions = function(element) {
|
||||||
|
funs.forEach(function(fun) {
|
||||||
|
fun(element);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const lineType = {
|
export const lineType = {
|
||||||
LINE: 0,
|
LINE: 0,
|
||||||
DOTTED_LINE: 1
|
DOTTED_LINE: 1
|
||||||
@@ -129,8 +226,53 @@ export const relationType = {
|
|||||||
DEPENDENCY: 3
|
DEPENDENCY: 3
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setupToolTips = function(element) {
|
||||||
|
let tooltipElem = d3.select('.mermaidTooltip');
|
||||||
|
if ((tooltipElem._groups || tooltipElem)[0][0] === null) {
|
||||||
|
tooltipElem = d3
|
||||||
|
.select('body')
|
||||||
|
.append('div')
|
||||||
|
.attr('class', 'mermaidTooltip')
|
||||||
|
.style('opacity', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const svg = d3.select(element).select('svg');
|
||||||
|
|
||||||
|
const nodes = svg.selectAll('g.node');
|
||||||
|
nodes
|
||||||
|
.on('mouseover', function() {
|
||||||
|
const el = d3.select(this);
|
||||||
|
const title = el.attr('title');
|
||||||
|
// Dont try to draw a tooltip if no data is provided
|
||||||
|
if (title === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rect = this.getBoundingClientRect();
|
||||||
|
|
||||||
|
tooltipElem
|
||||||
|
.transition()
|
||||||
|
.duration(200)
|
||||||
|
.style('opacity', '.9');
|
||||||
|
tooltipElem
|
||||||
|
.html(el.attr('title'))
|
||||||
|
.style('left', rect.left + (rect.right - rect.left) / 2 + 'px')
|
||||||
|
.style('top', rect.top - 14 + document.body.scrollTop + 'px');
|
||||||
|
el.classed('hover', true);
|
||||||
|
})
|
||||||
|
.on('mouseout', function() {
|
||||||
|
tooltipElem
|
||||||
|
.transition()
|
||||||
|
.duration(500)
|
||||||
|
.style('opacity', 0);
|
||||||
|
const el = d3.select(this);
|
||||||
|
el.classed('hover', false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
funs.push(setupToolTips);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
addClass,
|
addClass,
|
||||||
|
bindFunctions,
|
||||||
clear,
|
clear,
|
||||||
getClass,
|
getClass,
|
||||||
getClasses,
|
getClasses,
|
||||||
@@ -141,5 +283,8 @@ export default {
|
|||||||
addMembers,
|
addMembers,
|
||||||
cleanupLabel,
|
cleanupLabel,
|
||||||
lineType,
|
lineType,
|
||||||
relationType
|
relationType,
|
||||||
|
setClickEvent,
|
||||||
|
setCssClass,
|
||||||
|
setLink
|
||||||
};
|
};
|
||||||
|
@@ -250,6 +250,66 @@ describe('class diagram, ', function () {
|
|||||||
|
|
||||||
parser.parse(str);
|
parser.parse(str);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle click statement with link', function () {
|
||||||
|
const str =
|
||||||
|
'classDiagram\n' +
|
||||||
|
'class Class1 {\n' +
|
||||||
|
'%% Comment Class01 <|-- Class02\n' +
|
||||||
|
'int : test\n' +
|
||||||
|
'string : foo\n' +
|
||||||
|
'test()\n' +
|
||||||
|
'foo()\n' +
|
||||||
|
'}\n' +
|
||||||
|
'link Class01 "google.com" ';
|
||||||
|
|
||||||
|
parser.parse(str);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle click statement with link and tooltip', function () {
|
||||||
|
const str =
|
||||||
|
'classDiagram\n' +
|
||||||
|
'class Class1 {\n' +
|
||||||
|
'%% Comment Class01 <|-- Class02\n' +
|
||||||
|
'int : test\n' +
|
||||||
|
'string : foo\n' +
|
||||||
|
'test()\n' +
|
||||||
|
'foo()\n' +
|
||||||
|
'}\n' +
|
||||||
|
'link Class01 "google.com" "A Tooltip" ';
|
||||||
|
|
||||||
|
parser.parse(str);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle click statement with callback', function () {
|
||||||
|
const str =
|
||||||
|
'classDiagram\n' +
|
||||||
|
'class Class1 {\n' +
|
||||||
|
'%% Comment Class01 <|-- Class02\n' +
|
||||||
|
'int : test\n' +
|
||||||
|
'string : foo\n' +
|
||||||
|
'test()\n' +
|
||||||
|
'foo()\n' +
|
||||||
|
'}\n' +
|
||||||
|
'callback Class01 "functionCall" ';
|
||||||
|
|
||||||
|
parser.parse(str);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle click statement with callback and tooltip', function () {
|
||||||
|
const str =
|
||||||
|
'classDiagram\n' +
|
||||||
|
'class Class1 {\n' +
|
||||||
|
'%% Comment Class01 <|-- Class02\n' +
|
||||||
|
'int : test\n' +
|
||||||
|
'string : foo\n' +
|
||||||
|
'test()\n' +
|
||||||
|
'foo()\n' +
|
||||||
|
'}\n' +
|
||||||
|
'callback Class01 "functionCall" "A Tooltip" ';
|
||||||
|
|
||||||
|
parser.parse(str);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when fetching data from a classDiagram graph it', function () {
|
describe('when fetching data from a classDiagram graph it', function () {
|
||||||
@@ -464,5 +524,41 @@ describe('class diagram, ', function () {
|
|||||||
expect(testClass.methods.length).toBe(1);
|
expect(testClass.methods.length).toBe(1);
|
||||||
expect(testClass.methods[0]).toBe('someMethod()$');
|
expect(testClass.methods[0]).toBe('someMethod()$');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should associate link and css appropriately', function () {
|
||||||
|
const str = 'classDiagram\n' + 'class Class1\n' + 'Class1 : someMethod()\n' + 'link Class1 "google.com"';
|
||||||
|
parser.parse(str);
|
||||||
|
|
||||||
|
const testClass = parser.yy.getClass('Class1');
|
||||||
|
expect(testClass.link).toBe('about:blank');//('google.com'); security needs to be set to 'loose' for this to work right
|
||||||
|
expect(testClass.cssClasses.length).toBe(1);
|
||||||
|
expect(testClass.cssClasses[0]).toBe('clickable');
|
||||||
|
});
|
||||||
|
it('should associate link with tooltip', function () {
|
||||||
|
const str = 'classDiagram\n' + 'class Class1\n' + 'Class1 : someMethod()\n' + 'link Class1 "google.com" "A tooltip"';
|
||||||
|
parser.parse(str);
|
||||||
|
|
||||||
|
const testClass = parser.yy.getClass('Class1');
|
||||||
|
expect(testClass.link).toBe('about:blank');//('google.com'); security needs to be set to 'loose' for this to work right
|
||||||
|
expect(testClass.tooltip).toBe('A tooltip');
|
||||||
|
expect(testClass.cssClasses.length).toBe(1);
|
||||||
|
expect(testClass.cssClasses[0]).toBe('clickable');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should associate callback appropriately', function () {
|
||||||
|
spyOn(classDb, 'setClickEvent');
|
||||||
|
const str = 'classDiagram\n' + 'class Class1\n' + 'Class1 : someMethod()\n' + 'callback Class1 "functionCall"';
|
||||||
|
parser.parse(str);
|
||||||
|
|
||||||
|
expect(classDb.setClickEvent).toHaveBeenCalledWith('Class1', 'functionCall', undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should associate callback with tooltip', function () {
|
||||||
|
spyOn(classDb, 'setClickEvent');
|
||||||
|
const str = 'classDiagram\n' + 'class Class1\n' + 'Class1 : someMethod()\n' + 'callback Class1 "functionCall" "A tooltip"';
|
||||||
|
parser.parse(str);
|
||||||
|
|
||||||
|
expect(classDb.setClickEvent).toHaveBeenCalledWith('Class1', 'functionCall', 'A tooltip');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -280,6 +280,11 @@ const drawEdge = function(elem, path, relation) {
|
|||||||
const drawClass = function(elem, classDef) {
|
const drawClass = function(elem, classDef) {
|
||||||
logger.info('Rendering class ' + classDef);
|
logger.info('Rendering class ' + classDef);
|
||||||
|
|
||||||
|
let cssClassStr = 'classGroup ';
|
||||||
|
if (classDef.cssClasses.length > 0) {
|
||||||
|
cssClassStr = cssClassStr + classDef.cssClasses.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
const addTspan = function(textEl, txt, isFirst) {
|
const addTspan = function(textEl, txt, isFirst) {
|
||||||
let displayText = txt;
|
let displayText = txt;
|
||||||
let cssStyle = '';
|
let cssStyle = '';
|
||||||
@@ -326,13 +331,26 @@ const drawClass = function(elem, classDef) {
|
|||||||
const g = elem
|
const g = elem
|
||||||
.append('g')
|
.append('g')
|
||||||
.attr('id', id)
|
.attr('id', id)
|
||||||
.attr('class', 'classGroup');
|
.attr('class', cssClassStr);
|
||||||
|
|
||||||
// add title
|
// add title
|
||||||
const title = g
|
let title;
|
||||||
|
if (classDef.link) {
|
||||||
|
title = g
|
||||||
|
.append("svg:a")
|
||||||
|
.attr("xlink:href", classDef.link)
|
||||||
|
.attr('xlink:target', '_blank')
|
||||||
|
.attr('xlink:title', classDef.tooltip)
|
||||||
.append('text')
|
.append('text')
|
||||||
.attr('y', conf.textHeight + conf.padding)
|
.attr('y', conf.textHeight + conf.padding)
|
||||||
.attr('x', 0);
|
.attr('x', 0);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
title = g
|
||||||
|
.append('text')
|
||||||
|
.attr('y', conf.textHeight + conf.padding)
|
||||||
|
.attr('x', 0);
|
||||||
|
}
|
||||||
|
|
||||||
// add annotations
|
// add annotations
|
||||||
let isFirst = true;
|
let isFirst = true;
|
||||||
@@ -348,7 +366,6 @@ const drawClass = function(elem, classDef) {
|
|||||||
classTitleString += '<' + classDef.type + '>';
|
classTitleString += '<' + classDef.type + '>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// add class title
|
|
||||||
const classTitle = title
|
const classTitle = title
|
||||||
.append('tspan')
|
.append('tspan')
|
||||||
.text(classTitleString)
|
.text(classTitleString)
|
||||||
@@ -434,6 +451,7 @@ export const setConf = function(cnf) {
|
|||||||
conf[key] = cnf[key];
|
conf[key] = cnf[key];
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws a flowchart in the tag with id: id based on the graph definition in text.
|
* Draws a flowchart in the tag with id: id based on the graph definition in text.
|
||||||
* @param text
|
* @param text
|
||||||
@@ -470,10 +488,12 @@ export const draw = function(text, id) {
|
|||||||
for (let i = 0; i < keys.length; i++) {
|
for (let i = 0; i < keys.length; i++) {
|
||||||
const classDef = classes[keys[i]];
|
const classDef = classes[keys[i]];
|
||||||
const node = drawClass(diagram, classDef);
|
const node = drawClass(diagram, classDef);
|
||||||
|
|
||||||
// Add nodes to the graph. The first argument is the node id. The second is
|
// Add nodes to the graph. The first argument is the node id. The second is
|
||||||
// metadata about the node. In this case we're going to add labels to each of
|
// metadata about the node. In this case we're going to add labels to each of
|
||||||
// our nodes.
|
// our nodes.
|
||||||
g.setNode(node.id, node);
|
g.setNode(node.id, node);
|
||||||
|
|
||||||
logger.info('Org height: ' + node.height);
|
logger.info('Org height: ' + node.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -21,6 +21,9 @@
|
|||||||
|
|
||||||
|
|
||||||
"class" return 'CLASS';
|
"class" return 'CLASS';
|
||||||
|
//"click" return 'CLICK';
|
||||||
|
"callback" return 'CALLBACK';
|
||||||
|
"link" return 'LINK';
|
||||||
"<<" return 'ANNOTATION_START';
|
"<<" return 'ANNOTATION_START';
|
||||||
">>" return 'ANNOTATION_END';
|
">>" return 'ANNOTATION_END';
|
||||||
[~] this.begin("generic");
|
[~] this.begin("generic");
|
||||||
@@ -149,6 +152,7 @@ statement
|
|||||||
| classStatement
|
| classStatement
|
||||||
| methodStatement
|
| methodStatement
|
||||||
| annotationStatement
|
| annotationStatement
|
||||||
|
| clickStatement
|
||||||
;
|
;
|
||||||
|
|
||||||
classStatement
|
classStatement
|
||||||
@@ -198,6 +202,13 @@ lineType
|
|||||||
| DOTTED_LINE {$$=yy.lineType.DOTTED_LINE;}
|
| DOTTED_LINE {$$=yy.lineType.DOTTED_LINE;}
|
||||||
;
|
;
|
||||||
|
|
||||||
|
clickStatement
|
||||||
|
: CALLBACK className STR {$$ = $1;yy.setClickEvent($2, $3, undefined);}
|
||||||
|
| CALLBACK className STR STR {$$ = $1;yy.setClickEvent($2, $3, $4);}
|
||||||
|
| LINK className STR {$$ = $1;yy.setLink($2, $3, undefined);}
|
||||||
|
| LINK className STR STR {$$ = $1;yy.setLink($2, $3, $4);}
|
||||||
|
;
|
||||||
|
|
||||||
commentToken : textToken | graphCodeTokens ;
|
commentToken : textToken | graphCodeTokens ;
|
||||||
|
|
||||||
textToken : textNoTagsToken | TAGSTART | TAGEND | '==' | '--' | PCT | DEFAULT;
|
textToken : textNoTagsToken | TAGSTART | TAGEND | '==' | '--' | PCT | DEFAULT;
|
||||||
|
Reference in New Issue
Block a user