/** * Copyright (c) 2006-2015, JGraph Ltd * Copyright (c) 2006-2015, Gaudenz Alder */ /** * Class: mxOutline * * Implements an outline (aka overview) for a graph. Set to true * to enable updates while the source graph is panning. * * Example: * * (code) * var outline = new mxOutline(graph, div); * (end) * * If an outline is used in an in IE8 standards mode, the following * code makes sure that the shadow filter is not inherited and that any * transparent elements in the graph do not show the page background, but the * background of the graph container. * * (code) * if (document.documentMode == 8) * { * container.style.filter = 'progid:DXImageTransform.Microsoft.alpha(opacity=100)'; * } * (end) * * To move the graph to the top, left corner the following code can be used. * * (code) * var scale = graph.view.scale; * var bounds = graph.getGraphBounds(); * graph.view.setTranslate(-bounds.x / scale, -bounds.y / scale); * (end) * * To toggle the suspended mode, the following can be used. * * (code) * outline.suspended = !outln.suspended; * if (!outline.suspended) * { * outline.update(true); * } * (end) * * Constructor: mxOutline * * Constructs a new outline for the specified graph inside the given * container. * * Parameters: * * source - to create the outline for. * container - DOM node that will contain the outline. */ function mxOutline(source, container) { this.source = source; if (container != null) { this.init(container); } }; /** * Function: source * * Reference to the source . */ mxOutline.prototype.source = null; /** * Function: outline * * Reference to the that renders the outline. */ mxOutline.prototype.outline = null; /** * Function: graphRenderHint * * Renderhint to be used for the outline graph. Default is faster. */ mxOutline.prototype.graphRenderHint = mxConstants.RENDERING_HINT_FASTER; /** * Variable: enabled * * Specifies if events are handled. Default is true. */ mxOutline.prototype.enabled = true; /** * Variable: showViewport * * Specifies a viewport rectangle should be shown. Default is true. */ mxOutline.prototype.showViewport = true; /** * Variable: border * * Border to be added at the bottom and right. Default is 10. */ mxOutline.prototype.border = 10; /** * Variable: enabled * * Specifies the size of the sizer handler. Default is 8. */ mxOutline.prototype.sizerSize = 8; /** * Variable: labelsVisible * * Specifies if labels should be visible in the outline. Default is false. */ mxOutline.prototype.labelsVisible = false; /** * Variable: updateOnPan * * Specifies if should be called for in the source * graph. Default is false. */ mxOutline.prototype.updateOnPan = false; /** * Variable: sizerImage * * Optional to be used for the sizer. Default is null. */ mxOutline.prototype.sizerImage = null; /** * Variable: minScale * * Minimum scale to be used. Default is 0.0001. */ mxOutline.prototype.minScale = 0.0001; /** * Variable: suspended * * Optional boolean flag to suspend updates. Default is false. This flag will * also suspend repaints of the outline. To toggle this switch, use the * following code. * * (code) * nav.suspended = !nav.suspended; * * if (!nav.suspended) * { * nav.update(true); * } * (end) */ mxOutline.prototype.suspended = false; /** * Variable: forceVmlHandles * * Specifies if VML should be used to render the handles in this control. This * is true for IE8 standards mode and false for all other browsers and modes. * This is a workaround for rendering issues of HTML elements over elements * with filters in IE 8 standards mode. */ mxOutline.prototype.forceVmlHandles = document.documentMode == 8; /** * Function: createGraph * * Creates the used in the outline. */ mxOutline.prototype.createGraph = function(container) { var graph = new mxGraph(container, this.source.getModel(), this.graphRenderHint, this.source.getStylesheet()); graph.foldingEnabled = false; graph.autoScroll = false; return graph; }; /** * Function: init * * Initializes the outline inside the given container. */ mxOutline.prototype.init = function(container) { this.outline = this.createGraph(container); // Do not repaint when suspended var outlineGraphModelChanged = this.outline.graphModelChanged; this.outline.graphModelChanged = mxUtils.bind(this, function(changes) { if (!this.suspended && this.outline != null) { outlineGraphModelChanged.apply(this.outline, arguments); } }); // Enables faster painting in SVG if (mxClient.IS_SVG) { var node = this.outline.getView().getCanvas().parentNode; node.setAttribute('shape-rendering', 'optimizeSpeed'); node.setAttribute('image-rendering', 'optimizeSpeed'); } // Hides cursors and labels this.outline.labelsVisible = this.labelsVisible; this.outline.setEnabled(false); this.updateHandler = mxUtils.bind(this, function(sender, evt) { if (!this.suspended && !this.active) { this.update(); } }); // Updates the scale of the outline after a change of the main graph this.source.getModel().addListener(mxEvent.CHANGE, this.updateHandler); this.outline.addMouseListener(this); // Adds listeners to keep the outline in sync with the source graph var view = this.source.getView(); view.addListener(mxEvent.SCALE, this.updateHandler); view.addListener(mxEvent.TRANSLATE, this.updateHandler); view.addListener(mxEvent.SCALE_AND_TRANSLATE, this.updateHandler); view.addListener(mxEvent.DOWN, this.updateHandler); view.addListener(mxEvent.UP, this.updateHandler); // Updates blue rectangle on scroll mxEvent.addListener(this.source.container, 'scroll', this.updateHandler); this.panHandler = mxUtils.bind(this, function(sender) { if (this.updateOnPan) { this.updateHandler.apply(this, arguments); } }); this.source.addListener(mxEvent.PAN, this.panHandler); // Refreshes the graph in the outline after a refresh of the main graph this.refreshHandler = mxUtils.bind(this, function(sender) { this.outline.setStylesheet(this.source.getStylesheet()); this.outline.refresh(); }); this.source.addListener(mxEvent.REFRESH, this.refreshHandler); // Creates the blue rectangle for the viewport this.bounds = new mxRectangle(0, 0, 0, 0); this.selectionBorder = new mxRectangleShape(this.bounds, null, mxConstants.OUTLINE_COLOR, mxConstants.OUTLINE_STROKEWIDTH); this.selectionBorder.dialect = this.outline.dialect; if (this.forceVmlHandles) { this.selectionBorder.isHtmlAllowed = function() { return false; }; } this.selectionBorder.init(this.outline.getView().getOverlayPane()); // Handles event by catching the initial pointer start and then listening to the // complete gesture on the event target. This is needed because all the events // are routed via the initial element even if that element is removed from the // DOM, which happens when we repaint the selection border and zoom handles. var handler = mxUtils.bind(this, function(evt) { var t = mxEvent.getSource(evt); var redirect = mxUtils.bind(this, function(evt) { this.outline.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt)); }); var redirect2 = mxUtils.bind(this, function(evt) { mxEvent.removeGestureListeners(t, null, redirect, redirect2); this.outline.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt)); }); mxEvent.addGestureListeners(t, null, redirect, redirect2); this.outline.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt)); }); mxEvent.addGestureListeners(this.selectionBorder.node, handler); // Creates a small blue rectangle for sizing (sizer handle) this.sizer = this.createSizer(); if (this.forceVmlHandles) { this.sizer.isHtmlAllowed = function() { return false; }; } this.sizer.init(this.outline.getView().getOverlayPane()); if (this.enabled) { this.sizer.node.style.cursor = 'nwse-resize'; } mxEvent.addGestureListeners(this.sizer.node, handler); this.selectionBorder.node.style.display = (this.showViewport) ? '' : 'none'; this.sizer.node.style.display = this.selectionBorder.node.style.display; this.selectionBorder.node.style.cursor = 'move'; this.update(false); }; /** * Function: isEnabled * * Returns true if events are handled. This implementation * returns . */ mxOutline.prototype.isEnabled = function() { return this.enabled; }; /** * Function: setEnabled * * Enables or disables event handling. This implementation * updates . * * Parameters: * * value - Boolean that specifies the new enabled state. */ mxOutline.prototype.setEnabled = function(value) { this.enabled = value; }; /** * Function: setZoomEnabled * * Enables or disables the zoom handling by showing or hiding the respective * handle. * * Parameters: * * value - Boolean that specifies the new enabled state. */ mxOutline.prototype.setZoomEnabled = function(value) { this.sizer.node.style.visibility = (value) ? 'visible' : 'hidden'; }; /** * Function: refresh * * Invokes and revalidate the outline. This method is deprecated. */ mxOutline.prototype.refresh = function() { this.update(true); }; /** * Function: createSizer * * Creates the shape used as the sizer. */ mxOutline.prototype.createSizer = function() { if (this.sizerImage != null) { var sizer = new mxImageShape(new mxRectangle(0, 0, this.sizerImage.width, this.sizerImage.height), this.sizerImage.src); sizer.dialect = this.outline.dialect; return sizer; } else { var sizer = new mxRectangleShape(new mxRectangle(0, 0, this.sizerSize, this.sizerSize), mxConstants.OUTLINE_HANDLE_FILLCOLOR, mxConstants.OUTLINE_HANDLE_STROKECOLOR); sizer.dialect = this.outline.dialect; return sizer; } }; /** * Function: getSourceContainerSize * * Returns the size of the source container. */ mxOutline.prototype.getSourceContainerSize = function() { return new mxRectangle(0, 0, this.source.container.scrollWidth, this.source.container.scrollHeight); }; /** * Function: getOutlineOffset * * Returns the offset for drawing the outline graph. */ mxOutline.prototype.getOutlineOffset = function(scale) { return null; }; /** * Function: getSourceGraphBounds * * Returns the graph bound boxing of the source. */ mxOutline.prototype.getSourceGraphBounds = function() { return this.source.getGraphBounds(); }; /** * Function: update * * Updates the outline. */ mxOutline.prototype.update = function(revalidate) { if (this.source != null && this.source.container != null && this.outline != null && this.outline.container != null) { var sourceScale = this.source.view.scale; var scaledGraphBounds = this.getSourceGraphBounds(); var unscaledGraphBounds = new mxRectangle(scaledGraphBounds.x / sourceScale + this.source.panDx, scaledGraphBounds.y / sourceScale + this.source.panDy, scaledGraphBounds.width / sourceScale, scaledGraphBounds.height / sourceScale); var unscaledFinderBounds = new mxRectangle(0, 0, this.source.container.clientWidth / sourceScale, this.source.container.clientHeight / sourceScale); var union = unscaledGraphBounds.clone(); union.add(unscaledFinderBounds); // Zooms to the scrollable area if that is bigger than the graph var size = this.getSourceContainerSize(); var completeWidth = Math.max(size.width / sourceScale, union.width); var completeHeight = Math.max(size.height / sourceScale, union.height); var availableWidth = Math.max(0, this.outline.container.clientWidth - this.border); var availableHeight = Math.max(0, this.outline.container.clientHeight - this.border); var outlineScale = Math.min(availableWidth / completeWidth, availableHeight / completeHeight); var scale = (isNaN(outlineScale)) ? this.minScale : Math.max(this.minScale, outlineScale); if (scale > 0) { if (this.outline.getView().scale != scale) { this.outline.getView().scale = scale; revalidate = true; } var navView = this.outline.getView(); if (navView.currentRoot != this.source.getView().currentRoot) { navView.setCurrentRoot(this.source.getView().currentRoot); } var t = this.source.view.translate; var tx = t.x + this.source.panDx; var ty = t.y + this.source.panDy; var off = this.getOutlineOffset(scale); if (off != null) { tx += off.x; ty += off.y; } if (unscaledGraphBounds.x < 0) { tx = tx - unscaledGraphBounds.x; } if (unscaledGraphBounds.y < 0) { ty = ty - unscaledGraphBounds.y; } if (navView.translate.x != tx || navView.translate.y != ty) { navView.translate.x = tx; navView.translate.y = ty; revalidate = true; } // Prepares local variables for computations var t2 = navView.translate; scale = this.source.getView().scale; var scale2 = scale / navView.scale; var scale3 = 1.0 / navView.scale; var container = this.source.container; // Updates the bounds of the viewrect in the navigation this.bounds = new mxRectangle( (t2.x - t.x - this.source.panDx) / scale3, (t2.y - t.y - this.source.panDy) / scale3, (container.clientWidth / scale2), (container.clientHeight / scale2)); // Adds the scrollbar offset to the finder this.bounds.x += this.source.container.scrollLeft * navView.scale / scale; this.bounds.y += this.source.container.scrollTop * navView.scale / scale; var b = this.selectionBorder.bounds; if (b.x != this.bounds.x || b.y != this.bounds.y || b.width != this.bounds.width || b.height != this.bounds.height) { this.selectionBorder.bounds = this.bounds; this.selectionBorder.redraw(); } // Updates the bounds of the zoom handle at the bottom right var b = this.sizer.bounds; var b2 = new mxRectangle(this.bounds.x + this.bounds.width - b.width / 2, this.bounds.y + this.bounds.height - b.height / 2, b.width, b.height); if (b.x != b2.x || b.y != b2.y || b.width != b2.width || b.height != b2.height) { this.sizer.bounds = b2; // Avoids update of visibility in redraw for VML if (this.sizer.node.style.visibility != 'hidden') { this.sizer.redraw(); } } if (revalidate) { this.outline.view.revalidate(); } } } }; /** * Function: mouseDown * * Handles the event by starting a translation or zoom. */ mxOutline.prototype.mouseDown = function(sender, me) { if (this.enabled && this.showViewport) { var tol = (!mxEvent.isMouseEvent(me.getEvent())) ? this.source.tolerance : 0; var hit = (this.source.allowHandleBoundsCheck && (mxClient.IS_IE || tol > 0)) ? new mxRectangle(me.getGraphX() - tol, me.getGraphY() - tol, 2 * tol, 2 * tol) : null; this.zoom = me.isSource(this.sizer) || (hit != null && mxUtils.intersects(shape.bounds, hit)); this.startX = me.getX(); this.startY = me.getY(); this.active = true; if (this.source.useScrollbarsForPanning && mxUtils.hasScrollbars(this.source.container)) { this.dx0 = this.source.container.scrollLeft; this.dy0 = this.source.container.scrollTop; } else { this.dx0 = 0; this.dy0 = 0; } } me.consume(); }; /** * Function: mouseMove * * Handles the event by previewing the viewrect in and updating the * rectangle that represents the viewrect in the outline. */ mxOutline.prototype.mouseMove = function(sender, me) { if (this.active) { this.selectionBorder.node.style.display = (this.showViewport) ? '' : 'none'; this.sizer.node.style.display = this.selectionBorder.node.style.display; var delta = this.getTranslateForEvent(me); var dx = delta.x; var dy = delta.y; var bounds = null; if (!this.zoom) { // Previews the panning on the source graph var scale = this.outline.getView().scale; bounds = new mxRectangle(this.bounds.x + dx, this.bounds.y + dy, this.bounds.width, this.bounds.height); this.selectionBorder.bounds = bounds; this.selectionBorder.redraw(); dx /= scale; dx *= this.source.getView().scale; dy /= scale; dy *= this.source.getView().scale; this.source.panGraph(-dx - this.dx0, -dy - this.dy0); } else { // Does *not* preview zooming on the source graph var container = this.source.container; var viewRatio = container.clientWidth / container.clientHeight; dy = dx / viewRatio; bounds = new mxRectangle(this.bounds.x, this.bounds.y, Math.max(1, this.bounds.width + dx), Math.max(1, this.bounds.height + dy)); this.selectionBorder.bounds = bounds; this.selectionBorder.redraw(); } // Updates the zoom handle var b = this.sizer.bounds; this.sizer.bounds = new mxRectangle( bounds.x + bounds.width - b.width / 2, bounds.y + bounds.height - b.height / 2, b.width, b.height); // Avoids update of visibility in redraw for VML if (this.sizer.node.style.visibility != 'hidden') { this.sizer.redraw(); } me.consume(); } }; /** * Function: getTranslateForEvent * * Gets the translate for the given mouse event. Here is an example to limit * the outline to stay within positive coordinates: * * (code) * outline.getTranslateForEvent = function(me) * { * var pt = new mxPoint(me.getX() - this.startX, me.getY() - this.startY); * * if (!this.zoom) * { * var tr = this.source.view.translate; * pt.x = Math.max(tr.x * this.outline.view.scale, pt.x); * pt.y = Math.max(tr.y * this.outline.view.scale, pt.y); * } * * return pt; * }; * (end) */ mxOutline.prototype.getTranslateForEvent = function(me) { return new mxPoint(me.getX() - this.startX, me.getY() - this.startY); }; /** * Function: mouseUp * * Handles the event by applying the translation or zoom to . */ mxOutline.prototype.mouseUp = function(sender, me) { if (this.active) { var delta = this.getTranslateForEvent(me); var dx = delta.x; var dy = delta.y; if (Math.abs(dx) > 0 || Math.abs(dy) > 0) { if (!this.zoom) { // Applies the new translation if the source // has no scrollbars if (!this.source.useScrollbarsForPanning || !mxUtils.hasScrollbars(this.source.container)) { this.source.panGraph(0, 0); dx /= this.outline.getView().scale; dy /= this.outline.getView().scale; var t = this.source.getView().translate; this.source.getView().setTranslate(t.x - dx, t.y - dy); } } else { // Applies the new zoom var w = this.selectionBorder.bounds.width; var scale = this.source.getView().scale; this.source.zoomTo(Math.max(this.minScale, scale - (dx * scale) / w), false); } this.update(); me.consume(); } // Resets the state of the handler this.index = null; this.active = false; } }; /** * Function: destroy * * Destroy this outline and removes all listeners from . */ mxOutline.prototype.destroy = function() { if (this.source != null) { this.source.removeListener(this.panHandler); this.source.removeListener(this.refreshHandler); this.source.getModel().removeListener(this.updateHandler); this.source.getView().removeListener(this.updateHandler); mxEvent.removeListener(this.source.container, 'scroll', this.updateHandler); this.source = null; } if (this.outline != null) { this.outline.removeMouseListener(this); this.outline.destroy(); this.outline = null; } if (this.selectionBorder != null) { this.selectionBorder.destroy(); this.selectionBorder = null; } if (this.sizer != null) { this.sizer.destroy(); this.sizer = null; } };