dots-viewer: Refactor JavaScript into modular architecture
Split monolithic embedded JavaScript in overlay.html into dedicated modules: - tooltip.js: Custom tooltip functionality with interactive copy/paste mode - pipeline-navigation.js: Clickable pipeline-dot references for navigation - text-ellipsizer.js: Text ellipsizing with tooltip integration - svg-overlay-manager.js: Main coordinator orchestrating all functionality Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/9547>
This commit is contained in:
parent
4d5fa1c43b
commit
d3ad6136ef
@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Pipeline dot navigation functionality
|
||||
*
|
||||
* Makes pipeline-dot references clickable for navigation between pipeline views.
|
||||
* Handles URL generation and navigation for pipeline dot references.
|
||||
*/
|
||||
|
||||
class PipelineNavigationManager {
|
||||
constructor(tooltipManager) {
|
||||
this.tooltipManager = tooltipManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes all pipeline-dot references in SVG to make them clickable
|
||||
* @param {jQuery} $svg - jQuery object containing the SVG element
|
||||
*/
|
||||
setupPipelineDotNavigation($svg) {
|
||||
$svg.find(".cluster").each((index, cluster) => {
|
||||
$(cluster).find("text, tspan").each((index, element) => {
|
||||
const text = element.textContent;
|
||||
if (this.isPipelineDotReference(text)) {
|
||||
this.makePipelineDotClickable(element, text);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if text content is a pipeline-dot reference
|
||||
* @param {string} text - Text content to check
|
||||
* @returns {boolean} True if text is a pipeline-dot reference
|
||||
*/
|
||||
isPipelineDotReference(text) {
|
||||
return text && text.startsWith("pipeline-dot=") && text.includes(".dot");
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a pipeline-dot element clickable with proper styling and handlers
|
||||
* @param {Element} element - DOM element to make clickable
|
||||
* @param {string} text - Original text content
|
||||
*/
|
||||
makePipelineDotClickable(element, text) {
|
||||
const pipelineDot = this.extractPipelineDotFilename(text);
|
||||
|
||||
// Style as a clickable link
|
||||
$(element).css({
|
||||
'text-decoration': 'underline',
|
||||
'cursor': 'pointer',
|
||||
'color': '#007acc'
|
||||
});
|
||||
|
||||
// Add click handler for navigation
|
||||
$(element).off('click.pipeline-nav').on('click.pipeline-nav', (evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
// Only hide tooltip if it's not in interactive mode
|
||||
if (this.tooltipManager && !this.tooltipManager.isInteractive()) {
|
||||
this.tooltipManager.hideTooltip();
|
||||
}
|
||||
|
||||
this.navigateToPipeline(pipelineDot);
|
||||
});
|
||||
|
||||
// Add hover effects
|
||||
this.setupHoverEffects(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the pipeline dot filename from the full text
|
||||
* @param {string} text - Full pipeline-dot text
|
||||
* @returns {string} Extracted filename
|
||||
*/
|
||||
extractPipelineDotFilename(text) {
|
||||
let pipelineDot = text;
|
||||
if (pipelineDot.includes("pipeline-dot=\"")) {
|
||||
pipelineDot = pipelineDot.replace(/pipeline-dot="([^"]+)"/, "$1");
|
||||
} else {
|
||||
pipelineDot = pipelineDot.replace(/pipeline-dot=([^\s]+)/, "$1");
|
||||
}
|
||||
return pipelineDot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up hover effects for clickable pipeline-dot links
|
||||
* @param {Element} element - DOM element to add hover effects to
|
||||
*/
|
||||
setupHoverEffects(element) {
|
||||
$(element).on('mouseenter', function () {
|
||||
$(this).css({
|
||||
'color': '#0056b3',
|
||||
'cursor': 'pointer'
|
||||
});
|
||||
}).on('mouseleave', function () {
|
||||
$(this).css({
|
||||
'color': '#007acc',
|
||||
'cursor': 'pointer'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the specified pipeline
|
||||
* @param {string} pipelineDot - Pipeline dot filename to navigate to
|
||||
*/
|
||||
navigateToPipeline(pipelineDot) {
|
||||
// Check if we're in an iframe (overlay.html context)
|
||||
if (window.parent !== window) {
|
||||
// We're in an iframe, navigate the parent window
|
||||
const newUrl = window.parent.location.origin + "/?pipeline=" + encodeURIComponent(pipelineDot);
|
||||
window.parent.location.href = newUrl;
|
||||
} else {
|
||||
// We're in the main window
|
||||
const newUrl = window.location.origin + "/?pipeline=" + encodeURIComponent(pipelineDot);
|
||||
window.location.href = newUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export for module usage
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = PipelineNavigationManager;
|
||||
} else {
|
||||
window.PipelineNavigationManager = PipelineNavigationManager;
|
||||
}
|
@ -0,0 +1,173 @@
|
||||
/**
|
||||
* SVG Overlay Manager
|
||||
*
|
||||
* Main coordinator for all SVG overlay functionality including tooltips,
|
||||
* text ellipsizing, pipeline navigation, and drag-to-pan interaction.
|
||||
*/
|
||||
|
||||
class SvgOverlayManager {
|
||||
constructor() {
|
||||
this.tooltipManager = null;
|
||||
this.pipelineNavigationManager = null;
|
||||
this.textEllipsizerManager = null;
|
||||
this.gv = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes all managers and sets up the SVG overlay functionality
|
||||
*/
|
||||
init() {
|
||||
// Initialize managers
|
||||
this.tooltipManager = new TooltipManager();
|
||||
this.pipelineNavigationManager = new PipelineNavigationManager(this.tooltipManager);
|
||||
this.textEllipsizerManager = new TextEllipsizerManager(this.tooltipManager, this.pipelineNavigationManager);
|
||||
|
||||
// Set up GraphViz SVG functionality
|
||||
this.setupGraphvizSvg();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up GraphViz SVG functionality and event handlers
|
||||
*/
|
||||
setupGraphvizSvg() {
|
||||
$("#graph").graphviz({
|
||||
url: this.getSvgUrl(),
|
||||
ready: () => {
|
||||
this.onSvgReady();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when SVG is loaded and ready
|
||||
*/
|
||||
onSvgReady() {
|
||||
this.gv = $("#graph").data('graphviz.svg');
|
||||
const $svg = $("#graph svg");
|
||||
|
||||
// Set up node click functionality for highlighting
|
||||
this.setupNodeHighlighting();
|
||||
|
||||
// Set up smart drag-to-pan behavior that doesn't interfere with text selection
|
||||
this.setupSmartDragBehavior();
|
||||
|
||||
// Set up keyboard shortcuts
|
||||
this.setupKeyboardShortcuts();
|
||||
|
||||
// Set up save SVG functionality
|
||||
this.setupSaveSvg();
|
||||
|
||||
// Process SVG content
|
||||
this.processSvgContent($svg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes SVG content with all managers
|
||||
* @param {jQuery} $svg - jQuery object containing the SVG element
|
||||
*/
|
||||
processSvgContent($svg) {
|
||||
// Set up pipeline-dot navigation
|
||||
this.pipelineNavigationManager.setupPipelineDotNavigation($svg);
|
||||
|
||||
// Process text ellipsizing (this must come after pipeline navigation setup)
|
||||
this.textEllipsizerManager.ellipsizeLongText($svg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up node click functionality for highlighting connected nodes
|
||||
*/
|
||||
setupNodeHighlighting() {
|
||||
this.gv.nodes().on('click', function () {
|
||||
const gv = $("#graph").data('graphviz.svg');
|
||||
let $set = $();
|
||||
$set.push(this);
|
||||
$set = $set.add(gv.linkedFrom(this, true));
|
||||
$set = $set.add(gv.linkedTo(this, true));
|
||||
gv.highlight($set, true);
|
||||
gv.bringToFront($set);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up smart drag behavior that allows text selection while preserving pan functionality
|
||||
*/
|
||||
setupSmartDragBehavior() {
|
||||
const graphDiv = document.getElementById('graph');
|
||||
|
||||
// Intercept mousedown events to prevent dragscroll on text elements
|
||||
graphDiv.addEventListener('mousedown', (e) => {
|
||||
if (e.target.tagName === 'text' || e.target.tagName === 'tspan') {
|
||||
if (e.target.textContent &&
|
||||
e.target.textContent.startsWith("pipeline-dot=") &&
|
||||
e.target.textContent.includes(".dot")) {
|
||||
return; // Let pipeline dot click handler take precedence
|
||||
}
|
||||
e.stopPropagation(); // Stop dragscroll for regular text
|
||||
return true;
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up keyboard shortcuts for the SVG viewer
|
||||
*/
|
||||
setupKeyboardShortcuts() {
|
||||
$(document).on('keyup', (evt) => {
|
||||
if (evt.key == "Escape") {
|
||||
this.gv.highlight();
|
||||
} else if (evt.key == "w") {
|
||||
this.gv.scaleInView((this.gv.zoom.percentage + 100));
|
||||
} else if (evt.key == "s") {
|
||||
this.gv.scaleInView((this.gv.zoom.percentage - 100) || 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up SVG save functionality
|
||||
*/
|
||||
setupSaveSvg() {
|
||||
$("#save-svg").on('click', () => {
|
||||
const svgElement = $("#graph svg")[0];
|
||||
const svgData = new XMLSerializer().serializeToString(svgElement);
|
||||
const blob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const title = document.getElementById("title").textContent.trim();
|
||||
const downloadLink = document.createElement("a");
|
||||
downloadLink.href = url;
|
||||
downloadLink.download = title + ".svg";
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the SVG URL from query parameters
|
||||
* @returns {string} SVG URL
|
||||
*/
|
||||
getSvgUrl() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('svg');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the title from query parameters and sets it in the document
|
||||
*/
|
||||
setTitle() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const title = urlParams.get('title');
|
||||
if (title) {
|
||||
document.getElementById("title").textContent = title;
|
||||
document.title = "Dots viewer: " + title;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export for module usage
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = SvgOverlayManager;
|
||||
} else {
|
||||
window.SvgOverlayManager = SvgOverlayManager;
|
||||
}
|
@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Text ellipsizing functionality for SVG elements
|
||||
*
|
||||
* Ellipsizes long text content (>80 chars) and provides tooltips with full content.
|
||||
* Integrates with tooltip system for interactive copy/paste functionality.
|
||||
*/
|
||||
|
||||
class TextEllipsizerManager {
|
||||
constructor(tooltipManager, pipelineNavigationManager) {
|
||||
this.tooltipManager = tooltipManager;
|
||||
this.pipelineNavigationManager = pipelineNavigationManager;
|
||||
this.maxLength = 80;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes all text elements in SVG to ellipsize long content
|
||||
* @param {jQuery} $svg - jQuery object containing the SVG element
|
||||
*/
|
||||
ellipsizeLongText($svg) {
|
||||
$svg.find("text, tspan").each((index, element) => {
|
||||
const text = element.textContent;
|
||||
|
||||
if (this.shouldEllipsize(text)) {
|
||||
this.ellipsizeElement(element, text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if text should be ellipsized
|
||||
* @param {string} text - Text content to check
|
||||
* @returns {boolean} True if text should be ellipsized
|
||||
*/
|
||||
shouldEllipsize(text) {
|
||||
return text && text.length > this.maxLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ellipsizes an element and sets up tooltip functionality
|
||||
* @param {Element} element - DOM element to ellipsize
|
||||
* @param {string} originalText - Original full text content
|
||||
*/
|
||||
ellipsizeElement(element, originalText) {
|
||||
const ellipsizedText = originalText.substring(0, 77) + "...";
|
||||
|
||||
// Update the text content
|
||||
$(element).text(ellipsizedText);
|
||||
|
||||
// Store original text and mark as having tooltip
|
||||
$(element).data('original-text', originalText);
|
||||
$(element).attr('data-has-tooltip', 'true');
|
||||
|
||||
// Style to indicate there's more content
|
||||
$(element).css({
|
||||
'cursor': 'help'
|
||||
});
|
||||
|
||||
this.setupTooltipHandlers(element, originalText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up tooltip handlers for ellipsized elements
|
||||
* @param {Element} element - DOM element to add handlers to
|
||||
* @param {string} originalText - Original full text content
|
||||
*/
|
||||
setupTooltipHandlers(element, originalText) {
|
||||
const isPipelineDot = this.pipelineNavigationManager.isPipelineDotReference(originalText);
|
||||
|
||||
if (!isPipelineDot) {
|
||||
this.setupRegularTooltipHandlers(element, originalText);
|
||||
} else {
|
||||
this.setupPipelineDotTooltipHandlers(element, originalText);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up tooltip handlers for regular (non-pipeline-dot) elements
|
||||
* @param {Element} element - DOM element to add handlers to
|
||||
* @param {string} originalText - Original full text content
|
||||
*/
|
||||
setupRegularTooltipHandlers(element, originalText) {
|
||||
$(element).on('mouseenter', (e) => {
|
||||
this.tooltipManager.showTooltip(element, originalText, e);
|
||||
});
|
||||
|
||||
$(element).on('mousemove', (e) => {
|
||||
if (!this.tooltipManager.isInteractive()) {
|
||||
this.tooltipManager.showTooltip(element, originalText, e);
|
||||
}
|
||||
});
|
||||
|
||||
$(element).on('mouseleave.tooltip', () => {
|
||||
// Don't hide tooltip on mouseleave if it's interactive
|
||||
if (!this.tooltipManager.isInteractive()) {
|
||||
this.tooltipManager.hideTooltip();
|
||||
}
|
||||
});
|
||||
|
||||
// Double-click to make tooltip interactive
|
||||
$(element).on('dblclick', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (this.tooltipManager.$tooltip.hasClass('show')) {
|
||||
this.tooltipManager.makeTooltipInteractive();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up tooltip handlers for pipeline-dot elements (with navigation functionality)
|
||||
* @param {Element} element - DOM element to add handlers to
|
||||
* @param {string} originalText - Original full text content
|
||||
*/
|
||||
setupPipelineDotTooltipHandlers(element, originalText) {
|
||||
// Make it clickable for navigation
|
||||
this.pipelineNavigationManager.makePipelineDotClickable(element, originalText);
|
||||
|
||||
// Add tooltip functionality
|
||||
$(element).on('mouseenter', (e) => {
|
||||
$(element).css({
|
||||
'color': '#0056b3',
|
||||
'cursor': 'pointer'
|
||||
});
|
||||
this.tooltipManager.showTooltip(element, originalText, e);
|
||||
});
|
||||
|
||||
$(element).on('mousemove', (e) => {
|
||||
if (!this.tooltipManager.isInteractive()) {
|
||||
this.tooltipManager.showTooltip(element, originalText, e);
|
||||
}
|
||||
});
|
||||
|
||||
$(element).on('mouseleave.tooltip', () => {
|
||||
$(element).css({
|
||||
'color': '#007acc',
|
||||
'cursor': 'pointer'
|
||||
});
|
||||
// Don't hide tooltip on mouseleave if it's interactive
|
||||
if (!this.tooltipManager.isInteractive()) {
|
||||
this.tooltipManager.hideTooltip();
|
||||
}
|
||||
});
|
||||
|
||||
// Right-click to make tooltip interactive (since left-click navigates)
|
||||
$(element).on('contextmenu', (e) => {
|
||||
if (this.tooltipManager.$tooltip.hasClass('show')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.tooltipManager.makeTooltipInteractive();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Export for module usage
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = TextEllipsizerManager;
|
||||
} else {
|
||||
window.TextEllipsizerManager = TextEllipsizerManager;
|
||||
}
|
118
subprojects/gst-devtools/dots-viewer/static/js/tooltip.js
Normal file
118
subprojects/gst-devtools/dots-viewer/static/js/tooltip.js
Normal file
@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Custom tooltip functionality for SVG elements
|
||||
*
|
||||
* Provides hoverable tooltips with interactive copy/paste mode for ellipsized text.
|
||||
* Interactive mode allows users to select and copy tooltip content.
|
||||
*/
|
||||
|
||||
class TooltipManager {
|
||||
constructor() {
|
||||
this.$tooltip = null;
|
||||
this.tooltipText = '';
|
||||
this.isTooltipInteractive = false;
|
||||
this.currentTooltipElement = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Create custom tooltip element
|
||||
this.$tooltip = $('<div class="custom-tooltip"></div>').appendTo('body');
|
||||
|
||||
// Set up document click handler for hiding tooltips
|
||||
this.setupDocumentClickHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows tooltip with given text at event position
|
||||
* @param {Element} element - Element that triggered the tooltip
|
||||
* @param {string} text - Text to display in tooltip
|
||||
* @param {Event} event - Mouse event for positioning
|
||||
*/
|
||||
showTooltip(element, text, event) {
|
||||
this.tooltipText = text;
|
||||
this.currentTooltipElement = element;
|
||||
this.$tooltip.text(text);
|
||||
this.$tooltip.css({
|
||||
left: event.pageX + 10 + 'px',
|
||||
top: event.pageY - 30 + 'px'
|
||||
}).removeClass('interactive').addClass('show');
|
||||
this.isTooltipInteractive = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes tooltip interactive (selectable and fixed position)
|
||||
*/
|
||||
makeTooltipInteractive() {
|
||||
if (!this.isTooltipInteractive && this.currentTooltipElement) {
|
||||
this.$tooltip.addClass('interactive');
|
||||
this.isTooltipInteractive = true;
|
||||
|
||||
// Position tooltip in a fixed position relative to the element
|
||||
const elementOffset = $(this.currentTooltipElement).offset();
|
||||
|
||||
this.$tooltip.css({
|
||||
left: Math.min(elementOffset.left + 20, window.innerWidth - 420) + 'px',
|
||||
top: Math.max(elementOffset.top - 80, 10) + 'px',
|
||||
'pointer-events': 'auto'
|
||||
});
|
||||
|
||||
// Prevent mouseleave from hiding the tooltip by removing the handler temporarily
|
||||
$(this.currentTooltipElement).off('mouseleave.tooltip');
|
||||
|
||||
// Select all text when made interactive
|
||||
setTimeout(() => {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(this.$tooltip[0]);
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the tooltip and resets state
|
||||
*/
|
||||
hideTooltip() {
|
||||
this.$tooltip.removeClass('show interactive');
|
||||
this.isTooltipInteractive = false;
|
||||
this.currentTooltipElement = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up document click handler to hide tooltips when clicking outside
|
||||
*/
|
||||
setupDocumentClickHandler() {
|
||||
$(document).on('click', (e) => {
|
||||
if (this.isTooltipInteractive) {
|
||||
// Only hide interactive tooltip when clicking outside both tooltip and original element
|
||||
if (!$(e.target).closest('.custom-tooltip').length &&
|
||||
!$(e.target).is('[data-has-tooltip]') &&
|
||||
e.target !== this.currentTooltipElement) {
|
||||
this.hideTooltip();
|
||||
}
|
||||
} else {
|
||||
// Hide non-interactive tooltip when clicking anywhere except on elements with tooltips
|
||||
if (!$(e.target).is('[data-has-tooltip]')) {
|
||||
this.hideTooltip();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if tooltip is currently in interactive mode
|
||||
* @returns {boolean} True if tooltip is interactive
|
||||
*/
|
||||
isInteractive() {
|
||||
return this.isTooltipInteractive;
|
||||
}
|
||||
}
|
||||
|
||||
// Export for module usage
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = TooltipManager;
|
||||
} else {
|
||||
window.TooltipManager = TooltipManager;
|
||||
}
|
@ -39,13 +39,51 @@ furnished to do so, subject to the following conditions:
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.custom-tooltip {
|
||||
position: absolute;
|
||||
background-color: #333;
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
z-index: 10000;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
max-width: 400px;
|
||||
word-wrap: break-word;
|
||||
white-space: normal;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
}
|
||||
|
||||
.custom-tooltip.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.custom-tooltip.interactive {
|
||||
pointer-events: auto;
|
||||
cursor: text;
|
||||
border: 2px solid #555;
|
||||
}
|
||||
|
||||
.custom-tooltip.interactive::after {
|
||||
content: " (Text selected - Ctrl+C to copy)";
|
||||
font-size: 10px;
|
||||
color: #aaa;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
</style>
|
||||
<body>
|
||||
<h1 style="text-align: center" id="title"> {{ TITLE }}</h1>
|
||||
|
||||
<div id="graph" class="dragscroll" style="width: 100%; height: 100%; overflow: scroll;"></div>
|
||||
<div class="floating-rectangle" id="instructions">
|
||||
Click node to highlight<br/>Shift-Ctrl-scroll or w/s to zoom<br/>Esc to unhighlight
|
||||
Click node to highlight<br/>Shift-Ctrl-scroll or w/s to zoom<br/>Esc to unhighlight<br/>Double-click ellipsized text to copy
|
||||
</div>
|
||||
|
||||
<div class="floating-rectangle" id="actions" style="top: auto; bottom: 100px; left: 10px;">
|
||||
@ -54,133 +92,15 @@ furnished to do so, subject to the following conditions:
|
||||
|
||||
<script src='/dist/bundle.js'></script>
|
||||
<script type="text/javascript" src="/js/jquery.graphviz.svg.js"></script>
|
||||
<script type="text/javascript" src="/js/tooltip.js"></script>
|
||||
<script type="text/javascript" src="/js/pipeline-navigation.js"></script>
|
||||
<script type="text/javascript" src="/js/text-ellipsizer.js"></script>
|
||||
<script type="text/javascript" src="/js/svg-overlay-manager.js"></script>
|
||||
<script type="text/javascript">
|
||||
let url = new URL(window.location.href);
|
||||
let searchParams = new URLSearchParams(url.search);
|
||||
document.getElementById("title").innerHTML = searchParams.get("title");
|
||||
|
||||
// Make text selection work while preserving drag functionality
|
||||
function setupTextSelectionWithDrag() {
|
||||
const graphDiv = document.getElementById('graph');
|
||||
|
||||
graphDiv.addEventListener('mousedown', function(e) {
|
||||
if (e.target.tagName === 'text' || e.target.tagName === 'tspan') {
|
||||
if (e.target.textContent && e.target.textContent.startsWith("pipeline-dot=") && e.target.textContent.includes(".dot")) {
|
||||
// Let the pipeline dot click handler take precedence
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
// Allow normal text selection behavior
|
||||
return true;
|
||||
}
|
||||
}, true);
|
||||
|
||||
graphDiv.addEventListener('mouseover', function(e) {
|
||||
if (e.target.tagName === 'text' || e.target.tagName === 'tspan') {
|
||||
if (!(e.target.textContent && e.target.textContent.startsWith("pipeline-dot=") && e.target.textContent.includes(".dot"))) {
|
||||
e.target.style.cursor = 'text';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
graphDiv.addEventListener('mouseout', function(e) {
|
||||
if (e.target.tagName === 'text' || e.target.tagName === 'tspan') {
|
||||
if (!(e.target.textContent && e.target.textContent.startsWith("pipeline-dot=") && e.target.textContent.includes(".dot"))) {
|
||||
e.target.style.cursor = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function(){
|
||||
// Setup text selection behavior
|
||||
setupTextSelectionWithDrag();
|
||||
|
||||
$("#graph").graphviz({
|
||||
url: searchParams.get("svg"),
|
||||
ready: function() {
|
||||
var gv = this;
|
||||
gv.nodes().click(function () {
|
||||
var $set = $();
|
||||
$set.push(this);
|
||||
$set = $set.add(gv.linkedFrom(this, true));
|
||||
$set = $set.add(gv.linkedTo(this, true));
|
||||
gv.highlight($set, true);
|
||||
gv.bringToFront($set);
|
||||
});
|
||||
$(document).keydown(function (evt) {
|
||||
if (evt.key == "Escape") {
|
||||
gv.highlight();
|
||||
} else if (evt.key == "w") {
|
||||
gv.scaleInView((gv.zoom.percentage + 100));
|
||||
} else if (evt.key == "s") {
|
||||
gv.scaleInView((gv.zoom.percentage - 100) || 100);
|
||||
}
|
||||
});
|
||||
|
||||
$("#save-svg").click(function() {
|
||||
const svgElement = $("#graph svg")[0];
|
||||
const svgData = new XMLSerializer().serializeToString(svgElement);
|
||||
const blob = new Blob([svgData], {type: "image/svg+xml;charset=utf-8"});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const title = document.getElementById("title").textContent.trim();
|
||||
const downloadLink = document.createElement("a");
|
||||
downloadLink.href = url;
|
||||
downloadLink.download = title + ".svg";
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
|
||||
// Make pipeline dot references clickable for navigation between pipeline views
|
||||
$("#graph svg .cluster").each(function() {
|
||||
$(this).find("text, tspan").each(function() {
|
||||
var element = this;
|
||||
if (element.textContent && element.textContent.startsWith("pipeline-dot=") && element.textContent.includes(".dot")) {
|
||||
// Extract the pipeline dot filename from the text
|
||||
var pipelineDot = element.textContent;
|
||||
if (pipelineDot.includes("pipeline-dot=\"")) {
|
||||
pipelineDot = pipelineDot.replace(/pipeline-dot="([^"]+)"/, "$1");
|
||||
} else {
|
||||
pipelineDot = pipelineDot.replace(/pipeline-dot=([^\s]+)/, "$1");
|
||||
}
|
||||
|
||||
// Style as a clickable link
|
||||
$(element).css({
|
||||
'text-decoration': 'underline',
|
||||
'cursor': 'pointer',
|
||||
'color': '#007acc'
|
||||
});
|
||||
|
||||
// Add click handler for navigation
|
||||
$(element).off('click.pipeline-nav').on('click.pipeline-nav', function(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
var newUrl = window.location.origin + "/?pipeline=" + encodeURIComponent(pipelineDot);
|
||||
window.location.href = newUrl;
|
||||
});
|
||||
|
||||
// Add hover effects
|
||||
$(element).hover(
|
||||
function() {
|
||||
$(this).css({
|
||||
'color': '#0056b3',
|
||||
'cursor': 'pointer'
|
||||
});
|
||||
},
|
||||
function() {
|
||||
$(this).css({
|
||||
'color': '#007acc',
|
||||
'cursor': 'pointer'
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
$(document).ready(() => {
|
||||
const svgOverlayManager = new SvgOverlayManager();
|
||||
svgOverlayManager.setTitle();
|
||||
svgOverlayManager.init();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
Loading…
x
Reference in New Issue
Block a user