Compare commits

..

9 Commits

Author SHA1 Message Date
Shubham P
a8d89db397 Merge branch 'develop' into tooltip-positioning-issue 2025-11-11 09:58:43 +05:30
Shubham P
9448aec481 Merge branch 'develop' into tooltip-positioning-issue 2025-11-11 09:22:29 +05:30
darshanr0107
fcd2791b2d fix: Use DOMPurify to sanitize HTML content
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-10-16 11:52:26 +05:30
darshanr0107
feed9d75bb refactor: use a shared utility function for creating tooltip
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-10-15 19:48:52 +05:30
darshanr0107
f356836f71 Merge branch 'tooltip-positioning-issue' of https://github.com/mermaid-js/mermaid into tooltip-positioning-issue 2025-10-07 12:53:22 +05:30
darshanr0107
ff15e51d2e chore: added changeset
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-10-07 12:53:10 +05:30
autofix-ci[bot]
ddd4763db2 [autofix.ci] apply automated fixes 2025-10-07 06:50:56 +00:00
darshanr0107
6670ad7229 fix : escape HTML in tooltip titles to prevent DOM injection
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-10-07 12:15:32 +05:30
darshanr0107
b4a5fe6c45 fix: tooltip appears at bottom of page instead of near hovered element
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-10-07 11:56:26 +05:30
4 changed files with 56 additions and 32 deletions

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Correct tooltip placement to appear near hovered element

View File

@@ -1,4 +1,4 @@
import { select, type Selection } from 'd3';
import { select } from 'd3';
import { log } from '../../logger.js';
import { getConfig } from '../../diagram-api/diagramAPI.js';
import common from '../common/common.js';
@@ -12,6 +12,7 @@ import {
setDiagramTitle,
getDiagramTitle,
} from '../common/commonDb.js';
import { createTooltip } from '../common/svgDrawCommon.js';
import { ClassMember } from './classTypes.js';
import type {
ClassRelation,
@@ -26,6 +27,7 @@ import type {
} from './classTypes.js';
import type { Node, Edge } from '../../rendering-util/types.js';
import type { DiagramDB } from '../../diagram-api/types.js';
import DOMPurify from 'dompurify';
const MERMAID_DOM_ID_PREFIX = 'classId-';
let classCounter = 0;
@@ -483,43 +485,45 @@ export class ClassDB implements DiagramDB {
LOLLIPOP: 4,
};
// Utility function to escape HTML meta-characters
private escapeHtml(str: string): string {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
private readonly setupToolTips = (element: Element) => {
let tooltipElem: Selection<HTMLDivElement, unknown, HTMLElement, unknown> =
select('.mermaidTooltip');
// @ts-expect-error - Incorrect types
if ((tooltipElem._groups || tooltipElem)[0][0] === null) {
tooltipElem = select('body')
.append('div')
.attr('class', 'mermaidTooltip')
.style('opacity', 0);
}
const tooltipElem = createTooltip();
const svg = select(element).select('svg');
const nodes = svg.selectAll('g.node');
const nodes = svg.selectAll('g').filter(function () {
return select(this).attr('title') !== null;
});
nodes
.on('mouseover', (event: MouseEvent) => {
const el = select(event.currentTarget as HTMLElement);
const title = el.attr('title');
// Don't try to draw a tooltip if no data is provided
if (title === null) {
if (!title) {
return;
}
// @ts-ignore - getBoundingClientRect is not part of the d3 type definition
const rect = this.getBoundingClientRect();
const rect = (event.currentTarget as Element).getBoundingClientRect();
tooltipElem.transition().duration(200).style('opacity', '.9');
tooltipElem
.text(el.attr('title'))
.style('left', window.scrollX + rect.left + (rect.right - rect.left) / 2 + 'px')
.style('top', window.scrollY + rect.top - 14 + document.body.scrollTop + 'px');
tooltipElem.html(tooltipElem.html().replace(/&lt;br\/&gt;/g, '<br/>'));
.html(DOMPurify.sanitize(title))
.style('left', `${window.scrollX + rect.left + rect.width / 2}px`)
.style('top', `${window.scrollY + rect.bottom + 4}px`);
el.classed('hover', true);
})
.on('mouseout', (event: MouseEvent) => {
tooltipElem.transition().duration(500).style('opacity', 0);
const el = select(event.currentTarget as HTMLElement);
el.classed('hover', false);
select(event.currentTarget as HTMLElement).classed('hover', false);
});
};

View File

@@ -1,4 +1,5 @@
import { sanitizeUrl } from '@braintree/sanitize-url';
import { select } from 'd3';
import type { SVG, SVGGroup } from '../../diagram-api/types.js';
import { lineBreakRegex } from './common.js';
import type {
@@ -135,3 +136,24 @@ export const getTextObj = (): TextObject => {
};
return testObject;
};
export const createTooltip = () => {
let tooltipElem = select<HTMLDivElement, unknown>('.mermaidTooltip');
if (tooltipElem.empty()) {
tooltipElem = select('body')
.append('div')
.attr('class', 'mermaidTooltip')
.style('opacity', 0)
.style('position', 'absolute')
.style('text-align', 'center')
.style('max-width', '200px')
.style('padding', '2px')
.style('font-size', '12px')
.style('background', '#ffffde')
.style('border', '1px solid #333')
.style('border-radius', '2px')
.style('pointer-events', 'none')
.style('z-index', '100');
}
return tooltipElem;
};

View File

@@ -17,6 +17,7 @@ import {
setDiagramTitle,
getDiagramTitle,
} from '../common/commonDb.js';
import { createTooltip } from '../common/svgDrawCommon.js';
import type {
FlowClass,
FlowEdge,
@@ -26,7 +27,7 @@ import type {
FlowVertex,
FlowVertexTypeParam,
} from './types.js';
import DOMPurify from 'dompurify';
interface LinkData {
id: string;
}
@@ -574,15 +575,7 @@ You have to call mermaid.initialize.`
}
private setupToolTips(element: Element) {
let tooltipElem = select('.mermaidTooltip');
// @ts-ignore TODO: fix this
if ((tooltipElem._groups || tooltipElem)[0][0] === null) {
// @ts-ignore TODO: fix this
tooltipElem = select('body')
.append('div')
.attr('class', 'mermaidTooltip')
.style('opacity', 0);
}
const tooltipElem = createTooltip();
const svg = select(element).select('svg');
@@ -603,7 +596,7 @@ You have to call mermaid.initialize.`
.text(el.attr('title'))
.style('left', window.scrollX + rect.left + (rect.right - rect.left) / 2 + 'px')
.style('top', window.scrollY + rect.bottom + 'px');
tooltipElem.html(tooltipElem.html().replace(/&lt;br\/&gt;/g, '<br/>'));
tooltipElem.html(DOMPurify.sanitize(title));
el.classed('hover', true);
})
.on('mouseout', (e: MouseEvent) => {