Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save romualdrichard/31023136003b6e15f74f790a9b190d5b to your computer and use it in GitHub Desktop.

Select an option

Save romualdrichard/31023136003b6e15f74f790a9b190d5b to your computer and use it in GitHub Desktop.
#jarchi Export Single page html interactive
// Generate Single-page HTML Export - Enhanced with Interactive Views
//
// Based on: https://github.com/archi-contribs/jarchi-single-page-html-export
// Enhanced by: Claude (2026) - Added interactive SVG overlays
// STABLE VERSION - Based on v1.9 + v2.0 uncheck fix
//
// Requires jArchi - https://www.archimatetool.com/blog/2018/07/02/jarchi/
// Add this script to the repertory of jarchi-single-page-html-export
// === CONFIGURATION ===
var DEBUG = false; // Set to true for detailed logging
const USE_REQUIRE = true;
load(__DIR__ + 'libs/nashorn-polyfills.js');
if(USE_REQUIRE) {
_ = require(__DIR__ + "libs/underscore-min.js");
marked = require(__DIR__ + "libs/marked.min.js");
}
else {
load(__DIR__ + 'libs/underscore-min.js');
load(__DIR__ + 'libs/marked.min.js');
}
console.show();
console.clear();
// Helper function for conditional logging
function debug(msg) {
if(DEBUG) console.log(msg);
}
function info(msg) {
console.log(msg);
}
_.templateSettings = {
interpolate: /\{\{(.+?)\}\}/g
};
var mdOptions = {
gfm: true,
breaks: true,
smartLists: true,
smartypants: true
};
var roboto = readFully(__DIR__ + 'resources/roboto.css', 'UTF-8');
var icon = readFully(__DIR__ + 'resources/icon.css', 'UTF-8');
var picnic = readFully(__DIR__ + 'resources/picnic-custom.css', 'UTF-8');
var tplMainReport = _.template(readFully(__DIR__ + 'templates/main-report.html', 'UTF-8'));
var tplVisibilityRuleBold = _.template(readFully(__DIR__ + 'templates/visibility-rule-bold.tpl', 'UTF-8'));
var tplVisibilityRuleReveal = _.template(readFully(__DIR__ + 'templates/visibility-rule-reveal.tpl', 'UTF-8'));
var tplTreeView = _.template(readFully(__DIR__ + 'templates/model-tree-view.tpl', 'UTF-8'));
var tplTreeFolder = _.template(readFully(__DIR__ + 'templates/model-tree-folder.tpl', 'UTF-8'));
var tplViewTitle = _.template(readFully(__DIR__ + 'templates/view-title.tpl', 'UTF-8'));
var tplViewDiagram = _.template(readFully(__DIR__ + 'templates/view-diagram.tpl', 'UTF-8'));
var tplDocumentation = _.template(readFully(__DIR__ + 'templates/documentation.tpl', 'UTF-8'));
var tplElement = _.template(readFully(__DIR__ + 'templates/element.tpl', 'UTF-8'));
var tplRelationship = _.template(readFully(__DIR__ + 'templates/relationship.tpl', 'UTF-8'));
var visibilityRulesBold = '';
var visibilityRulesReveal = '';
var inputCheckbox = '';
var treeFolder = '';
var treeContent = '';
var viewTitles = '';
var viewDiagrams = '';
var viewDocumentations = '';
var viewsIdsByConceptId = {};
var elements = '';
var elementsCollection;
var relationships = '';
var relationshipsCollection;
var interactiveSVGOverlays = '';
var interactiveElementData = {};
var exportedViewIds = [];
var allFolders = $('folder');
var viewsFolder = $(model).children().filter('folder.Views');
var viewFolders = viewsFolder.find('folder');
var nonViewFolders = allFolders.not(viewsFolder).not(viewFolders);
var folders = selection.filter("folder").not(nonViewFolders);
if(! folders.size()) {
folders = viewsFolder;
debug("All views have been selected.");
}
var filePath = window.promptSaveFile({ title: "Save as HTML file", filterExtensions : [ "*.html" , "*.*"], fileName: model.name + "-interactive.html" });
if(!filePath) {
info('Export cancelled by user.');
exit();
}
info('');
info('=== GENERATING INTERACTIVE HTML EXPORT ===');
info('');
_.chain(folders).sortBy(function(f) { return f.name; }).each(function(f) {
exportViews(f);
});
info('');
info('=== EXPORT COMPLETE ===');
info('Total views exported: ' + exportedViewIds.length);
var checkboxCount = 0;
exportedViewIds.forEach(function(viewId) {
inputCheckbox += '<input type="checkbox" id="id-' + viewId + '" class="view-checkbox" />\n';
checkboxCount++;
});
info('Generated ' + checkboxCount + ' checkboxes');
function generateInteractiveSVG(view, viewImageBase64) {
var visualObjects = [];
var elementDataForView = {};
var minX = 999999, minY = 999999, maxX = -999999, maxY = -999999;
var hasBendPointsOutOfBounds = false; // Track if bend points extend beyond object bounds
var pngWidth = 800;
var pngHeight = 600;
try {
var bytes = java.util.Base64.getDecoder().decode(viewImageBase64);
var bais = new java.io.ByteArrayInputStream(bytes);
var ImageIO = Java.type("javax.imageio.ImageIO");
var image = ImageIO.read(bais);
pngWidth = image.getWidth();
pngHeight = image.getHeight();
bais.close();
} catch(e) {
debug(" Warning: Could not read PNG dimensions");
}
// Function to calculate absolute coordinates by traversing visual parent hierarchy
function getAbsoluteCoordinates(visualObj) {
var absX = visualObj.bounds.x;
var absY = visualObj.bounds.y;
// Traverse up the visual parent hierarchy
var parent = $(visualObj).parent().first();
while(parent && parent.type !== 'archimate-diagram-model') {
if(parent.bounds) {
absX += parent.bounds.x;
absY += parent.bounds.y;
}
parent = $(parent).parent().first();
}
return { x: absX, y: absY };
}
// CRITICAL FIX: Detect negative coordinates - check ALL visual objects (including notes)
var negativeOffsetX = 0;
var negativeOffsetY = 0;
$(view).find().each(function(obj) {
if(obj.bounds) {
// Check ALL objects, including notes, to get true view bounds
var absCoords = getAbsoluteCoordinates(obj);
// Track most negative coordinates
if(absCoords.x < negativeOffsetX) {
negativeOffsetX = absCoords.x;
}
if(absCoords.y < negativeOffsetY) {
negativeOffsetY = absCoords.y;
}
}
});
// CRITICAL: Check bend points with correct weight-based calculation
// Based on: https://github.com/archimatetool/archi-scripting-plugin/issues/42
$(view).find('relationship').each(function(conn) {
if(conn.relativeBendpoints && conn.relativeBendpoints.length > 0) {
var sourceObj = conn.source;
var targetObj = conn.target;
// Skip connection-to-connection (not supported)
if(!sourceObj || !sourceObj.bounds || !targetObj || !targetObj.bounds) {
return;
}
// Skip if bounds are invalid
if(!sourceObj.bounds.width || !sourceObj.bounds.height || !targetObj.bounds.width || !targetObj.bounds.height) {
return;
}
var srcAbsCoords = getAbsoluteCoordinates(sourceObj);
var tgtAbsCoords = getAbsoluteCoordinates(targetObj);
// Validate absolute coordinates
if(isNaN(srcAbsCoords.x) || isNaN(srcAbsCoords.y) || isNaN(tgtAbsCoords.x) || isNaN(tgtAbsCoords.y)) {
return;
}
var bpcount = conn.relativeBendpoints.length + 1;
// Use classic for loop instead of forEach (jArchi/Nashorn compatibility)
for(var i = 0; i < conn.relativeBendpoints.length; i++) {
var bp = conn.relativeBendpoints[i];
// Validate bendpoint values
if(isNaN(bp.startX) || isNaN(bp.startY) || isNaN(bp.endX) || isNaN(bp.endY)) {
continue;
}
var bpindex = i + 1;
var bpweight = bpindex / bpcount;
// Start component (relative to source center)
var startX = (srcAbsCoords.x + (sourceObj.bounds.width / 2)) + bp.startX;
startX *= (1.0 - bpweight);
var startY = (srcAbsCoords.y + (sourceObj.bounds.height / 2)) + bp.startY;
startY *= (1.0 - bpweight);
// End component (relative to target center)
var endX = (tgtAbsCoords.x + (targetObj.bounds.width / 2)) + bp.endX;
endX *= bpweight;
var endY = (tgtAbsCoords.y + (targetObj.bounds.height / 2)) + bp.endY;
endY *= bpweight;
// Absolute position
var absX = startX + endX;
var absY = startY + endY;
// Final validation
if(!isNaN(absX) && !isNaN(absY)) {
if(absX < negativeOffsetX) negativeOffsetX = absX;
if(absY < negativeOffsetY) negativeOffsetY = absY;
}
}
}
});
// Convert negative offsets to positive values to add to all coordinates
negativeOffsetX = Math.abs(negativeOffsetX);
negativeOffsetY = Math.abs(negativeOffsetY);
if(negativeOffsetX > 0 || negativeOffsetY > 0) {
debug(' ⚠️ Negative coordinates detected! Applying offset: x+' + negativeOffsetX + ', y+' + negativeOffsetY);
}
// First pass: calculate bounds on ALL visual objects (notes, groups, everything)
// But only collect objects with concepts OR view references for interactive mapping
$(view).find().each(function(obj) {
if(obj.bounds && obj.bounds.width && obj.bounds.height) {
// Calculate absolute coordinates and apply negative offset correction
var absCoords = getAbsoluteCoordinates(obj);
var x = absCoords.x + negativeOffsetX;
var y = absCoords.y + negativeOffsetY;
var width = obj.bounds.width;
var height = obj.bounds.height;
// CRITICAL: Update bounds with ALL visual objects
// This ensures bounds match exactly what Archi uses for PNG generation
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x + width);
maxY = Math.max(maxY, y + height);
// Collect objects for interactive mapping:
// - Objects with ArchiMate concepts (elements, relationships)
// - View references (archimate-diagram-model, diagram-model-reference)
var hasArchiMateConcept = obj.concept && obj.concept.id;
var isViewReference = obj.type === 'archimate-diagram-model' || obj.type === 'diagram-model-reference';
if(hasArchiMateConcept || isViewReference) {
visualObjects.push(obj);
}
}
});
// Save object bounds before adding bend points
var objectMinX = minX;
var objectMinY = minY;
var objectMaxX = maxX;
var objectMaxY = maxY;
// CRITICAL: Include bend points in bounds with correct weight-based calculation
$(view).find('relationship').each(function(conn) {
if(conn.relativeBendpoints && conn.relativeBendpoints.length > 0) {
var sourceObj = conn.source;
var targetObj = conn.target;
if(!sourceObj || !sourceObj.bounds || !targetObj || !targetObj.bounds) {
return;
}
if(!sourceObj.bounds.width || !sourceObj.bounds.height || !targetObj.bounds.width || !targetObj.bounds.height) {
return;
}
var srcAbsCoords = getAbsoluteCoordinates(sourceObj);
var tgtAbsCoords = getAbsoluteCoordinates(targetObj);
if(isNaN(srcAbsCoords.x) || isNaN(srcAbsCoords.y) || isNaN(tgtAbsCoords.x) || isNaN(tgtAbsCoords.y)) {
return;
}
// Apply negative offset to source and target centers
var srcCenterX = srcAbsCoords.x + negativeOffsetX + (sourceObj.bounds.width / 2);
var srcCenterY = srcAbsCoords.y + negativeOffsetY + (sourceObj.bounds.height / 2);
var tgtCenterX = tgtAbsCoords.x + negativeOffsetX + (targetObj.bounds.width / 2);
var tgtCenterY = tgtAbsCoords.y + negativeOffsetY + (targetObj.bounds.height / 2);
var bpcount = conn.relativeBendpoints.length + 1;
// Use classic for loop instead of forEach (jArchi/Nashorn compatibility)
for(var i = 0; i < conn.relativeBendpoints.length; i++) {
var bp = conn.relativeBendpoints[i];
if(isNaN(bp.startX) || isNaN(bp.startY) || isNaN(bp.endX) || isNaN(bp.endY)) {
continue;
}
var bpindex = i + 1;
var bpweight = bpindex / bpcount;
// Start component
var startX = (srcCenterX + bp.startX) * (1.0 - bpweight);
var startY = (srcCenterY + bp.startY) * (1.0 - bpweight);
// End component
var endX = (tgtCenterX + bp.endX) * bpweight;
var endY = (tgtCenterY + bp.endY) * bpweight;
// Absolute position
var absX = startX + endX;
var absY = startY + endY;
if(!isNaN(absX) && !isNaN(absY)) {
// Add margin for connection line width and arrow heads
// Observed: 22px total difference, so ~11px per side
var connectionMargin = 11;
var bpMinX = absX - connectionMargin;
var bpMinY = absY - connectionMargin;
var bpMaxX = absX + connectionMargin;
var bpMaxY = absY + connectionMargin;
// Check if bend point extends beyond object bounds
if(bpMinX < objectMinX || bpMinY < objectMinY || bpMaxX > objectMaxX || bpMaxY > objectMaxY) {
hasBendPointsOutOfBounds = true;
}
minX = Math.min(minX, bpMinX);
minY = Math.min(minY, bpMinY);
maxX = Math.max(maxX, bpMaxX);
maxY = Math.max(maxY, bpMaxY);
}
}
}
});
// CRITICAL FIX: After calculating bounds, if minX or minY are not 0, we need to normalize
// This happens when all objects are shifted positively after negative offset correction
debug(' Initial bounds: minX=' + minX + ', minY=' + minY + ', maxX=' + maxX + ', maxY=' + maxY);
// CRITICAL FIX: Reverse Z-order so foreground objects are clickable first
visualObjects.reverse();
// CRITICAL FIX: Sort by nesting depth - containers first, children last (clickable on top)
// Calculate absolute coordinates and detect nesting
var objectsWithDepth = [];
visualObjects.forEach(function(obj) {
var absCoords = getAbsoluteCoordinates(obj);
// Apply negative offset correction
var absX = absCoords.x + negativeOffsetX;
var absY = absCoords.y + negativeOffsetY;
var absRight = absX + obj.bounds.width;
var absBottom = absY + obj.bounds.height;
// Count how many objects are inside this one (depth = container level)
var childrenCount = 0;
visualObjects.forEach(function(other) {
if(obj.id !== other.id) {
var otherAbs = getAbsoluteCoordinates(other);
var otherX = otherAbs.x + negativeOffsetX;
var otherY = otherAbs.y + negativeOffsetY;
var otherRight = otherX + other.bounds.width;
var otherBottom = otherY + other.bounds.height;
// Check if 'other' is completely inside 'obj'
if(otherX >= absX && otherY >= absY &&
otherRight <= absRight && otherBottom <= absBottom) {
childrenCount++;
}
}
});
objectsWithDepth.push({
obj: obj,
absCoords: { x: absX, y: absY },
childrenCount: childrenCount
});
});
// Sort: objects with MORE children first (containers at bottom)
// objects with FEWER children last (leaf elements clickable on top)
objectsWithDepth.sort(function(a, b) {
return b.childrenCount - a.childrenCount;
});
debug(' Objects sorted by depth (containers first): ' + objectsWithDepth.length);
// Fine-tune minX ONLY if bend points extend beyond object bounds
if(hasBendPointsOutOfBounds) {
minX -= 5;
debug(' Bend points extend beyond objects - applied offset correction: minX -= 5');
}
var boundsWidth = maxX - minX;
var boundsHeight = maxY - minY;
var scaleX = pngWidth / boundsWidth;
var scaleY = pngHeight / boundsHeight;
debug(' Final bounds for SVG: ' + boundsWidth.toFixed(1) + 'x' + boundsHeight.toFixed(1) + ' (scale: ' + scaleX.toFixed(6) + ', ' + scaleY.toFixed(6) + ')');
var svgRects = '';
for(var i = 0; i < objectsWithDepth.length; i++) {
var item = objectsWithDepth[i];
var visualObj = item.obj;
// Use pre-calculated absolute coordinates
var x = item.absCoords.x;
var y = item.absCoords.y;
var width = visualObj.bounds.width;
var height = visualObj.bounds.height;
var scaledX = (x - minX) * scaleX;
var scaledY = (y - minY) * scaleY;
var scaledWidth = width * scaleX;
var scaledHeight = height * scaleY;
var concept = visualObj.concept || visualObj;
var props = {};
if(concept.prop) {
var propNames = concept.prop();
for(var j = 0; j < propNames.length; j++) {
var propName = propNames[j];
props[propName] = concept.prop(propName);
}
}
elementDataForView[concept.id] = {
name: concept.name || 'Sans nom',
type: concept.type,
documentation: concept.documentation || '',
properties: props
};
var targetView = '';
var targetViewId = null;
if(visualObj.type === 'archimate-diagram-model') {
if(visualObj.getRefView && visualObj.getRefView()) {
var refView = visualObj.getRefView();
targetViewId = refView.id;
debug(' ✓ View ref: "' + refView.name + '" (ID: ' + targetViewId + ')');
}
}
else if(visualObj.type === 'diagram-model-reference') {
if(visualObj.getRefView && visualObj.getRefView()) {
var refView = visualObj.getRefView();
targetViewId = refView.id;
debug(' ✓ Diagram ref: "' + refView.name + '" (ID: ' + targetViewId + ')');
}
}
if(targetViewId) {
targetView = ' data-target-view="' + targetViewId + '"';
}
svgRects += '<rect class="interactive-element" ';
svgRects += 'x="' + scaledX.toFixed(2) + '" y="' + scaledY.toFixed(2) + '" ';
svgRects += 'width="' + scaledWidth.toFixed(2) + '" height="' + scaledHeight.toFixed(2) + '" ';
svgRects += 'data-element-id="' + concept.id + '" ';
svgRects += 'data-element-name="' + _.escape(concept.name || '') + '" ';
svgRects += 'data-element-type="' + concept.type + '"';
svgRects += targetView + '/>\n';
}
// CRITICAL: Also add relationships to elementDataStore (they don't have bounds so weren't added above)
$(view).find('relationship').each(function(rel) {
var concept = rel.concept;
if(concept) {
var props = {};
if(concept.prop) {
var propNames = concept.prop();
for(var j = 0; j < propNames.length; j++) {
var propName = propNames[j];
props[propName] = concept.prop(propName);
}
}
elementDataForView[concept.id] = {
name: concept.name || '',
type: concept.type,
documentation: concept.documentation || '',
properties: props
};
}
});
interactiveElementData['id-' + view.id] = elementDataForView;
var svg = '<svg class="view-overlay" ';
svg += 'width="100%" height="100%" ';
svg += 'viewBox="0 0 ' + pngWidth + ' ' + pngHeight + '" ';
svg += 'preserveAspectRatio="xMidYMid meet" ';
svg += 'xmlns="http://www.w3.org/2000/svg">\n';
svg += svgRects;
svg += '</svg>';
return svg;
}
function exportViews(folder) {
debug('Exporting folder: ' + folder.name);
var previousContent = treeContent;
treeContent = '';
_.chain($(folder).children('folder')).sortBy(function(f) { return f.name; }).each(function(f) {
exportViews(f)
});
var views = _.chain($(folder).children('view')).sortBy(function(v) { return v.name; }).value();
var totalViews = views.length;
var viewCount = 0;
views.forEach(function(v) {
viewCount++;
exportedViewIds.push(v.id);
info('Processing view ' + viewCount + '/' + totalViews + ': ' + v.name);
treeContent += tplTreeView({viewId: 'id-'+v.id, viewName: _.escape(v.name)});
visibilityRulesBold += tplVisibilityRuleBold({viewId: 'id-'+v.id});
visibilityRulesReveal += tplVisibilityRuleReveal({viewId: 'id-'+v.id});
viewTitles += tplViewTitle({viewId: 'id-'+v.id, viewName: _.escape(v.name)});
var viewImage = $.model.renderViewAsBase64(v, "PNG", {margin: 0});
var interactiveSVG = generateInteractiveSVG(v, viewImage);
viewDiagrams += '<div class="interactive-view-wrapper" data-view-id="id-' + v.id + '">\n';
viewDiagrams += tplViewDiagram({viewId: 'id-'+v.id, viewImage: viewImage});
viewDiagrams += interactiveSVG;
viewDiagrams += '</div>\n';
viewDocumentations += tplDocumentation({
viewId: 'id-'+v.id,
documentationText: _.escape(v.documentation).replace(/\n/g, '<br>'),
documentationMarkdown: marked(_.escape(v.documentation), mdOptions)
});
$(v).find('element').each(function(e) {
if(elementsCollection) {
if(!elementsCollection.contains(e.concept)) elementsCollection.add($(e.concept));
} else {
elementsCollection = $(e.concept);
}
viewsIdsByConceptId[e.concept.id] += ' id-'+v.id;
});
$(v).find('relationship').each(function(r) {
if(relationshipsCollection) {
if(!relationshipsCollection.contains(r.concept)) relationshipsCollection.add($(r.concept));
} else {
relationshipsCollection = $(r.concept);
}
if(!viewsIdsByConceptId[r.concept.id]) viewsIdsByConceptId[r.concept.id] = '';
viewsIdsByConceptId[r.concept.id] += ' id-'+v.id;
});
});
treeContent = tplTreeFolder({folderId: 'id-'+folder.id, folderName: _.escape(folder.name), folderContent: treeContent});
treeContent = previousContent + treeContent;
}
_.chain(elementsCollection).sortBy(function(e) { return e.name; }).each(function(e) {
elements += tplElement({
viewsIds: viewsIdsByConceptId[e.id],
elementName: _.escape(e.name),
elementType: properCase(e.type),
elementDocumentationText: _.escape(e.documentation).replace(/\n/g, '<br>'),
elementDocumentationMarkdown: marked(_.escape(e.documentation), mdOptions)
});
});
_.chain(relationshipsCollection).sortBy(function(r) { return r.name; }).each(function(r) {
var relHtml = tplRelationship({
viewsIds: viewsIdsByConceptId[r.id],
relationshipName: _.escape(r.name),
relationshipType: properCase(r.type),
relationshipSource: _.escape(r.source.name),
relationshipTarget: _.escape(r.target.name),
relationshipDocumentationText: _.escape(r.documentation).replace(/\n/g, '<br>'),
relationshipDocumentationMarkdown: marked(_.escape(r.documentation), mdOptions)
});
// Add data-relationship-id with the concept ID
relHtml = relHtml.replace('<tr class="hidden', '<tr data-relationship-id="' + r.id + '" class="hidden');
relationships += relHtml;
});
var interactiveCSS = `
<style>
.interactive-view-wrapper { position: relative; display: inline-block; }
.view-overlay { position: absolute; top: 10px; left: 0; width: 100%; height: 100%; pointer-events: none; }
/* Prevent text overflow in table cells */
.tabs table td {
overflow: hidden;
word-wrap: break-word;
word-break: break-word;
}
.tabs table td div {
overflow: hidden;
word-wrap: break-word;
word-break: break-word;
max-height: 200px;
overflow-y: auto;
}
.interactive-element { fill: transparent; stroke: transparent; stroke-width: 0; cursor: pointer; pointer-events: all; transition: all 0.2s ease; }
.interactive-element:hover { fill: rgba(66, 165, 245, 0.25); stroke: #42A5F5; stroke-width: 2; }
.interactive-element[data-target-view]:hover { fill: rgba(76, 175, 80, 0.25); stroke: #4CAF50; stroke-width: 3; cursor: alias; }
.element-modal { display: none; position: fixed; z-index: 10000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.7); overflow: auto; }
.element-modal-content { background-color: #fefefe; margin: 5% auto; padding: 0; width: 90%; max-width: 700px; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.3); }
.modal-header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 12px 12px 0 0; position: relative; }
.modal-close { position: absolute; top: 15px; right: 20px; color: white; font-size: 32px; font-weight: bold; cursor: pointer; }
.modal-close:hover { color: #ddd; }
.modal-body { padding: 20px; }
.element-type-badge { display: inline-block; padding: 5px 12px; background-color: rgba(255,255,255,0.2); border-radius: 15px; font-size: 12px; margin-top: 5px; }
.element-documentation { background-color: #f8f9fa; padding: 15px; border-radius: 6px; margin: 15px 0; border-left: 4px solid #667eea; }
.properties-table { width: 100%; border-collapse: collapse; margin-top: 10px; }
.properties-table th, .properties-table td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
.properties-table th { background-color: #667eea; color: white; }
.view-navigation-btn { display: inline-block; margin-top: 15px; padding: 10px 20px; background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%); color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; }
.view-navigation-btn:hover { background: linear-gradient(135deg, #45a049 0%, #4CAF50 100%); }
.element-tooltip { position: absolute; background-color: rgba(0, 0, 0, 0.9); color: white; padding: 8px 12px; border-radius: 4px; font-size: 12px; pointer-events: none; z-index: 9999; display: none; max-width: 250px; }
.view-checkbox { display: none; }
</style>
`;
var elementDataJSON = JSON.stringify(interactiveElementData).replace(/'/g, "\\'");
var interactiveJS = `
<script>
// Make these globally accessible
window.elementDataStore = ` + elementDataJSON + `;
window.viewIdCache = null;
var elementDataStore = window.elementDataStore;
var viewIdCache = window.viewIdCache;
document.addEventListener('DOMContentLoaded', function() {
console.log('=== Interactive Features (STABLE) ===');
// CRITICAL: At startup, check id-model to show model documentation
var modelCheckbox = document.getElementById('id-model');
if(modelCheckbox) {
modelCheckbox.checked = true;
console.log('Model checkbox checked at startup');
}
// Uncheck all view checkboxes
document.querySelectorAll('input[type="checkbox"].view-checkbox').forEach(function(cb) {
cb.checked = false;
});
console.log('All view checkboxes unchecked at startup');
buildViewIdCache();
// CRITICAL: Uncheck other views AND id-model when a view is checked
document.querySelectorAll('input[type="checkbox"].view-checkbox').forEach(function(checkbox) {
checkbox.addEventListener('change', function() {
if(this.checked) {
// Uncheck id-model to hide model documentation
var modelCheckbox = document.getElementById('id-model');
if(modelCheckbox && modelCheckbox.checked) {
modelCheckbox.checked = false;
}
// Uncheck all other view checkboxes
document.querySelectorAll('input[type="checkbox"].view-checkbox').forEach(function(cb) {
if(cb !== checkbox && cb.checked) cb.checked = false;
});
// Refresh clickable elements/relationships for new view
setTimeout(function() {
addClickableElementsAndRelationships();
}, 100);
}
});
});
// CRITICAL: Also add clickable elements when clicking on tree labels (first interaction)
document.querySelectorAll('label[for^="id-id-"]').forEach(function(label) {
label.addEventListener('click', function() {
setTimeout(function() {
addClickableElementsAndRelationships();
}, 200);
});
});
// CRITICAL: Also refresh when switching tabs (Documentation/Elements/Relationships)
document.querySelectorAll('input[name="view-detail"]').forEach(function(tabRadio) {
tabRadio.addEventListener('change', function() {
setTimeout(function() {
addClickableElementsAndRelationships();
}, 100);
});
});
var modal = document.createElement('div');
modal.className = 'element-modal';
modal.id = 'elementModal';
modal.innerHTML = '<div class="element-modal-content"><div class="modal-header"><span class="modal-close">&times;</span><div id="modalHeader"></div></div><div class="modal-body" id="modalBody"></div></div>';
document.body.appendChild(modal);
var tooltip = document.createElement('div');
tooltip.className = 'element-tooltip';
tooltip.id = 'elementTooltip';
document.body.appendChild(tooltip);
modal.querySelector('.modal-close').onclick = function() { modal.style.display = 'none'; };
window.onclick = function(e) { if(e.target == modal) modal.style.display = 'none'; };
document.querySelectorAll('.interactive-element').forEach(function(el) {
el.addEventListener('click', function(e) {
e.stopPropagation();
var tv = this.getAttribute('data-target-view');
tv ? navigateToView(tv) : showElementModal(this);
});
el.addEventListener('mouseenter', function() {
tooltip.textContent = this.getAttribute('data-element-name') + ' (' + formatType(this.getAttribute('data-element-type')) + ')';
tooltip.style.display = 'block';
});
el.addEventListener('mousemove', function(e) {
tooltip.style.left = (e.pageX + 10) + 'px';
tooltip.style.top = (e.pageY + 10) + 'px';
});
el.addEventListener('mouseleave', function() { tooltip.style.display = 'none'; });
});
// CRITICAL: Make element names and relationship types clickable
addClickableElementsAndRelationships();
});
// Make elements in Elements table and relationships in Relationships table clickable
function addClickableElementsAndRelationships() {
// Elements table - make element names clickable
var elementRows = document.querySelectorAll('#view-elements:checked ~ .row table tbody tr');
elementRows.forEach(function(row) {
if(row.style.display === 'none') return; // Skip hidden rows
var cells = row.querySelectorAll('td');
if(cells.length >= 3) {
var nameCell = cells[0];
var typeCell = cells[1];
// Make name clickable
nameCell.style.cursor = 'pointer';
nameCell.style.color = '#0074D9';
nameCell.style.textDecoration = 'underline';
nameCell.addEventListener('click', function() {
var elementName = nameCell.textContent.trim();
var elementType = typeCell.textContent.trim();
// Find element in elementDataStore by name
showElementFromTable(elementName, elementType);
});
}
});
// Relationships table - make relationship types clickable
var relationshipRows = document.querySelectorAll('#view-relationships:checked ~ .row table tbody tr');
relationshipRows.forEach(function(row) {
if(row.style.display === 'none') return; // Skip hidden rows
var relationshipId = row.getAttribute('data-relationship-id');
var cells = row.querySelectorAll('td');
if(cells.length >= 5) {
var nameCell = cells[0];
var typeCell = cells[1];
var sourceCell = cells[2];
var targetCell = cells[3];
// Make type clickable
typeCell.style.cursor = 'pointer';
typeCell.style.color = '#0074D9';
typeCell.style.textDecoration = 'underline';
typeCell.addEventListener('click', function() {
var relName = nameCell.textContent.trim();
var relType = typeCell.textContent.trim();
var relSource = sourceCell.textContent.trim();
var relTarget = targetCell.textContent.trim();
showRelationshipFromTable(relationshipId, relName, relType, relSource, relTarget);
});
}
});
}
// Show element from table click - find it in elementDataStore
function showElementFromTable(elementName, elementType) {
// Find current active view
var activeCheckbox = document.querySelector('.view-checkbox:checked');
if(!activeCheckbox) return;
var viewId = activeCheckbox.id;
// Find element in elementDataStore by name
var elementData = null;
var elementId = null;
if(elementDataStore[viewId]) {
for(var id in elementDataStore[viewId]) {
var data = elementDataStore[viewId][id];
if(data.name === elementName) {
elementData = data;
elementId = id;
break;
}
}
}
// Show modal
var modal = document.getElementById('elementModal');
var modalHeader = document.getElementById('modalHeader');
var modalBody = document.getElementById('modalBody');
modalHeader.innerHTML = '<h2>' + elementName + '</h2>' +
'<span class="element-type-badge">' + elementType + '</span>';
var content = '';
if(elementData) {
if(elementData.documentation) {
content += '<div class="element-documentation">' + elementData.documentation + '</div>';
}
var propKeys = Object.keys(elementData.properties || {});
if(propKeys.length > 0) {
content += '<h3>Propriétés</h3>';
content += '<table class="properties-table">';
content += '<tr><th>Propriété</th><th>Valeur</th></tr>';
propKeys.forEach(function(key) {
content += '<tr><td>' + key + '</td><td>' + elementData.properties[key] + '</td></tr>';
});
content += '</table>';
}
} else {
content += '<p>Propriétés non disponibles</p>';
}
modalBody.innerHTML = content;
modal.style.display = 'block';
}
// Show relationship from table click
function showRelationshipFromTable(relationshipId, relName, relType, relSource, relTarget) {
// Find current active view
var activeCheckbox = document.querySelector('.view-checkbox:checked');
if(!activeCheckbox) return;
var viewId = activeCheckbox.id;
// Find relationship in elementDataStore by ArchiMate ID
var relData = null;
if(elementDataStore[viewId]) {
// Search by the ArchiMate concept ID
relData = elementDataStore[viewId][relationshipId];
// If not found directly, it might be stored under a visual object ID
// We need to search through all items
if(!relData) {
for(var id in elementDataStore[viewId]) {
var data = elementDataStore[viewId][id];
// The visual object might have the relationship ID as its concept
if(data.type && data.type.includes('relationship')) {
// This is a relationship visual object
// The ID we're looking for matches the concept ID
relData = data;
console.log('Found relationship by type search:', id, data.name);
break;
}
}
}
}
// Show modal
var modal = document.getElementById('elementModal');
var modalHeader = document.getElementById('modalHeader');
var modalBody = document.getElementById('modalBody');
modalHeader.innerHTML = '<h2>' + (relName || relType) + '</h2>' +
'<span class="element-type-badge">' + relType + '</span>';
var content = '';
// Documentation first
if(relData && relData.documentation) {
content += '<div class="element-documentation">' + relData.documentation + '</div>';
}
// Properties table
content += '<h3>Relation</h3>';
content += '<table class="properties-table">';
content += '<tr><th>Propriété</th><th>Valeur</th></tr>';
content += '<tr><td>Source</td><td>' + relSource + '</td></tr>';
content += '<tr><td>Target</td><td>' + relTarget + '</td></tr>';
// Add custom properties
if(relData && relData.properties) {
var propKeys = Object.keys(relData.properties);
if(propKeys.length > 0) {
propKeys.forEach(function(key) {
content += '<tr><td>' + key + '</td><td>' + relData.properties[key] + '</td></tr>';
});
}
}
content += '</table>';
// Debug
console.log('Relationship lookup:');
console.log(' ID:', relationshipId);
console.log(' Name:', relName);
console.log(' View:', viewId);
console.log(' Found:', !!relData);
if(relData) {
console.log(' Props:', Object.keys(relData.properties || {}).length);
} else {
console.log(' Available IDs:', Object.keys(elementDataStore[viewId] || {}));
}
content += '</table>';
modalBody.innerHTML = content;
modal.style.display = 'block';
}
function buildViewIdCache() {
viewIdCache = {};
var checkboxes = document.querySelectorAll('input[type="checkbox"].view-checkbox');
console.log('Found ' + checkboxes.length + ' view checkboxes');
checkboxes.forEach(function(cb) {
var id = cb.id;
if(id && id.indexOf('-') !== -1) {
viewIdCache[id] = id;
var cleanId = id;
if(cleanId.startsWith('id-')) {
cleanId = cleanId.substring(3);
viewIdCache[cleanId] = id;
}
if(cleanId.startsWith('id-')) {
var doubleClean = cleanId.substring(3);
viewIdCache[doubleClean] = id;
viewIdCache['id-' + doubleClean] = id;
}
viewIdCache['id-' + id] = id;
}
});
console.log('Cache: ' + Object.keys(viewIdCache).length + ' mappings');
}
function formatType(type) {
return type.replace('archimate-', '').replace(/-/g, ' ').split(' ').map(function(w) {
return w.charAt(0).toUpperCase() + w.slice(1);
}).join(' ');
}
function showElementModal(element) {
var elementId = element.getAttribute('data-element-id');
var elementName = element.getAttribute('data-element-name');
var elementType = element.getAttribute('data-element-type');
var targetView = element.getAttribute('data-target-view');
var viewWrapper = element.closest('.interactive-view-wrapper');
var viewId = viewWrapper.getAttribute('data-view-id');
var elementData = elementDataStore[viewId] ? elementDataStore[viewId][elementId] : null;
document.getElementById('modalHeader').innerHTML = '<h2>' + elementName + '</h2><span class="element-type-badge">' + formatType(elementType) + '</span>';
var content = '';
if(elementData) {
if(elementData.documentation) content += '<div class="element-documentation">' + elementData.documentation + '</div>';
var propKeys = Object.keys(elementData.properties);
if(propKeys.length > 0) {
content += '<h3>Propriétés</h3><table class="properties-table"><tr><th>Propriété</th><th>Valeur</th></tr>';
propKeys.forEach(function(key) {
content += '<tr><td>' + key + '</td><td>' + elementData.properties[key] + '</td></tr>';
});
content += '</table>';
}
}
if(targetView) content += '<button class="view-navigation-btn" onclick="navigateToView(\\'' + targetView + '\\')">Naviguer vers la vue →</button>';
document.getElementById('modalBody').innerHTML = content;
document.getElementById('elementModal').style.display = 'block';
}
function navigateToView(rawViewId) {
console.log('Navigation: ' + rawViewId);
var modal = document.getElementById('elementModal');
if(modal) modal.style.display = 'none';
var actualCheckboxId = viewIdCache[rawViewId];
if(!actualCheckboxId) {
var variations = [rawViewId, 'id-' + rawViewId, 'id-id-' + rawViewId, rawViewId.replace(/^id-/, ''), 'id-id-' + rawViewId.replace(/^id-/, '')];
for(var i = 0; i < variations.length; i++) {
if(document.getElementById(variations[i])) {
actualCheckboxId = variations[i];
break;
}
}
}
if(!actualCheckboxId) {
console.error('Checkbox not found for: ' + rawViewId);
return;
}
// Uncheck all views and id-model
var modelCheckbox = document.getElementById('id-model');
if(modelCheckbox && modelCheckbox.checked) {
modelCheckbox.checked = false;
}
document.querySelectorAll('input[type="checkbox"].view-checkbox').forEach(function(cb) {
if(cb.checked) cb.checked = false;
});
var checkbox = document.getElementById(actualCheckboxId);
checkbox.checked = true;
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
// Refresh clickable elements/relationships
setTimeout(function() {
addClickableElementsAndRelationships();
}, 100);
setTimeout(function() {
var viewSection = document.querySelector('[data-view-id="' + actualCheckboxId + '"]');
if(viewSection) viewSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 150);
}
</script>
`;
var mainReport = tplMainReport({
roboto: roboto, icon: icon, picnic: picnic, modelTitle: _.escape(model.name),
visibilityRulesBold: visibilityRulesBold, visibilityRulesReveal: visibilityRulesReveal,
inputCheckbox: inputCheckbox, treeContent: treeContent, viewTitles: viewTitles, viewDiagrams: viewDiagrams,
modelPurposeText: _.escape(model.purpose).replace(/\n/g, '<br>'),
modelPurposeMarkdown: marked(_.escape(model.purpose), mdOptions),
viewDocumentations: viewDocumentations, elements: elements, relationships: relationships,
sidebarBgColor: '#37474f', sidebarColor: '#DDDDDD', sidebarWidth: '350px', sidebarMargin: '10px',
sidebarFooterHeight: '0px', headerHeight: '60px', headerBgColor: '#0074D9', headerColor: '#fff',
mainBgColor: '#fff', mainColor: '#000', mainMargin: '20px', mainHeaderMargin: '35px',
mainHeaderBgColor: '#eceff1', mainHeaderColor: '#546e7a', treeMargin: '1.3em'
});
mainReport = mainReport.replace('</head>', interactiveCSS + '\n</head>');
mainReport = mainReport.replace('</body>', interactiveJS + '\n</body>');
$.fs.writeFile(filePath, mainReport, 'UTF-8');
info('');
info('🎉 EXPORT SUCCESSFUL!');
info(' File: ' + filePath);
info(' Views: ' + checkboxCount);
info(' Elements: ' + (elementsCollection ? elementsCollection.size() : 0));
info(' Relationships: ' + (relationshipsCollection ? relationshipsCollection.size() : 0));
info('');
info('Features:');
info(' ✓ Interactive SVG overlays with clickable elements');
info(' ✓ View navigation (tree + links)');
info(' ✓ Element and relationship properties');
info(' ✓ Correct mapping with bend points support');
info('');
function properCase(str) {
return str.replace(/\w*/g, function(txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
}).replace('-', ' ');
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment