You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

827 lines
23 KiB

/**
* A Draw.io diagram viewer component with configurable toolbar buttons for editing, deleting and zooming buttons.
* Parameters :
* diagramName - name of the diagram
* attachmentId - ID of the diagram attachment
* ceoId - page ID or issue key
* readerOpts - JSON object with options :
* loadUrl - url from which to load the diagram
* editUrl - url of the editor
* stylePath - url from which to load the css
* stencilPath - url from which to load the stencils
* imagePath - url from which to load the graph mages
* resourcePath - path to the translations
* viewerToolbar - show toolbar or not
* autoSize - resizes the graph container to match the graph bounds
* width - width of the container
* height - height of the container
* disableButtons - disables all buttons
* center - should the diagram be horizontally centered or not
* evaluation - evaluation mode
* lightbox - boolean indicating if this viewer is a lightbox
* graphDocument - optional document containing the XML data
*/
function DrawioViewer(diagramName, attachmentId, ceoId, readerOpts, lightbox, graphDocument, connect)
{
this.id = attachmentId;
this.diagramName = diagramName;
this.ceoId = ceoId;
this.options = readerOpts;
this.lightbox = lightbox;
this.graphDocument = graphDocument;
this.connect = connect;
this.paddingBottom = (connect) ? 0 : 30;
// Overrides browser language with Confluence user language
var lang = null;
// Language is in the readOpts in Server and in the URL in Connect
if (!connect && readerOpts.language != null)
{
lang = readerOpts.language
}
else if (connect != null && urlParams['loc'] != null)
{
lang = urlParams['loc'];
var dash = lang.indexOf('-');
if (dash >= 0)
{
lang = lang.substring(0, dash);
}
}
// Only german and english supported currently. English is default.
if (lang == 'de')
{
mxClient.language = 'de';
}
// Special extension used for the message bundle. For this bundle there is only a German
// translation and the default English bundle so we temporarily override isLanguageSupported
// to return true only if German is used and fallback to English for all other languages.
var prevExtension = mxResources.extension;
var prevIsLangSupported = mxResources.isLanguageSupported;
mxResources.extension = '.txt';
mxResources.isLanguageSupported = function(lan)
{
return lan == 'de';
};
mxResources.add(this.options.resourcePath);
// Restores previous settings
mxResources.extension = prevExtension;
mxResources.isLanguageSupported = prevIsLangSupported;
this.buttons = this.createButtons();
this.transparentImage = Editor.prototype.transparentImage;
this.extractGraphModel = Editor.prototype.extractGraphModel;
this.setGraphXml = Editor.prototype.setGraphXml;
this.readGraphState = Editor.prototype.readGraphState;
this.resetGraph = Editor.prototype.resetGraph;
this.decompress = Editor.prototype.decompress;
this.updateGraphComponents = Editor.prototype.updateGraphComponents;
this.fireEvent = Editor.prototype.fireEvent;
this.addListener = Editor.prototype.addListener;
this.originalNoForeignObject = Editor.prototype.originalNoForeignObject;
this.gridImage = '';
this.addListener('resetGraphView', this.resetGraphView);
}
DrawioViewer.prototype = new mxEventSource();
DrawioViewer.prototype.graph = null;
DrawioViewer.prototype.id = null;
DrawioViewer.prototype.toolbar = null;
DrawioViewer.prototype.options = null;
DrawioViewer.prototype.originX = 0;
DrawioViewer.prototype.originY = 0;
DrawioViewer.prototype.popupWindow = null;
DrawioViewer.prototype.buttons = {};
DrawioViewer.prototype.graphXmlString = null;
DrawioViewer.prototype.installToolbar = function()
{
this.toolbar = document.createElement('div');
var toolbar = this.toolbar;
var container = this.graph.container;
toolbar.id = 'diagramly-reader-toolbar-' + this.id;
toolbar.className = 'diagramly-reader-toolbar';
toolbar.style.position = 'absolute';
container.parentNode.appendChild(toolbar);
toolbar.style.height = '30px';
toolbar.style.width = this.countVisibleButtons() * 29 + 'px';
// Makes sure the toolbar is always visible and
// disables toolbar for all overflow content
container.parentNode.style.overflow = 'visible';
if (this.lightbox)
{
toolbar.style.bottom = '4px';
toolbar.style.left = '50%';
toolbar.style.width = this.countVisibleButtons() * 29 + 'px';
toolbar.style.marginLeft = -Math.round(this.countVisibleButtons() * 29 / 2) + 'px'
}
else
{
container.parentNode.style.paddingBottom = this.paddingBottom + 'px';
var bs = this.graph.getBorderSizes();
toolbar.style.bottom = (container.offsetTop + bs.y + 4) + 'px';
toolbar.style.left = (container.offsetLeft + bs.x) + 'px';
if (!mxClient.IS_TOUCH)
{
toolbar.style.display = 'none';
$(container.parentNode).hover(function()
{
toolbar.style.bottom = (container.offsetTop + bs.y + 4) + 'px';
toolbar.style.left = (container.offsetLeft + bs.x) + 'px';
$(toolbar).fadeIn(100);
},
function()
{
$(toolbar).fadeOut(100);
});
}
}
}
DrawioViewer.prototype.countVisibleButtons = function()
{
var counter = 0;
for(var key in this.buttons)
{
var button = this.buttons[key];
if (button.visible)
{
counter++;
}
}
return counter;
}
DrawioViewer.prototype.init = function()
{
this.loadStylesheet();
// Makes the shadow brighter
mxConstants.SHADOWCOLOR = '#000000';
mxConstants.SHADOW_OPACITY = 0.25;
this.graph.setEnabled(false);
this.graph.autoScroll = false;
this.graph.container.style.overflow = 'hidden';
this.graph.container.style.cursor = 'move';
// Panning only enabled in lightbox to allow text selection in viewer
this.graph.setPanning(true);
// Workaround for context trigger starting panning if ignoreCell is true
this.graph.panningHandler.useLeftButtonForPanning = true;
this.graph.panningHandler.usePopupTrigger = false;
this.graph.panningHandler.ignoreCell = true;
this.graph.panningHandler.isForcePanningEvent = function(me)
{
return mxEvent.isLeftMouseButton(me.getEvent());
};
// Folding only enabled in lightbox
this.graph.foldingEnabled = this.lightbox;
// Overrides click handler to ignore graph enabled state
if (this.graph.foldingEnabled)
{
this.graph.cellRenderer.createControlClickHandler = function(state)
{
var graph = state.view.graph;
return function (evt)
{
var collapse = !graph.isCellCollapsed(state.cell);
graph.foldCells(collapse, false, [state.cell], null, evt);
mxEvent.consume(evt);
};
};
}
else
{
// Hides collapse/expand icon if folding is disabled
this.graph.getFoldingImage = function()
{
return null;
};
};
// HTML entities are displayed as plain text in wrapped plain text labels
this.graph.cellRenderer.getLabelValue = function(state)
{
var result = mxCellRenderer.prototype.getLabelValue.apply(this, arguments);
if (state.view.graph.isHtmlLabel(state.cell))
{
if (state.style['html'] != 1)
{
result = mxUtils.htmlEntities(result, false);
}
else
{
result = Graph.sanitizeHtml(result);
}
}
return result;
};
// Enables links if graph is "disabled" (ie. read-only)
this.graph.click = function(me)
{
var cell = me.getCell();
if (cell != null && !me.isConsumed() && (mxEvent.isTouchEvent(me.getEvent()) ||
mxEvent.isLeftMouseButton(me.getEvent())))
{
var href = this.getLinkForCell(cell);
// Test cases:
// 1) the relative link without the Conf base path, with a leading slash, e.g. /download/attachment/....
// 2) the relative link with the Conf base path, with a leading slash, e.g. /confluence/download/attachmentss/...
// 3) the relative link without the conf base path, without a leading slash, e.g. download/attachments/...
// 4) the full absolute path, e.g. https://localhost:1990/confluence/download/attachments/...
// 5) full path without protocol, e.g. //confluence/download/attachments/...
if (href != null)
{
var r = new RegExp('^(?:[a-z]+:)?//', 'i'); // https://stackoverflow.com/questions/10687099/how-to-test-if-a-url-string-is-absolute-or-relative
if (!r.test(href))
{
// relative link
if (href.lastIndexOf('/', 0) !== 0) // http://stackoverflow.com/a/4579228/226469 seems to be the fastest check
{
// Need a leading slash in case we need to prepend the base path
href = '/' + href;
}
// var cp = AJS.Confluence.getContextPath(); // TODO confluence call in a common module
// Originally, links included the base path (but not the host), so there might be cases of the base path
// already being prepended. If the base path has changed, we can't recover those cases
// window.location.href = href.substring(0, cp.length) === cp ? href : cp + href; // prepends the context path if it's not already there
window.location.href = href;
}
else
{
// Test if it's an absolute URL, but on the same domain (i.e. open in same window)
// There's a security setting (unknown which) that seems to stop the IE hack below working in
// IE 11, https://drawio.atlassian.net/browse/DS-175, https://drawio.atlassian.net/browse/DFCS-52
// Worst case is these users will open an absolute same domain link in a window
var link = document.createElement('a');
link.href = href;
link.href = link.href; // hack to populate 'host' under IE
if (link.host === location.host)
{
window.location.href = href;
}
else
{
window.open(href);
}
}
}
me.consume();
}
};
this.graph.setTooltips(!mxClient.IS_TOUCH);
if (this.options.width != null)
{
this.graph.container.style.width = this.options.width + 'px';
}
if (this.options.height != null)
{
this.graph.container.style.height = this.options.height + 'px';
}
// Accumulates the zoom factor while the rendering is taking place
// so that not the complete sequence of zoom steps must be painted
var graph = this.graph;
graph.updateZoomTimeout = null;
graph.cumulativeZoomFactor = 1;
graph.lazyZoom = function(zoomIn)
{
if (this.updateZoomTimeout != null)
{
window.clearTimeout(this.updateZoomTimeout);
}
if (zoomIn)
{
this.cumulativeZoomFactor *= this.zoomFactor;
}
else
{
this.cumulativeZoomFactor /= this.zoomFactor;
}
this.cumulativeZoomFactor = Math.round(this.view.scale * this.cumulativeZoomFactor * 100) / 100 / this.view.scale;
this.updateZoomTimeout = window.setTimeout(mxUtils.bind(this, function()
{
this.zoom(this.cumulativeZoomFactor);
this.cumulativeZoomFactor = 1;
this.updateZoomTimeout = null;
}), 20);
};
if (this.lightbox)
{
mxEvent.addMouseWheelListener(mxUtils.bind(this, function(evt, up)
{
if (!mxClient.IS_MAC || !mxEvent.isControlDown(evt))
{
var source = mxEvent.getSource(evt);
while (source != null)
{
if (source == graph.container)
{
graph.lazyZoom(up);
mxEvent.consume(evt);
return;
}
source = source.parentNode;
}
}
}));
}
};
DrawioViewer.prototype.resetGraphView = function()
{
this.graph.pageBreaksVisible = false;
this.graph.preferPageSize = false;
this.graph.pageVisible = false;
if (!this.lightbox)
{
var update = mxUtils.bind(this, function()
{
this.graph.centerZoom = this.graph.panningHandler.panningEnabled;
// If width and height are specified the height is overridden to match the diagram size
var autoSizeWidth = this.options.width == null;
var autoSizeHeight = this.options.height == null;
var bounds = this.graph.getGraphBounds();
var ratio = bounds.width / bounds.height;
var width = autoSizeWidth ? bounds.width + 2 : this.options.width;
var height = autoSizeHeight ? (width / ratio) + 1 : this.options.height;
this.graph.container.style.width = Math.ceil(width) + 'px';
this.graph.container.style.height = Math.ceil(height) + 'px';
this.graph.container.style.maxWidth = '100%';
if (autoSizeWidth && autoSizeHeight)
{
this.translateOrigin();
// Used for fast restore of initial position in zoom to fit button
this.initialX = this.graph.view.translate.x;
this.initialY = this.graph.view.translate.y;
}
else if (this.options.zoomToFit)
{
this.graph.fit();
}
//set the border after calling updateGraphComponnets() because the call sets it to ''
this.graph.container.style.border = this.options.border ? '1px solid #DDDDDD' : 'none';
this.graph.container.style.backgroundColor = (this.graph.background == null ||
this.graph.background == 'none') ? '#ffffff' : this.graph.background;
});
// Workaround for invisible container is to move the container to the document body for rendering
if (!this.connect && (this.graph.container.clientWidth == 0 || this.graph.container.clientHeight == 0))
{
var previousParent = this.graph.container.parentNode;
var nextSibling = this.graph.container.nextSibling;
var prevPosition = this.graph.container.style.position;
var prevVisible = this.graph.container.style.visible;
// Moves to document body for rendering (needed for text measuring)
this.graph.container.style.position = 'absolute';
this.graph.container.style.visible = 'hidden';
document.body.appendChild(this.graph.container);
// Refresh required in visible DOM to update text bounding boxes
this.graph.refresh();
update();
// Move it back into DOM tree position
if (nextSibling != null)
{
nextSibling.parentNode.insertBefore(this.graph.container, nextSibling);
}
else
{
previousParent.appendChild(this.graph.container);
}
// Restore position CSS
this.graph.container.style.visible = prevVisible;
this.graph.container.style.position = prevPosition;
}
else
{
update();
}
}
else
{
this.graph.container.style.backgroundColor = (this.graph.background == null ||
this.graph.background == 'none') ? '#ffffff' : this.graph.background;
}
};
DrawioViewer.prototype.translateOrigin = function()
{
var bounds = this.graph.getGraphBounds();
this.graph.view.setTranslate(this.originX - Math.floor(bounds.x), this.originY - Math.floor(bounds.y));
};
DrawioViewer.prototype.loadGraph = function(diagramName, ceoId)
{
var spinner = this.createSpinner(this.graph.container);
try
{
mxUtils.get(this.options.loadUrl, mxUtils.bind(this, function(req)
{
spinner.stop();
if (req.getStatus() < 200 || req.getStatus() > 299)
{
this.showWarning(mxResources.get('error') + ' ' + req.getStatus());
this.graph.container.style.border = this.options.border ? '1px solid #DDDDDD' : 'none';
this.graph.container.style.backgroundColor = '#ffffff';
this.graph.container.style.height = '20px';
}
else
{
var json = JSON.parse(req.getText());
this.graphXmlString = json.xml;
var doc = mxUtils.parseXml(json.xml);
this.xmlDoc = doc;
this.filename = json.filename;
this.setGraphXml(doc.documentElement);
this.graphDocument = doc;
}
}),
function()
{
spinner.stop();
});
}
catch (e)
{
spinner.stop();
}
};
DrawioViewer.prototype.loadStylesheet = function()
{
var node = mxUtils.load(this.options.stylePath + '/default.xml').getDocumentElement();
var dec = new mxCodec(node.ownerDocument);
dec.decode(node, this.graph.getStylesheet());
};
DrawioViewer.prototype.renderButtons = function()
{
for (var key in this.buttons)
{
var button = this.buttons[key];
if (button.visible)
{
this.addToolbarButton(this.toolbar, button);
}
}
};
DrawioViewer.prototype.addToolbarButton = function(toolbar, drawioButton)
{
var enabled = typeof drawioButton.enabled === 'undefined' ? true : enabled;
var button = drawioButton.linkButton ? document.createElement('a') : document.createElement('div');
button.className = 'diagramly-reader-toolbar-button';
if (drawioButton.icon != null)
{
var img = document.createElement('img');
img.setAttribute('src', drawioButton.icon);
img.style.verticalAlign = 'middle';
img.style.marginRight = '2px';
button.appendChild(img);
button.title = drawioButton.label;
}
if (!drawioButton.enabled)
{
button.style.opacity = 0.2;
} else
{
if(drawioButton.linkButton)
{
button.href = drawioButton.url;
}
else
{
mxEvent.addListener(button, 'click', function(evt)
{
drawioButton.clickHandler.apply(this, arguments);
});
}
mxEvent.addListener(button, 'mouseover', function(evt)
{
button.classList.add('diagramly-reader-toolbar-button-hover');
});
mxEvent.addListener(button, 'mouseout', function(evt)
{
button.className = 'diagramly-reader-toolbar-button';
});
}
toolbar.appendChild(button);
return button;
};
DrawioViewer.prototype.createSpinner = function(container)
{
var opts =
{
lines : 12, // The number of lines to draw
length : 12, // The length of each line
width : 5, // The line thickness
radius : 10, // The radius of the inner circle
rotate : 0, // The rotation offset
color : '#000', // #rgb or #rrggbb
speed : 1, // Rounds per second
trail : 60, // Afterglow percentage
shadow : false, // Whether to render a shadow
hwaccel : false, // Whether to use hardware acceleration
className : 'spinner', // The CSS class to assign to the spinner
zIndex : 2e9 // The z-index (defaults to 2000000000)
};
return new Spinner(opts).spin(container);
};
DrawioViewer.prototype.show = function(container)
{
this.graph = new Graph(container);
this.graph.id = this.id;
this.init();
// Uses the XML document that was loaded for the viewer in the lightbox
if (this.graphDocument != null)
{
this.setGraphXml(this.graphDocument.documentElement);
}
else
{
this.loadGraph(this.diagramName, this.ceoId);
}
if (this.options.viewerToolbar)
{
this.installToolbar();
this.renderButtons();
}
if (this.options.licenseStatus == 'NO_LICENSE')
{
this.showWarning(mxResources.get('drawio.reader.noLicense'));
}
else if (this.options.licenseStatus == 'EVAL_LICENSE')
{
this.showWarning(mxResources.get('drawio.reader.evaluation'));
}
else if (this.options.licenseStatus == 'EVAL_EXPIRED')
{
this.showWarning(mxResources.get('drawio.reader.evaluationExpired'));
}
else if (this.options.licenseStatus == 'VERSION_MISMATCH')
{
this.showWarning(mxResources.get('drawio.reader.versionMismatch', ['https://support.draw.io/pages/viewpage.action?pageId=11829320']));
}
else if (this.options.licenseStatus == 'USER_MISMATCH')
{
this.showWarning(mxResources.get('drawio.reader.userMismatch', ['https://support.draw.io/pages/viewpage.action?pageId=11829323']));
}
};
DrawioViewer.prototype.showWarning = function(msg)
{
var div = document.createElement('div');
div.style.position = 'absolute';
div.style.overflow = 'hidden';
div.style.left = '0px';
div.style.top = '0px';
div.style.right = '0px';
div.style.fontSize = '12px';
div.style.margin = '2px';
mxUtils.setOpacity(div, 50);
div.style.color = 'gray';
div.style.textAlign = 'center';
div.style.whiteSpace = 'nowrap';
span = document.createElement('span');
span.innerHTML = msg;
div.appendChild(span);
this.graph.container.parentNode.appendChild(div);
};
DrawioViewer.prototype.showLightbox = function()
{
console.log('Lightbox feature not implemented.');
};
DrawioViewer.prototype.createButtons = function()
{
var viewer = this;
var buttons = {};
var enableButton = typeof this.options.disableButtons === 'undefined' ? true : !this.options.disableButtons;
var canEdit = this.options.userCanEdit && enableButton;
var canRemove = this.options.userCanRemove && enableButton;
var autoSizeWidth = this.options.width == null;
var autoSizeHeight = this.options.height == null;
buttons[DrawioViewerActions.EDIT] = new DrawioViewerButton(
{
label : mxResources.get('diagramly.reader.edit'),
icon : viewer.options.imagePath + '/edit.png',
url : viewer.options.editUrl,
enabled : canEdit,
linkButton : true
});
buttons[DrawioViewerActions.REMOVE] = new DrawioViewerButton(
{
label : mxResources.get('diagramly.reader.remove'),
icon : viewer.options.imagePath + '/remove.png',
clickHandler : function()
{
if (confirm(mxResources.get('diagramly.reader.confirmDelete')))
{
window.location.href = viewer.options.removeUrl;
}
},
enabled : canRemove
});
buttons[DrawioViewerActions.ACTUAL_SIZE] = new DrawioViewerButton(
{
label : mxResources.get('diagramly.reader.zoomActual'),
icon : viewer.options.imagePath + '/zoomActual.png',
clickHandler : function()
{
viewer.graph.zoomActual();
viewer.translateOrigin();
},
enabled : enableButton
});
buttons[DrawioViewerActions.ZOOM_TO_FIT] = new DrawioViewerButton(
{
label : (this.lightbox) ? mxResources.get('diagramly.reader.zoomActual') : mxResources.get('diagramly.reader.fit'),
icon : (this.lightbox) ? viewer.options.imagePath + '/zoomActual.png' : viewer.options.imagePath + '/zoomFit.gif',
clickHandler : mxUtils.bind(this, function()
{
if (this.lightbox)
{
// NOTE: Maxscale is 1 here so only make smaller but not larger
viewer.graph.fit(8);
viewer.graph.center(true, true, null, 0.42);
}
else
{
if (autoSizeWidth && autoSizeHeight)
{
this.graph.view.scaleAndTranslate(1, this.initialX, this.initialY);
}
else
{
this.graph.fit();
}
}
}),
enabled : enableButton
});
buttons[DrawioViewerActions.ZOOM_OUT] = new DrawioViewerButton(
{
label : mxResources.get('diagramly.reader.zoomOut'),
icon : viewer.options.imagePath + '/zoomOut.gif',
clickHandler : function()
{
viewer.graph.zoomOut();
},
enabled : enableButton
});
buttons[DrawioViewerActions.ZOOM_IN] = new DrawioViewerButton(
{
label : mxResources.get('diagramly.reader.zoomIn'),
icon : viewer.options.imagePath + '/zoomIn.gif',
clickHandler : function()
{
viewer.graph.zoomIn();
},
enabled : enableButton
});
buttons[DrawioViewerActions.EXPAND] = new DrawioViewerButton(
{
label : mxResources.get('diagramly.reader.fullScreen'),
icon : viewer.options.imagePath + '/largeView.png',
clickHandler : mxUtils.bind(this, function()
{
this.showLightbox();
}),
enabled : enableButton
});
buttons[DrawioViewerActions.CLOSE] = new DrawioViewerButton(
{
label : mxResources.get('diagramly.reader.closeFullScreen'),
icon : viewer.options.imagePath + '/closeLargeView.gif',
clickHandler : function()
{
viewer.popupWindow.remove();
},
enabled : enableButton,
visible : false
});
return buttons;
};
DrawioViewerActions =
{
EDIT : 'edit',
REMOVE : 'remove',
ACTUAL_SIZE : 'actualSize',
ZOOM_TO_FIT : 'zoomToFit',
ZOOM_OUT : 'zoomOut',
ZOOM_IN : 'zoomIn',
EXPAND : 'expand',
CLOSE : 'close'
};
function DrawioViewerButton(options)
{
this.label = options.label;
this.clickHandler = options.clickHandler;
this.enabled = typeof options.enabled != 'undefined' ? options.enabled : true;
this.icon = options.icon;
this.visible = typeof options.visible != 'undefined' ? options.visible : true;
this.linkButton = typeof options.linkButton != 'undefined' ? options.linkButton : false;
this.url = options.url;
};
DrawioViewerButton.prototype.label = null;
DrawioViewerButton.prototype.clickHandler = null;
DrawioViewerButton.prototype.enabled = true;
DrawioViewerButton.prototype.icon = null;
DrawioViewerButton.prototype.visible = true;
DrawioViewerButton.prototype.linkButton = false;
DrawioViewerButton.prototype.url = null;