Skip to content

Commit

Permalink
Merge pull request mermaid-js#1165 from jgreywolf/1064-ClickEventInCl…
Browse files Browse the repository at this point in the history
…assDiagram

1064 click event in class diagram
  • Loading branch information
knsv authored Jan 2, 2020
2 parents 465e99a + d63eb39 commit 7bd1408
Show file tree
Hide file tree
Showing 5 changed files with 322 additions and 7 deletions.
44 changes: 44 additions & 0 deletions cypress/integration/rendering/classDiagram.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,4 +228,48 @@ describe('Class diagram', () => {
);
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~ {
&lt;&lt;service&gt;&gt;
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~ {
&lt;&lt;service&gt;&gt;
int id
test()
}
callback class01 "functionCall" "A Tooltip"
`,
{}
);
cy.get('svg');
});
});
147 changes: 146 additions & 1 deletion src/diagrams/class/classDb.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import * as d3 from 'd3';
import { sanitizeUrl } from '@braintree/sanitize-url';
import { logger } from '../../logger';
import { getConfig } from '../../config';

const MERMAID_DOM_ID_PREFIX = '';

const config = getConfig();

let relations = [];
let classes = {};

let funs = [];

const splitClassNameAndType = function(id) {
let genericType = '';
let className = id;
Expand All @@ -29,6 +38,7 @@ export const addClass = function(id) {
classes[classId.className] = {
id: classId.className,
type: classId.type,
cssClasses: [],
methods: [],
members: [],
annotations: []
Expand All @@ -38,6 +48,8 @@ export const addClass = function(id) {
export const clear = function() {
relations = [];
classes = {};
funs = [];
funs.push(setupToolTips);
};

export const getClass = function(id) {
Expand Down Expand Up @@ -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 = {
LINE: 0,
DOTTED_LINE: 1
Expand All @@ -129,8 +226,53 @@ export const relationType = {
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 {
addClass,
bindFunctions,
clear,
getClass,
getClasses,
Expand All @@ -141,5 +283,8 @@ export default {
addMembers,
cleanupLabel,
lineType,
relationType
relationType,
setClickEvent,
setCssClass,
setLink
};
96 changes: 96 additions & 0 deletions src/diagrams/class/classDiagram.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,66 @@ describe('class diagram, ', function () {

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 () {
Expand Down Expand Up @@ -464,5 +524,41 @@ describe('class diagram, ', function () {
expect(testClass.methods.length).toBe(1);
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');
});
});
});
Loading

0 comments on commit 7bd1408

Please sign in to comment.