mirror of
				https://github.com/mermaid-js/mermaid.git
				synced 2025-10-31 02:44:17 +01: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'); | ||||
|   }); | ||||
|  | ||||
|   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 { 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; | ||||
| @@ -29,6 +38,7 @@ export const addClass = function(id) { | ||||
|   classes[classId.className] = { | ||||
|     id: classId.className, | ||||
|     type: classId.type, | ||||
|     cssClasses: [], | ||||
|     methods: [], | ||||
|     members: [], | ||||
|     annotations: [] | ||||
| @@ -38,6 +48,8 @@ export const addClass = function(id) { | ||||
| export const clear = function() { | ||||
|   relations = []; | ||||
|   classes = {}; | ||||
|   funs = []; | ||||
|   funs.push(setupToolTips); | ||||
| }; | ||||
|  | ||||
| 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 = { | ||||
|   LINE: 0, | ||||
|   DOTTED_LINE: 1 | ||||
| @@ -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, | ||||
| @@ -141,5 +283,8 @@ export default { | ||||
|   addMembers, | ||||
|   cleanupLabel, | ||||
|   lineType, | ||||
|   relationType | ||||
|   relationType, | ||||
|   setClickEvent, | ||||
|   setCssClass, | ||||
|   setLink | ||||
| }; | ||||
|   | ||||
| @@ -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 () { | ||||
| @@ -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'); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -280,6 +280,11 @@ const drawEdge = function(elem, path, relation) { | ||||
| const drawClass = function(elem, 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) { | ||||
|     let displayText = txt; | ||||
|     let cssStyle = ''; | ||||
| @@ -326,13 +331,26 @@ const drawClass = function(elem, classDef) { | ||||
|   const g = elem | ||||
|     .append('g') | ||||
|     .attr('id', id) | ||||
|     .attr('class', 'classGroup'); | ||||
|     .attr('class', cssClassStr); | ||||
|  | ||||
|   // 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') | ||||
|       .attr('y', conf.textHeight + conf.padding) | ||||
|       .attr('x', 0); | ||||
|   } | ||||
|   else { | ||||
|     title = g | ||||
|       .append('text') | ||||
|       .attr('y', conf.textHeight + conf.padding) | ||||
|       .attr('x', 0); | ||||
|   } | ||||
|  | ||||
|   // add annotations | ||||
|   let isFirst = true; | ||||
| @@ -348,7 +366,6 @@ const drawClass = function(elem, classDef) { | ||||
|     classTitleString += '<' + classDef.type + '>'; | ||||
|   } | ||||
|  | ||||
|   // add class title | ||||
|   const classTitle = title | ||||
|     .append('tspan') | ||||
|     .text(classTitleString) | ||||
| @@ -434,6 +451,7 @@ export const setConf = function(cnf) { | ||||
|     conf[key] = cnf[key]; | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Draws a flowchart in the tag with id: id based on the graph definition in text. | ||||
|  * @param text | ||||
| @@ -470,10 +488,12 @@ export const draw = function(text, id) { | ||||
|   for (let i = 0; i < keys.length; i++) { | ||||
|     const classDef = classes[keys[i]]; | ||||
|     const node = drawClass(diagram, classDef); | ||||
|  | ||||
|     // 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 | ||||
|     // our nodes. | ||||
|     g.setNode(node.id, node); | ||||
|  | ||||
|     logger.info('Org height: ' + node.height); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -21,6 +21,9 @@ | ||||
|  | ||||
|  | ||||
| "class"               return 'CLASS'; | ||||
| //"click"               return 'CLICK'; | ||||
| "callback"            return 'CALLBACK'; | ||||
| "link"                return 'LINK'; | ||||
| "<<"                  return 'ANNOTATION_START'; | ||||
| ">>"                  return 'ANNOTATION_END'; | ||||
| [~]                   this.begin("generic"); | ||||
| @@ -149,6 +152,7 @@ statement | ||||
|     | classStatement | ||||
|     | methodStatement | ||||
|     | annotationStatement | ||||
|     | clickStatement | ||||
|     ; | ||||
|  | ||||
| classStatement | ||||
| @@ -198,6 +202,13 @@ lineType | ||||
|     | 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 ; | ||||
|  | ||||
| textToken      : textNoTagsToken | TAGSTART | TAGEND | '=='  | '--' | PCT | DEFAULT; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Justin Greywolf
					Justin Greywolf