UNPKG

drawio-offline

Version:
1,811 lines (1,549 loc) 145 kB
/** * Copyright (c) 2006-2012, JGraph Ltd */ /** * Constructs a new graph editor */ EditorUi = function(editor, container, lightbox) { mxEventSource.call(this); this.destroyFunctions = []; this.editor = editor || new Editor(); this.container = container || document.body; var graph = this.editor.graph; graph.lightbox = lightbox; this.initialDefaultVertexStyle = mxUtils.clone(graph.defaultVertexStyle); this.initialDefaultEdgeStyle = mxUtils.clone(graph.defaultEdgeStyle); // Faster scrollwheel zoom is possible with CSS transforms if (graph.useCssTransforms) { this.lazyZoomDelay = 0; } // Pre-fetches submenu image or replaces with embedded image if supported if (mxClient.IS_SVG) { mxPopupMenu.prototype.submenuImage = 'data:image/gif;base64,R0lGODlhCQAJAIAAAP///zMzMyH5BAEAAAAALAAAAAAJAAkAAAIPhI8WebHsHopSOVgb26AAADs='; } else { new Image().src = mxPopupMenu.prototype.submenuImage; } // Pre-fetches connect image if (!mxClient.IS_SVG && mxConnectionHandler.prototype.connectImage != null) { new Image().src = mxConnectionHandler.prototype.connectImage.src; } // Disables graph and forced panning in chromeless mode if (this.editor.chromeless && !this.editor.editable) { this.footerHeight = 0; graph.isEnabled = function() { return false; }; graph.panningHandler.isForcePanningEvent = function(me) { return !mxEvent.isPopupTrigger(me.getEvent()); }; } // Creates the user interface this.actions = new Actions(this); this.menus = this.createMenus(); if (!graph.standalone) { // Stores the current style and assigns it to new cells var styles = ['rounded', 'shadow', 'glass', 'dashed', 'dashPattern', 'labelBackgroundColor', 'comic', 'sketch', 'fillWeight', 'hachureGap', 'hachureAngle', 'jiggle', 'disableMultiStroke', 'disableMultiStrokeFill', 'fillStyle', 'curveFitting', 'simplification', 'sketchStyle', 'pointerEvents']; var connectStyles = ['shape', 'edgeStyle', 'curved', 'rounded', 'elbow', 'jumpStyle', 'jumpSize', 'comic', 'sketch', 'fillWeight', 'hachureGap', 'hachureAngle', 'jiggle', 'disableMultiStroke', 'disableMultiStrokeFill', 'fillStyle', 'curveFitting', 'simplification', 'sketchStyle']; // Note: Everything that is not in styles is ignored (styles is augmented below) this.setDefaultStyle = function(cell) { try { var state = graph.view.getState(cell); if (state != null) { // Ignores default styles var clone = cell.clone(); clone.style = '' var defaultStyle = graph.getCellStyle(clone); var values = []; var keys = []; for (var key in state.style) { if (defaultStyle[key] != state.style[key]) { values.push(state.style[key]); keys.push(key); } } // Handles special case for value "none" var cellStyle = graph.getModel().getStyle(state.cell); var tokens = (cellStyle != null) ? cellStyle.split(';') : []; for (var i = 0; i < tokens.length; i++) { var tmp = tokens[i]; var pos = tmp.indexOf('='); if (pos >= 0) { var key = tmp.substring(0, pos); var value = tmp.substring(pos + 1); if (defaultStyle[key] != null && value == 'none') { values.push(value); keys.push(key); } } } // Resets current style if (graph.getModel().isEdge(state.cell)) { graph.currentEdgeStyle = {}; } else { graph.currentVertexStyle = {} } this.fireEvent(new mxEventObject('styleChanged', 'keys', keys, 'values', values, 'cells', [state.cell])); } } catch (e) { this.handleError(e); } }; this.clearDefaultStyle = function() { graph.currentEdgeStyle = mxUtils.clone(graph.defaultEdgeStyle); graph.currentVertexStyle = mxUtils.clone(graph.defaultVertexStyle); // Updates UI this.fireEvent(new mxEventObject('styleChanged', 'keys', [], 'values', [], 'cells', [])); }; // Keys that should be ignored if the cell has a value (known: new default for all cells is html=1 so // for the html key this effecticely only works for edges inserted via the connection handler) var valueStyles = ['fontFamily', 'fontSource', 'fontSize', 'fontColor']; for (var i = 0; i < valueStyles.length; i++) { if (mxUtils.indexOf(styles, valueStyles[i]) < 0) { styles.push(valueStyles[i]); } } // Keys that always update the current edge style regardless of selection var alwaysEdgeStyles = ['edgeStyle', 'startArrow', 'startFill', 'startSize', 'endArrow', 'endFill', 'endSize']; // Keys that are ignored together (if one appears all are ignored) var keyGroups = [['startArrow', 'startFill', 'endArrow', 'endFill'], ['startSize', 'endSize'], ['sourcePerimeterSpacing', 'targetPerimeterSpacing'], ['strokeColor', 'strokeWidth'], ['fillColor', 'gradientColor'], ['align', 'verticalAlign'], ['opacity'], ['html']]; // Adds all keys used above to the styles array for (var i = 0; i < keyGroups.length; i++) { for (var j = 0; j < keyGroups[i].length; j++) { styles.push(keyGroups[i][j]); } } for (var i = 0; i < connectStyles.length; i++) { if (mxUtils.indexOf(styles, connectStyles[i]) < 0) { styles.push(connectStyles[i]); } } // Implements a global current style for edges and vertices that is applied to new cells var insertHandler = function(cells, asText, model, vertexStyle, edgeStyle, applyAll, recurse) { vertexStyle = (vertexStyle != null) ? vertexStyle : graph.currentVertexStyle; edgeStyle = (edgeStyle != null) ? edgeStyle : graph.currentEdgeStyle; model = (model != null) ? model : graph.getModel(); if (recurse) { var temp = []; for (var i = 0; i < cells.length; i++) { temp = temp.concat(model.getDescendants(cells[i])); } cells = temp; } model.beginUpdate(); try { for (var i = 0; i < cells.length; i++) { var cell = cells[i]; var appliedStyles; if (asText) { // Applies only basic text styles appliedStyles = ['fontSize', 'fontFamily', 'fontColor']; } else { // Removes styles defined in the cell style from the styles to be applied var cellStyle = model.getStyle(cell); var tokens = (cellStyle != null) ? cellStyle.split(';') : []; appliedStyles = styles.slice(); for (var j = 0; j < tokens.length; j++) { var tmp = tokens[j]; var pos = tmp.indexOf('='); if (pos >= 0) { var key = tmp.substring(0, pos); var index = mxUtils.indexOf(appliedStyles, key); if (index >= 0) { appliedStyles.splice(index, 1); } // Handles special cases where one defined style ignores other styles for (var k = 0; k < keyGroups.length; k++) { var group = keyGroups[k]; if (mxUtils.indexOf(group, key) >= 0) { for (var l = 0; l < group.length; l++) { var index2 = mxUtils.indexOf(appliedStyles, group[l]); if (index2 >= 0) { appliedStyles.splice(index2, 1); } } } } } } } // Applies the current style to the cell var edge = model.isEdge(cell); var current = (edge) ? edgeStyle : vertexStyle; var newStyle = model.getStyle(cell); for (var j = 0; j < appliedStyles.length; j++) { var key = appliedStyles[j]; var styleValue = current[key]; if (styleValue != null && (key != 'shape' || edge)) { // Special case: Connect styles are not applied here but in the connection handler if (!edge || applyAll || mxUtils.indexOf(connectStyles, key) < 0) { newStyle = mxUtils.setStyle(newStyle, key, styleValue); } } } if (Editor.simpleLabels) { newStyle = mxUtils.setStyle(mxUtils.setStyle( newStyle, 'html', null), 'whiteSpace', null); } model.setStyle(cell, newStyle); } } finally { model.endUpdate(); } }; graph.addListener('cellsInserted', function(sender, evt) { insertHandler(evt.getProperty('cells')); }); graph.addListener('textInserted', function(sender, evt) { insertHandler(evt.getProperty('cells'), true); }); this.insertHandler = insertHandler; this.createDivs(); this.createUi(); this.refresh(); // Disables HTML and text selection var textEditing = mxUtils.bind(this, function(evt) { if (evt == null) { evt = window.event; } return graph.isEditing() || (evt != null && this.isSelectionAllowed(evt)); }); // Disables text selection while not editing and no dialog visible if (this.container == document.body) { this.menubarContainer.onselectstart = textEditing; this.menubarContainer.onmousedown = textEditing; this.toolbarContainer.onselectstart = textEditing; this.toolbarContainer.onmousedown = textEditing; this.diagramContainer.onselectstart = textEditing; this.diagramContainer.onmousedown = textEditing; this.sidebarContainer.onselectstart = textEditing; this.sidebarContainer.onmousedown = textEditing; this.formatContainer.onselectstart = textEditing; this.formatContainer.onmousedown = textEditing; this.footerContainer.onselectstart = textEditing; this.footerContainer.onmousedown = textEditing; if (this.tabContainer != null) { // Mouse down is needed for drag and drop this.tabContainer.onselectstart = textEditing; } } // And uses built-in context menu while editing if (!this.editor.chromeless || this.editor.editable) { // Allows context menu for links in hints var linkHandler = function(evt) { if (evt != null) { var source = mxEvent.getSource(evt); if (source.nodeName == 'A') { while (source != null) { if (source.className == 'geHint') { return true; } source = source.parentNode; } } } return textEditing(evt); }; if (mxClient.IS_IE && (typeof(document.documentMode) === 'undefined' || document.documentMode < 9)) { mxEvent.addListener(this.diagramContainer, 'contextmenu', linkHandler); } else { // Allows browser context menu outside of diagram and sidebar this.diagramContainer.oncontextmenu = linkHandler; } } else { graph.panningHandler.usePopupTrigger = false; } // Contains the main graph instance inside the given panel graph.init(this.diagramContainer); // Improves line wrapping for in-place editor if (mxClient.IS_SVG && graph.view.getDrawPane() != null) { var root = graph.view.getDrawPane().ownerSVGElement; if (root != null) { root.style.position = 'absolute'; } } // Creates hover icons this.hoverIcons = this.createHoverIcons(); // Hides hover icons when cells are moved if (graph.graphHandler != null) { var graphHandlerStart = graph.graphHandler.start; graph.graphHandler.start = function() { if (ui.hoverIcons != null) { ui.hoverIcons.reset(); } graphHandlerStart.apply(this, arguments); }; } // Adds tooltip when mouse is over scrollbars to show space-drag panning option mxEvent.addListener(this.diagramContainer, 'mousemove', mxUtils.bind(this, function(evt) { var off = mxUtils.getOffset(this.diagramContainer); if (mxEvent.getClientX(evt) - off.x - this.diagramContainer.clientWidth > 0 || mxEvent.getClientY(evt) - off.y - this.diagramContainer.clientHeight > 0) { this.diagramContainer.setAttribute('title', mxResources.get('panTooltip')); } else { this.diagramContainer.removeAttribute('title'); } })); // Escape key hides dialogs, adds space+drag panning var spaceKeyPressed = false; // Overrides hovericons to disable while space key is pressed var hoverIconsIsResetEvent = this.hoverIcons.isResetEvent; this.hoverIcons.isResetEvent = function(evt, allowShift) { return spaceKeyPressed || hoverIconsIsResetEvent.apply(this, arguments); }; this.keydownHandler = mxUtils.bind(this, function(evt) { if (evt.which == 32 /* Space */ && !graph.isEditing()) { spaceKeyPressed = true; this.hoverIcons.reset(); graph.container.style.cursor = 'move'; // Disables scroll after space keystroke with scrollbars if (!graph.isEditing() && mxEvent.getSource(evt) == graph.container) { mxEvent.consume(evt); } } else if (!mxEvent.isConsumed(evt) && evt.keyCode == 27 /* Escape */) { this.hideDialog(null, true); } }); mxEvent.addListener(document, 'keydown', this.keydownHandler); this.keyupHandler = mxUtils.bind(this, function(evt) { graph.container.style.cursor = ''; spaceKeyPressed = false; }); mxEvent.addListener(document, 'keyup', this.keyupHandler); // Forces panning for middle and right mouse buttons var panningHandlerIsForcePanningEvent = graph.panningHandler.isForcePanningEvent; graph.panningHandler.isForcePanningEvent = function(me) { // Ctrl+left button is reported as right button in FF on Mac return panningHandlerIsForcePanningEvent.apply(this, arguments) || spaceKeyPressed || (mxEvent.isMouseEvent(me.getEvent()) && (this.usePopupTrigger || !mxEvent.isPopupTrigger(me.getEvent())) && ((!mxEvent.isControlDown(me.getEvent()) && mxEvent.isRightMouseButton(me.getEvent())) || mxEvent.isMiddleMouseButton(me.getEvent()))); }; // Ctrl/Cmd+Enter applies editing value except in Safari where Ctrl+Enter creates // a new line (while Enter creates a new paragraph and Shift+Enter stops) var cellEditorIsStopEditingEvent = graph.cellEditor.isStopEditingEvent; graph.cellEditor.isStopEditingEvent = function(evt) { return cellEditorIsStopEditingEvent.apply(this, arguments) || (evt.keyCode == 13 && ((!mxClient.IS_SF && mxEvent.isControlDown(evt)) || (mxClient.IS_MAC && mxEvent.isMetaDown(evt)) || (mxClient.IS_SF && mxEvent.isShiftDown(evt)))); }; // Adds space+wheel for zoom var graphIsZoomWheelEvent = graph.isZoomWheelEvent; graph.isZoomWheelEvent = function() { return spaceKeyPressed || graphIsZoomWheelEvent.apply(this, arguments); }; // Switches toolbar for text editing var textMode = false; var fontMenu = null; var sizeMenu = null; var nodes = null; var updateToolbar = mxUtils.bind(this, function() { if (this.toolbar != null && textMode != graph.cellEditor.isContentEditing()) { var node = this.toolbar.container.firstChild; var newNodes = []; while (node != null) { var tmp = node.nextSibling; if (mxUtils.indexOf(this.toolbar.staticElements, node) < 0) { node.parentNode.removeChild(node); newNodes.push(node); } node = tmp; } // Saves references to special items var tmp1 = this.toolbar.fontMenu; var tmp2 = this.toolbar.sizeMenu; if (nodes == null) { this.toolbar.createTextToolbar(); } else { for (var i = 0; i < nodes.length; i++) { this.toolbar.container.appendChild(nodes[i]); } // Restores references to special items this.toolbar.fontMenu = fontMenu; this.toolbar.sizeMenu = sizeMenu; } textMode = graph.cellEditor.isContentEditing(); fontMenu = tmp1; sizeMenu = tmp2; nodes = newNodes; } }); var ui = this; // Overrides cell editor to update toolbar var cellEditorStartEditing = graph.cellEditor.startEditing; graph.cellEditor.startEditing = function() { cellEditorStartEditing.apply(this, arguments); updateToolbar(); if (graph.cellEditor.isContentEditing()) { var updating = false; var updateCssHandler = function() { if (!updating) { updating = true; window.setTimeout(function() { var node = graph.getSelectedEditingElement(); if (node != null) { var css = mxUtils.getCurrentStyle(node); if (css != null && ui.toolbar != null) { ui.toolbar.setFontName(Graph.stripQuotes(css.fontFamily)); ui.toolbar.setFontSize(parseInt(css.fontSize)); } } updating = false; }, 0); } }; mxEvent.addListener(graph.cellEditor.textarea, 'input', updateCssHandler) mxEvent.addListener(graph.cellEditor.textarea, 'touchend', updateCssHandler); mxEvent.addListener(graph.cellEditor.textarea, 'mouseup', updateCssHandler); mxEvent.addListener(graph.cellEditor.textarea, 'keyup', updateCssHandler); updateCssHandler(); } }; // Updates toolbar and handles possible errors var cellEditorStopEditing = graph.cellEditor.stopEditing; graph.cellEditor.stopEditing = function(cell, trigger) { try { cellEditorStopEditing.apply(this, arguments); updateToolbar(); } catch (e) { ui.handleError(e); } }; // Enables scrollbars and sets cursor style for the container graph.container.setAttribute('tabindex', '0'); graph.container.style.cursor = 'default'; // Workaround for page scroll if embedded via iframe if (window.self === window.top && graph.container.parentNode != null) { try { graph.container.focus(); } catch (e) { // ignores error in old versions of IE } } // Keeps graph container focused on mouse down var graphFireMouseEvent = graph.fireMouseEvent; graph.fireMouseEvent = function(evtName, me, sender) { if (evtName == mxEvent.MOUSE_DOWN) { this.container.focus(); } graphFireMouseEvent.apply(this, arguments); }; // Configures automatic expand on mouseover graph.popupMenuHandler.autoExpand = true; // Installs context menu if (this.menus != null) { graph.popupMenuHandler.factoryMethod = mxUtils.bind(this, function(menu, cell, evt) { this.menus.createPopupMenu(menu, cell, evt); }); } // Hides context menu mxEvent.addGestureListeners(document, mxUtils.bind(this, function(evt) { graph.popupMenuHandler.hideMenu(); })); // Create handler for key events this.keyHandler = this.createKeyHandler(editor); // Getter for key handler this.getKeyHandler = function() { return keyHandler; }; graph.connectionHandler.addListener(mxEvent.CONNECT, function(sender, evt) { var cells = [evt.getProperty('cell')]; if (evt.getProperty('terminalInserted')) { cells.push(evt.getProperty('terminal')); } insertHandler(cells); }); this.addListener('styleChanged', mxUtils.bind(this, function(sender, evt) { // Checks if edges and/or vertices were modified var cells = evt.getProperty('cells'); var vertex = false; var edge = false; if (cells.length > 0) { for (var i = 0; i < cells.length; i++) { vertex = graph.getModel().isVertex(cells[i]) || vertex; edge = graph.getModel().isEdge(cells[i]) || edge; if (edge && vertex) { break; } } } else { vertex = true; edge = true; } var keys = evt.getProperty('keys'); var values = evt.getProperty('values'); for (var i = 0; i < keys.length; i++) { var common = mxUtils.indexOf(valueStyles, keys[i]) >= 0; // Ignores transparent stroke colors if (keys[i] != 'strokeColor' || (values[i] != null && values[i] != 'none')) { // Special case: Edge style and shape if (mxUtils.indexOf(connectStyles, keys[i]) >= 0) { if (edge || mxUtils.indexOf(alwaysEdgeStyles, keys[i]) >= 0) { if (values[i] == null) { delete graph.currentEdgeStyle[keys[i]]; } else { graph.currentEdgeStyle[keys[i]] = values[i]; } } // Uses style for vertex if defined in styles else if (vertex && mxUtils.indexOf(styles, keys[i]) >= 0) { if (values[i] == null) { delete graph.currentVertexStyle[keys[i]]; } else { graph.currentVertexStyle[keys[i]] = values[i]; } } } else if (mxUtils.indexOf(styles, keys[i]) >= 0) { if (vertex || common) { if (values[i] == null) { delete graph.currentVertexStyle[keys[i]]; } else { graph.currentVertexStyle[keys[i]] = values[i]; } } if (edge || common || mxUtils.indexOf(alwaysEdgeStyles, keys[i]) >= 0) { if (values[i] == null) { delete graph.currentEdgeStyle[keys[i]]; } else { graph.currentEdgeStyle[keys[i]] = values[i]; } } } } } if (this.toolbar != null) { this.toolbar.setFontName(graph.currentVertexStyle['fontFamily'] || Menus.prototype.defaultFont); this.toolbar.setFontSize(graph.currentVertexStyle['fontSize'] || Menus.prototype.defaultFontSize); if (this.toolbar.edgeStyleMenu != null) { // Updates toolbar icon for edge style var edgeStyleDiv = this.toolbar.edgeStyleMenu.getElementsByTagName('div')[0]; if (graph.currentEdgeStyle['edgeStyle'] == 'orthogonalEdgeStyle' && graph.currentEdgeStyle['curved'] == '1') { edgeStyleDiv.className = 'geSprite geSprite-curved'; } else if (graph.currentEdgeStyle['edgeStyle'] == 'straight' || graph.currentEdgeStyle['edgeStyle'] == 'none' || graph.currentEdgeStyle['edgeStyle'] == null) { edgeStyleDiv.className = 'geSprite geSprite-straight'; } else if (graph.currentEdgeStyle['edgeStyle'] == 'entityRelationEdgeStyle') { edgeStyleDiv.className = 'geSprite geSprite-entity'; } else if (graph.currentEdgeStyle['edgeStyle'] == 'elbowEdgeStyle') { edgeStyleDiv.className = 'geSprite geSprite-' + ((graph.currentEdgeStyle['elbow'] == 'vertical') ? 'verticalelbow' : 'horizontalelbow'); } else if (graph.currentEdgeStyle['edgeStyle'] == 'isometricEdgeStyle') { edgeStyleDiv.className = 'geSprite geSprite-' + ((graph.currentEdgeStyle['elbow'] == 'vertical') ? 'verticalisometric' : 'horizontalisometric'); } else { edgeStyleDiv.className = 'geSprite geSprite-orthogonal'; } } if (this.toolbar.edgeShapeMenu != null) { // Updates icon for edge shape var edgeShapeDiv = this.toolbar.edgeShapeMenu.getElementsByTagName('div')[0]; if (graph.currentEdgeStyle['shape'] == 'link') { edgeShapeDiv.className = 'geSprite geSprite-linkedge'; } else if (graph.currentEdgeStyle['shape'] == 'flexArrow') { edgeShapeDiv.className = 'geSprite geSprite-arrow'; } else if (graph.currentEdgeStyle['shape'] == 'arrow') { edgeShapeDiv.className = 'geSprite geSprite-simplearrow'; } else { edgeShapeDiv.className = 'geSprite geSprite-connection'; } } // Updates icon for optinal line start shape if (this.toolbar.lineStartMenu != null) { var lineStartDiv = this.toolbar.lineStartMenu.getElementsByTagName('div')[0]; lineStartDiv.className = this.getCssClassForMarker('start', graph.currentEdgeStyle['shape'], graph.currentEdgeStyle[mxConstants.STYLE_STARTARROW], mxUtils.getValue(graph.currentEdgeStyle, 'startFill', '1')); } // Updates icon for optinal line end shape if (this.toolbar.lineEndMenu != null) { var lineEndDiv = this.toolbar.lineEndMenu.getElementsByTagName('div')[0]; lineEndDiv.className = this.getCssClassForMarker('end', graph.currentEdgeStyle['shape'], graph.currentEdgeStyle[mxConstants.STYLE_ENDARROW], mxUtils.getValue(graph.currentEdgeStyle, 'endFill', '1')); } } })); // Update font size and font family labels if (this.toolbar != null) { var update = mxUtils.bind(this, function() { var ff = graph.currentVertexStyle['fontFamily'] || 'Helvetica'; var fs = String(graph.currentVertexStyle['fontSize'] || '12'); var state = graph.getView().getState(graph.getSelectionCell()); if (state != null) { ff = state.style[mxConstants.STYLE_FONTFAMILY] || ff; fs = state.style[mxConstants.STYLE_FONTSIZE] || fs; if (ff.length > 10) { ff = ff.substring(0, 8) + '...'; } } this.toolbar.setFontName(ff); this.toolbar.setFontSize(fs); }); graph.getSelectionModel().addListener(mxEvent.CHANGE, update); graph.getModel().addListener(mxEvent.CHANGE, update); } // Makes sure the current layer is visible when cells are added graph.addListener(mxEvent.CELLS_ADDED, function(sender, evt) { var cells = evt.getProperty('cells'); var parent = evt.getProperty('parent'); if (graph.getModel().isLayer(parent) && !graph.isCellVisible(parent) && cells != null && cells.length > 0) { graph.getModel().setVisible(parent, true); } }); // Global handler to hide the current menu this.gestureHandler = mxUtils.bind(this, function(evt) { if (this.currentMenu != null && mxEvent.getSource(evt) != this.currentMenu.div) { this.hideCurrentMenu(); } }); mxEvent.addGestureListeners(document, this.gestureHandler); // Updates the editor UI after the window has been resized or the orientation changes // Timeout is workaround for old IE versions which have a delay for DOM client sizes. // Should not use delay > 0 to avoid handle multiple repaints during window resize this.resizeHandler = mxUtils.bind(this, function() { window.setTimeout(mxUtils.bind(this, function() { if (this.editor.graph != null) { this.refresh(); } }), 0); }); mxEvent.addListener(window, 'resize', this.resizeHandler); this.orientationChangeHandler = mxUtils.bind(this, function() { this.refresh(); }); mxEvent.addListener(window, 'orientationchange', this.orientationChangeHandler); // Workaround for bug on iOS see // http://stackoverflow.com/questions/19012135/ios-7-ipad-safari-landscape-innerheight-outerheight-layout-issue if (mxClient.IS_IOS && !window.navigator.standalone) { this.scrollHandler = mxUtils.bind(this, function() { window.scrollTo(0, 0); }); mxEvent.addListener(window, 'scroll', this.scrollHandler); } /** * Sets the initial scrollbar locations after a file was loaded. */ this.editor.addListener('resetGraphView', mxUtils.bind(this, function() { this.resetScrollbars(); })); /** * Repaints the grid. */ this.addListener('gridEnabledChanged', mxUtils.bind(this, function() { graph.view.validateBackground(); })); this.addListener('backgroundColorChanged', mxUtils.bind(this, function() { graph.view.validateBackground(); })); /** * Repaints the grid. */ graph.addListener('gridSizeChanged', mxUtils.bind(this, function() { if (graph.isGridEnabled()) { graph.view.validateBackground(); } })); // Resets UI, updates action and menu states this.editor.resetGraph(); } this.init(); if (!graph.standalone) { this.open(); } }; // Extends mxEventSource mxUtils.extend(EditorUi, mxEventSource); /** * Global config that specifies if the compact UI elements should be used. */ EditorUi.compactUi = true; /** * Specifies the size of the split bar. */ EditorUi.prototype.splitSize = (mxClient.IS_TOUCH || mxClient.IS_POINTER) ? 12 : 8; /** * Specifies the height of the menubar. Default is 30. */ EditorUi.prototype.menubarHeight = 30; /** * Specifies the width of the format panel should be enabled. Default is true. */ EditorUi.prototype.formatEnabled = true; /** * Specifies the width of the format panel. Default is 240. */ EditorUi.prototype.formatWidth = 240; /** * Specifies the height of the toolbar. Default is 38. */ EditorUi.prototype.toolbarHeight = 38; /** * Specifies the height of the footer. Default is 28. */ EditorUi.prototype.footerHeight = 28; /** * Specifies the height of the optional sidebarFooterContainer. Default is 34. */ EditorUi.prototype.sidebarFooterHeight = 34; /** * Specifies the position of the horizontal split bar. Default is 240 or 118 for * screen widths <= 640px. */ EditorUi.prototype.hsplitPosition = (screen.width <= 640) ? 118 : ((urlParams['sidebar-entries'] != 'large') ? 212 : 240); /** * Specifies if animations are allowed in <executeLayout>. Default is true. */ EditorUi.prototype.allowAnimation = true; /** * Default is 2. */ EditorUi.prototype.lightboxMaxFitScale = 2; /** * Default is 4. */ EditorUi.prototype.lightboxVerticalDivider = 4; /** * Specifies if single click on horizontal split should collapse sidebar. Default is false. */ EditorUi.prototype.hsplitClickEnabled = false; /** * Installs the listeners to update the action states. */ EditorUi.prototype.init = function() { var graph = this.editor.graph; if (!graph.standalone) { if (urlParams['shape-picker'] != '0') { this.installShapePicker(); } // Hides tooltips and connection points when scrolling mxEvent.addListener(graph.container, 'scroll', mxUtils.bind(this, function() { graph.tooltipHandler.hide(); if (graph.connectionHandler != null && graph.connectionHandler.constraintHandler != null) { graph.connectionHandler.constraintHandler.reset(); } })); // Hides tooltip on escape graph.addListener(mxEvent.ESCAPE, mxUtils.bind(this, function() { graph.tooltipHandler.hide(); var rb = graph.getRubberband(); if (rb != null) { rb.cancel(); } })); mxEvent.addListener(graph.container, 'keydown', mxUtils.bind(this, function(evt) { this.onKeyDown(evt); })); mxEvent.addListener(graph.container, 'keypress', mxUtils.bind(this, function(evt) { this.onKeyPress(evt); })); // Updates action states this.addUndoListener(); this.addBeforeUnloadListener(); graph.getSelectionModel().addListener(mxEvent.CHANGE, mxUtils.bind(this, function() { this.updateActionStates(); })); graph.getModel().addListener(mxEvent.CHANGE, mxUtils.bind(this, function() { this.updateActionStates(); })); // Changes action states after change of default parent var graphSetDefaultParent = graph.setDefaultParent; var ui = this; this.editor.graph.setDefaultParent = function() { graphSetDefaultParent.apply(this, arguments); ui.updateActionStates(); }; // Hack to make editLink available in vertex handler graph.editLink = ui.actions.get('editLink').funct; this.updateActionStates(); this.initClipboard(); this.initCanvas(); if (this.format != null) { this.format.init(); } } }; /** * Returns true if the given event should start editing. This implementation returns true. */ EditorUi.prototype.installShapePicker = function() { var graph = this.editor.graph; var ui = this; // Uses this event to process mouseDown to check the selection state before it is changed graph.addListener(mxEvent.FIRE_MOUSE_EVENT, mxUtils.bind(this, function(sender, evt) { if (evt.getProperty('eventName') == 'mouseDown') { ui.hideShapePicker(); } })); graph.addListener(mxEvent.ESCAPE, mxUtils.bind(this, function() { ui.hideShapePicker(true); })); graph.getSelectionModel().addListener(mxEvent.CHANGE, mxUtils.bind(this, function() { ui.hideShapePicker(true); })); graph.getModel().addListener(mxEvent.CHANGE, mxUtils.bind(this, function() { ui.hideShapePicker(true); })); // Counts as popup menu var popupMenuHandlerIsMenuShowing = graph.popupMenuHandler.isMenuShowing; graph.popupMenuHandler.isMenuShowing = function() { return popupMenuHandlerIsMenuShowing.apply(this, arguments) || ui.shapePicker != null; }; // Adds dbl click dialog for inserting shapes var graphDblClick = graph.dblClick; graph.dblClick = function(evt, cell) { if (this.isEnabled()) { if (cell == null && ui.sidebar != null && !mxEvent.isShiftDown(evt)) { mxEvent.consume(evt); var pt = mxUtils.convertPoint(this.container, mxEvent.getClientX(evt), mxEvent.getClientY(evt)); // Asynchronous to avoid direct insert after double tap window.setTimeout(mxUtils.bind(this, function() { ui.showShapePicker(pt.x, pt.y); }), 30); } else { graphDblClick.apply(this, arguments); } } }; if (this.hoverIcons != null) { var hoverIconsDrag = this.hoverIcons.drag; this.hoverIcons.drag = function() { ui.hideShapePicker(); hoverIconsDrag.apply(this, arguments); }; var hoverIconsExecute = this.hoverIcons.execute; this.hoverIcons.execute = function(state, dir, me) { var evt = me.getEvent(); if (!this.graph.isCloneEvent(evt) && !mxEvent.isShiftDown(evt)) { this.graph.connectVertex(state.cell, dir, this.graph.defaultEdgeLength, evt, null, null, mxUtils.bind(this, function(x, y, execute) { var temp = graph.getCompositeParent(state.cell); var geo = graph.getCellGeometry(temp); me.consume(); while (temp != null && graph.model.isVertex(temp) && geo != null && geo.relative) { cell = temp; temp = graph.model.getParent(cell) geo = graph.getCellGeometry(temp); } // Asynchronous to avoid direct insert after double tap window.setTimeout(mxUtils.bind(this, function() { ui.showShapePicker(me.getGraphX(), me.getGraphY(), temp, mxUtils.bind(this, function(cell) { execute(cell); }), dir); }), 30); }), mxUtils.bind(this, function(result) { this.graph.selectCellsForConnectVertex(result, evt, this); })); } else { hoverIconsExecute.apply(this, arguments); } }; } }; /** * Creates a temporary graph instance for rendering off-screen content. */ EditorUi.prototype.showShapePicker = function(x, y, source, callback, direction) { var div = this.createShapePicker(x, y, source, callback, direction, mxUtils.bind(this, function() { this.hideShapePicker(); }), this.getCellsForShapePicker(source)); if (div != null) { if (this.hoverIcons != null) { this.hoverIcons.reset(); } var graph = this.editor.graph; graph.popupMenuHandler.hideMenu(); graph.tooltipHandler.hideTooltip(); this.hideCurrentMenu(); this.hideShapePicker(); this.shapePickerCallback = callback; this.shapePicker = div; } }; /** * Creates a temporary graph instance for rendering off-screen content. */ EditorUi.prototype.createShapePicker = function(x, y, source, callback, direction, afterClick, cells) { var div = null; if (cells != null && cells.length > 0) { var ui = this; var graph = this.editor.graph; div = document.createElement('div'); var sourceState = graph.view.getState(source); var style = (source != null && (sourceState == null || !graph.isTransparentState(sourceState))) ? graph.copyStyle(source) : null; // Do not place entry under pointer for touch devices var w = (cells.length < 6) ? cells.length * 35 : 140; div.className = 'geToolbarContainer geSidebarContainer geSidebar'; div.style.cssText = 'position:absolute;left:' + x + 'px;top:' + y + 'px;width:' + w + 'px;border-radius:10px;padding:4px;text-align:center;' + 'box-shadow:0px 0px 3px 1px #d1d1d1;padding: 6px 0 8px 0;'; mxUtils.setPrefixedStyle(div.style, 'transform', 'translate(-22px,-22px)'); if (graph.background != null && graph.background != mxConstants.NONE) { div.style.backgroundColor = graph.background; } graph.container.appendChild(div); var addCell = mxUtils.bind(this, function(cell) { // Wrapper needed to catch events var node = document.createElement('a'); node.className = 'geItem'; node.style.cssText = 'position:relative;display:inline-block;position:relative;' + 'width:30px;height:30px;cursor:pointer;overflow:hidden;padding:3px 0 0 3px;'; div.appendChild(node); if (style != null && urlParams['sketch'] != '1') { this.sidebar.graph.pasteStyle(style, [cell]); } else { ui.insertHandler([cell], cell.value != '' && urlParams['sketch'] != '1', this.sidebar.graph.model); } this.sidebar.createThumb([cell], 25, 25, node, null, true, false, cell.geometry.width, cell.geometry.height); mxEvent.addListener(node, 'click', function() { var clone = graph.cloneCell(cell); if (callback != null) { callback(clone); } else { clone.geometry.x = graph.snap(Math.round(x / graph.view.scale) - graph.view.translate.x - cell.geometry.width / 2); clone.geometry.y = graph.snap(Math.round(y / graph.view.scale) - graph.view.translate.y - cell.geometry.height / 2); graph.model.beginUpdate(); try { graph.addCell(clone); } finally { graph.model.endUpdate(); } graph.setSelectionCell(clone); graph.scrollCellToVisible(clone); graph.startEditingAtCell(clone); if (ui.hoverIcons != null) { ui.hoverIcons.update(graph.view.getState(clone)); } } if (afterClick != null) { afterClick(); } }); }); for (var i = 0; i < cells.length; i++) { addCell(cells[i]); } } return div; }; /** * Creates a temporary graph instance for rendering off-screen content. */ EditorUi.prototype.getCellsForShapePicker = function(cell) { var createVertex = mxUtils.bind(this, function(style, w, h, value) { return this.editor.graph.createVertex(null, null, value || '', 0, 0, w || 120, h || 60, style, false); }); return [(cell != null) ? this.editor.graph.cloneCell(cell) : createVertex('text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;', 40, 20, 'Text'), createVertex('whiteSpace=wrap;html=1;'), createVertex('rounded=1;whiteSpace=wrap;html=1;'), createVertex('ellipse;whiteSpace=wrap;html=1;'), createVertex('rhombus;whiteSpace=wrap;html=1;', 80, 80), createVertex('shape=parallelogram;perimeter=parallelogramPerimeter;whiteSpace=wrap;html=1;fixedSize=1;'), createVertex('shape=trapezoid;perimeter=trapezoidPerimeter;whiteSpace=wrap;html=1;fixedSize=1;', 120, 60), createVertex('shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;', 120, 80), createVertex('shape=step;perimeter=stepPerimeter;whiteSpace=wrap;html=1;fixedSize=1;', 120, 80), createVertex('shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;'), createVertex('triangle;whiteSpace=wrap;html=1;', 60, 80), createVertex('shape=document;whiteSpace=wrap;html=1;boundedLbl=1;', 120, 80), createVertex('shape=tape;whiteSpace=wrap;html=1;', 120, 100), createVertex('ellipse;shape=cloud;whiteSpace=wrap;html=1;', 120, 80), createVertex('shape=cylinder;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;', 60, 80), createVertex('shape=callout;rounded=1;whiteSpace=wrap;html=1;perimeter=calloutPerimeter;', 120, 80), createVertex('shape=doubleArrow;whiteSpace=wrap;html=1;arrowWidth=0.4;arrowSize=0.3;'), createVertex('shape=singleArrow;whiteSpace=wrap;html=1;arrowWidth=0.4;arrowSize=0.4;', 80, 60), createVertex('shape=singleArrow;whiteSpace=wrap;html=1;arrowWidth=0.4;arrowSize=0.4;flipH=1;', 80, 60), createVertex('shape=waypoint;sketch=0;size=6;pointerEvents=1;points=[];fillColor=none;resizable=0;rotatable=0;perimeter=centerPerimeter;snapToPoint=1;', 40, 40)]; }; /** * Creates a temporary graph instance for rendering off-screen content. */ EditorUi.prototype.hideShapePicker = function(cancel) { if (this.shapePicker != null) { this.shapePicker.parentNode.removeChild(this.shapePicker); this.shapePicker = null; if (!cancel && this.shapePickerCallback != null) { this.shapePickerCallback(); } this.shapePickerCallback = null; } }; /** * Returns true if the given event should start editing. This implementation returns true. */ EditorUi.prototype.onKeyDown = function(evt) { var graph = this.editor.graph; // Tab selects next cell, shift+tab while editing inserts tab if (evt.which == 9 && graph.isEnabled() && !mxEvent.isAltDown(evt)) { if (graph.isEditing() && mxEvent.isShiftDown(evt)) { try { var editor = graph.cellEditor.textarea; var doc = editor.ownerDocument.defaultView; var sel = doc.getSelection(); var range = sel.getRangeAt(0); // LATER: Fix normalized tab after editing plain text labels var tabNode = document.createElement('span'); tabNode.style.whiteSpace = 'pre'; tabNode.appendChild(document.createTextNode('\t')); range.insertNode(tabNode); range.setStartAfter(tabNode); range.setEndAfter(tabNode); sel.removeAllRanges(); sel.addRange(range); } catch (e) { // ignore console.error(e); } } else { if (graph.isEditing()) { graph.stopEditing(false); } else { graph.selectCell(!mxEvent.isShiftDown(evt)); } } mxEvent.consume(evt); } }; /** * Returns true if the given event should start editing. This implementation returns true. */ EditorUi.prototype.onKeyPress = function(evt) { var graph = this.editor.graph; // KNOWN: Focus does not work if label is empty in quirks mode if (this.isImmediateEditingEvent(evt) && !graph.isEditing() && !graph.isSelectionEmpty() && evt.which !== 0 && evt.which !== 27 && !mxEvent.isAltDown(evt) && !mxEvent.isControlDown(evt) && !mxEvent.isMetaDown(evt)) { graph.escape(); graph.startEditing(); // Workaround for FF where char is lost if cursor is placed before char if (mxClient.IS_FF) { var ce = graph.cellEditor; if (ce.textarea != null) { ce.textarea.innerHTML = String.fromCharCode(evt.which); // Moves cursor to end of textarea var range = document.createRange(); range.selectNodeContents(ce.textarea); range.collapse(false); var sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); } } } }; /** * Returns true if the given event should start editing. This implementation returns true. */ EditorUi.prototype.isImmediateEditingEvent = function(evt) { return true; }; /** * Private helper method. */ EditorUi.prototype.getCssClassForMarker = function(prefix, shape, marker, fill) { var result = ''; if (shape == 'flexArrow') { result = (marker != null && marker != mxConstants.NONE) ? 'geSprite geSprite-' + prefix + 'blocktrans' : 'geSprite geSprite-noarrow'; } else { // SVG marker sprites if (marker == 'box' || marker == 'halfCircle') { result = 'geSprite geSvgSprite geSprite-' + marker + ((prefix == 'end') ? ' geFlipSprite' : ''); } else if (marker == mxConstants.ARROW_CLASSIC) { result = (fill == '1') ? 'geSprite geSprite-' + prefix + 'classic' : 'geSprite geSprite-' + prefix + 'classictrans'; } else if (marker == mxConstants.ARROW_CLASSIC_THIN) { result = (fill == '1') ? 'geSprite geSprite-' + prefix + 'classicthin' : 'geSprite geSprite-' + prefix + 'classicthintrans'; } else if (marker == mxConstants.ARROW_OPEN) { result = 'geSprite geSprite-' + prefix + 'open'; } else if (marker == mxConstants.ARROW_OPEN_THIN) { result = 'geSprite geSprite-' + prefix + 'openthin'; } else if (marker == mxConstants.ARROW_BLOCK) { result = (fill == '1') ? 'geSprite geSprite-' + prefix + 'block' : 'geSprite geSprite-' + prefix + 'blocktrans'; } else if (marker == mxConstants.ARROW_BLOCK_THIN) { result = (fill == '1') ? 'geSprite geSprite-' + prefix + 'blockthin' : 'geSprite geSprite-' + prefix + 'blockthintrans'; } else if (marker == mxConstants.ARROW_OVAL) { result = (fill == '1') ? 'geSprite geSprite-' + prefix + 'oval' : 'geSprite geSprite-' + prefix + 'ovaltrans'; } else if (marker == mxConstants.ARROW_DIAMOND) { result = (fill == '1') ? 'geSprite geSprite-' + prefix + 'diamond' : 'geSprite geSprite-' + prefix + 'diamondtrans'; } else if (marker == mxConstants.ARROW_DIAMOND_THIN) { result = (fill == '1') ? 'geSprite geSprite-' + prefix + 'thindiamond' : 'geSprite geSprite-' + prefix + 'thindiamondtrans'; } else if (marker == 'openAsync') { result = 'geSprite geSprite-' + prefix + 'openasync'; } else if (marker == 'dash') { result = 'geSprite geSprite-' + prefix + 'dash'; } else if (marker == 'cross') { result = 'geSprite geSprite-' + prefix + 'cross'; } else if (marker == 'async') { result = (fill == '1') ? 'geSprite geSprite-' + prefix + 'async' : 'geSprite geSprite-' + prefix + 'asynctrans'; } else if (marker == 'circle' || marker == 'circlePlus') { result = (fill == '1' || marker == 'circle') ? 'geSprite geSprite-' + prefix + 'circle' : 'geSprite geSprite-' + prefix + 'circleplus'; } else if (marker == 'ERone') { result = 'geSprite geSprite-' + prefix + 'erone'; } else if (marker == 'ERmandOne') { result = 'geSprite geSprite-' + prefix + 'eronetoone'; } else if (marker == 'ERmany') { result = 'geSprite geSprite-' + prefix + 'ermany'; } else if (marker == 'ERoneToMany') { result = 'geSprite geSprite-' + prefix + 'eronetomany'; } else if (marker == 'ERzeroToOne') { result = 'geSprite geSprite-' + prefix + 'eroneopt'; } else if (marker == 'ERzeroToMany') { result = 'geSprite geSprite-' + prefix + 'ermanyopt'; } else { result = 'geSprite geSprite-noarrow'; } } return result; }; /** * Overridden in Menus.js */ EditorUi.prototype.createMenus = function() { return null; }; /** * Hook for allowing selection and context menu for certain events. */ EditorUi.prototype.updatePasteActionStates = function() { var graph = this.editor.graph; var paste = this.actions.get('paste'); var pasteHere = this.actions.get('pasteHere'); paste.setEnabled(this.editor.graph.cellEditor.isContentEditing() || (((!mxClient.IS_FF && navigator.clipboard != null) || !mxClipboard.isEmpty()) && graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent()))); pasteHere.setEnabled(paste.isEnabled()); }; /** * Hook for allowing selection and context menu for certain events. */ EditorUi.prototype.initClipboard = function() { var ui = this; var mxClipboardCut = mxClipboard.cut; mxClipboard.cut = function(graph) { if (graph.cellEditor.isContentEditing()) { document.execCommand('cut', false, null); } else { mxClipboardCut.apply(this, arguments); } ui.updatePasteActionStates(); }; var mxClipboardCopy = mxClipboard.copy; mxClipboard.copy = function(graph) { var result = null; if (graph.cellEditor.isContentEditing()) { document.execCommand('copy', false, null); } else { result = result || graph.getSelectionCells(); result = graph.getExportableCells(graph.model.getTopmostCells(result)); var cloneMap = new Object(); var lookup = graph.createCellLookup(result); var clones = graph.cloneCells(result, null, cloneMap); // Uses temporary model to force new IDs to be assigned // to avoid having to carry over the mapping from object // ID to cell ID to the paste operation var model = new mxGraphModel(); var parent = model.getChildAt(model.getRoot(), 0); for (var i = 0; i < clones.length; i++) { model.add(parent, clones[i]); // Checks for orphaned relative children and makes absolute var state = graph.view.getState(result[i]); if (state != null) { var geo = graph.getCellGeometry(clones[i]); if (geo != null && geo.relative && !model.isEdge(result[i]) && lookup[mxObjectIdentity.get(model.getParent(result[i]))] == null) { geo.offset = null; geo.relative = false; geo.x = state.x / state.view.scale - state.view.translate.x; geo.y = state.y / state.view.scale - state.view.translate.y; } } } graph.updateCustomLinks(graph.createCellMapping(cloneMap, lookup), clones); mxClipboard.insertCount = 1; mxClipboard.setCells(clones); } ui.updatePasteActionStates(); return result; }; var mxClipboardPaste = mxClipboard.paste; mxClipboard.paste = function(graph) { var result = null; if (graph.cellEditor.isContentEditing()) { document.execCommand('paste', false, null); } else { result = mxClipboardPaste.apply(this, arguments); } ui.updatePasteActionStates(); return result; }; // Overrides cell editor to update paste action state var cellEditorStartEditing = this.editor.graph.cellEditor.startEditing; this.editor.graph.cellEditor.startEditing = function() { cellEditorStartEditing.apply(this, arguments); ui.updatePasteActionStates(); }; var cellEditorStopEditing = this.editor.graph.cellEditor.stopEditing; this.editor.graph.cellEditor.stopEditing = function(cell, trigger) { cellEditorStopEditing.apply(this, arguments); ui.updatePasteActionStates(); }; this.updatePasteActionStates(); }; /** * Delay between zoom steps when not using preview. */ EditorUi.prototype.lazyZoomDelay = 20; /** * Delay before update of DOM when using preview. */ EditorUi.prototype.wheelZoomDelay = 400; /** * Delay before update of DOM when using preview.