From e6fbfcb1e8b01d02d6d3ba8dd8ea35bfb4c5ffc6 Mon Sep 17 00:00:00 2001 From: Justin Greywolf Date: Thu, 2 Jan 2020 11:04:37 -0800 Subject: [PATCH 1/4] 1064 Adding tooltip and function calls to click evens in class diagrams --- src/diagrams/class/classDb.js | 27 ++++++++++--------- src/diagrams/class/classRenderer.js | 12 ++++++--- src/diagrams/flowchart/flowDb.js | 34 ++++------------------- src/mermaidAPI.js | 3 +++ src/themes/class.scss | 4 +++ src/utils.js | 42 ++++++++++++++++++++++++++++- 6 files changed, 76 insertions(+), 46 deletions(-) diff --git a/src/diagrams/class/classDb.js b/src/diagrams/class/classDb.js index 4eb3641a0..252a3f069 100644 --- a/src/diagrams/class/classDb.js +++ b/src/diagrams/class/classDb.js @@ -1,9 +1,9 @@ import * as d3 from 'd3'; -import { sanitizeUrl } from '@braintree/sanitize-url'; import { logger } from '../../logger'; import { getConfig } from '../../config'; +import utils from '../../utils'; -const MERMAID_DOM_ID_PREFIX = ''; +const MERMAID_DOM_ID_PREFIX = 'classid-'; const config = getConfig(); @@ -155,14 +155,10 @@ export const setLink = function(ids, linkStr, tooltip) { 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; - } + classes[id].link = utils.formatUrl(linkStr, config); if (tooltip) { - classes[id].tooltip = tooltip; + classes[id].tooltip = utils.sanitize(tooltip, config); } } }); @@ -182,9 +178,10 @@ export const setClickEvent = function(ids, functionName, tooltip) { setCssClass(ids, 'clickable'); }; -const setClickFunc = function(_id, functionName) { +const setClickFunc = function(_id, functionName, tooltip) { let id = _id; - if (_id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id; + let elemId = MERMAID_DOM_ID_PREFIX + id; + if (config.securityLevel !== 'loose') { return; } @@ -192,14 +189,18 @@ const setClickFunc = function(_id, functionName) { return; } if (typeof classes[id] !== 'undefined') { + if (tooltip) { + classes[id].tooltip = utils.sanitize(tooltip, config); + } + funs.push(function() { - const elem = document.querySelector(`[id="${id}"]`); + const elem = document.querySelector(`[id="${elemId}"]`); if (elem !== null) { - elem.setAttribute('title', classes[id].tooltip); + elem.addEventListener( 'click', function() { - window[functionName](id); + window[functionName](elemId); }, false ); diff --git a/src/diagrams/class/classRenderer.js b/src/diagrams/class/classRenderer.js index 89096d95f..a4a02af8b 100644 --- a/src/diagrams/class/classRenderer.js +++ b/src/diagrams/class/classRenderer.js @@ -8,6 +8,7 @@ import { parser } from './parser/classDiagram'; parser.yy = classDb; +const MERMAID_DOM_ID_PREFIX = 'classid-'; let idCache = {}; let classCnt = 0; @@ -319,7 +320,7 @@ const drawClass = function(elem, classDef) { } }; - const id = 'classId' + classCnt; + const id = MERMAID_DOM_ID_PREFIX + classDef.id; const classInfo = { id: id, label: classDef.id, @@ -339,8 +340,7 @@ const drawClass = function(elem, classDef) { title = g .append('svg:a') .attr('xlink:href', classDef.link) - .attr('xlink:target', '_blank') - .attr('xlink:title', classDef.tooltip) + .attr('target', '_blank') .append('text') .attr('y', conf.textHeight + conf.padding) .attr('x', 0); @@ -432,6 +432,12 @@ const drawClass = function(elem, classDef) { x.setAttribute('x', (rectWidth - x.getBBox().width) / 2); }); + if (classDef.tooltip) { + const tooltip = title + .insert('title') + .text(classDef.tooltip); + } + membersLine.attr('x2', rectWidth); methodsLine.attr('x2', rectWidth); diff --git a/src/diagrams/flowchart/flowDb.js b/src/diagrams/flowchart/flowDb.js index d166ec263..4b78231c2 100644 --- a/src/diagrams/flowchart/flowDb.js +++ b/src/diagrams/flowchart/flowDb.js @@ -1,5 +1,4 @@ import * as d3 from 'd3'; -import { sanitizeUrl } from '@braintree/sanitize-url'; import { logger } from '../../logger'; import utils from '../../utils'; import { getConfig } from '../../config'; @@ -20,25 +19,6 @@ let direction; // Functions to be run after graph rendering let funs = []; -const sanitize = text => { - let txt = text; - let htmlLabels = true; - if ( - config.flowchart && - (config.flowchart.htmlLabels === false || config.flowchart.htmlLabels === 'false') - ) - htmlLabels = false; - if (config.securityLevel !== 'loose' && htmlLabels) { // eslint-disable-line - txt = txt.replace(/
/g, '#br#'); - txt = txt.replace(//g, '#br#'); - txt = txt.replace(//g, '>'); - txt = txt.replace(/=/g, '='); - txt = txt.replace(/#br#/g, '
'); - } - - return txt; -}; - /** * Function called by parser when a node definition has been found * @param id @@ -63,7 +43,7 @@ export const addVertex = function(_id, text, type, style, classes) { vertices[id] = { id: id, styles: [], classes: [] }; } if (typeof text !== 'undefined') { - txt = sanitize(text.trim()); + txt = utils.sanitize(text.trim(), config); // strip quotes if string starts and ends with a quote if (txt[0] === '"' && txt[txt.length - 1] === '"') { @@ -113,7 +93,7 @@ export const addSingleLink = function(_start, _end, type, linktext) { linktext = type.text; if (typeof linktext !== 'undefined') { - edge.text = sanitize(linktext.trim()); + edge.text = utils.sanitize(linktext.trim(), config); // strip quotes if string starts and exnds with a quote if (edge.text[0] === '"' && edge.text[edge.text.length - 1] === '"') { @@ -225,7 +205,7 @@ export const setClass = function(ids, className) { const setTooltip = function(ids, tooltip) { ids.split(',').forEach(function(id) { if (typeof tooltip !== 'undefined') { - tooltips[id] = sanitize(tooltip); + tooltips[id] = utils.sanitize(tooltip, config); } }); }; @@ -266,11 +246,7 @@ export const setLink = function(ids, linkStr, tooltip) { let id = _id; if (_id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id; if (typeof vertices[id] !== 'undefined') { - if (config.securityLevel !== 'loose') { - vertices[id].link = sanitizeUrl(linkStr); // .replace(/javascript:.*/g, '') - } else { - vertices[id].link = linkStr; - } + vertices[id].link = utils.formatUrl(linkStr, config); } }); setTooltip(ids, tooltip); @@ -429,7 +405,7 @@ export const addSubGraph = function(_id, list, _title) { id = id || 'subGraph' + subCount; if (id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id; title = title || ''; - title = sanitize(title); + title = utils.sanitize(title, config); subCount = subCount + 1; const subGraph = { id: id, nodes: nodeList, title: title.trim(), classes: [] }; subGraphs.push(subGraph); diff --git a/src/mermaidAPI.js b/src/mermaidAPI.js index f95f16003..975b38299 100644 --- a/src/mermaidAPI.js +++ b/src/mermaidAPI.js @@ -613,6 +613,9 @@ const render = function(id, txt, cb, container) { case 'gantt': cb(svgCode, ganttDb.bindFunctions); break; + case 'class': + cb(svgCode, classDb.bindFunctions); + break; default: cb(svgCode); } diff --git a/src/themes/class.scss b/src/themes/class.scss index 90ac82da1..7207355bb 100644 --- a/src/themes/class.scss +++ b/src/themes/class.scss @@ -10,6 +10,10 @@ g.classGroup text { } } +g.clickable { + cursor: pointer; +} + g.classGroup rect { fill: $nodeBkg; stroke: $nodeBorder; diff --git a/src/utils.js b/src/utils.js index 05e3a127d..0f1a87203 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,5 +1,6 @@ import * as d3 from 'd3'; import { logger } from './logger'; +import { sanitizeUrl } from '@braintree/sanitize-url'; /** * @function detectType @@ -73,6 +74,43 @@ export const interpolateToCurve = (interpolate, defaultCurve) => { return d3[curveName] || defaultCurve; }; +export const sanitize = (text, config) => { + let txt = text; + let htmlLabels = true; + if ( + config.flowchart && + (config.flowchart.htmlLabels === false || config.flowchart.htmlLabels === 'false') + ) + htmlLabels = false; + + if (config.securityLevel !== 'loose' && htmlLabels) { // eslint-disable-line + txt = txt.replace(/
/g, '#br#'); + txt = txt.replace(//g, '#br#'); + txt = txt.replace(//g, '>'); + txt = txt.replace(/=/g, '='); + txt = txt.replace(/#br#/g, '
'); + } + + return txt; +}; + +export const formatUrl = (linkStr, config) => { + let url = linkStr; + + if (config.securityLevel !== 'loose') { + return sanitizeUrl(url); + } else { + url = url.trim(); + if (!!url) { + if (!/^(https?:)?\/\//i.test(url)) { + url = 'http://' + url; + } + } + + return url; + } +} + const distance = (p1, p2) => p1 && p2 ? Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)) : 0; @@ -174,5 +212,7 @@ export default { isSubstringInArray, interpolateToCurve, calcLabelPosition, - calcCardinalityPosition + calcCardinalityPosition, + sanitize, + formatUrl }; From 7f31e624cab93f0ae70587524416d7c8f21624de Mon Sep 17 00:00:00 2001 From: Justin Greywolf Date: Thu, 2 Jan 2020 11:24:06 -0800 Subject: [PATCH 2/4] Addressing code style issues --- src/diagrams/class/classDb.js | 2 +- src/diagrams/class/classRenderer.js | 5 +---- src/utils.js | 13 ++++++------- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/diagrams/class/classDb.js b/src/diagrams/class/classDb.js index 252a3f069..35e40411d 100644 --- a/src/diagrams/class/classDb.js +++ b/src/diagrams/class/classDb.js @@ -196,7 +196,7 @@ const setClickFunc = function(_id, functionName, tooltip) { funs.push(function() { const elem = document.querySelector(`[id="${elemId}"]`); if (elem !== null) { - + elem.addEventListener( 'click', function() { diff --git a/src/diagrams/class/classRenderer.js b/src/diagrams/class/classRenderer.js index a4a02af8b..ae0696cf6 100644 --- a/src/diagrams/class/classRenderer.js +++ b/src/diagrams/class/classRenderer.js @@ -11,7 +11,6 @@ parser.yy = classDb; const MERMAID_DOM_ID_PREFIX = 'classid-'; let idCache = {}; -let classCnt = 0; const conf = { dividerMargin: 10, padding: 5, @@ -433,8 +432,7 @@ const drawClass = function(elem, classDef) { }); if (classDef.tooltip) { - const tooltip = title - .insert('title') + title.insert('title') .text(classDef.tooltip); } @@ -445,7 +443,6 @@ const drawClass = function(elem, classDef) { classInfo.height = classBox.height + conf.padding + 0.5 * conf.dividerMargin; idCache[id] = classInfo; - classCnt++; return classInfo; }; diff --git a/src/utils.js b/src/utils.js index 0f1a87203..1e17366d1 100644 --- a/src/utils.js +++ b/src/utils.js @@ -95,13 +95,12 @@ export const sanitize = (text, config) => { }; export const formatUrl = (linkStr, config) => { - let url = linkStr; + let url = linkStr.trim(); - if (config.securityLevel !== 'loose') { - return sanitizeUrl(url); - } else { - url = url.trim(); - if (!!url) { + if (url) { + if (config.securityLevel !== 'loose') { + return sanitizeUrl(url); + } else { if (!/^(https?:)?\/\//i.test(url)) { url = 'http://' + url; } @@ -109,7 +108,7 @@ export const formatUrl = (linkStr, config) => { return url; } -} +}; const distance = (p1, p2) => p1 && p2 ? Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)) : 0; From 38097c9095af4cd485c96e646c96e9a0e5c3e05b Mon Sep 17 00:00:00 2001 From: Justin Greywolf Date: Thu, 2 Jan 2020 11:27:56 -0800 Subject: [PATCH 3/4] code style --- src/diagrams/class/classDb.js | 1 - src/diagrams/class/classRenderer.js | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/diagrams/class/classDb.js b/src/diagrams/class/classDb.js index 35e40411d..b3a67167e 100644 --- a/src/diagrams/class/classDb.js +++ b/src/diagrams/class/classDb.js @@ -196,7 +196,6 @@ const setClickFunc = function(_id, functionName, tooltip) { funs.push(function() { const elem = document.querySelector(`[id="${elemId}"]`); if (elem !== null) { - elem.addEventListener( 'click', function() { diff --git a/src/diagrams/class/classRenderer.js b/src/diagrams/class/classRenderer.js index ae0696cf6..c46ab5c85 100644 --- a/src/diagrams/class/classRenderer.js +++ b/src/diagrams/class/classRenderer.js @@ -432,8 +432,7 @@ const drawClass = function(elem, classDef) { }); if (classDef.tooltip) { - title.insert('title') - .text(classDef.tooltip); + title.insert('title').text(classDef.tooltip); } membersLine.attr('x2', rectWidth); From 591a104c80060858148962cc1c8466143d9a8d3f Mon Sep 17 00:00:00 2001 From: Justin Greywolf Date: Fri, 3 Jan 2020 10:14:07 -0800 Subject: [PATCH 4/4] 1064 Class diagram interactivity docs Updated documentation for Class Diagram with details for interactivity --- docs/classDiagram.md | 95 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/docs/classDiagram.md b/docs/classDiagram.md index dc61f79cf..f5987df35 100644 --- a/docs/classDiagram.md +++ b/docs/classDiagram.md @@ -345,6 +345,101 @@ class Shape{ ``` +## Interaction + +It is possible to bind a click event to a node, the click can lead to either a javascript callback or to a link which will be opened in a new browser tab. **Note**: This functionality is disabled when using `securityLevel='strict'` and enabled when using `securityLevel='loose'`. + +You would define these actions on a separate line after all classes have been declared. + +``` +action className "reference" "tooltip" +``` + +* _action_ is either `link` or `callback`, depending on which type of interaction you want to have called +* _className_ is the id of the node that the action will be associated with +* _reference_ is either the url link, or the function name for callback. (note: callback function will be called with the nodeId as parameter). +* (_optional_) tooltip is a string to be displayed when hovering over element (note: The styles of the tooltip are set by the class .mermaidTooltip.) + +### Examples: + +*URL Link:* + +``` +classDiagram +class Shape +link Shape "http://www.github.com" "This is a tooltip for a link" +``` + +*Callback:* + +``` +classDiagram +class Shape +callback Shape "callbackFunction" "This is a tooltip for a callback" +``` + +``` + + +``` + ## Styling Styling of the class diagram is done by defining a number of css classes. During rendering these classes are extracted from the file located at src/themes/class.scss