From e6fbfcb1e8b01d02d6d3ba8dd8ea35bfb4c5ffc6 Mon Sep 17 00:00:00 2001 From: Justin Greywolf Date: Thu, 2 Jan 2020 11:04:37 -0800 Subject: [PATCH] 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 };