diff options
Diffstat (limited to 'src/js')
138 files changed, 79732 insertions, 0 deletions
diff --git a/src/js/editor/mxDefaultKeyHandler.js b/src/js/editor/mxDefaultKeyHandler.js new file mode 100644 index 0000000..3814e5e --- /dev/null +++ b/src/js/editor/mxDefaultKeyHandler.js @@ -0,0 +1,126 @@ +/** + * $Id: mxDefaultKeyHandler.js,v 1.26 2010-01-02 09:45:15 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxDefaultKeyHandler + * + * Binds keycodes to actionnames in an editor. This aggregates an internal + * <handler> and extends the implementation of <mxKeyHandler.escape> to not + * only cancel the editing, but also hide the properties dialog and fire an + * <mxEditor.escape> event via <editor>. An instance of this class is created + * by <mxEditor> and stored in <mxEditor.keyHandler>. + * + * Example: + * + * Bind the delete key to the delete action in an existing editor. + * + * (code) + * var keyHandler = new mxDefaultKeyHandler(editor); + * keyHandler.bindAction(46, 'delete'); + * (end) + * + * Codec: + * + * This class uses the <mxDefaultKeyHandlerCodec> to read configuration + * data into an existing instance. See <mxDefaultKeyHandlerCodec> for a + * description of the configuration format. + * + * Keycodes: + * + * See <mxKeyHandler>. + * + * An <mxEvent.ESCAPE> event is fired via the editor if the escape key is + * pressed. + * + * Constructor: mxDefaultKeyHandler + * + * Constructs a new default key handler for the <mxEditor.graph> in the + * given <mxEditor>. (The editor may be null if a prototypical instance for + * a <mxDefaultKeyHandlerCodec> is created.) + * + * Parameters: + * + * editor - Reference to the enclosing <mxEditor>. + */ +function mxDefaultKeyHandler(editor) +{ + if (editor != null) + { + this.editor = editor; + this.handler = new mxKeyHandler(editor.graph); + + // Extends the escape function of the internal key + // handle to hide the properties dialog and fire + // the escape event via the editor instance + var old = this.handler.escape; + + this.handler.escape = function(evt) + { + old.apply(this, arguments); + editor.hideProperties(); + editor.fireEvent(new mxEventObject(mxEvent.ESCAPE, 'event', evt)); + }; + } +}; + +/** + * Variable: editor + * + * Reference to the enclosing <mxEditor>. + */ +mxDefaultKeyHandler.prototype.editor = null; + +/** + * Variable: handler + * + * Holds the <mxKeyHandler> for key event handling. + */ +mxDefaultKeyHandler.prototype.handler = null; + +/** + * Function: bindAction + * + * Binds the specified keycode to the given action in <editor>. The + * optional control flag specifies if the control key must be pressed + * to trigger the action. + * + * Parameters: + * + * code - Integer that specifies the keycode. + * action - Name of the action to execute in <editor>. + * control - Optional boolean that specifies if control must be pressed. + * Default is false. + */ +mxDefaultKeyHandler.prototype.bindAction = function (code, action, control) +{ + var keyHandler = mxUtils.bind(this, function() + { + this.editor.execute(action); + }); + + // Binds the function to control-down keycode + if (control) + { + this.handler.bindControlKey(code, keyHandler); + } + + // Binds the function to the normal keycode + else + { + this.handler.bindKey(code, keyHandler); + } +}; + +/** + * Function: destroy + * + * Destroys the <handler> associated with this object. This does normally + * not need to be called, the <handler> is destroyed automatically when the + * window unloads (in IE) by <mxEditor>. + */ +mxDefaultKeyHandler.prototype.destroy = function () +{ + this.handler.destroy(); + this.handler = null; +}; diff --git a/src/js/editor/mxDefaultPopupMenu.js b/src/js/editor/mxDefaultPopupMenu.js new file mode 100644 index 0000000..01c65b5 --- /dev/null +++ b/src/js/editor/mxDefaultPopupMenu.js @@ -0,0 +1,300 @@ +/** + * $Id: mxDefaultPopupMenu.js,v 1.29 2012-07-03 06:30:25 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxDefaultPopupMenu + * + * Creates popupmenus for mouse events. This object holds an XML node + * which is a description of the popup menu to be created. In + * <createMenu>, the configuration is applied to the context and + * the resulting menu items are added to the menu dynamically. See + * <createMenu> for a description of the configuration format. + * + * This class does not create the DOM nodes required for the popup menu, it + * only parses an XML description to invoke the respective methods on an + * <mxPopupMenu> each time the menu is displayed. + * + * Codec: + * + * This class uses the <mxDefaultPopupMenuCodec> to read configuration + * data into an existing instance, however, the actual parsing is done + * by this class during program execution, so the format is described + * below. + * + * Constructor: mxDefaultPopupMenu + * + * Constructs a new popupmenu-factory based on given configuration. + * + * Paramaters: + * + * config - XML node that contains the configuration data. + */ +function mxDefaultPopupMenu(config) +{ + this.config = config; +}; + +/** + * Variable: imageBasePath + * + * Base path for all icon attributes in the config. Default is null. + */ +mxDefaultPopupMenu.prototype.imageBasePath = null; + +/** + * Variable: config + * + * XML node used as the description of new menu items. This node is + * used in <createMenu> to dynamically create the menu items if their + * respective conditions evaluate to true for the given arguments. + */ +mxDefaultPopupMenu.prototype.config = null; + +/** + * Function: createMenu + * + * This function is called from <mxEditor> to add items to the + * given menu based on <config>. The config is a sequence of + * the following nodes and attributes. + * + * Child Nodes: + * + * add - Adds a new menu item. See below for attributes. + * separator - Adds a separator. No attributes. + * condition - Adds a custom condition. Name attribute. + * + * The add-node may have a child node that defines a function to be invoked + * before the action is executed (or instead of an action to be executed). + * + * Attributes: + * + * as - Resource key for the label (needs entry in property file). + * action - Name of the action to execute in enclosing editor. + * icon - Optional icon (relative/absolute URL). + * iconCls - Optional CSS class for the icon. + * if - Optional name of condition that must be true(see below). + * name - Name of custom condition. Only for condition nodes. + * + * Conditions: + * + * nocell - No cell under the mouse. + * ncells - More than one cell selected. + * notRoot - Drilling position is other than home. + * cell - Cell under the mouse. + * notEmpty - Exactly one cell with children under mouse. + * expandable - Exactly one expandable cell under mouse. + * collapsable - Exactly one collapsable cell under mouse. + * validRoot - Exactly one cell which is a possible root under mouse. + * swimlane - Exactly one cell which is a swimlane under mouse. + * + * Example: + * + * To add a new item for a given action to the popupmenu: + * + * (code) + * <mxDefaultPopupMenu as="popupHandler"> + * <add as="delete" action="delete" icon="images/delete.gif" if="cell"/> + * </mxDefaultPopupMenu> + * (end) + * + * To add a new item for a custom function: + * + * (code) + * <mxDefaultPopupMenu as="popupHandler"> + * <add as="action1"><![CDATA[ + * function (editor, cell, evt) + * { + * editor.execute('action1', cell, 'myArg'); + * } + * ]]></add> + * </mxDefaultPopupMenu> + * (end) + * + * The above example invokes action1 with an additional third argument via + * the editor instance. The third argument is passed to the function that + * defines action1. If the add-node has no action-attribute, then only the + * function defined in the text content is executed, otherwise first the + * function and then the action defined in the action-attribute is + * executed. The function in the text content has 3 arguments, namely the + * <mxEditor> instance, the <mxCell> instance under the mouse, and the + * native mouse event. + * + * Custom Conditions: + * + * To add a new condition for popupmenu items: + * + * (code) + * <condition name="condition1"><![CDATA[ + * function (editor, cell, evt) + * { + * return cell != null; + * } + * ]]></condition> + * (end) + * + * The new condition can then be used in any item as follows: + * + * (code) + * <add as="action1" action="action1" icon="action1.gif" if="condition1"/> + * (end) + * + * The order in which the items and conditions appear is not significant as + * all connditions are evaluated before any items are created. + * + * Parameters: + * + * editor - Enclosing <mxEditor> instance. + * menu - <mxPopupMenu> that is used for adding items and separators. + * cell - Optional <mxCell> which is under the mousepointer. + * evt - Optional mouse event which triggered the menu. + */ +mxDefaultPopupMenu.prototype.createMenu = function(editor, menu, cell, evt) +{ + if (this.config != null) + { + var conditions = this.createConditions(editor, cell, evt); + var item = this.config.firstChild; + + this.addItems(editor, menu, cell, evt, conditions, item, null); + } +}; + +/** + * Function: addItems + * + * Recursively adds the given items and all of its children into the given menu. + * + * Parameters: + * + * editor - Enclosing <mxEditor> instance. + * menu - <mxPopupMenu> that is used for adding items and separators. + * cell - Optional <mxCell> which is under the mousepointer. + * evt - Optional mouse event which triggered the menu. + * conditions - Array of names boolean conditions. + * item - XML node that represents the current menu item. + * parent - DOM node that represents the parent menu item. + */ +mxDefaultPopupMenu.prototype.addItems = function(editor, menu, cell, evt, conditions, item, parent) +{ + var addSeparator = false; + + while (item != null) + { + if (item.nodeName == 'add') + { + var condition = item.getAttribute('if'); + + if (condition == null || conditions[condition]) + { + var as = item.getAttribute('as'); + as = mxResources.get(as) || as; + var funct = mxUtils.eval(mxUtils.getTextContent(item)); + var action = item.getAttribute('action'); + var icon = item.getAttribute('icon'); + var iconCls = item.getAttribute('iconCls'); + + if (addSeparator) + { + menu.addSeparator(parent); + addSeparator = false; + } + + if (icon != null && this.imageBasePath) + { + icon = this.imageBasePath + icon; + } + + var row = this.addAction(menu, editor, as, icon, funct, action, cell, parent, iconCls); + this.addItems(editor, menu, cell, evt, conditions, item.firstChild, row); + } + } + else if (item.nodeName == 'separator') + { + addSeparator = true; + } + + item = item.nextSibling; + } +}; + +/** + * Function: addAction + * + * Helper method to bind an action to a new menu item. + * + * Parameters: + * + * menu - <mxPopupMenu> that is used for adding items and separators. + * editor - Enclosing <mxEditor> instance. + * lab - String that represents the label of the menu item. + * icon - Optional URL that represents the icon of the menu item. + * action - Optional name of the action to execute in the given editor. + * funct - Optional function to execute before the optional action. The + * function takes an <mxEditor>, the <mxCell> under the mouse and the + * mouse event that triggered the call. + * cell - Optional <mxCell> to use as an argument for the action. + * parent - DOM node that represents the parent menu item. + * iconCls - Optional CSS class for the menu icon. + */ +mxDefaultPopupMenu.prototype.addAction = function(menu, editor, lab, icon, funct, action, cell, parent, iconCls) +{ + var clickHandler = function(evt) + { + if (typeof(funct) == 'function') + { + funct.call(editor, editor, cell, evt); + } + + if (action != null) + { + editor.execute(action, cell, evt); + } + }; + + return menu.addItem(lab, icon, clickHandler, parent, iconCls); +}; + +/** + * Function: createConditions + * + * Evaluates the default conditions for the given context. + */ +mxDefaultPopupMenu.prototype.createConditions = function(editor, cell, evt) +{ + // Creates array with conditions + var model = editor.graph.getModel(); + var childCount = model.getChildCount(cell); + + // Adds some frequently used conditions + var conditions = []; + conditions['nocell'] = cell == null; + conditions['ncells'] = editor.graph.getSelectionCount() > 1; + conditions['notRoot'] = model.getRoot() != + model.getParent(editor.graph.getDefaultParent()); + conditions['cell'] = cell != null; + + var isCell = cell != null && editor.graph.getSelectionCount() == 1; + conditions['nonEmpty'] = isCell && childCount > 0; + conditions['expandable'] = isCell && editor.graph.isCellFoldable(cell, false); + conditions['collapsable'] = isCell && editor.graph.isCellFoldable(cell, true); + conditions['validRoot'] = isCell && editor.graph.isValidRoot(cell); + conditions['emptyValidRoot'] = conditions['validRoot'] && childCount == 0; + conditions['swimlane'] = isCell && editor.graph.isSwimlane(cell); + + // Evaluates dynamic conditions from config file + var condNodes = this.config.getElementsByTagName('condition'); + + for (var i=0; i<condNodes.length; i++) + { + var funct = mxUtils.eval(mxUtils.getTextContent(condNodes[i])); + var name = condNodes[i].getAttribute('name'); + + if (name != null && typeof(funct) == 'function') + { + conditions[name] = funct(editor, cell, evt); + } + } + + return conditions; +}; diff --git a/src/js/editor/mxDefaultToolbar.js b/src/js/editor/mxDefaultToolbar.js new file mode 100644 index 0000000..3f8f901 --- /dev/null +++ b/src/js/editor/mxDefaultToolbar.js @@ -0,0 +1,567 @@ +/** + * $Id: mxDefaultToolbar.js,v 1.67 2012-04-11 07:00:52 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxDefaultToolbar + * + * Toolbar for the editor. This modifies the state of the graph + * or inserts new cells upon mouse clicks. + * + * Example: + * + * Create a toolbar with a button to copy the selection into the clipboard, + * and a combo box with one action to paste the selection from the clipboard + * into the graph. + * + * (code) + * var toolbar = new mxDefaultToolbar(container, editor); + * toolbar.addItem('Copy', null, 'copy'); + * + * var combo = toolbar.addActionCombo('More actions...'); + * toolbar.addActionOption(combo, 'Paste', 'paste'); + * (end) + * + * Codec: + * + * This class uses the <mxDefaultToolbarCodec> to read configuration + * data into an existing instance. See <mxDefaultToolbarCodec> for a + * description of the configuration format. + * + * Constructor: mxDefaultToolbar + * + * Constructs a new toolbar for the given container and editor. The + * container and editor may be null if a prototypical instance for a + * <mxDefaultKeyHandlerCodec> is created. + * + * Parameters: + * + * container - DOM node that contains the toolbar. + * editor - Reference to the enclosing <mxEditor>. + */ +function mxDefaultToolbar(container, editor) +{ + this.editor = editor; + + if (container != null && + editor != null) + { + this.init(container); + } +}; + +/** + * Variable: editor + * + * Reference to the enclosing <mxEditor>. + */ +mxDefaultToolbar.prototype.editor = null; + +/** + * Variable: toolbar + * + * Holds the internal <mxToolbar>. + */ +mxDefaultToolbar.prototype.toolbar = null; + +/** + * Variable: resetHandler + * + * Reference to the function used to reset the <toolbar>. + */ +mxDefaultToolbar.prototype.resetHandler = null; + +/** + * Variable: spacing + * + * Defines the spacing between existing and new vertices in + * gridSize units when a new vertex is dropped on an existing + * cell. Default is 4 (40 pixels). + */ +mxDefaultToolbar.prototype.spacing = 4; + +/** + * Variable: connectOnDrop + * + * Specifies if elements should be connected if new cells are dropped onto + * connectable elements. Default is false. + */ +mxDefaultToolbar.prototype.connectOnDrop = false; + +/** + * Variable: init + * + * Constructs the <toolbar> for the given container and installs a listener + * that updates the <mxEditor.insertFunction> on <editor> if an item is + * selected in the toolbar. This assumes that <editor> is not null. + * + * Parameters: + * + * container - DOM node that contains the toolbar. + */ +mxDefaultToolbar.prototype.init = function(container) +{ + if (container != null) + { + this.toolbar = new mxToolbar(container); + + // Installs the insert function in the editor if an item is + // selected in the toolbar + this.toolbar.addListener(mxEvent.SELECT, + mxUtils.bind(this, function(sender, evt) + { + var funct = evt.getProperty('function'); + + if (funct != null) + { + this.editor.insertFunction = mxUtils.bind(this, function() + { + funct.apply(this, arguments); + this.toolbar.resetMode(); + }); + } + else + { + this.editor.insertFunction = null; + } + }) + ); + + // Resets the selected tool after a doubleclick or escape keystroke + this.resetHandler = mxUtils.bind(this, function() + { + if (this.toolbar != null) + { + this.toolbar.resetMode(true); + } + }); + + this.editor.graph.addListener(mxEvent.DOUBLE_CLICK, this.resetHandler); + this.editor.addListener(mxEvent.ESCAPE, this.resetHandler); + } +}; + +/** + * Function: addItem + * + * Adds a new item that executes the given action in <editor>. The title, + * icon and pressedIcon are used to display the toolbar item. + * + * Parameters: + * + * title - String that represents the title (tooltip) for the item. + * icon - URL of the icon to be used for displaying the item. + * action - Name of the action to execute when the item is clicked. + * pressed - Optional URL of the icon for the pressed state. + */ +mxDefaultToolbar.prototype.addItem = function(title, icon, action, pressed) +{ + var clickHandler = mxUtils.bind(this, function() + { + if (action != null && action.length > 0) + { + this.editor.execute(action); + } + }); + + return this.toolbar.addItem(title, icon, clickHandler, pressed); +}; + +/** + * Function: addSeparator + * + * Adds a vertical separator using the optional icon. + * + * Parameters: + * + * icon - Optional URL of the icon that represents the vertical separator. + * Default is <mxClient.imageBasePath> + '/separator.gif'. + */ +mxDefaultToolbar.prototype.addSeparator = function(icon) +{ + icon = icon || mxClient.imageBasePath + '/separator.gif'; + this.toolbar.addSeparator(icon); +}; + +/** + * Function: addCombo + * + * Helper method to invoke <mxToolbar.addCombo> on <toolbar> and return the + * resulting DOM node. + */ +mxDefaultToolbar.prototype.addCombo = function() +{ + return this.toolbar.addCombo(); +}; + +/** + * Function: addActionCombo + * + * Helper method to invoke <mxToolbar.addActionCombo> on <toolbar> using + * the given title and return the resulting DOM node. + * + * Parameters: + * + * title - String that represents the title of the combo. + */ +mxDefaultToolbar.prototype.addActionCombo = function(title) +{ + return this.toolbar.addActionCombo(title); +}; + +/** + * Function: addActionOption + * + * Binds the given action to a option with the specified label in the + * given combo. Combo is an object returned from an earlier call to + * <addCombo> or <addActionCombo>. + * + * Parameters: + * + * combo - DOM node that represents the combo box. + * title - String that represents the title of the combo. + * action - Name of the action to execute in <editor>. + */ +mxDefaultToolbar.prototype.addActionOption = function(combo, title, action) +{ + var clickHandler = mxUtils.bind(this, function() + { + this.editor.execute(action); + }); + + this.addOption(combo, title, clickHandler); +}; + +/** + * Function: addOption + * + * Helper method to invoke <mxToolbar.addOption> on <toolbar> and return + * the resulting DOM node that represents the option. + * + * Parameters: + * + * combo - DOM node that represents the combo box. + * title - String that represents the title of the combo. + * value - Object that represents the value of the option. + */ +mxDefaultToolbar.prototype.addOption = function(combo, title, value) +{ + return this.toolbar.addOption(combo, title, value); +}; + +/** + * Function: addMode + * + * Creates an item for selecting the given mode in the <editor>'s graph. + * Supported modenames are select, connect and pan. + * + * Parameters: + * + * title - String that represents the title of the item. + * icon - URL of the icon that represents the item. + * mode - String that represents the mode name to be used in + * <mxEditor.setMode>. + * pressed - Optional URL of the icon that represents the pressed state. + * funct - Optional JavaScript function that takes the <mxEditor> as the + * first and only argument that is executed after the mode has been + * selected. + */ +mxDefaultToolbar.prototype.addMode = function(title, icon, mode, pressed, funct) +{ + var clickHandler = mxUtils.bind(this, function() + { + this.editor.setMode(mode); + + if (funct != null) + { + funct(this.editor); + } + }); + + return this.toolbar.addSwitchMode(title, icon, clickHandler, pressed); +}; + +/** + * Function: addPrototype + * + * Creates an item for inserting a clone of the specified prototype cell into + * the <editor>'s graph. The ptype may either be a cell or a function that + * returns a cell. + * + * Parameters: + * + * title - String that represents the title of the item. + * icon - URL of the icon that represents the item. + * ptype - Function or object that represents the prototype cell. If ptype + * is a function then it is invoked with no arguments to create new + * instances. + * pressed - Optional URL of the icon that represents the pressed state. + * insert - Optional JavaScript function that handles an insert of the new + * cell. This function takes the <mxEditor>, new cell to be inserted, mouse + * event and optional <mxCell> under the mouse pointer as arguments. + * toggle - Optional boolean that specifies if the item can be toggled. + * Default is true. + */ +mxDefaultToolbar.prototype.addPrototype = function(title, icon, ptype, pressed, insert, toggle) +{ + // Creates a wrapper function that is in charge of constructing + // the new cell instance to be inserted into the graph + var factory = function() + { + if (typeof(ptype) == 'function') + { + return ptype(); + } + else if (ptype != null) + { + return ptype.clone(); + } + + return null; + }; + + // Defines the function for a click event on the graph + // after this item has been selected in the toolbar + var clickHandler = mxUtils.bind(this, function(evt, cell) + { + if (typeof(insert) == 'function') + { + insert(this.editor, factory(), evt, cell); + } + else + { + this.drop(factory(), evt, cell); + } + + this.toolbar.resetMode(); + mxEvent.consume(evt); + }); + + var img = this.toolbar.addMode(title, icon, clickHandler, pressed, null, toggle); + + // Creates a wrapper function that calls the click handler without + // the graph argument + var dropHandler = function(graph, evt, cell) + { + clickHandler(evt, cell); + }; + + this.installDropHandler(img, dropHandler); + + return img; +}; + +/** + * Function: drop + * + * Handles a drop from a toolbar item to the graph. The given vertex + * represents the new cell to be inserted. This invokes <insert> or + * <connect> depending on the given target cell. + * + * Parameters: + * + * vertex - <mxCell> to be inserted. + * evt - Mouse event that represents the drop. + * target - Optional <mxCell> that represents the drop target. + */ +mxDefaultToolbar.prototype.drop = function(vertex, evt, target) +{ + var graph = this.editor.graph; + var model = graph.getModel(); + + if (target == null || + model.isEdge(target) || + !this.connectOnDrop || + !graph.isCellConnectable(target)) + { + while (target != null && + !graph.isValidDropTarget(target, [vertex], evt)) + { + target = model.getParent(target); + } + + this.insert(vertex, evt, target); + } + else + { + this.connect(vertex, evt, target); + } +}; + +/** + * Function: insert + * + * Handles a drop by inserting the given vertex into the given parent cell + * or the default parent if no parent is specified. + * + * Parameters: + * + * vertex - <mxCell> to be inserted. + * evt - Mouse event that represents the drop. + * parent - Optional <mxCell> that represents the parent. + */ +mxDefaultToolbar.prototype.insert = function(vertex, evt, target) +{ + var graph = this.editor.graph; + + if (graph.canImportCell(vertex)) + { + var x = mxEvent.getClientX(evt); + var y = mxEvent.getClientY(evt); + var pt = mxUtils.convertPoint(graph.container, x, y); + + // Splits the target edge or inserts into target group + if (graph.isSplitEnabled() && + graph.isSplitTarget(target, [vertex], evt)) + { + return graph.splitEdge(target, [vertex], null, pt.x, pt.y); + } + else + { + return this.editor.addVertex(target, vertex, pt.x, pt.y); + } + } + + return null; +}; + +/** + * Function: connect + * + * Handles a drop by connecting the given vertex to the given source cell. + * + * vertex - <mxCell> to be inserted. + * evt - Mouse event that represents the drop. + * source - Optional <mxCell> that represents the source terminal. + */ +mxDefaultToolbar.prototype.connect = function(vertex, evt, source) +{ + var graph = this.editor.graph; + var model = graph.getModel(); + + if (source != null && + graph.isCellConnectable(vertex) && + graph.isEdgeValid(null, source, vertex)) + { + var edge = null; + + model.beginUpdate(); + try + { + var geo = model.getGeometry(source); + var g = model.getGeometry(vertex).clone(); + + // Moves the vertex away from the drop target that will + // be used as the source for the new connection + g.x = geo.x + (geo.width - g.width) / 2; + g.y = geo.y + (geo.height - g.height) / 2; + + var step = this.spacing * graph.gridSize; + var dist = model.getDirectedEdgeCount(source, true) * 20; + + if (this.editor.horizontalFlow) + { + g.x += (g.width + geo.width) / 2 + step + dist; + } + else + { + g.y += (g.height + geo.height) / 2 + step + dist; + } + + vertex.setGeometry(g); + + // Fires two add-events with the code below - should be fixed + // to only fire one add event for both inserts + var parent = model.getParent(source); + graph.addCell(vertex, parent); + graph.constrainChild(vertex); + + // Creates the edge using the editor instance and calls + // the second function that fires an add event + edge = this.editor.createEdge(source, vertex); + + if (model.getGeometry(edge) == null) + { + var edgeGeometry = new mxGeometry(); + edgeGeometry.relative = true; + + model.setGeometry(edge, edgeGeometry); + } + + graph.addEdge(edge, parent, source, vertex); + } + finally + { + model.endUpdate(); + } + + graph.setSelectionCells([vertex, edge]); + graph.scrollCellToVisible(vertex); + } +}; + +/** + * Function: installDropHandler + * + * Makes the given img draggable using the given function for handling a + * drop event. + * + * Parameters: + * + * img - DOM node that represents the image. + * dropHandler - Function that handles a drop of the image. + */ +mxDefaultToolbar.prototype.installDropHandler = function (img, dropHandler) +{ + var sprite = document.createElement('img'); + sprite.setAttribute('src', img.getAttribute('src')); + + // Handles delayed loading of the images + var loader = mxUtils.bind(this, function(evt) + { + // Preview uses the image node with double size. Later this can be + // changed to use a separate preview and guides, but for this the + // dropHandler must use the additional x- and y-arguments and the + // dragsource which makeDraggable returns much be configured to + // use guides via mxDragSource.isGuidesEnabled. + sprite.style.width = (2 * img.offsetWidth) + 'px'; + sprite.style.height = (2 * img.offsetHeight) + 'px'; + + mxUtils.makeDraggable(img, this.editor.graph, dropHandler, + sprite); + mxEvent.removeListener(sprite, 'load', loader); + }); + + if (mxClient.IS_IE) + { + loader(); + } + else + { + mxEvent.addListener(sprite, 'load', loader); + } +}; + +/** + * Function: destroy + * + * Destroys the <toolbar> associated with this object and removes all + * installed listeners. This does normally not need to be called, the + * <toolbar> is destroyed automatically when the window unloads (in IE) by + * <mxEditor>. + */ +mxDefaultToolbar.prototype.destroy = function () +{ + if (this.resetHandler != null) + { + this.editor.graph.removeListener('dblclick', this.resetHandler); + this.editor.removeListener('escape', this.resetHandler); + this.resetHandler = null; + } + + if (this.toolbar != null) + { + this.toolbar.destroy(); + this.toolbar = null; + } +}; diff --git a/src/js/editor/mxEditor.js b/src/js/editor/mxEditor.js new file mode 100644 index 0000000..9c57a9c --- /dev/null +++ b/src/js/editor/mxEditor.js @@ -0,0 +1,3220 @@ +/** + * $Id: mxEditor.js,v 1.231 2012-12-03 18:02:25 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxEditor + * + * Extends <mxEventSource> to implement a application wrapper for a graph that + * adds <actions>, I/O using <mxCodec>, auto-layout using <mxLayoutManager>, + * command history using <undoManager>, and standard dialogs and widgets, eg. + * properties, help, outline, toolbar, and popupmenu. It also adds <templates> + * to be used as cells in toolbars, auto-validation using the <validation> + * flag, attribute cycling using <cycleAttributeValues>, higher-level events + * such as <root>, and backend integration using <urlPost>, <urlImage>, + * <urlInit>, <urlNotify> and <urlPoll>. + * + * Actions: + * + * Actions are functions stored in the <actions> array under their names. The + * functions take the <mxEditor> as the first, and an optional <mxCell> as the + * second argument and are invoked using <execute>. Any additional arguments + * passed to execute are passed on to the action as-is. + * + * A list of built-in actions is available in the <addActions> description. + * + * Read/write Diagrams: + * + * To read a diagram from an XML string, for example from a textfield within the + * page, the following code is used: + * + * (code) + * var doc = mxUtils.parseXML(xmlString); + * var node = doc.documentElement; + * editor.readGraphModel(node); + * (end) + * + * For reading a diagram from a remote location, use the <open> method. + * + * To save diagrams in XML on a server, you can set the <urlPost> variable. + * This variable will be used in <getUrlPost> to construct a URL for the post + * request that is issued in the <save> method. The post request contains the + * XML representation of the diagram as returned by <writeGraphModel> in the + * xml parameter. + * + * On the server side, the post request is processed using standard + * technologies such as Java Servlets, CGI, .NET or ASP. + * + * Here are some examples of processing a post request in various languages. + * + * - Java: URLDecoder.decode(request.getParameter("xml"), "UTF-8").replace("\n", "
") + * + * Note that the linefeeds should only be replaced if the XML is + * processed in Java, for example when creating an image, but not + * if the XML is passed back to the client-side. + * + * - .NET: HttpUtility.UrlDecode(context.Request.Params["xml"]) + * - PHP: urldecode($_POST["xml"]) + * + * Creating images: + * + * A backend (Java, PHP or C#) is required for creating images. The + * distribution contains an example for each backend (ImageHandler.java, + * ImageHandler.cs and graph.php). More information about using a backend + * to create images can be found in the readme.html files. Note that the + * preview is implemented using VML/SVG in the browser and does not require + * a backend. The backend is only required to creates images (bitmaps). + * + * Special characters: + * + * Note There are five characters that should always appear in XML content as + * escapes, so that they do not interact with the syntax of the markup. These + * are part of the language for all documents based on XML and for HTML. + * + * - < (<) + * - > (>) + * - & (&) + * - " (") + * - ' (') + * + * Although it is part of the XML language, ' is not defined in HTML. + * For this reason the XHTML specification recommends instead the use of + * ' if text may be passed to a HTML user agent. + * + * If you are having problems with special characters on the server-side then + * you may want to try the <escapePostData> flag. + * + * For converting decimal escape sequences inside strings, a user has provided + * us with the following function: + * + * (code) + * function html2js(text) + * { + * var entitySearch = /&#[0-9]+;/; + * var entity; + * + * while (entity = entitySearch.exec(text)) + * { + * var charCode = entity[0].substring(2, entity[0].length -1); + * text = text.substring(0, entity.index) + * + String.fromCharCode(charCode) + * + text.substring(entity.index + entity[0].length); + * } + * + * return text; + * } + * (end) + * + * Otherwise try using hex escape sequences and the built-in unescape function + * for converting such strings. + * + * Local Files: + * + * For saving and opening local files, no standardized method exists that + * works across all browsers. The recommended way of dealing with local files + * is to create a backend that streams the XML data back to the browser (echo) + * as an attachment so that a Save-dialog is displayed on the client-side and + * the file can be saved to the local disk. + * + * For example, in PHP the code that does this looks as follows. + * + * (code) + * $xml = stripslashes($_POST["xml"]); + * header("Content-Disposition: attachment; filename=\"diagram.xml\""); + * echo($xml); + * (end) + * + * To open a local file, the file should be uploaded via a form in the browser + * and then opened from the server in the editor. + * + * Cell Properties: + * + * The properties displayed in the properties dialog are the attributes and + * values of the cell's user object, which is an XML node. The XML node is + * defined in the templates section of the config file. + * + * The templates are stored in <mxEditor.templates> and contain cells which + * are cloned at insertion time to create new vertices by use of drag and + * drop from the toolbar. Each entry in the toolbar for adding a new vertex + * must refer to an existing template. + * + * In the following example, the task node is a business object and only the + * mxCell node and its mxGeometry child contain graph information: + * + * (code) + * <Task label="Task" description=""> + * <mxCell vertex="true"> + * <mxGeometry as="geometry" width="72" height="32"/> + * </mxCell> + * </Task> + * (end) + * + * The idea is that the XML representation is inverse from the in-memory + * representation: The outer XML node is the user object and the inner node is + * the cell. This means the user object of the cell is the Task node with no + * children for the above example: + * + * (code) + * <Task label="Task" description=""/> + * (end) + * + * The Task node can have any tag name, attributes and child nodes. The + * <mxCodec> will use the XML hierarchy as the user object, while removing the + * "known annotations", such as the mxCell node. At save-time the cell data + * will be "merged" back into the user object. The user object is only modified + * via the properties dialog during the lifecycle of the cell. + * + * In the default implementation of <createProperties>, the user object's + * attributes are put into a form for editing. Attributes are changed using + * the <mxCellAttributeChange> action in the model. The dialog can be replaced + * by overriding the <createProperties> hook or by replacing the showProperties + * action in <actions>. Alternatively, the entry in the config file's popupmenu + * section can be modified to invoke a different action. + * + * If you want to displey the properties dialog on a doubleclick, you can set + * <mxEditor.dblClickAction> to showProperties as follows: + * + * (code) + * editor.dblClickAction = 'showProperties'; + * (end) + * + * Popupmenu and Toolbar: + * + * The toolbar and popupmenu are typically configured using the respective + * sections in the config file, that is, the popupmenu is defined as follows: + * + * (code) + * <mxEditor> + * <mxDefaultPopupMenu as="popupHandler"> + * <add as="cut" action="cut" icon="images/cut.gif"/> + * ... + * (end) + * + * New entries can be added to the toolbar by inserting an add-node into the + * above configuration. Existing entries may be removed and changed by + * modifying or removing the respective entries in the configuration. + * The configuration is read by the <mxDefaultPopupMenuCodec>, the format of the + * configuration is explained in <mxDefaultPopupMenu.decode>. + * + * The toolbar is defined in the mxDefaultToolbar section. Items can be added + * and removed in this section. + * + * (code) + * <mxEditor> + * <mxDefaultToolbar> + * <add as="save" action="save" icon="images/save.gif"/> + * <add as="Swimlane" template="swimlane" icon="images/swimlane.gif"/> + * ... + * (end) + * + * The format of the configuration is described in + * <mxDefaultToolbarCodec.decode>. + * + * Ids: + * + * For the IDs, there is an implicit behaviour in <mxCodec>: It moves the Id + * from the cell to the user object at encoding time and vice versa at decoding + * time. For example, if the Task node from above has an id attribute, then + * the <mxCell.id> of the corresponding cell will have this value. If there + * is no Id collision in the model, then the cell may be retrieved using this + * Id with the <mxGraphModel.getCell> function. If there is a collision, a new + * Id will be created for the cell using <mxGraphModel.createId>. At encoding + * time, this new Id will replace the value previously stored under the id + * attribute in the Task node. + * + * See <mxEditorCodec>, <mxDefaultToolbarCodec> and <mxDefaultPopupMenuCodec> + * for information about configuring the editor and user interface. + * + * Programmatically inserting cells: + * + * For inserting a new cell, say, by clicking a button in the document, + * the following code can be used. This requires an reference to the editor. + * + * (code) + * var userObject = new Object(); + * var parent = editor.graph.getDefaultParent(); + * var model = editor.graph.model; + * model.beginUpdate(); + * try + * { + * editor.graph.insertVertex(parent, null, userObject, 20, 20, 80, 30); + * } + * finally + * { + * model.endUpdate(); + * } + * (end) + * + * If a template cell from the config file should be inserted, then a clone + * of the template can be created as follows. The clone is then inserted using + * the add function instead of addVertex. + * + * (code) + * var template = editor.templates['task']; + * var clone = editor.graph.model.cloneCell(template); + * (end) + * + * Resources: + * + * resources/editor - Language resources for mxEditor + * + * Callback: onInit + * + * Called from within the constructor. In the callback, + * "this" refers to the editor instance. + * + * Cookie: mxgraph=seen + * + * Set when the editor is started. Never expires. Use + * <resetFirstTime> to reset this cookie. This cookie + * only exists if <onInit> is implemented. + * + * Event: mxEvent.OPEN + * + * Fires after a file was opened in <open>. The <code>filename</code> property + * contains the filename that was used. The same value is also available in + * <filename>. + * + * Event: mxEvent.SAVE + * + * Fires after the current file was saved in <save>. The <code>url</code> + * property contains the URL that was used for saving. + * + * Event: mxEvent.POST + * + * Fires if a successful response was received in <postDiagram>. The + * <code>request</code> property contains the <mxXmlRequest>, the + * <code>url</code> and <code>data</code> properties contain the URL and the + * data that were used in the post request. + * + * Event: mxEvent.ROOT + * + * Fires when the current root has changed, or when the title of the current + * root has changed. This event has no properties. + * + * Event: mxEvent.SESSION + * + * Fires when anything in the session has changed. The <code>session</code> + * property contains the respective <mxSession>. + * + * Event: mxEvent.BEFORE_ADD_VERTEX + * + * Fires before a vertex is added in <addVertex>. The <code>vertex</code> + * property contains the new vertex and the <code>parent</code> property + * contains its parent. + * + * Event: mxEvent.ADD_VERTEX + * + * Fires between begin- and endUpdate in <addVertex>. The <code>vertex</code> + * property contains the vertex that is being inserted. + * + * Event: mxEvent.AFTER_ADD_VERTEX + * + * Fires after a vertex was inserted and selected in <addVertex>. The + * <code>vertex</code> property contains the new vertex. + * + * Example: + * + * For starting an in-place edit after a new vertex has been added to the + * graph, the following code can be used. + * + * (code) + * editor.addListener(mxEvent.AFTER_ADD_VERTEX, function(sender, evt) + * { + * var vertex = evt.getProperty('vertex'); + * + * if (editor.graph.isCellEditable(vertex)) + * { + * editor.graph.startEditingAtCell(vertex); + * } + * }); + * (end) + * + * Event: mxEvent.ESCAPE + * + * Fires when the escape key is pressed. The <code>event</code> property + * contains the key event. + * + * Constructor: mxEditor + * + * Constructs a new editor. This function invokes the <onInit> callback + * upon completion. + * + * Example: + * + * (code) + * var config = mxUtils.load('config/diagrameditor.xml').getDocumentElement(); + * var editor = new mxEditor(config); + * (end) + * + * Parameters: + * + * config - Optional XML node that contains the configuration. + */ +function mxEditor(config) +{ + this.actions = []; + this.addActions(); + + // Executes the following only if a document has been instanciated. + // That is, don't execute when the editorcodec is setup. + if (document.body != null) + { + // Defines instance fields + this.cycleAttributeValues = []; + this.popupHandler = new mxDefaultPopupMenu(); + this.undoManager = new mxUndoManager(); + + // Creates the graph and toolbar without the containers + this.graph = this.createGraph(); + this.toolbar = this.createToolbar(); + + // Creates the global keyhandler (requires graph instance) + this.keyHandler = new mxDefaultKeyHandler(this); + + // Configures the editor using the URI + // which was passed to the ctor + this.configure(config); + + // Assigns the swimlaneIndicatorColorAttribute on the graph + this.graph.swimlaneIndicatorColorAttribute = this.cycleAttributeName; + + // Initializes the session if the urlInit + // member field of this editor is set. + if (!mxClient.IS_LOCAL && this.urlInit != null) + { + this.session = this.createSession(); + } + + // Checks ifthe <onInit> hook has been set + if (this.onInit != null) + { + // Invokes the <onInit> hook + this.onInit(); + } + + // Automatic deallocation of memory + if (mxClient.IS_IE) + { + mxEvent.addListener(window, 'unload', mxUtils.bind(this, function() + { + this.destroy(); + })); + } + } +}; + +/** + * Installs the required language resources at class + * loading time. + */ +if (mxLoadResources) +{ + mxResources.add(mxClient.basePath+'/resources/editor'); +} + +/** + * Extends mxEventSource. + */ +mxEditor.prototype = new mxEventSource(); +mxEditor.prototype.constructor = mxEditor; + +/** + * Group: Controls and Handlers + */ + +/** + * Variable: askZoomResource + * + * Specifies the resource key for the zoom dialog. If the resource for this + * key does not exist then the value is used as the error message. Default + * is 'askZoom'. + */ +mxEditor.prototype.askZoomResource = (mxClient.language != 'none') ? 'askZoom' : ''; + +/** + * Variable: lastSavedResource + * + * Specifies the resource key for the last saved info. If the resource for + * this key does not exist then the value is used as the error message. + * Default is 'lastSaved'. + */ +mxEditor.prototype.lastSavedResource = (mxClient.language != 'none') ? 'lastSaved' : ''; + +/** + * Variable: currentFileResource + * + * Specifies the resource key for the current file info. If the resource for + * this key does not exist then the value is used as the error message. + * Default is 'lastSaved'. + */ +mxEditor.prototype.currentFileResource = (mxClient.language != 'none') ? 'currentFile' : ''; + +/** + * Variable: propertiesResource + * + * Specifies the resource key for the properties window title. If the + * resource for this key does not exist then the value is used as the + * error message. Default is 'properties'. + */ +mxEditor.prototype.propertiesResource = (mxClient.language != 'none') ? 'properties' : ''; + +/** + * Variable: tasksResource + * + * Specifies the resource key for the tasks window title. If the + * resource for this key does not exist then the value is used as the + * error message. Default is 'tasks'. + */ +mxEditor.prototype.tasksResource = (mxClient.language != 'none') ? 'tasks' : ''; + +/** + * Variable: helpResource + * + * Specifies the resource key for the help window title. If the + * resource for this key does not exist then the value is used as the + * error message. Default is 'help'. + */ +mxEditor.prototype.helpResource = (mxClient.language != 'none') ? 'help' : ''; + +/** + * Variable: outlineResource + * + * Specifies the resource key for the outline window title. If the + * resource for this key does not exist then the value is used as the + * error message. Default is 'outline'. + */ +mxEditor.prototype.outlineResource = (mxClient.language != 'none') ? 'outline' : ''; + +/** + * Variable: outline + * + * Reference to the <mxWindow> that contains the outline. The <mxOutline> + * is stored in outline.outline. + */ +mxEditor.prototype.outline = null; + +/** + * Variable: graph + * + * Holds a <mxGraph> for displaying the diagram. The graph + * is created in <setGraphContainer>. + */ +mxEditor.prototype.graph = null; + +/** + * Variable: graphRenderHint + * + * Holds the render hint used for creating the + * graph in <setGraphContainer>. See <mxGraph>. + * Default is null. + */ +mxEditor.prototype.graphRenderHint = null; + +/** + * Variable: toolbar + * + * Holds a <mxDefaultToolbar> for displaying the toolbar. The + * toolbar is created in <setToolbarContainer>. + */ +mxEditor.prototype.toolbar = null; + +/** + * Variable: session + * + * Holds a <mxSession> instance associated with this editor. + */ +mxEditor.prototype.session = null; + +/** + * Variable: status + * + * DOM container that holds the statusbar. Default is null. + * Use <setStatusContainer> to set this value. + */ +mxEditor.prototype.status = null; + +/** + * Variable: popupHandler + * + * Holds a <mxDefaultPopupMenu> for displaying + * popupmenus. + */ +mxEditor.prototype.popupHandler = null; + +/** + * Variable: undoManager + * + * Holds an <mxUndoManager> for the command history. + */ +mxEditor.prototype.undoManager = null; + +/** + * Variable: keyHandler + * + * Holds a <mxDefaultKeyHandler> for handling keyboard events. + * The handler is created in <setGraphContainer>. + */ +mxEditor.prototype.keyHandler = null; + +/** + * Group: Actions and Options + */ + +/** + * Variable: actions + * + * Maps from actionnames to actions, which are functions taking + * the editor and the cell as arguments. Use <addAction> + * to add or replace an action and <execute> to execute an action + * by name, passing the cell to be operated upon as the second + * argument. + */ +mxEditor.prototype.actions = null; + +/** + * Variable: dblClickAction + * + * Specifies the name of the action to be executed + * when a cell is double clicked. Default is edit. + * + * To handle a singleclick, use the following code. + * + * (code) + * editor.graph.addListener(mxEvent.CLICK, function(sender, evt) + * { + * var e = evt.getProperty('event'); + * var cell = evt.getProperty('cell'); + * + * if (cell != null && !e.isConsumed()) + * { + * // Do something useful with cell... + * e.consume(); + * } + * }); + * (end) + */ +mxEditor.prototype.dblClickAction = 'edit'; + +/** + * Variable: swimlaneRequired + * + * Specifies if new cells must be inserted + * into an existing swimlane. Otherwise, cells + * that are not swimlanes can be inserted as + * top-level cells. Default is false. + */ +mxEditor.prototype.swimlaneRequired = false; + +/** + * Variable: disableContextMenu + * + * Specifies if the context menu should be disabled in the graph container. + * Default is true. + */ +mxEditor.prototype.disableContextMenu = true; + +/** + * Group: Templates + */ + +/** + * Variable: insertFunction + * + * Specifies the function to be used for inserting new + * cells into the graph. This is assigned from the + * <mxDefaultToolbar> if a vertex-tool is clicked. + */ +mxEditor.prototype.insertFunction = null; + +/** + * Variable: forcedInserting + * + * Specifies if a new cell should be inserted on a single + * click even using <insertFunction> if there is a cell + * under the mousepointer, otherwise the cell under the + * mousepointer is selected. Default is false. + */ +mxEditor.prototype.forcedInserting = false; + +/** + * Variable: templates + * + * Maps from names to protoype cells to be used + * in the toolbar for inserting new cells into + * the diagram. + */ +mxEditor.prototype.templates = null; + +/** + * Variable: defaultEdge + * + * Prototype edge cell that is used for creating + * new edges. + */ +mxEditor.prototype.defaultEdge = null; + +/** + * Variable: defaultEdgeStyle + * + * Specifies the edge style to be returned in <getEdgeStyle>. + * Default is null. + */ +mxEditor.prototype.defaultEdgeStyle = null; + +/** + * Variable: defaultGroup + * + * Prototype group cell that is used for creating + * new groups. + */ +mxEditor.prototype.defaultGroup = null; + +/** + * Variable: graphRenderHint + * + * Default size for the border of new groups. If null, + * then then <mxGraph.gridSize> is used. Default is + * null. + */ +mxEditor.prototype.groupBorderSize = null; + +/** + * Group: Backend Integration + */ + +/** + * Variable: filename + * + * Contains the URL of the last opened file as a string. + * Default is null. + */ +mxEditor.prototype.filename = null; + +/** + * Variable: lineFeed + * + * Character to be used for encoding linefeeds in <save>. Default is '
'. + */ +mxEditor.prototype.linefeed = '
'; + +/** + * Variable: postParameterName + * + * Specifies if the name of the post parameter that contains the diagram + * data in a post request to the server. Default is xml. + */ +mxEditor.prototype.postParameterName = 'xml'; + +/** + * Variable: escapePostData + * + * Specifies if the data in the post request for saving a diagram + * should be converted using encodeURIComponent. Default is true. + */ +mxEditor.prototype.escapePostData = true; + +/** + * Variable: urlPost + * + * Specifies the URL to be used for posting the diagram + * to a backend in <save>. + */ +mxEditor.prototype.urlPost = null; + +/** + * Variable: urlImage + * + * Specifies the URL to be used for creating a bitmap of + * the graph in the image action. + */ +mxEditor.prototype.urlImage = null; + +/** + * Variable: urlInit + * + * Specifies the URL to be used for initializing the session. + */ +mxEditor.prototype.urlInit = null; + +/** + * Variable: urlNotify + * + * Specifies the URL to be used for notifying the backend + * in the session. + */ +mxEditor.prototype.urlNotify = null; + +/** + * Variable: urlPoll + * + * Specifies the URL to be used for polling in the session. + */ +mxEditor.prototype.urlPoll = null; + +/** + * Group: Autolayout + */ + +/** + * Variable: horizontalFlow + * + * Specifies the direction of the flow + * in the diagram. This is used in the + * layout algorithms. Default is false, + * ie. vertical flow. + */ +mxEditor.prototype.horizontalFlow = false; + +/** + * Variable: layoutDiagram + * + * Specifies if the top-level elements in the + * diagram should be layed out using a vertical + * or horizontal stack depending on the setting + * of <horizontalFlow>. The spacing between the + * swimlanes is specified by <swimlaneSpacing>. + * Default is false. + * + * If the top-level elements are swimlanes, then + * the intra-swimlane layout is activated by + * the <layoutSwimlanes> switch. + */ +mxEditor.prototype.layoutDiagram = false; + +/** + * Variable: swimlaneSpacing + * + * Specifies the spacing between swimlanes if + * automatic layout is turned on in + * <layoutDiagram>. Default is 0. + */ +mxEditor.prototype.swimlaneSpacing = 0; + +/** + * Variable: maintainSwimlanes + * + * Specifies if the swimlanes should be kept at the same + * width or height depending on the setting of + * <horizontalFlow>. Default is false. + * + * For horizontal flows, all swimlanes + * have the same height and for vertical flows, all swimlanes + * have the same width. Furthermore, the swimlanes are + * automatically "stacked" if <layoutDiagram> is true. + */ +mxEditor.prototype.maintainSwimlanes = false; + +/** + * Variable: layoutSwimlanes + * + * Specifies if the children of swimlanes should + * be layed out, either vertically or horizontally + * depending on <horizontalFlow>. + * Default is false. + */ +mxEditor.prototype.layoutSwimlanes = false; + +/** + * Group: Attribute Cycling + */ + +/** + * Variable: cycleAttributeValues + * + * Specifies the attribute values to be cycled when + * inserting new swimlanes. Default is an empty + * array. + */ +mxEditor.prototype.cycleAttributeValues = null; + +/** + * Variable: cycleAttributeIndex + * + * Index of the last consumed attribute index. If a new + * swimlane is inserted, then the <cycleAttributeValues> + * at this index will be used as the value for + * <cycleAttributeName>. Default is 0. + */ +mxEditor.prototype.cycleAttributeIndex = 0; + +/** + * Variable: cycleAttributeName + * + * Name of the attribute to be assigned a <cycleAttributeValues> + * when inserting new swimlanes. Default is fillColor. + */ +mxEditor.prototype.cycleAttributeName = 'fillColor'; + +/** + * Group: Windows + */ + +/** + * Variable: tasks + * + * Holds the <mxWindow> created in <showTasks>. + */ +mxEditor.prototype.tasks = null; + +/** + * Variable: tasksWindowImage + * + * Icon for the tasks window. + */ +mxEditor.prototype.tasksWindowImage = null; + +/** + * Variable: tasksTop + * + * Specifies the top coordinate of the tasks window in pixels. + * Default is 20. + */ +mxEditor.prototype.tasksTop = 20; + +/** + * Variable: help + * + * Holds the <mxWindow> created in <showHelp>. + */ +mxEditor.prototype.help = null; + +/** + * Variable: helpWindowImage + * + * Icon for the help window. + */ +mxEditor.prototype.helpWindowImage = null; + +/** + * Variable: urlHelp + * + * Specifies the URL to be used for the contents of the + * Online Help window. This is usually specified in the + * resources file under urlHelp for language-specific + * online help support. + */ +mxEditor.prototype.urlHelp = null; + +/** + * Variable: helpWidth + * + * Specifies the width of the help window in pixels. + * Default is 300. + */ +mxEditor.prototype.helpWidth = 300; + +/** + * Variable: helpWidth + * + * Specifies the width of the help window in pixels. + * Default is 260. + */ +mxEditor.prototype.helpHeight = 260; + +/** + * Variable: propertiesWidth + * + * Specifies the width of the properties window in pixels. + * Default is 240. + */ +mxEditor.prototype.propertiesWidth = 240; + +/** + * Variable: propertiesHeight + * + * Specifies the height of the properties window in pixels. + * If no height is specified then the window will be automatically + * sized to fit its contents. Default is null. + */ +mxEditor.prototype.propertiesHeight = null; + +/** + * Variable: movePropertiesDialog + * + * Specifies if the properties dialog should be automatically + * moved near the cell it is displayed for, otherwise the + * dialog is not moved. This value is only taken into + * account if the dialog is already visible. Default is false. + */ +mxEditor.prototype.movePropertiesDialog = false; + +/** + * Variable: validating + * + * Specifies if <mxGraph.validateGraph> should automatically be invoked after + * each change. Default is false. + */ +mxEditor.prototype.validating = false; + +/** + * Variable: modified + * + * True if the graph has been modified since it was last saved. + */ +mxEditor.prototype.modified = false; + +/** + * Function: isModified + * + * Returns <modified>. + */ +mxEditor.prototype.isModified = function () +{ + return this.modified; +}; + +/** + * Function: setModified + * + * Sets <modified> to the specified boolean value. + */ +mxEditor.prototype.setModified = function (value) +{ + this.modified = value; +}; + +/** + * Function: addActions + * + * Adds the built-in actions to the editor instance. + * + * save - Saves the graph using <urlPost>. + * print - Shows the graph in a new print preview window. + * show - Shows the graph in a new window. + * exportImage - Shows the graph as a bitmap image using <getUrlImage>. + * refresh - Refreshes the graph's display. + * cut - Copies the current selection into the clipboard + * and removes it from the graph. + * copy - Copies the current selection into the clipboard. + * paste - Pastes the clipboard into the graph. + * delete - Removes the current selection from the graph. + * group - Puts the current selection into a new group. + * ungroup - Removes the selected groups and selects the children. + * undo - Undoes the last change on the graph model. + * redo - Redoes the last change on the graph model. + * zoom - Sets the zoom via a dialog. + * zoomIn - Zooms into the graph. + * zoomOut - Zooms out of the graph + * actualSize - Resets the scale and translation on the graph. + * fit - Changes the scale so that the graph fits into the window. + * showProperties - Shows the properties dialog. + * selectAll - Selects all cells. + * selectNone - Clears the selection. + * selectVertices - Selects all vertices. + * selectEdges = Selects all edges. + * edit - Starts editing the current selection cell. + * enterGroup - Drills down into the current selection cell. + * exitGroup - Moves up in the drilling hierachy + * home - Moves to the topmost parent in the drilling hierarchy + * selectPrevious - Selects the previous cell. + * selectNext - Selects the next cell. + * selectParent - Selects the parent of the selection cell. + * selectChild - Selects the first child of the selection cell. + * collapse - Collapses the currently selected cells. + * expand - Expands the currently selected cells. + * bold - Toggle bold text style. + * italic - Toggle italic text style. + * underline - Toggle underline text style. + * shadow - Toggle shadow text style. + * alignCellsLeft - Aligns the selection cells at the left. + * alignCellsCenter - Aligns the selection cells in the center. + * alignCellsRight - Aligns the selection cells at the right. + * alignCellsTop - Aligns the selection cells at the top. + * alignCellsMiddle - Aligns the selection cells in the middle. + * alignCellsBottom - Aligns the selection cells at the bottom. + * alignFontLeft - Sets the horizontal text alignment to left. + * alignFontCenter - Sets the horizontal text alignment to center. + * alignFontRight - Sets the horizontal text alignment to right. + * alignFontTop - Sets the vertical text alignment to top. + * alignFontMiddle - Sets the vertical text alignment to middle. + * alignFontBottom - Sets the vertical text alignment to bottom. + * toggleTasks - Shows or hides the tasks window. + * toggleHelp - Shows or hides the help window. + * toggleOutline - Shows or hides the outline window. + * toggleConsole - Shows or hides the console window. + */ +mxEditor.prototype.addActions = function () +{ + this.addAction('save', function(editor) + { + editor.save(); + }); + + this.addAction('print', function(editor) + { + var preview = new mxPrintPreview(editor.graph, 1); + preview.open(); + }); + + this.addAction('show', function(editor) + { + mxUtils.show(editor.graph, null, 10, 10); + }); + + this.addAction('exportImage', function(editor) + { + var url = editor.getUrlImage(); + + if (url == null || mxClient.IS_LOCAL) + { + editor.execute('show'); + } + else + { + var node = mxUtils.getViewXml(editor.graph, 1); + var xml = mxUtils.getXml(node, '\n'); + + mxUtils.submit(url, editor.postParameterName + '=' + + encodeURIComponent(xml), document, '_blank'); + } + }); + + this.addAction('refresh', function(editor) + { + editor.graph.refresh(); + }); + + this.addAction('cut', function(editor) + { + if (editor.graph.isEnabled()) + { + mxClipboard.cut(editor.graph); + } + }); + + this.addAction('copy', function(editor) + { + if (editor.graph.isEnabled()) + { + mxClipboard.copy(editor.graph); + } + }); + + this.addAction('paste', function(editor) + { + if (editor.graph.isEnabled()) + { + mxClipboard.paste(editor.graph); + } + }); + + this.addAction('delete', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.removeCells(); + } + }); + + this.addAction('group', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.setSelectionCell(editor.groupCells()); + } + }); + + this.addAction('ungroup', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.setSelectionCells(editor.graph.ungroupCells()); + } + }); + + this.addAction('removeFromParent', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.removeCellsFromParent(); + } + }); + + this.addAction('undo', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.undo(); + } + }); + + this.addAction('redo', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.redo(); + } + }); + + this.addAction('zoomIn', function(editor) + { + editor.graph.zoomIn(); + }); + + this.addAction('zoomOut', function(editor) + { + editor.graph.zoomOut(); + }); + + this.addAction('actualSize', function(editor) + { + editor.graph.zoomActual(); + }); + + this.addAction('fit', function(editor) + { + editor.graph.fit(); + }); + + this.addAction('showProperties', function(editor, cell) + { + editor.showProperties(cell); + }); + + this.addAction('selectAll', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.selectAll(); + } + }); + + this.addAction('selectNone', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.clearSelection(); + } + }); + + this.addAction('selectVertices', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.selectVertices(); + } + }); + + this.addAction('selectEdges', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.selectEdges(); + } + }); + + this.addAction('edit', function(editor, cell) + { + if (editor.graph.isEnabled() && + editor.graph.isCellEditable(cell)) + { + editor.graph.startEditingAtCell(cell); + } + }); + + this.addAction('toBack', function(editor, cell) + { + if (editor.graph.isEnabled()) + { + editor.graph.orderCells(true); + } + }); + + this.addAction('toFront', function(editor, cell) + { + if (editor.graph.isEnabled()) + { + editor.graph.orderCells(false); + } + }); + + this.addAction('enterGroup', function(editor, cell) + { + editor.graph.enterGroup(cell); + }); + + this.addAction('exitGroup', function(editor) + { + editor.graph.exitGroup(); + }); + + this.addAction('home', function(editor) + { + editor.graph.home(); + }); + + this.addAction('selectPrevious', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.selectPreviousCell(); + } + }); + + this.addAction('selectNext', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.selectNextCell(); + } + }); + + this.addAction('selectParent', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.selectParentCell(); + } + }); + + this.addAction('selectChild', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.selectChildCell(); + } + }); + + this.addAction('collapse', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.foldCells(true); + } + }); + + this.addAction('collapseAll', function(editor) + { + if (editor.graph.isEnabled()) + { + var cells = editor.graph.getChildVertices(); + editor.graph.foldCells(true, false, cells); + } + }); + + this.addAction('expand', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.foldCells(false); + } + }); + + this.addAction('expandAll', function(editor) + { + if (editor.graph.isEnabled()) + { + var cells = editor.graph.getChildVertices(); + editor.graph.foldCells(false, false, cells); + } + }); + + this.addAction('bold', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.toggleCellStyleFlags( + mxConstants.STYLE_FONTSTYLE, + mxConstants.FONT_BOLD); + } + }); + + this.addAction('italic', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.toggleCellStyleFlags( + mxConstants.STYLE_FONTSTYLE, + mxConstants.FONT_ITALIC); + } + }); + + this.addAction('underline', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.toggleCellStyleFlags( + mxConstants.STYLE_FONTSTYLE, + mxConstants.FONT_UNDERLINE); + } + }); + + this.addAction('shadow', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.toggleCellStyleFlags( + mxConstants.STYLE_FONTSTYLE, + mxConstants.FONT_SHADOW); + } + }); + + this.addAction('alignCellsLeft', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.alignCells(mxConstants.ALIGN_LEFT); + } + }); + + this.addAction('alignCellsCenter', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.alignCells(mxConstants.ALIGN_CENTER); + } + }); + + this.addAction('alignCellsRight', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.alignCells(mxConstants.ALIGN_RIGHT); + } + }); + + this.addAction('alignCellsTop', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.alignCells(mxConstants.ALIGN_TOP); + } + }); + + this.addAction('alignCellsMiddle', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.alignCells(mxConstants.ALIGN_MIDDLE); + } + }); + + this.addAction('alignCellsBottom', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.alignCells(mxConstants.ALIGN_BOTTOM); + } + }); + + this.addAction('alignFontLeft', function(editor) + { + + editor.graph.setCellStyles( + mxConstants.STYLE_ALIGN, + mxConstants.ALIGN_LEFT); + }); + + this.addAction('alignFontCenter', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.setCellStyles( + mxConstants.STYLE_ALIGN, + mxConstants.ALIGN_CENTER); + } + }); + + this.addAction('alignFontRight', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.setCellStyles( + mxConstants.STYLE_ALIGN, + mxConstants.ALIGN_RIGHT); + } + }); + + this.addAction('alignFontTop', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.setCellStyles( + mxConstants.STYLE_VERTICAL_ALIGN, + mxConstants.ALIGN_TOP); + } + }); + + this.addAction('alignFontMiddle', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.setCellStyles( + mxConstants.STYLE_VERTICAL_ALIGN, + mxConstants.ALIGN_MIDDLE); + } + }); + + this.addAction('alignFontBottom', function(editor) + { + if (editor.graph.isEnabled()) + { + editor.graph.setCellStyles( + mxConstants.STYLE_VERTICAL_ALIGN, + mxConstants.ALIGN_BOTTOM); + } + }); + + this.addAction('zoom', function(editor) + { + var current = editor.graph.getView().scale*100; + var scale = parseFloat(mxUtils.prompt( + mxResources.get(editor.askZoomResource) || + editor.askZoomResource, + current))/100; + + if (!isNaN(scale)) + { + editor.graph.getView().setScale(scale); + } + }); + + this.addAction('toggleTasks', function(editor) + { + if (editor.tasks != null) + { + editor.tasks.setVisible(!editor.tasks.isVisible()); + } + else + { + editor.showTasks(); + } + }); + + this.addAction('toggleHelp', function(editor) + { + if (editor.help != null) + { + editor.help.setVisible(!editor.help.isVisible()); + } + else + { + editor.showHelp(); + } + }); + + this.addAction('toggleOutline', function(editor) + { + if (editor.outline == null) + { + editor.showOutline(); + } + else + { + editor.outline.setVisible(!editor.outline.isVisible()); + } + }); + + this.addAction('toggleConsole', function(editor) + { + mxLog.setVisible(!mxLog.isVisible()); + }); +}; + +/** + * Function: createSession + * + * Creates and returns and <mxSession> using <urlInit>, <urlPoll> and <urlNotify>. + */ +mxEditor.prototype.createSession = function () +{ + // Routes any change events from the session + // through the editor and dispatches them as + // a session event. + var sessionChanged = mxUtils.bind(this, function(session) + { + this.fireEvent(new mxEventObject(mxEvent.SESSION, 'session', session)); + }); + + return this.connect(this.urlInit, this.urlPoll, + this.urlNotify, sessionChanged); +}; + +/** + * Function: configure + * + * Configures the editor using the specified node. To load the + * configuration from a given URL the following code can be used to obtain + * the XML node. + * + * (code) + * var node = mxUtils.load(url).getDocumentElement(); + * (end) + * + * Parameters: + * + * node - XML node that contains the configuration. + */ +mxEditor.prototype.configure = function (node) +{ + if (node != null) + { + // Creates a decoder for the XML data + // and uses it to configure the editor + var dec = new mxCodec(node.ownerDocument); + dec.decode(node, this); + + // Resets the counters, modified state and + // command history + this.resetHistory(); + } +}; + +/** + * Function: resetFirstTime + * + * Resets the cookie that is used to remember if the editor has already + * been used. + */ +mxEditor.prototype.resetFirstTime = function () +{ + document.cookie = + 'mxgraph=seen; expires=Fri, 27 Jul 2001 02:47:11 UTC; path=/'; +}; + +/** + * Function: resetHistory + * + * Resets the command history, modified state and counters. + */ +mxEditor.prototype.resetHistory = function () +{ + this.lastSnapshot = new Date().getTime(); + this.undoManager.clear(); + this.ignoredChanges = 0; + this.setModified(false); +}; + +/** + * Function: addAction + * + * Binds the specified actionname to the specified function. + * + * Parameters: + * + * actionname - String that specifies the name of the action + * to be added. + * funct - Function that implements the new action. The first + * argument of the function is the editor it is used + * with, the second argument is the cell it operates + * upon. + * + * Example: + * (code) + * editor.addAction('test', function(editor, cell) + * { + * mxUtils.alert("test "+cell); + * }); + * (end) + */ +mxEditor.prototype.addAction = function (actionname, funct) +{ + this.actions[actionname] = funct; +}; + +/** + * Function: execute + * + * Executes the function with the given name in <actions> passing the + * editor instance and given cell as the first and second argument. All + * additional arguments are passed to the action as well. This method + * contains a try-catch block and displays an error message if an action + * causes an exception. The exception is re-thrown after the error + * message was displayed. + * + * Example: + * + * (code) + * editor.execute("showProperties", cell); + * (end) + */ +mxEditor.prototype.execute = function (actionname, cell, evt) +{ + var action = this.actions[actionname]; + + if (action != null) + { + try + { + // Creates the array of arguments by replacing the actionname + // with the editor instance in the args of this function + var args = arguments; + args[0] = this; + + // Invokes the function on the editor using the args + action.apply(this, args); + } + catch (e) + { + mxUtils.error('Cannot execute ' + actionname + + ': ' + e.message, 280, true); + + throw e; + } + } + else + { + mxUtils.error('Cannot find action '+actionname, 280, true); + } +}; + +/** + * Function: addTemplate + * + * Adds the specified template under the given name in <templates>. + */ +mxEditor.prototype.addTemplate = function (name, template) +{ + this.templates[name] = template; +}; + +/** + * Function: getTemplate + * + * Returns the template for the given name. + */ +mxEditor.prototype.getTemplate = function (name) +{ + return this.templates[name]; +}; + +/** + * Function: createGraph + * + * Creates the <graph> for the editor. The graph is created with no + * container and is initialized from <setGraphContainer>. + */ +mxEditor.prototype.createGraph = function () +{ + var graph = new mxGraph(null, null, this.graphRenderHint); + + // Enables rubberband, tooltips, panning + graph.setTooltips(true); + graph.setPanning(true); + + // Overrides the dblclick method on the graph to + // invoke the dblClickAction for a cell and reset + // the selection tool in the toolbar + this.installDblClickHandler(graph); + + // Installs the command history + this.installUndoHandler(graph); + + // Installs the handlers for the root event + this.installDrillHandler(graph); + + // Installs the handler for validation + this.installChangeHandler(graph); + + // Installs the handler for calling the + // insert function and consume the + // event if an insert function is defined + this.installInsertHandler(graph); + + // Redirects the function for creating the + // popupmenu items + graph.panningHandler.factoryMethod = + mxUtils.bind(this, function(menu, cell, evt) + { + return this.createPopupMenu(menu, cell, evt); + }); + + // Redirects the function for creating + // new connections in the diagram + graph.connectionHandler.factoryMethod = + mxUtils.bind(this, function(source, target) + { + return this.createEdge(source, target); + }); + + // Maintains swimlanes and installs autolayout + this.createSwimlaneManager(graph); + this.createLayoutManager(graph); + + return graph; +}; + +/** + * Function: createSwimlaneManager + * + * Sets the graph's container using <mxGraph.init>. + */ +mxEditor.prototype.createSwimlaneManager = function (graph) +{ + var swimlaneMgr = new mxSwimlaneManager(graph, false); + + swimlaneMgr.isHorizontal = mxUtils.bind(this, function() + { + return this.horizontalFlow; + }); + + swimlaneMgr.isEnabled = mxUtils.bind(this, function() + { + return this.maintainSwimlanes; + }); + + return swimlaneMgr; +}; + +/** + * Function: createLayoutManager + * + * Creates a layout manager for the swimlane and diagram layouts, that + * is, the locally defined inter- and intraswimlane layouts. + */ +mxEditor.prototype.createLayoutManager = function (graph) +{ + var layoutMgr = new mxLayoutManager(graph); + + var self = this; // closure + layoutMgr.getLayout = function(cell) + { + var layout = null; + var model = self.graph.getModel(); + + if (model.getParent(cell) != null) + { + // Executes the swimlane layout if a child of + // a swimlane has been changed. The layout is + // lazy created in createSwimlaneLayout. + if (self.layoutSwimlanes && + graph.isSwimlane(cell)) + { + if (self.swimlaneLayout == null) + { + self.swimlaneLayout = self.createSwimlaneLayout(); + } + + layout = self.swimlaneLayout; + } + + // Executes the diagram layout if the modified + // cell is a top-level cell. The layout is + // lazy created in createDiagramLayout. + else if (self.layoutDiagram && + (graph.isValidRoot(cell) || + model.getParent(model.getParent(cell)) == null)) + { + if (self.diagramLayout == null) + { + self.diagramLayout = self.createDiagramLayout(); + } + + layout = self.diagramLayout; + } + } + + return layout; + }; + + return layoutMgr; +}; + +/** + * Function: setGraphContainer + * + * Sets the graph's container using <mxGraph.init>. + */ +mxEditor.prototype.setGraphContainer = function (container) +{ + if (this.graph.container == null) + { + // Creates the graph instance inside the given container and render hint + //this.graph = new mxGraph(container, null, this.graphRenderHint); + this.graph.init(container); + + // Install rubberband selection as the last + // action handler in the chain + this.rubberband = new mxRubberband(this.graph); + + // Disables the context menu + if (this.disableContextMenu) + { + mxEvent.disableContextMenu(container); + } + + // Workaround for stylesheet directives in IE + if (mxClient.IS_QUIRKS) + { + new mxDivResizer(container); + } + } +}; + +/** + * Function: installDblClickHandler + * + * Overrides <mxGraph.dblClick> to invoke <dblClickAction> + * on a cell and reset the selection tool in the toolbar. + */ +mxEditor.prototype.installDblClickHandler = function (graph) +{ + // Installs a listener for double click events + graph.addListener(mxEvent.DOUBLE_CLICK, + mxUtils.bind(this, function(sender, evt) + { + var cell = evt.getProperty('cell'); + + if (cell != null && + graph.isEnabled() && + this.dblClickAction != null) + { + this.execute(this.dblClickAction, cell); + evt.consume(); + } + }) + ); +}; + +/** + * Function: installUndoHandler + * + * Adds the <undoManager> to the graph model and the view. + */ +mxEditor.prototype.installUndoHandler = function (graph) +{ + var listener = mxUtils.bind(this, function(sender, evt) + { + var edit = evt.getProperty('edit'); + this.undoManager.undoableEditHappened(edit); + }); + + graph.getModel().addListener(mxEvent.UNDO, listener); + graph.getView().addListener(mxEvent.UNDO, listener); + + // Keeps the selection state in sync + var undoHandler = function(sender, evt) + { + var changes = evt.getProperty('edit').changes; + graph.setSelectionCells(graph.getSelectionCellsForChanges(changes)); + }; + + this.undoManager.addListener(mxEvent.UNDO, undoHandler); + this.undoManager.addListener(mxEvent.REDO, undoHandler); +}; + +/** + * Function: installDrillHandler + * + * Installs listeners for dispatching the <root> event. + */ +mxEditor.prototype.installDrillHandler = function (graph) +{ + var listener = mxUtils.bind(this, function(sender) + { + this.fireEvent(new mxEventObject(mxEvent.ROOT)); + }); + + graph.getView().addListener(mxEvent.DOWN, listener); + graph.getView().addListener(mxEvent.UP, listener); +}; + +/** + * Function: installChangeHandler + * + * Installs the listeners required to automatically validate + * the graph. On each change of the root, this implementation + * fires a <root> event. + */ +mxEditor.prototype.installChangeHandler = function (graph) +{ + var listener = mxUtils.bind(this, function(sender, evt) + { + // Updates the modified state + this.setModified(true); + + // Automatically validates the graph + // after each change + if (this.validating == true) + { + graph.validateGraph(); + } + + // Checks if the root has been changed + var changes = evt.getProperty('edit').changes; + + for (var i = 0; i < changes.length; i++) + { + var change = changes[i]; + + if (change instanceof mxRootChange || + (change instanceof mxValueChange && + change.cell == this.graph.model.root) || + (change instanceof mxCellAttributeChange && + change.cell == this.graph.model.root)) + { + this.fireEvent(new mxEventObject(mxEvent.ROOT)); + break; + } + } + }); + + graph.getModel().addListener(mxEvent.CHANGE, listener); +}; + +/** + * Function: installInsertHandler + * + * Installs the handler for invoking <insertFunction> if + * one is defined. + */ +mxEditor.prototype.installInsertHandler = function (graph) +{ + var self = this; // closure + var insertHandler = + { + mouseDown: function(sender, me) + { + if (self.insertFunction != null && + !me.isPopupTrigger() && + (self.forcedInserting || + me.getState() == null)) + { + self.graph.clearSelection(); + self.insertFunction(me.getEvent(), me.getCell()); + + // Consumes the rest of the events + // for this gesture (down, move, up) + this.isActive = true; + me.consume(); + } + }, + + mouseMove: function(sender, me) + { + if (this.isActive) + { + me.consume(); + } + }, + + mouseUp: function(sender, me) + { + if (this.isActive) + { + this.isActive = false; + me.consume(); + } + } + }; + + graph.addMouseListener(insertHandler); +}; + +/** + * Function: createDiagramLayout + * + * Creates the layout instance used to layout the + * swimlanes in the diagram. + */ +mxEditor.prototype.createDiagramLayout = function () +{ + var gs = this.graph.gridSize; + var layout = new mxStackLayout(this.graph, !this.horizontalFlow, + this.swimlaneSpacing, 2*gs, 2*gs); + + // Overrides isIgnored to only take into account swimlanes + layout.isVertexIgnored = function(cell) + { + return !layout.graph.isSwimlane(cell); + }; + + return layout; +}; + +/** + * Function: createSwimlaneLayout + * + * Creates the layout instance used to layout the + * children of each swimlane. + */ +mxEditor.prototype.createSwimlaneLayout = function () +{ + return new mxCompactTreeLayout(this.graph, this.horizontalFlow); +}; + +/** + * Function: createToolbar + * + * Creates the <toolbar> with no container. + */ +mxEditor.prototype.createToolbar = function () +{ + return new mxDefaultToolbar(null, this); +}; + +/** + * Function: setToolbarContainer + * + * Initializes the toolbar for the given container. + */ +mxEditor.prototype.setToolbarContainer = function (container) +{ + this.toolbar.init(container); + + // Workaround for stylesheet directives in IE + if (mxClient.IS_QUIRKS) + { + new mxDivResizer(container); + } +}; + +/** + * Function: setStatusContainer + * + * Creates the <status> using the specified container. + * + * This implementation adds listeners in the editor to + * display the last saved time and the current filename + * in the status bar. + * + * Parameters: + * + * container - DOM node that will contain the statusbar. + */ +mxEditor.prototype.setStatusContainer = function (container) +{ + if (this.status == null) + { + this.status = container; + + // Prints the last saved time in the status bar + // when files are saved + this.addListener(mxEvent.SAVE, mxUtils.bind(this, function() + { + var tstamp = new Date().toLocaleString(); + this.setStatus((mxResources.get(this.lastSavedResource) || + this.lastSavedResource)+': '+tstamp); + })); + + // Updates the statusbar to display the filename + // when new files are opened + this.addListener(mxEvent.OPEN, mxUtils.bind(this, function() + { + this.setStatus((mxResources.get(this.currentFileResource) || + this.currentFileResource)+': '+this.filename); + })); + + // Workaround for stylesheet directives in IE + if (mxClient.IS_QUIRKS) + { + new mxDivResizer(container); + } + } +}; + +/** + * Function: setStatus + * + * Display the specified message in the status bar. + * + * Parameters: + * + * message - String the specified the message to + * be displayed. + */ +mxEditor.prototype.setStatus = function (message) +{ + if (this.status != null && message != null) + { + this.status.innerHTML = message; + } +}; + +/** + * Function: setTitleContainer + * + * Creates a listener to update the inner HTML of the + * specified DOM node with the value of <getTitle>. + * + * Parameters: + * + * container - DOM node that will contain the title. + */ +mxEditor.prototype.setTitleContainer = function (container) +{ + this.addListener(mxEvent.ROOT, mxUtils.bind(this, function(sender) + { + container.innerHTML = this.getTitle(); + })); + + // Workaround for stylesheet directives in IE + if (mxClient.IS_QUIRKS) + { + new mxDivResizer(container); + } +}; + +/** + * Function: treeLayout + * + * Executes a vertical or horizontal compact tree layout + * using the specified cell as an argument. The cell may + * either be a group or the root of a tree. + * + * Parameters: + * + * cell - <mxCell> to use in the compact tree layout. + * horizontal - Optional boolean to specify the tree's + * orientation. Default is true. + */ +mxEditor.prototype.treeLayout = function (cell, horizontal) +{ + if (cell != null) + { + var layout = new mxCompactTreeLayout(this.graph, horizontal); + layout.execute(cell); + } +}; + +/** + * Function: getTitle + * + * Returns the string value for the current root of the + * diagram. + */ +mxEditor.prototype.getTitle = function () +{ + var title = ''; + var graph = this.graph; + var cell = graph.getCurrentRoot(); + + while (cell != null && + graph.getModel().getParent( + graph.getModel().getParent(cell)) != null) + { + // Append each label of a valid root + if (graph.isValidRoot(cell)) + { + title = ' > ' + + graph.convertValueToString(cell) + title; + } + + cell = graph.getModel().getParent(cell); + } + + var prefix = this.getRootTitle(); + + return prefix + title; +}; + +/** + * Function: getRootTitle + * + * Returns the string value of the root cell in + * <mxGraph.model>. + */ +mxEditor.prototype.getRootTitle = function () +{ + var root = this.graph.getModel().getRoot(); + return this.graph.convertValueToString(root); +}; + +/** + * Function: undo + * + * Undo the last change in <graph>. + */ +mxEditor.prototype.undo = function () +{ + this.undoManager.undo(); +}; + +/** + * Function: redo + * + * Redo the last change in <graph>. + */ +mxEditor.prototype.redo = function () +{ + this.undoManager.redo(); +}; + +/** + * Function: groupCells + * + * Invokes <createGroup> to create a new group cell and the invokes + * <mxGraph.groupCells>, using the grid size of the graph as the spacing + * in the group's content area. + */ +mxEditor.prototype.groupCells = function () +{ + var border = (this.groupBorderSize != null) ? + this.groupBorderSize : + this.graph.gridSize; + return this.graph.groupCells(this.createGroup(), border); +}; + +/** + * Function: createGroup + * + * Creates and returns a clone of <defaultGroup> to be used + * as a new group cell in <group>. + */ +mxEditor.prototype.createGroup = function () +{ + var model = this.graph.getModel(); + + return model.cloneCell(this.defaultGroup); +}; + +/** + * Function: open + * + * Opens the specified file synchronously and parses it using + * <readGraphModel>. It updates <filename> and fires an <open>-event after + * the file has been opened. Exceptions should be handled as follows: + * + * (code) + * try + * { + * editor.open(filename); + * } + * catch (e) + * { + * mxUtils.error('Cannot open ' + filename + + * ': ' + e.message, 280, true); + * } + * (end) + * + * Parameters: + * + * filename - URL of the file to be opened. + */ +mxEditor.prototype.open = function (filename) +{ + if (filename != null) + { + var xml = mxUtils.load(filename).getXml(); + this.readGraphModel(xml.documentElement); + this.filename = filename; + + this.fireEvent(new mxEventObject(mxEvent.OPEN, 'filename', filename)); + } +}; + +/** + * Function: readGraphModel + * + * Reads the specified XML node into the existing graph model and resets + * the command history and modified state. + */ +mxEditor.prototype.readGraphModel = function (node) +{ + var dec = new mxCodec(node.ownerDocument); + dec.decode(node, this.graph.getModel()); + this.resetHistory(); +}; + +/** + * Function: save + * + * Posts the string returned by <writeGraphModel> to the given URL or the + * URL returned by <getUrlPost>. The actual posting is carried out by + * <postDiagram>. If the URL is null then the resulting XML will be + * displayed using <mxUtils.popup>. Exceptions should be handled as + * follows: + * + * (code) + * try + * { + * editor.save(); + * } + * catch (e) + * { + * mxUtils.error('Cannot save : ' + e.message, 280, true); + * } + * (end) + */ +mxEditor.prototype.save = function (url, linefeed) +{ + // Gets the URL to post the data to + url = url || this.getUrlPost(); + + // Posts the data if the URL is not empty + if (url != null && url.length > 0) + { + var data = this.writeGraphModel(linefeed); + this.postDiagram(url, data); + + // Resets the modified flag + this.setModified(false); + } + + // Dispatches a save event + this.fireEvent(new mxEventObject(mxEvent.SAVE, 'url', url)); +}; + +/** + * Function: postDiagram + * + * Hook for subclassers to override the posting of a diagram + * represented by the given node to the given URL. This fires + * an asynchronous <post> event if the diagram has been posted. + * + * Example: + * + * To replace the diagram with the diagram in the response, use the + * following code. + * + * (code) + * editor.addListener(mxEvent.POST, function(sender, evt) + * { + * // Process response (replace diagram) + * var req = evt.getProperty('request'); + * var root = req.getDocumentElement(); + * editor.graph.readGraphModel(root) + * }); + * (end) + */ +mxEditor.prototype.postDiagram = function (url, data) +{ + if (this.escapePostData) + { + data = encodeURIComponent(data); + } + + mxUtils.post(url, this.postParameterName+'='+data, + mxUtils.bind(this, function(req) + { + this.fireEvent(new mxEventObject(mxEvent.POST, + 'request', req, 'url', url, 'data', data)); + }) + ); +}; + +/** + * Function: writeGraphModel + * + * Hook to create the string representation of the diagram. The default + * implementation uses an <mxCodec> to encode the graph model as + * follows: + * + * (code) + * var enc = new mxCodec(); + * var node = enc.encode(this.graph.getModel()); + * return mxUtils.getXml(node, this.linefeed); + * (end) + * + * Parameters: + * + * linefeed - Optional character to be used as the linefeed. Default is + * <linefeed>. + */ +mxEditor.prototype.writeGraphModel = function (linefeed) +{ + linefeed = (linefeed != null) ? linefeed : this.linefeed; + var enc = new mxCodec(); + var node = enc.encode(this.graph.getModel()); + + return mxUtils.getXml(node, linefeed); +}; + +/** + * Function: getUrlPost + * + * Returns the URL to post the diagram to. This is used + * in <save>. The default implementation returns <urlPost>, + * adding <code>?draft=true</code>. + */ +mxEditor.prototype.getUrlPost = function () +{ + return this.urlPost; +}; + +/** + * Function: getUrlImage + * + * Returns the URL to create the image with. This is typically + * the URL of a backend which accepts an XML representation + * of a graph view to create an image. The function is used + * in the image action to create an image. This implementation + * returns <urlImage>. + */ +mxEditor.prototype.getUrlImage = function () +{ + return this.urlImage; +}; + +/** + * Function: connect + * + * Creates and returns a session for the specified parameters, installing + * the onChange function as a change listener for the session. + */ +mxEditor.prototype.connect = function (urlInit, urlPoll, urlNotify, onChange) +{ + var session = null; + + if (!mxClient.IS_LOCAL) + { + session = new mxSession(this.graph.getModel(), + urlInit, urlPoll, urlNotify); + + // Resets the undo history if the session was initialized which is the + // case if the message carries a namespace to be used for new IDs. + session.addListener(mxEvent.RECEIVE, + mxUtils.bind(this, function(sender, evt) + { + var node = evt.getProperty('node'); + + if (node.getAttribute('namespace') != null) + { + this.resetHistory(); + } + }) + ); + + // Installs the listener for all events + // that signal a change of the session + session.addListener(mxEvent.DISCONNECT, onChange); + session.addListener(mxEvent.CONNECT, onChange); + session.addListener(mxEvent.NOTIFY, onChange); + session.addListener(mxEvent.GET, onChange); + session.start(); + } + + return session; +}; + +/** + * Function: swapStyles + * + * Swaps the styles for the given names in the graph's + * stylesheet and refreshes the graph. + */ +mxEditor.prototype.swapStyles = function (first, second) +{ + var style = this.graph.getStylesheet().styles[second]; + this.graph.getView().getStylesheet().putCellStyle( + second, this.graph.getStylesheet().styles[first]); + this.graph.getStylesheet().putCellStyle(first, style); + this.graph.refresh(); +}; + +/** + * Function: showProperties + * + * Creates and shows the properties dialog for the given + * cell. The content area of the dialog is created using + * <createProperties>. + */ +mxEditor.prototype.showProperties = function (cell) +{ + cell = cell || this.graph.getSelectionCell(); + + // Uses the root node for the properties dialog + // if not cell was passed in and no cell is + // selected + if (cell == null) + { + cell = this.graph.getCurrentRoot(); + + if (cell == null) + { + cell = this.graph.getModel().getRoot(); + } + } + + if (cell != null) + { + // Makes sure there is no in-place editor in the + // graph and computes the location of the dialog + this.graph.stopEditing(true); + + var offset = mxUtils.getOffset(this.graph.container); + var x = offset.x+10; + var y = offset.y; + + // Avoids moving the dialog if it is alredy open + if (this.properties != null && !this.movePropertiesDialog) + { + x = this.properties.getX(); + y = this.properties.getY(); + } + + // Places the dialog near the cell for which it + // displays the properties + else + { + var bounds = this.graph.getCellBounds(cell); + + if (bounds != null) + { + x += bounds.x+Math.min(200, bounds.width); + y += bounds.y; + } + } + + // Hides the existing properties dialog and creates a new one with the + // contents created in the hook method + this.hideProperties(); + var node = this.createProperties(cell); + + if (node != null) + { + // Displays the contents in a window and stores a reference to the + // window for later hiding of the window + this.properties = new mxWindow(mxResources.get(this.propertiesResource) || + this.propertiesResource, node, x, y, this.propertiesWidth, this.propertiesHeight, false); + this.properties.setVisible(true); + } + } +}; + +/** + * Function: isPropertiesVisible + * + * Returns true if the properties dialog is currently visible. + */ +mxEditor.prototype.isPropertiesVisible = function () +{ + return this.properties != null; +}; + +/** + * Function: createProperties + * + * Creates and returns the DOM node that represents the contents + * of the properties dialog for the given cell. This implementation + * works for user objects that are XML nodes and display all the + * node attributes in a form. + */ +mxEditor.prototype.createProperties = function (cell) +{ + var model = this.graph.getModel(); + var value = model.getValue(cell); + + if (mxUtils.isNode(value)) + { + // Creates a form for the user object inside + // the cell + var form = new mxForm('properties'); + + // Adds a readonly field for the cell id + var id = form.addText('ID', cell.getId()); + id.setAttribute('readonly', 'true'); + + var geo = null; + var yField = null; + var xField = null; + var widthField = null; + var heightField = null; + + // Adds fields for the location and size + if (model.isVertex(cell)) + { + geo = model.getGeometry(cell); + + if (geo != null) + { + yField = form.addText('top', geo.y); + xField = form.addText('left', geo.x); + widthField = form.addText('width', geo.width); + heightField = form.addText('height', geo.height); + } + } + + // Adds a field for the cell style + var tmp = model.getStyle(cell); + var style = form.addText('Style', tmp || ''); + + // Creates textareas for each attribute of the + // user object within the cell + var attrs = value.attributes; + var texts = []; + + for (var i = 0; i < attrs.length; i++) + { + // Creates a textarea with more lines for + // the cell label + var val = attrs[i].nodeValue; + texts[i] = form.addTextarea(attrs[i].nodeName, val, + (attrs[i].nodeName == 'label') ? 4 : 2); + } + + // Adds an OK and Cancel button to the dialog + // contents and implements the respective + // actions below + + // Defines the function to be executed when the + // OK button is pressed in the dialog + var okFunction = mxUtils.bind(this, function() + { + // Hides the dialog + this.hideProperties(); + + // Supports undo for the changes on the underlying + // XML structure / XML node attribute changes. + model.beginUpdate(); + try + { + if (geo != null) + { + geo = geo.clone(); + + geo.x = parseFloat(xField.value); + geo.y = parseFloat(yField.value); + geo.width = parseFloat(widthField.value); + geo.height = parseFloat(heightField.value); + + model.setGeometry(cell, geo); + } + + // Applies the style + if (style.value.length > 0) + { + model.setStyle(cell, style.value); + } + else + { + model.setStyle(cell, null); + } + + // Creates an undoable change for each + // attribute and executes it using the + // model, which will also make the change + // part of the current transaction + for (var i=0; i<attrs.length; i++) + { + var edit = new mxCellAttributeChange( + cell, attrs[i].nodeName, + texts[i].value); + model.execute(edit); + } + + // Checks if the graph wants cells to + // be automatically sized and updates + // the size as an undoable step if + // the feature is enabled + if (this.graph.isAutoSizeCell(cell)) + { + this.graph.updateCellSize(cell); + } + } + finally + { + model.endUpdate(); + } + }); + + // Defines the function to be executed when the + // Cancel button is pressed in the dialog + var cancelFunction = mxUtils.bind(this, function() + { + // Hides the dialog + this.hideProperties(); + }); + + form.addButtons(okFunction, cancelFunction); + + return form.table; + } + + return null; +}; + +/** + * Function: hideProperties + * + * Hides the properties dialog. + */ +mxEditor.prototype.hideProperties = function () +{ + if (this.properties != null) + { + this.properties.destroy(); + this.properties = null; + } +}; + +/** + * Function: showTasks + * + * Shows the tasks window. The tasks window is created using <createTasks>. The + * default width of the window is 200 pixels, the y-coordinate of the location + * can be specifies in <tasksTop> and the x-coordinate is right aligned with a + * 20 pixel offset from the right border. To change the location of the tasks + * window, the following code can be used: + * + * (code) + * var oldShowTasks = mxEditor.prototype.showTasks; + * mxEditor.prototype.showTasks = function() + * { + * oldShowTasks.apply(this, arguments); // "supercall" + * + * if (this.tasks != null) + * { + * this.tasks.setLocation(10, 10); + * } + * }; + * (end) + */ +mxEditor.prototype.showTasks = function () +{ + if (this.tasks == null) + { + var div = document.createElement('div'); + div.style.padding = '4px'; + div.style.paddingLeft = '20px'; + var w = document.body.clientWidth; + var wnd = new mxWindow( + mxResources.get(this.tasksResource) || + this.tasksResource, + div, w - 220, this.tasksTop, 200); + wnd.setClosable(true); + wnd.destroyOnClose = false; + + // Installs a function to update the contents + // of the tasks window on every change of the + // model, selection or root. + var funct = mxUtils.bind(this, function(sender) + { + mxEvent.release(div); + div.innerHTML = ''; + this.createTasks(div); + }); + + this.graph.getModel().addListener(mxEvent.CHANGE, funct); + this.graph.getSelectionModel().addListener(mxEvent.CHANGE, funct); + this.graph.addListener(mxEvent.ROOT, funct); + + // Assigns the icon to the tasks window + if (this.tasksWindowImage != null) + { + wnd.setImage(this.tasksWindowImage); + } + + this.tasks = wnd; + this.createTasks(div); + } + + this.tasks.setVisible(true); +}; + +/** + * Function: refreshTasks + * + * Updates the contents of the tasks window using <createTasks>. + */ +mxEditor.prototype.refreshTasks = function (div) +{ + if (this.tasks != null) + { + var div = this.tasks.content; + mxEvent.release(div); + div.innerHTML = ''; + this.createTasks(div); + } +}; + +/** + * Function: createTasks + * + * Updates the contents of the given DOM node to + * display the tasks associated with the current + * editor state. This is invoked whenever there + * is a possible change of state in the editor. + * Default implementation is empty. + */ +mxEditor.prototype.createTasks = function (div) +{ + // override +}; + +/** + * Function: showHelp + * + * Shows the help window. If the help window does not exist + * then it is created using an iframe pointing to the resource + * for the <code>urlHelp</code> key or <urlHelp> if the resource + * is undefined. + */ +mxEditor.prototype.showHelp = function (tasks) +{ + if (this.help == null) + { + var frame = document.createElement('iframe'); + frame.setAttribute('src', mxResources.get('urlHelp') || this.urlHelp); + frame.setAttribute('height', '100%'); + frame.setAttribute('width', '100%'); + frame.setAttribute('frameBorder', '0'); + frame.style.backgroundColor = 'white'; + + var w = document.body.clientWidth; + var h = (document.body.clientHeight || document.documentElement.clientHeight); + + var wnd = new mxWindow(mxResources.get(this.helpResource) || this.helpResource, + frame, (w-this.helpWidth)/2, (h-this.helpHeight)/3, this.helpWidth, this.helpHeight); + wnd.setMaximizable(true); + wnd.setClosable(true); + wnd.destroyOnClose = false; + wnd.setResizable(true); + + // Assigns the icon to the help window + if (this.helpWindowImage != null) + { + wnd.setImage(this.helpWindowImage); + } + + // Workaround for ignored iframe height 100% in FF + if (mxClient.IS_NS) + { + var handler = function(sender) + { + var h = wnd.div.offsetHeight; + frame.setAttribute('height', (h-26)+'px'); + }; + + wnd.addListener(mxEvent.RESIZE_END, handler); + wnd.addListener(mxEvent.MAXIMIZE, handler); + wnd.addListener(mxEvent.NORMALIZE, handler); + wnd.addListener(mxEvent.SHOW, handler); + } + + this.help = wnd; + } + + this.help.setVisible(true); +}; + +/** + * Function: showOutline + * + * Shows the outline window. If the window does not exist, then it is + * created using an <mxOutline>. + */ +mxEditor.prototype.showOutline = function () +{ + var create = this.outline == null; + + if (create) + { + var div = document.createElement('div'); + div.style.overflow = 'hidden'; + div.style.width = '100%'; + div.style.height = '100%'; + div.style.background = 'white'; + div.style.cursor = 'move'; + + var wnd = new mxWindow( + mxResources.get(this.outlineResource) || + this.outlineResource, + div, 600, 480, 200, 200, false); + + // Creates the outline in the specified div + // and links it to the existing graph + var outline = new mxOutline(this.graph, div); + wnd.setClosable(true); + wnd.setResizable(true); + wnd.destroyOnClose = false; + + wnd.addListener(mxEvent.RESIZE_END, function() + { + outline.update(); + }); + + this.outline = wnd; + this.outline.outline = outline; + } + + // Finally shows the outline + this.outline.setVisible(true); + this.outline.outline.update(true); +}; + +/** + * Function: setMode + * + * Puts the graph into the specified mode. The following modenames are + * supported: + * + * select - Selects using the left mouse button, new connections + * are disabled. + * connect - Selects using the left mouse button or creates new + * connections if mouse over cell hotspot. See <mxConnectionHandler>. + * pan - Pans using the left mouse button, new connections are disabled. + */ +mxEditor.prototype.setMode = function(modename) +{ + if (modename == 'select') + { + this.graph.panningHandler.useLeftButtonForPanning = false; + this.graph.setConnectable(false); + } + else if (modename == 'connect') + { + this.graph.panningHandler.useLeftButtonForPanning = false; + this.graph.setConnectable(true); + } + else if (modename == 'pan') + { + this.graph.panningHandler.useLeftButtonForPanning = true; + this.graph.setConnectable(false); + } +}; + +/** + * Function: createPopupMenu + * + * Uses <popupHandler> to create the menu in the graph's + * panning handler. The redirection is setup in + * <setToolbarContainer>. + */ +mxEditor.prototype.createPopupMenu = function (menu, cell, evt) +{ + this.popupHandler.createMenu(this, menu, cell, evt); +}; + +/** + * Function: createEdge + * + * Uses <defaultEdge> as the prototype for creating new edges + * in the connection handler of the graph. The style of the + * edge will be overridden with the value returned by + * <getEdgeStyle>. + */ +mxEditor.prototype.createEdge = function (source, target) +{ + // Clones the defaultedge prototype + var e = null; + + if (this.defaultEdge != null) + { + var model = this.graph.getModel(); + e = model.cloneCell(this.defaultEdge); + } + else + { + e = new mxCell(''); + e.setEdge(true); + + var geo = new mxGeometry(); + geo.relative = true; + e.setGeometry(geo); + } + + // Overrides the edge style + var style = this.getEdgeStyle(); + + if (style != null) + { + e.setStyle(style); + } + + return e; +}; + +/** + * Function: getEdgeStyle + * + * Returns a string identifying the style of new edges. + * The function is used in <createEdge> when new edges + * are created in the graph. + */ +mxEditor.prototype.getEdgeStyle = function () +{ + return this.defaultEdgeStyle; +}; + +/** + * Function: consumeCycleAttribute + * + * Returns the next attribute in <cycleAttributeValues> + * or null, if not attribute should be used in the + * specified cell. + */ +mxEditor.prototype.consumeCycleAttribute = function (cell) +{ + return (this.cycleAttributeValues != null && + this.cycleAttributeValues.length > 0 && + this.graph.isSwimlane(cell)) ? + this.cycleAttributeValues[this.cycleAttributeIndex++ % + this.cycleAttributeValues.length] : null; +}; + +/** + * Function: cycleAttribute + * + * Uses the returned value from <consumeCycleAttribute> + * as the value for the <cycleAttributeName> key in + * the given cell's style. + */ +mxEditor.prototype.cycleAttribute = function (cell) +{ + if (this.cycleAttributeName != null) + { + var value = this.consumeCycleAttribute(cell); + + if (value != null) + { + cell.setStyle(cell.getStyle()+';'+ + this.cycleAttributeName+'='+value); + } + } +}; + +/** + * Function: addVertex + * + * Adds the given vertex as a child of parent at the specified + * x and y coordinate and fires an <addVertex> event. + */ +mxEditor.prototype.addVertex = function (parent, vertex, x, y) +{ + var model = this.graph.getModel(); + + while (parent != null && !this.graph.isValidDropTarget(parent)) + { + parent = model.getParent(parent); + } + + parent = (parent != null) ? parent : this.graph.getSwimlaneAt(x, y); + var scale = this.graph.getView().scale; + + var geo = model.getGeometry(vertex); + var pgeo = model.getGeometry(parent); + + if (this.graph.isSwimlane(vertex) && + !this.graph.swimlaneNesting) + { + parent = null; + } + else if (parent == null && this.swimlaneRequired) + { + return null; + } + else if (parent != null && pgeo != null) + { + // Keeps vertex inside parent + var state = this.graph.getView().getState(parent); + + if (state != null) + { + x -= state.origin.x * scale; + y -= state.origin.y * scale; + + if (this.graph.isConstrainedMoving) + { + var width = geo.width; + var height = geo.height; + var tmp = state.x+state.width; + + if (x+width > tmp) + { + x -= x+width - tmp; + } + + tmp = state.y+state.height; + + if (y+height > tmp) + { + y -= y+height - tmp; + } + } + } + else if (pgeo != null) + { + x -= pgeo.x*scale; + y -= pgeo.y*scale; + } + } + + geo = geo.clone(); + geo.x = this.graph.snap(x / scale - + this.graph.getView().translate.x - + this.graph.gridSize/2); + geo.y = this.graph.snap(y / scale - + this.graph.getView().translate.y - + this.graph.gridSize/2); + vertex.setGeometry(geo); + + if (parent == null) + { + parent = this.graph.getDefaultParent(); + } + + this.cycleAttribute(vertex); + this.fireEvent(new mxEventObject(mxEvent.BEFORE_ADD_VERTEX, + 'vertex', vertex, 'parent', parent)); + + model.beginUpdate(); + try + { + vertex = this.graph.addCell(vertex, parent); + + if (vertex != null) + { + this.graph.constrainChild(vertex); + + this.fireEvent(new mxEventObject(mxEvent.ADD_VERTEX, 'vertex', vertex)); + } + } + finally + { + model.endUpdate(); + } + + if (vertex != null) + { + this.graph.setSelectionCell(vertex); + this.graph.scrollCellToVisible(vertex); + this.fireEvent(new mxEventObject(mxEvent.AFTER_ADD_VERTEX, 'vertex', vertex)); + } + + return vertex; +}; + +/** + * Function: destroy + * + * Removes the editor and all its associated resources. This does not + * normally need to be called, it is called automatically when the window + * unloads. + */ +mxEditor.prototype.destroy = function () +{ + if (!this.destroyed) + { + this.destroyed = true; + + if (this.tasks != null) + { + this.tasks.destroy(); + } + + if (this.outline != null) + { + this.outline.destroy(); + } + + if (this.properties != null) + { + this.properties.destroy(); + } + + if (this.keyHandler != null) + { + this.keyHandler.destroy(); + } + + if (this.rubberband != null) + { + this.rubberband.destroy(); + } + + if (this.toolbar != null) + { + this.toolbar.destroy(); + } + + if (this.graph != null) + { + this.graph.destroy(); + } + + this.status = null; + this.templates = null; + } +}; diff --git a/src/js/handler/mxCellHighlight.js b/src/js/handler/mxCellHighlight.js new file mode 100644 index 0000000..f967f00 --- /dev/null +++ b/src/js/handler/mxCellHighlight.js @@ -0,0 +1,271 @@ +/** + * $Id: mxCellHighlight.js,v 1.25 2012-09-27 14:43:40 boris Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxCellHighlight + * + * A helper class to highlight cells. Here is an example for a given cell. + * + * (code) + * var highlight = new mxCellHighlight(graph, '#ff0000', 2); + * highlight.highlight(graph.view.getState(cell))); + * (end) + * + * Constructor: mxCellHighlight + * + * Constructs a cell highlight. + */ +function mxCellHighlight(graph, highlightColor, strokeWidth) +{ + if (graph != null) + { + this.graph = graph; + this.highlightColor = (highlightColor != null) ? highlightColor : mxConstants.DEFAULT_VALID_COLOR; + this.strokeWidth = (strokeWidth != null) ? strokeWidth : mxConstants.HIGHLIGHT_STROKEWIDTH; + + // Updates the marker if the graph changes + this.repaintHandler = mxUtils.bind(this, function() + { + this.repaint(); + }); + + this.graph.getView().addListener(mxEvent.SCALE, this.repaintHandler); + this.graph.getView().addListener(mxEvent.TRANSLATE, this.repaintHandler); + this.graph.getView().addListener(mxEvent.SCALE_AND_TRANSLATE, this.repaintHandler); + this.graph.getModel().addListener(mxEvent.CHANGE, this.repaintHandler); + + // Hides the marker if the current root changes + this.resetHandler = mxUtils.bind(this, function() + { + this.hide(); + }); + + this.graph.getView().addListener(mxEvent.DOWN, this.resetHandler); + this.graph.getView().addListener(mxEvent.UP, this.resetHandler); + } +}; + +/** + * Variable: keepOnTop + * + * Specifies if the highlights should appear on top of everything + * else in the overlay pane. Default is false. + */ +mxCellHighlight.prototype.keepOnTop = false; + +/** + * Variable: graph + * + * Reference to the enclosing <mxGraph>. + */ +mxCellHighlight.prototype.graph = true; + +/** + * Variable: state + * + * Reference to the <mxCellState>. + */ +mxCellHighlight.prototype.state = null; + +/** + * Variable: spacing + * + * Specifies the spacing between the highlight for vertices and the vertex. + * Default is 2. + */ +mxCellHighlight.prototype.spacing = 2; + +/** + * Variable: resetHandler + * + * Holds the handler that automatically invokes reset if the highlight + * should be hidden. + */ +mxCellHighlight.prototype.resetHandler = null; + +/** + * Function: setHighlightColor + * + * Sets the color of the rectangle used to highlight drop targets. + * + * Parameters: + * + * color - String that represents the new highlight color. + */ +mxCellHighlight.prototype.setHighlightColor = function(color) +{ + this.highlightColor = color; + + if (this.shape != null) + { + if (this.shape.dialect == mxConstants.DIALECT_SVG) + { + this.shape.innerNode.setAttribute('stroke', color); + } + else if (this.shape.dialect == mxConstants.DIALECT_VML) + { + this.shape.node.strokecolor = color; + } + } +}; + +/** + * Function: drawHighlight + * + * Creates and returns the highlight shape for the given state. + */ +mxCellHighlight.prototype.drawHighlight = function() +{ + this.shape = this.createShape(); + this.repaint(); + + if (!this.keepOnTop && this.shape.node.parentNode.firstChild != this.shape.node) + { + this.shape.node.parentNode.insertBefore(this.shape.node, this.shape.node.parentNode.firstChild); + } + + // Workaround to force a repaint in AppleWebKit + if (this.graph.model.isEdge(this.state.cell)) + { + mxUtils.repaintGraph(this.graph, this.shape.points[0]); + } +}; + +/** + * Function: createShape + * + * Creates and returns the highlight shape for the given state. + */ +mxCellHighlight.prototype.createShape = function() +{ + var shape = null; + + if (this.graph.model.isEdge(this.state.cell)) + { + shape = new mxPolyline(this.state.absolutePoints, + this.highlightColor, this.strokeWidth); + } + else + { + shape = new mxRectangleShape( new mxRectangle(), + null, this.highlightColor, this.strokeWidth); + } + + shape.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ? + mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG; + shape.init(this.graph.getView().getOverlayPane()); + mxEvent.redirectMouseEvents(shape.node, this.graph, this.state); + + return shape; +}; + + +/** + * Function: repaint + * + * Updates the highlight after a change of the model or view. + */ +mxCellHighlight.prototype.repaint = function() +{ + if (this.state != null && this.shape != null) + { + if (this.graph.model.isEdge(this.state.cell)) + { + this.shape.points = this.state.absolutePoints; + } + else + { + this.shape.bounds = new mxRectangle(this.state.x - this.spacing, this.state.y - this.spacing, + this.state.width + 2 * this.spacing, this.state.height + 2 * this.spacing); + } + + // Uses cursor from shape in highlight + if (this.state.shape != null) + { + this.shape.setCursor(this.state.shape.getCursor()); + } + + var alpha = (!this.graph.model.isEdge(this.state.cell)) ? Number(this.state.style[mxConstants.STYLE_ROTATION] || '0') : 0; + + // Event-transparency + if (this.shape.dialect == mxConstants.DIALECT_SVG) + { + this.shape.node.setAttribute('style', 'pointer-events:none;'); + + if (alpha != 0) + { + var cx = this.state.getCenterX(); + var cy = this.state.getCenterY(); + var transform = 'rotate(' + alpha + ' ' + cx + ' ' + cy + ')'; + + this.shape.node.setAttribute('transform', transform); + } + } + else + { + this.shape.node.style.background = ''; + + if (alpha != 0) + { + this.shape.node.rotation = alpha; + } + } + + this.shape.redraw(); + } +}; + +/** + * Function: hide + * + * Resets the state of the cell marker. + */ +mxCellHighlight.prototype.hide = function() +{ + this.highlight(null); +}; + +/** + * Function: mark + * + * Marks the <markedState> and fires a <mark> event. + */ +mxCellHighlight.prototype.highlight = function(state) +{ + if (this.state != state) + { + if (this.shape != null) + { + this.shape.destroy(); + this.shape = null; + } + + this.state = state; + + if (this.state != null) + { + this.drawHighlight(); + } + } +}; + +/** + * Function: destroy + * + * Destroys the handler and all its resources and DOM nodes. + */ +mxCellHighlight.prototype.destroy = function() +{ + this.graph.getView().removeListener(this.repaintHandler); + this.graph.getModel().removeListener(this.repaintHandler); + + this.graph.getView().removeListener(this.resetHandler); + this.graph.getModel().removeListener(this.resetHandler); + + if (this.shape != null) + { + this.shape.destroy(); + this.shape = null; + } +}; diff --git a/src/js/handler/mxCellMarker.js b/src/js/handler/mxCellMarker.js new file mode 100644 index 0000000..b336278 --- /dev/null +++ b/src/js/handler/mxCellMarker.js @@ -0,0 +1,419 @@ +/** + * $Id: mxCellMarker.js,v 1.30 2011-07-15 12:57:50 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxCellMarker + * + * A helper class to process mouse locations and highlight cells. + * + * Helper class to highlight cells. To add a cell marker to an existing graph + * for highlighting all cells, the following code is used: + * + * (code) + * var marker = new mxCellMarker(graph); + * graph.addMouseListener({ + * mouseDown: function() {}, + * mouseMove: function(sender, me) + * { + * marker.process(me); + * }, + * mouseUp: function() {} + * }); + * (end) + * + * Event: mxEvent.MARK + * + * Fires after a cell has been marked or unmarked. The <code>state</code> + * property contains the marked <mxCellState> or null if no state is marked. + * + * Constructor: mxCellMarker + * + * Constructs a new cell marker. + * + * Parameters: + * + * graph - Reference to the enclosing <mxGraph>. + * validColor - Optional marker color for valid states. Default is + * <mxConstants.DEFAULT_VALID_COLOR>. + * invalidColor - Optional marker color for invalid states. Default is + * <mxConstants.DEFAULT_INVALID_COLOR>. + * hotspot - Portion of the width and hight where a state intersects a + * given coordinate pair. A value of 0 means always highlight. Default is + * <mxConstants.DEFAULT_HOTSPOT>. + */ +function mxCellMarker(graph, validColor, invalidColor, hotspot) +{ + if (graph != null) + { + this.graph = graph; + this.validColor = (validColor != null) ? validColor : mxConstants.DEFAULT_VALID_COLOR; + this.invalidColor = (validColor != null) ? invalidColor : mxConstants.DEFAULT_INVALID_COLOR; + this.hotspot = (hotspot != null) ? hotspot : mxConstants.DEFAULT_HOTSPOT; + + this.highlight = new mxCellHighlight(graph); + } +}; + +/** + * Extends mxEventSource. + */ +mxCellMarker.prototype = new mxEventSource(); +mxCellMarker.prototype.constructor = mxCellMarker; + +/** + * Variable: graph + * + * Reference to the enclosing <mxGraph>. + */ +mxCellMarker.prototype.graph = null; + +/** + * Variable: enabled + * + * Specifies if the marker is enabled. Default is true. + */ +mxCellMarker.prototype.enabled = true; + +/** + * Variable: hotspot + * + * Specifies the portion of the width and height that should trigger + * a highlight. The area around the center of the cell to be marked is used + * as the hotspot. Possible values are between 0 and 1. Default is + * mxConstants.DEFAULT_HOTSPOT. + */ +mxCellMarker.prototype.hotspot = mxConstants.DEFAULT_HOTSPOT; + +/** + * Variable: hotspotEnabled + * + * Specifies if the hotspot is enabled. Default is false. + */ +mxCellMarker.prototype.hotspotEnabled = false; + +/** + * Variable: validColor + * + * Holds the valid marker color. + */ +mxCellMarker.prototype.validColor = null; + +/** + * Variable: invalidColor + * + * Holds the invalid marker color. + */ +mxCellMarker.prototype.invalidColor = null; + +/** + * Variable: currentColor + * + * Holds the current marker color. + */ +mxCellMarker.prototype.currentColor = null; + +/** + * Variable: validState + * + * Holds the marked <mxCellState> if it is valid. + */ +mxCellMarker.prototype.validState = null; + +/** + * Variable: markedState + * + * Holds the marked <mxCellState>. + */ +mxCellMarker.prototype.markedState = null; + +/** + * Function: setEnabled + * + * Enables or disables event handling. This implementation + * updates <enabled>. + * + * Parameters: + * + * enabled - Boolean that specifies the new enabled state. + */ +mxCellMarker.prototype.setEnabled = function(enabled) +{ + this.enabled = enabled; +}; + +/** + * Function: isEnabled + * + * Returns true if events are handled. This implementation + * returns <enabled>. + */ +mxCellMarker.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setHotspot + * + * Sets the <hotspot>. + */ +mxCellMarker.prototype.setHotspot = function(hotspot) +{ + this.hotspot = hotspot; +}; + +/** + * Function: getHotspot + * + * Returns the <hotspot>. + */ +mxCellMarker.prototype.getHotspot = function() +{ + return this.hotspot; +}; + +/** + * Function: setHotspotEnabled + * + * Specifies whether the hotspot should be used in <intersects>. + */ +mxCellMarker.prototype.setHotspotEnabled = function(enabled) +{ + this.hotspotEnabled = enabled; +}; + +/** + * Function: isHotspotEnabled + * + * Returns true if hotspot is used in <intersects>. + */ +mxCellMarker.prototype.isHotspotEnabled = function() +{ + return this.hotspotEnabled; +}; + +/** + * Function: hasValidState + * + * Returns true if <validState> is not null. + */ +mxCellMarker.prototype.hasValidState = function() +{ + return this.validState != null; +}; + +/** + * Function: getValidState + * + * Returns the <validState>. + */ +mxCellMarker.prototype.getValidState = function() +{ + return this.validState; +}; + +/** + * Function: getMarkedState + * + * Returns the <markedState>. + */ +mxCellMarker.prototype.getMarkedState = function() +{ + return this.markedState; +}; + +/** + * Function: reset + * + * Resets the state of the cell marker. + */ +mxCellMarker.prototype.reset = function() +{ + this.validState = null; + + if (this.markedState != null) + { + this.markedState = null; + this.unmark(); + } +}; + +/** + * Function: process + * + * Processes the given event and cell and marks the state returned by + * <getState> with the color returned by <getMarkerColor>. If the + * markerColor is not null, then the state is stored in <markedState>. If + * <isValidState> returns true, then the state is stored in <validState> + * regardless of the marker color. The state is returned regardless of the + * marker color and valid state. + */ +mxCellMarker.prototype.process = function(me) +{ + var state = null; + + if (this.isEnabled()) + { + state = this.getState(me); + var isValid = (state != null) ? this.isValidState(state) : false; + var color = this.getMarkerColor(me.getEvent(), state, isValid); + + if (isValid) + { + this.validState = state; + } + else + { + this.validState = null; + } + + if (state != this.markedState || color != this.currentColor) + { + this.currentColor = color; + + if (state != null && this.currentColor != null) + { + this.markedState = state; + this.mark(); + } + else if (this.markedState != null) + { + this.markedState = null; + this.unmark(); + } + } + } + + return state; +}; + +/** + * Function: markCell + * + * Marks the given cell using the given color, or <validColor> if no color is specified. + */ +mxCellMarker.prototype.markCell = function(cell, color) +{ + var state = this.graph.getView().getState(cell); + + if (state != null) + { + this.currentColor = (color != null) ? color : this.validColor; + this.markedState = state; + this.mark(); + } +}; + +/** + * Function: mark + * + * Marks the <markedState> and fires a <mark> event. + */ +mxCellMarker.prototype.mark = function() +{ + this.highlight.setHighlightColor(this.currentColor); + this.highlight.highlight(this.markedState); + this.fireEvent(new mxEventObject(mxEvent.MARK, 'state', this.markedState)); +}; + +/** + * Function: unmark + * + * Hides the marker and fires a <mark> event. + */ +mxCellMarker.prototype.unmark = function() +{ + this.mark(); +}; + +/** + * Function: isValidState + * + * Returns true if the given <mxCellState> is a valid state. If this + * returns true, then the state is stored in <validState>. The return value + * of this method is used as the argument for <getMarkerColor>. + */ +mxCellMarker.prototype.isValidState = function(state) +{ + return true; +}; + +/** + * Function: getMarkerColor + * + * Returns the valid- or invalidColor depending on the value of isValid. + * The given <mxCellState> is ignored by this implementation. + */ +mxCellMarker.prototype.getMarkerColor = function(evt, state, isValid) +{ + return (isValid) ? this.validColor : this.invalidColor; +}; + +/** + * Function: getState + * + * Uses <getCell>, <getStateToMark> and <intersects> to return the + * <mxCellState> for the given <mxMouseEvent>. + */ +mxCellMarker.prototype.getState = function(me) +{ + var view = this.graph.getView(); + cell = this.getCell(me); + var state = this.getStateToMark(view.getState(cell)); + + return (state != null && this.intersects(state, me)) ? state : null; +}; + +/** + * Function: getCell + * + * Returns the <mxCell> for the given event and cell. This returns the + * given cell. + */ +mxCellMarker.prototype.getCell = function(me) +{ + return me.getCell(); +}; + +/** + * Function: getStateToMark + * + * Returns the <mxCellState> to be marked for the given <mxCellState> under + * the mouse. This returns the given state. + */ +mxCellMarker.prototype.getStateToMark = function(state) +{ + return state; +}; + +/** + * Function: intersects + * + * Returns true if the given coordinate pair intersects the given state. + * This returns true if the <hotspot> is 0 or the coordinates are inside + * the hotspot for the given cell state. + */ +mxCellMarker.prototype.intersects = function(state, me) +{ + if (this.hotspotEnabled) + { + return mxUtils.intersectsHotspot(state, me.getGraphX(), me.getGraphY(), + this.hotspot, mxConstants.MIN_HOTSPOT_SIZE, + mxConstants.MAX_HOTSPOT_SIZE); + } + + return true; +}; + +/** + * Function: destroy + * + * Destroys the handler and all its resources and DOM nodes. + */ +mxCellMarker.prototype.destroy = function() +{ + this.graph.getView().removeListener(this.resetHandler); + this.graph.getModel().removeListener(this.resetHandler); + this.highlight.destroy(); +}; diff --git a/src/js/handler/mxCellTracker.js b/src/js/handler/mxCellTracker.js new file mode 100644 index 0000000..5adcd6a --- /dev/null +++ b/src/js/handler/mxCellTracker.js @@ -0,0 +1,149 @@ +/** + * $Id: mxCellTracker.js,v 1.9 2011-08-28 09:49:46 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxCellTracker + * + * Event handler that highlights cells. Inherits from <mxCellMarker>. + * + * Example: + * + * (code) + * new mxCellTracker(graph, '#00FF00'); + * (end) + * + * For detecting dragEnter, dragOver and dragLeave on cells, the following + * code can be used: + * + * (code) + * graph.addMouseListener( + * { + * cell: null, + * mouseDown: function(sender, me) { }, + * mouseMove: function(sender, me) + * { + * var tmp = me.getCell(); + * + * if (tmp != this.cell) + * { + * if (this.cell != null) + * { + * this.dragLeave(me.getEvent(), this.cell); + * } + * + * this.cell = tmp; + * + * if (this.cell != null) + * { + * this.dragEnter(me.getEvent(), this.cell); + * } + * } + * + * if (this.cell != null) + * { + * this.dragOver(me.getEvent(), this.cell); + * } + * }, + * mouseUp: function(sender, me) { }, + * dragEnter: function(evt, cell) + * { + * mxLog.debug('dragEnter', cell.value); + * }, + * dragOver: function(evt, cell) + * { + * mxLog.debug('dragOver', cell.value); + * }, + * dragLeave: function(evt, cell) + * { + * mxLog.debug('dragLeave', cell.value); + * } + * }); + * (end) + * + * Constructor: mxCellTracker + * + * Constructs an event handler that highlights cells. + * + * Parameters: + * + * graph - Reference to the enclosing <mxGraph>. + * color - Color of the highlight. Default is blue. + * funct - Optional JavaScript function that is used to override + * <mxCellMarker.getCell>. + */ +function mxCellTracker(graph, color, funct) +{ + mxCellMarker.call(this, graph, color); + + this.graph.addMouseListener(this); + + if (funct != null) + { + this.getCell = funct; + } + + // Automatic deallocation of memory + if (mxClient.IS_IE) + { + mxEvent.addListener(window, 'unload', mxUtils.bind(this, function() + { + this.destroy(); + })); + } +}; + +/** + * Extends mxCellMarker. + */ +mxCellTracker.prototype = new mxCellMarker(); +mxCellTracker.prototype.constructor = mxCellTracker; + +/** + * Function: mouseDown + * + * Ignores the event. The event is not consumed. + */ +mxCellTracker.prototype.mouseDown = function(sender, me) { }; + +/** + * Function: mouseMove + * + * Handles the event by highlighting the cell under the mousepointer if it + * is over the hotspot region of the cell. + */ +mxCellTracker.prototype.mouseMove = function(sender, me) +{ + if (this.isEnabled()) + { + this.process(me); + } +}; + +/** + * Function: mouseUp + * + * Handles the event by reseting the highlight. + */ +mxCellTracker.prototype.mouseUp = function(sender, me) +{ + this.reset(); +}; + +/** + * Function: destroy + * + * Destroys the object and all its resources and DOM nodes. This doesn't + * normally need to be called. It is called automatically when the window + * unloads. + */ +mxCellTracker.prototype.destroy = function() +{ + if (!this.destroyed) + { + this.destroyed = true; + + this.graph.removeMouseListener(this); + mxCellMarker.prototype.destroy.apply(this); + } +}; diff --git a/src/js/handler/mxConnectionHandler.js b/src/js/handler/mxConnectionHandler.js new file mode 100644 index 0000000..07daaf8 --- /dev/null +++ b/src/js/handler/mxConnectionHandler.js @@ -0,0 +1,1969 @@ +/** + * $Id: mxConnectionHandler.js,v 1.216 2012-12-07 15:17:37 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxConnectionHandler + * + * Graph event handler that creates new connections. Uses <mxTerminalMarker> + * for finding and highlighting the source and target vertices and + * <factoryMethod> to create the edge instance. This handler is built-into + * <mxGraph.connectionHandler> and enabled using <mxGraph.setConnectable>. + * + * Example: + * + * (code) + * new mxConnectionHandler(graph, function(source, target, style) + * { + * edge = new mxCell('', new mxGeometry()); + * edge.setEdge(true); + * edge.setStyle(style); + * edge.geometry.relative = true; + * return edge; + * }); + * (end) + * + * Here is an alternative solution that just sets a specific user object for + * new edges by overriding <insertEdge>. + * + * (code) + * mxConnectionHandlerInsertEdge = mxConnectionHandler.prototype.insertEdge; + * mxConnectionHandler.prototype.insertEdge = function(parent, id, value, source, target, style) + * { + * value = 'Test'; + * + * return mxConnectionHandlerInsertEdge.apply(this, arguments); + * }; + * (end) + * + * Using images to trigger connections: + * + * This handler uses mxTerminalMarker to find the source and target cell for + * the new connection and creates a new edge using <connect>. The new edge is + * created using <createEdge> which in turn uses <factoryMethod> or creates a + * new default edge. + * + * The handler uses a "highlight-paradigm" for indicating if a cell is being + * used as a source or target terminal, as seen in MS Visio and other products. + * In order to allow both, moving and connecting cells at the same time, + * <mxConstants.DEFAULT_HOTSPOT> is used in the handler to determine the hotspot + * of a cell, that is, the region of the cell which is used to trigger a new + * connection. The constant is a value between 0 and 1 that specifies the + * amount of the width and height around the center to be used for the hotspot + * of a cell and its default value is 0.5. In addition, + * <mxConstants.MIN_HOTSPOT_SIZE> defines the minimum number of pixels for the + * width and height of the hotspot. + * + * This solution, while standards compliant, may be somewhat confusing because + * there is no visual indicator for the hotspot and the highlight is seen to + * switch on and off while the mouse is being moved in and out. Furthermore, + * this paradigm does not allow to create different connections depending on + * the highlighted hotspot as there is only one hotspot per cell and it + * normally does not allow cells to be moved and connected at the same time as + * there is no clear indication of the connectable area of the cell. + * + * To come across these issues, the handle has an additional <createIcons> hook + * with a default implementation that allows to create one icon to be used to + * trigger new connections. If this icon is specified, then new connections can + * only be created if the image is clicked while the cell is being highlighted. + * The <createIcons> hook may be overridden to create more than one + * <mxImageShape> for creating new connections, but the default implementation + * supports one image and is used as follows: + * + * In order to display the "connect image" whenever the mouse is over the cell, + * an DEFAULT_HOTSPOT of 1 should be used: + * + * (code) + * mxConstants.DEFAULT_HOTSPOT = 1; + * (end) + * + * In order to avoid confusion with the highlighting, the highlight color + * should not be used with a connect image: + * + * (code) + * mxConstants.HIGHLIGHT_COLOR = null; + * (end) + * + * To install the image, the connectImage field of the mxConnectionHandler must + * be assigned a new <mxImage> instance: + * + * (code) + * mxConnectionHandler.prototype.connectImage = new mxImage('images/green-dot.gif', 14, 14); + * (end) + * + * This will use the green-dot.gif with a width and height of 14 pixels as the + * image to trigger new connections. In createIcons the icon field of the + * handler will be set in order to remember the icon that has been clicked for + * creating the new connection. This field will be available under selectedIcon + * in the connect method, which may be overridden to take the icon that + * triggered the new connection into account. This is useful if more than one + * icon may be used to create a connection. + * + * Group: Events + * + * Event: mxEvent.START + * + * Fires when a new connection is being created by the user. The <code>state</code> + * property contains the state of the source cell. + * + * Event: mxEvent.CONNECT + * + * Fires between begin- and endUpdate in <connect>. The <code>cell</code> + * property contains the inserted edge, the <code>event</code> and <code>target</code> + * properties contain the respective arguments that were passed to <connect> (where + * target corresponds to the dropTarget argument). + * + * Note that the target is the cell under the mouse where the mouse button was released. + * Depending on the logic in the handler, this doesn't necessarily have to be the target + * of the inserted edge. To print the source, target or any optional ports IDs that the + * edge is connected to, the following code can be used. To get more details about the + * actual connection point, <mxGraph.getConnectionConstraint> can be used. To resolve + * the port IDs, use <mxGraphModel.getCell>. + * + * (code) + * graph.connectionHandler.addListener(mxEvent.CONNECT, function(sender, evt) + * { + * var edge = evt.getProperty('cell'); + * var source = graph.getModel().getTerminal(edge, true); + * var target = graph.getModel().getTerminal(edge, false); + * + * var style = graph.getCellStyle(edge); + * var sourcePortId = style[mxConstants.STYLE_SOURCE_PORT]; + * var targetPortId = style[mxConstants.STYLE_TARGET_PORT]; + * + * mxLog.show(); + * mxLog.debug('connect', edge, source.id, target.id, sourcePortId, targetPortId); + * }); + * (end) + * + * Event: mxEvent.RESET + * + * Fires when the <reset> method is invoked. + * + * Constructor: mxConnectionHandler + * + * Constructs an event handler that connects vertices using the specified + * factory method to create the new edges. Modify + * <mxConstants.ACTIVE_REGION> to setup the region on a cell which triggers + * the creation of a new connection or use connect icons as explained + * above. + * + * Parameters: + * + * graph - Reference to the enclosing <mxGraph>. + * factoryMethod - Optional function to create the edge. The function takes + * the source and target <mxCell> as the first and second argument and an + * optional cell style from the preview as the third argument. It returns + * the <mxCell> that represents the new edge. + */ +function mxConnectionHandler(graph, factoryMethod) +{ + if (graph != null) + { + this.graph = graph; + this.factoryMethod = factoryMethod; + this.init(); + } +}; + +/** + * Extends mxEventSource. + */ +mxConnectionHandler.prototype = new mxEventSource(); +mxConnectionHandler.prototype.constructor = mxConnectionHandler; + +/** + * Variable: graph + * + * Reference to the enclosing <mxGraph>. + */ +mxConnectionHandler.prototype.graph = null; + +/** + * Variable: factoryMethod + * + * Function that is used for creating new edges. The function takes the + * source and target <mxCell> as the first and second argument and returns + * a new <mxCell> that represents the edge. This is used in <createEdge>. + */ +mxConnectionHandler.prototype.factoryMethod = true; + +/** + * Variable: moveIconFront + * + * Specifies if icons should be displayed inside the graph container instead + * of the overlay pane. This is used for HTML labels on vertices which hide + * the connect icon. This has precendence over <moveIconBack> when set + * to true. Default is false. + */ +mxConnectionHandler.prototype.moveIconFront = false; + +/** + * Variable: moveIconBack + * + * Specifies if icons should be moved to the back of the overlay pane. This can + * be set to true if the icons of the connection handler conflict with other + * handles, such as the vertex label move handle. Default is false. + */ +mxConnectionHandler.prototype.moveIconBack = false; + +/** + * Variable: connectImage + * + * <mxImage> that is used to trigger the creation of a new connection. This + * is used in <createIcons>. Default is null. + */ +mxConnectionHandler.prototype.connectImage = null; + +/** + * Variable: targetConnectImage + * + * Specifies if the connect icon should be centered on the target state + * while connections are being previewed. Default is false. + */ +mxConnectionHandler.prototype.targetConnectImage = false; + +/** + * Variable: enabled + * + * Specifies if events are handled. Default is true. + */ +mxConnectionHandler.prototype.enabled = true; + +/** + * Variable: select + * + * Specifies if new edges should be selected. Default is true. + */ +mxConnectionHandler.prototype.select = true; + +/** + * Variable: createTarget + * + * Specifies if <createTargetVertex> should be called if no target was under the + * mouse for the new connection. Setting this to true means the connection + * will be drawn as valid if no target is under the mouse, and + * <createTargetVertex> will be called before the connection is created between + * the source cell and the newly created vertex in <createTargetVertex>, which + * can be overridden to create a new target. Default is false. + */ +mxConnectionHandler.prototype.createTarget = false; + +/** + * Variable: marker + * + * Holds the <mxTerminalMarker> used for finding source and target cells. + */ +mxConnectionHandler.prototype.marker = null; + +/** + * Variable: constraintHandler + * + * Holds the <mxConstraintHandler> used for drawing and highlighting + * constraints. + */ +mxConnectionHandler.prototype.constraintHandler = null; + +/** + * Variable: error + * + * Holds the current validation error while connections are being created. + */ +mxConnectionHandler.prototype.error = null; + +/** + * Variable: waypointsEnabled + * + * Specifies if single clicks should add waypoints on the new edge. Default is + * false. + */ +mxConnectionHandler.prototype.waypointsEnabled = false; + +/** + * Variable: tapAndHoldEnabled + * + * Specifies if tap and hold should be used for starting connections on touch-based + * devices. Default is true. + */ +mxConnectionHandler.prototype.tapAndHoldEnabled = true; + +/** + * Variable: tapAndHoldDelay + * + * Specifies the time for a tap and hold. Default is 500 ms. + */ +mxConnectionHandler.prototype.tapAndHoldDelay = 500; + +/** + * Variable: tapAndHoldInProgress + * + * True if the timer for tap and hold events is running. + */ +mxConnectionHandler.prototype.tapAndHoldInProgress = false; + +/** + * Variable: tapAndHoldValid + * + * True as long as the timer is running and the touch events + * stay within the given <tapAndHoldTolerance>. + */ +mxConnectionHandler.prototype.tapAndHoldValid = false; + +/** + * Variable: tapAndHoldTolerance + * + * Specifies the tolerance for a tap and hold. Default is 4 pixels. + */ +mxConnectionHandler.prototype.tapAndHoldTolerance = 4; + +/** + * Variable: initialTouchX + * + * Holds the x-coordinate of the intial touch event for tap and hold. + */ +mxConnectionHandler.prototype.initialTouchX = 0; + +/** + * Variable: initialTouchY + * + * Holds the y-coordinate of the intial touch event for tap and hold. + */ +mxConnectionHandler.prototype.initialTouchY = 0; + +/** + * Variable: ignoreMouseDown + * + * Specifies if the connection handler should ignore the state of the mouse + * button when highlighting the source. Default is false, that is, the + * handler only highlights the source if no button is being pressed. + */ +mxConnectionHandler.prototype.ignoreMouseDown = false; + +/** + * Variable: first + * + * Holds the <mxPoint> where the mouseDown took place while the handler is + * active. + */ +mxConnectionHandler.prototype.first = null; + +/** + * Variable: connectIconOffset + * + * Holds the offset for connect icons during connection preview. + * Default is mxPoint(0, <mxConstants.TOOLTIP_VERTICAL_OFFSET>). + * Note that placing the icon under the mouse pointer with an + * offset of (0,0) will affect hit detection. + */ +mxConnectionHandler.prototype.connectIconOffset = new mxPoint(0, mxConstants.TOOLTIP_VERTICAL_OFFSET); + +/** + * Variable: edgeState + * + * Optional <mxCellState> that represents the preview edge while the + * handler is active. This is created in <createEdgeState>. + */ +mxConnectionHandler.prototype.edgeState = null; + +/** + * Variable: changeHandler + * + * Holds the change event listener for later removal. + */ +mxConnectionHandler.prototype.changeHandler = null; + +/** + * Variable: drillHandler + * + * Holds the drill event listener for later removal. + */ +mxConnectionHandler.prototype.drillHandler = null; + +/** + * Variable: mouseDownCounter + * + * Counts the number of mouseDown events since the start. The initial mouse + * down event counts as 1. + */ +mxConnectionHandler.prototype.mouseDownCounter = 0; + +/** + * Variable: movePreviewAway + * + * Switch to enable moving the preview away from the mousepointer. This is required in browsers + * where the preview cannot be made transparent to events and if the built-in hit detection on + * the HTML elements in the page should be used. Default is the value of <mxClient.IS_VML>. + */ +mxConnectionHandler.prototype.movePreviewAway = mxClient.IS_VML; + +/** + * Function: isEnabled + * + * Returns true if events are handled. This implementation + * returns <enabled>. + */ +mxConnectionHandler.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Enables or disables event handling. This implementation + * updates <enabled>. + * + * Parameters: + * + * enabled - Boolean that specifies the new enabled state. + */ +mxConnectionHandler.prototype.setEnabled = function(enabled) +{ + this.enabled = enabled; +}; + +/** + * Function: isCreateTarget + * + * Returns <createTarget>. + */ +mxConnectionHandler.prototype.isCreateTarget = function() +{ + return this.createTarget; +}; + +/** + * Function: setCreateTarget + * + * Sets <createTarget>. + */ +mxConnectionHandler.prototype.setCreateTarget = function(value) +{ + this.createTarget = value; +}; + +/** + * Function: createShape + * + * Creates the preview shape for new connections. + */ +mxConnectionHandler.prototype.createShape = function() +{ + // Creates the edge preview + var shape = new mxPolyline([], mxConstants.INVALID_COLOR); + shape.isDashed = true; + shape.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ? + mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG; + shape.init(this.graph.getView().getOverlayPane()); + + // Event-transparency + if (this.graph.dialect == mxConstants.DIALECT_SVG) + { + // Sets event transparency on the internal shapes that represent + // the actual dashed line on the screen + shape.pipe.setAttribute('style', 'pointer-events:none;'); + shape.innerNode.setAttribute('style', 'pointer-events:none;'); + } + else + { + // Workaround no event transparency for preview in IE + // FIXME: 3,3 pixel offset for custom hit detection in IE + var getState = mxUtils.bind(this, function(evt) + { + var pt = mxUtils.convertPoint(this.graph.container, mxEvent.getClientX(evt), mxEvent.getClientY(evt)); + + return this.graph.view.getState(this.graph.getCellAt(pt.x, pt.y)); + }); + + // Redirects events on the shape to the graph + mxEvent.redirectMouseEvents(shape.node, this.graph, getState); + } + + return shape; +}; + +/** + * Function: init + * + * Initializes the shapes required for this connection handler. This should + * be invoked if <mxGraph.container> is assigned after the connection + * handler has been created. + */ +mxConnectionHandler.prototype.init = function() +{ + this.graph.addMouseListener(this); + this.marker = this.createMarker(); + this.constraintHandler = new mxConstraintHandler(this.graph); + + // Redraws the icons if the graph changes + this.changeHandler = mxUtils.bind(this, function(sender) + { + if (this.iconState != null) + { + this.iconState = this.graph.getView().getState(this.iconState.cell); + } + + if (this.iconState != null) + { + this.redrawIcons(this.icons, this.iconState); + } + else + { + this.destroyIcons(this.icons); + this.previous = null; + } + + this.constraintHandler.reset(); + }); + + this.graph.getModel().addListener(mxEvent.CHANGE, this.changeHandler); + this.graph.getView().addListener(mxEvent.SCALE, this.changeHandler); + this.graph.getView().addListener(mxEvent.TRANSLATE, this.changeHandler); + this.graph.getView().addListener(mxEvent.SCALE_AND_TRANSLATE, this.changeHandler); + + // Removes the icon if we step into/up or start editing + this.drillHandler = mxUtils.bind(this, function(sender) + { + this.destroyIcons(this.icons); + }); + + this.graph.addListener(mxEvent.START_EDITING, this.drillHandler); + this.graph.getView().addListener(mxEvent.DOWN, this.drillHandler); + this.graph.getView().addListener(mxEvent.UP, this.drillHandler); +}; + +/** + * Function: isConnectableCell + * + * Returns true if the given cell is connectable. This is a hook to + * disable floating connections. This implementation returns true. + */ +mxConnectionHandler.prototype.isConnectableCell = function(cell) +{ + return true; +}; + +/** + * Function: createMarker + * + * Creates and returns the <mxCellMarker> used in <marker>. + */ +mxConnectionHandler.prototype.createMarker = function() +{ + var marker = new mxCellMarker(this.graph); + marker.hotspotEnabled = true; + + // Overrides to return cell at location only if valid (so that + // there is no highlight for invalid cells) + marker.getCell = mxUtils.bind(this, function(evt, cell) + { + var cell = mxCellMarker.prototype.getCell.apply(marker, arguments); + this.error = null; + + if (!this.isConnectableCell(cell)) + { + return null; + } + + if (cell != null) + { + if (this.isConnecting()) + { + if (this.previous != null) + { + this.error = this.validateConnection(this.previous.cell, cell); + + if (this.error != null && this.error.length == 0) + { + cell = null; + + // Enables create target inside groups + if (this.isCreateTarget()) + { + this.error = null; + } + } + } + } + else if (!this.isValidSource(cell)) + { + cell = null; + } + } + else if (this.isConnecting() && !this.isCreateTarget() && + !this.graph.allowDanglingEdges) + { + this.error = ''; + } + + return cell; + }); + + // Sets the highlight color according to validateConnection + marker.isValidState = mxUtils.bind(this, function(state) + { + if (this.isConnecting()) + { + return this.error == null; + } + else + { + return mxCellMarker.prototype.isValidState.apply(marker, arguments); + } + }); + + // Overrides to use marker color only in highlight mode or for + // target selection + marker.getMarkerColor = mxUtils.bind(this, function(evt, state, isValid) + { + return (this.connectImage == null || this.isConnecting()) ? + mxCellMarker.prototype.getMarkerColor.apply(marker, arguments) : + null; + }); + + // Overrides to use hotspot only for source selection otherwise + // intersects always returns true when over a cell + marker.intersects = mxUtils.bind(this, function(state, evt) + { + if (this.connectImage != null || this.isConnecting()) + { + return true; + } + + return mxCellMarker.prototype.intersects.apply(marker, arguments); + }); + + return marker; +}; + +/** + * Function: start + * + * Starts a new connection for the given state and coordinates. + */ +mxConnectionHandler.prototype.start = function(state, x, y, edgeState) +{ + this.previous = state; + this.first = new mxPoint(x, y); + this.edgeState = (edgeState != null) ? edgeState : this.createEdgeState(null); + + // Marks the source state + this.marker.currentColor = this.marker.validColor; + this.marker.markedState = state; + this.marker.mark(); + + this.fireEvent(new mxEventObject(mxEvent.START, 'state', this.previous)); +}; + +/** + * Function: isConnecting + * + * Returns true if the source terminal has been clicked and a new + * connection is currently being previewed. + */ +mxConnectionHandler.prototype.isConnecting = function() +{ + return this.first != null && this.shape != null; +}; + +/** + * Function: isValidSource + * + * Returns <mxGraph.isValidSource> for the given source terminal. + * + * Parameters: + * + * cell - <mxCell> that represents the source terminal. + */ +mxConnectionHandler.prototype.isValidSource = function(cell) +{ + return this.graph.isValidSource(cell); +}; + +/** + * Function: isValidTarget + * + * Returns true. The call to <mxGraph.isValidTarget> is implicit by calling + * <mxGraph.getEdgeValidationError> in <validateConnection>. This is an + * additional hook for disabling certain targets in this specific handler. + * + * Parameters: + * + * cell - <mxCell> that represents the target terminal. + */ +mxConnectionHandler.prototype.isValidTarget = function(cell) +{ + return true; +}; + +/** + * Function: validateConnection + * + * Returns the error message or an empty string if the connection for the + * given source target pair is not valid. Otherwise it returns null. This + * implementation uses <mxGraph.getEdgeValidationError>. + * + * Parameters: + * + * source - <mxCell> that represents the source terminal. + * target - <mxCell> that represents the target terminal. + */ +mxConnectionHandler.prototype.validateConnection = function(source, target) +{ + if (!this.isValidTarget(target)) + { + return ''; + } + + return this.graph.getEdgeValidationError(null, source, target); +}; + +/** + * Function: getConnectImage + * + * Hook to return the <mxImage> used for the connection icon of the given + * <mxCellState>. This implementation returns <connectImage>. + * + * Parameters: + * + * state - <mxCellState> whose connect image should be returned. + */ +mxConnectionHandler.prototype.getConnectImage = function(state) +{ + return this.connectImage; +}; + +/** + * Function: isMoveIconToFrontForState + * + * Returns true if the state has a HTML label in the graph's container, otherwise + * it returns <moveIconFront>. + * + * Parameters: + * + * state - <mxCellState> whose connect icons should be returned. + */ +mxConnectionHandler.prototype.isMoveIconToFrontForState = function(state) +{ + if (state.text != null && state.text.node.parentNode == this.graph.container) + { + return true; + } + + return this.moveIconFront; +}; + +/** + * Function: createIcons + * + * Creates the array <mxImageShapes> that represent the connect icons for + * the given <mxCellState>. + * + * Parameters: + * + * state - <mxCellState> whose connect icons should be returned. + */ +mxConnectionHandler.prototype.createIcons = function(state) +{ + var image = this.getConnectImage(state); + + if (image != null && state != null) + { + this.iconState = state; + var icons = []; + + // Cannot use HTML for the connect icons because the icon receives all + // mouse move events in IE, must use VML and SVG instead even if the + // connect-icon appears behind the selection border and the selection + // border consumes the events before the icon gets a chance + var bounds = new mxRectangle(0, 0, image.width, image.height); + var icon = new mxImageShape(bounds, image.src, null, null, 0); + icon.preserveImageAspect = false; + + if (this.isMoveIconToFrontForState(state)) + { + icon.dialect = mxConstants.DIALECT_STRICTHTML; + icon.init(this.graph.container); + } + else + { + icon.dialect = (this.graph.dialect == mxConstants.DIALECT_SVG) ? + mxConstants.DIALECT_SVG : + mxConstants.DIALECT_VML; + icon.init(this.graph.getView().getOverlayPane()); + + // Move the icon back in the overlay pane + if (this.moveIconBack && icon.node.previousSibling != null) + { + icon.node.parentNode.insertBefore(icon.node, icon.node.parentNode.firstChild); + } + } + + icon.node.style.cursor = mxConstants.CURSOR_CONNECT; + + // Events transparency + var getState = mxUtils.bind(this, function() + { + return (this.currentState != null) ? this.currentState : state; + }); + + // Updates the local icon before firing the mouse down event. + var mouseDown = mxUtils.bind(this, function(evt) + { + if (!mxEvent.isConsumed(evt)) + { + this.icon = icon; + this.graph.fireMouseEvent(mxEvent.MOUSE_DOWN, + new mxMouseEvent(evt, getState())); + } + }); + + mxEvent.redirectMouseEvents(icon.node, this.graph, getState, mouseDown); + + icons.push(icon); + this.redrawIcons(icons, this.iconState); + + return icons; + } + + return null; +}; + +/** + * Function: redrawIcons + * + * Redraws the given array of <mxImageShapes>. + * + * Parameters: + * + * icons - Optional array of <mxImageShapes> to be redrawn. + */ +mxConnectionHandler.prototype.redrawIcons = function(icons, state) +{ + if (icons != null && icons[0] != null && state != null) + { + var pos = this.getIconPosition(icons[0], state); + icons[0].bounds.x = pos.x; + icons[0].bounds.y = pos.y; + icons[0].redraw(); + } +}; + +/** + * Function: redrawIcons + * + * Redraws the given array of <mxImageShapes>. + * + * Parameters: + * + * icons - Optional array of <mxImageShapes> to be redrawn. + */ +mxConnectionHandler.prototype.getIconPosition = function(icon, state) +{ + var scale = this.graph.getView().scale; + var cx = state.getCenterX(); + var cy = state.getCenterY(); + + if (this.graph.isSwimlane(state.cell)) + { + var size = this.graph.getStartSize(state.cell); + + cx = (size.width != 0) ? state.x + size.width * scale / 2 : cx; + cy = (size.height != 0) ? state.y + size.height * scale / 2 : cy; + } + + return new mxPoint(cx - icon.bounds.width / 2, + cy - icon.bounds.height / 2); +}; + +/** + * Function: destroyIcons + * + * Destroys the given array of <mxImageShapes>. + * + * Parameters: + * + * icons - Optional array of <mxImageShapes> to be destroyed. + */ +mxConnectionHandler.prototype.destroyIcons = function(icons) +{ + if (icons != null) + { + this.iconState = null; + + for (var i = 0; i < icons.length; i++) + { + icons[i].destroy(); + } + } +}; + +/** + * Function: isStartEvent + * + * Returns true if the given mouse down event should start this handler. The + * This implementation returns true if the event does not force marquee + * selection, and the currentConstraint and currentFocus of the + * <constraintHandler> are not null, or <previous> and <error> are not null and + * <icons> is null or <icons> and <icon> are not null. + */ +mxConnectionHandler.prototype.isStartEvent = function(me) +{ + return !this.graph.isForceMarqueeEvent(me.getEvent()) && + ((this.constraintHandler.currentFocus != null && + this.constraintHandler.currentConstraint != null) || + (this.previous != null && this.error == null && + (this.icons == null || (this.icons != null && this.icon != null)))); +}; + +/** + * Function: mouseDown + * + * Handles the event by initiating a new connection. + */ +mxConnectionHandler.prototype.mouseDown = function(sender, me) +{ + this.mouseDownCounter++; + + if (this.isEnabled() && this.graph.isEnabled() && !me.isConsumed() && + !this.isConnecting() && this.isStartEvent(me)) + { + if (this.constraintHandler.currentConstraint != null && + this.constraintHandler.currentFocus != null && + this.constraintHandler.currentPoint != null) + { + this.sourceConstraint = this.constraintHandler.currentConstraint; + this.previous = this.constraintHandler.currentFocus; + this.first = this.constraintHandler.currentPoint.clone(); + } + else + { + // Stores the location of the initial mousedown + this.first = new mxPoint(me.getGraphX(), me.getGraphY()); + } + + this.edgeState = this.createEdgeState(me); + this.mouseDownCounter = 1; + + if (this.waypointsEnabled && this.shape == null) + { + this.waypoints = null; + this.shape = this.createShape(); + } + + // Stores the starting point in the geometry of the preview + if (this.previous == null && this.edgeState != null) + { + var pt = this.graph.getPointForEvent(me.getEvent()); + this.edgeState.cell.geometry.setTerminalPoint(pt, true); + } + + this.fireEvent(new mxEventObject(mxEvent.START, 'state', this.previous)); + + me.consume(); + } + // Handles connecting via tap and hold + else if (mxClient.IS_TOUCH && this.tapAndHoldEnabled && !this.tapAndHoldInProgress && + this.isEnabled() && this.graph.isEnabled() && !this.isConnecting()) + { + this.tapAndHoldInProgress = true; + this.initialTouchX = me.getX(); + this.initialTouchY = me.getY(); + var state = this.graph.view.getState(this.marker.getCell(me)); + + var handler = function() + { + if (this.tapAndHoldValid) + { + this.tapAndHold(me, state); + } + + this.tapAndHoldInProgress = false; + this.tapAndHoldValid = false; + }; + + if (this.tapAndHoldThread) + { + window.clearTimeout(this.tapAndHoldThread); + } + + this.tapAndHoldThread = window.setTimeout(mxUtils.bind(this, handler), this.tapAndHoldDelay); + this.tapAndHoldValid = true; + } + + this.selectedIcon = this.icon; + this.icon = null; +}; + +/** + * Function: tapAndHold + * + * Handles the <mxMouseEvent> by highlighting the <mxCellState>. + * + * Parameters: + * + * me - <mxMouseEvent> that represents the touch event. + * state - Optional <mxCellState> that is associated with the event. + */ +mxConnectionHandler.prototype.tapAndHold = function(me, state) +{ + if (state != null) + { + this.marker.currentColor = this.marker.validColor; + this.marker.markedState = state; + this.marker.mark(); + + this.first = new mxPoint(me.getGraphX(), me.getGraphY()); + this.edgeState = this.createEdgeState(me); + this.previous = state; + this.fireEvent(new mxEventObject(mxEvent.START, 'state', this.previous)); + } +}; + +/** + * Function: isImmediateConnectSource + * + * Returns true if a tap on the given source state should immediately start + * connecting. This implementation returns true if the state is not movable + * in the graph. + */ +mxConnectionHandler.prototype.isImmediateConnectSource = function(state) +{ + return !this.graph.isCellMovable(state.cell); +}; + +/** + * Function: createEdgeState + * + * Hook to return an <mxCellState> which may be used during the preview. + * This implementation returns null. + * + * Use the following code to create a preview for an existing edge style: + * + * [code] + * graph.connectionHandler.createEdgeState = function(me) + * { + * var edge = graph.createEdge(null, null, null, null, null, 'edgeStyle=elbowEdgeStyle'); + * + * return new mxCellState(this.graph.view, edge, this.graph.getCellStyle(edge)); + * }; + * [/code] + */ +mxConnectionHandler.prototype.createEdgeState = function(me) +{ + return null; +}; + +/** + * Function: updateCurrentState + * + * Updates the current state for a given mouse move event by using + * the <marker>. + */ +mxConnectionHandler.prototype.updateCurrentState = function(me) +{ + var state = this.marker.process(me); + this.constraintHandler.update(me, this.first == null); + this.currentState = state; +}; + +/** + * Function: convertWaypoint + * + * Converts the given point from screen coordinates to model coordinates. + */ +mxConnectionHandler.prototype.convertWaypoint = function(point) +{ + var scale = this.graph.getView().getScale(); + var tr = this.graph.getView().getTranslate(); + + point.x = point.x / scale - tr.x; + point.y = point.y / scale - tr.y; +}; + +/** + * Function: mouseMove + * + * Handles the event by updating the preview edge or by highlighting + * a possible source or target terminal. + */ +mxConnectionHandler.prototype.mouseMove = function(sender, me) +{ + if (this.tapAndHoldValid) + { + this.tapAndHoldValid = + Math.abs(this.initialTouchX - me.getX()) < this.tapAndHoldTolerance && + Math.abs(this.initialTouchY - me.getY()) < this.tapAndHoldTolerance; + } + + if (!me.isConsumed() && (this.ignoreMouseDown || this.first != null || !this.graph.isMouseDown)) + { + // Handles special case when handler is disabled during highlight + if (!this.isEnabled() && this.currentState != null) + { + this.destroyIcons(this.icons); + this.currentState = null; + } + + if (this.first != null || (this.isEnabled() && this.graph.isEnabled())) + { + this.updateCurrentState(me); + } + + if (this.first != null) + { + var view = this.graph.getView(); + var scale = view.scale; + var point = new mxPoint(this.graph.snap(me.getGraphX() / scale) * scale, + this.graph.snap(me.getGraphY() / scale) * scale); + var constraint = null; + var current = point; + + // Uses the current point from the constraint handler if available + if (this.constraintHandler.currentConstraint != null && + this.constraintHandler.currentFocus != null && + this.constraintHandler.currentPoint != null) + { + constraint = this.constraintHandler.currentConstraint; + current = this.constraintHandler.currentPoint.clone(); + } + + var pt2 = this.first; + + // Moves the connect icon with the mouse + if (this.selectedIcon != null) + { + var w = this.selectedIcon.bounds.width; + var h = this.selectedIcon.bounds.height; + + if (this.currentState != null && this.targetConnectImage) + { + var pos = this.getIconPosition(this.selectedIcon, this.currentState); + this.selectedIcon.bounds.x = pos.x; + this.selectedIcon.bounds.y = pos.y; + } + else + { + var bounds = new mxRectangle(me.getGraphX() + this.connectIconOffset.x, + me.getGraphY() + this.connectIconOffset.y, w, h); + this.selectedIcon.bounds = bounds; + } + + this.selectedIcon.redraw(); + } + + // Uses edge state to compute the terminal points + if (this.edgeState != null) + { + this.edgeState.absolutePoints = [null, (this.currentState != null) ? null : current]; + this.graph.view.updateFixedTerminalPoint(this.edgeState, this.previous, true, this.sourceConstraint); + + if (this.currentState != null) + { + if (constraint == null) + { + constraint = this.graph.getConnectionConstraint(this.edgeState, this.previous, false); + } + + this.edgeState.setAbsoluteTerminalPoint(null, false); + this.graph.view.updateFixedTerminalPoint(this.edgeState, this.currentState, false, constraint); + } + + // Scales and translates the waypoints to the model + var realPoints = null; + + if (this.waypoints != null) + { + realPoints = []; + + for (var i = 0; i < this.waypoints.length; i++) + { + var pt = this.waypoints[i].clone(); + this.convertWaypoint(pt); + realPoints[i] = pt; + } + } + + this.graph.view.updatePoints(this.edgeState, realPoints, this.previous, this.currentState); + this.graph.view.updateFloatingTerminalPoints(this.edgeState, this.previous, this.currentState); + current = this.edgeState.absolutePoints[this.edgeState.absolutePoints.length - 1]; + pt2 = this.edgeState.absolutePoints[0]; + } + else + { + if (this.currentState != null) + { + if (this.constraintHandler.currentConstraint == null) + { + var tmp = this.getTargetPerimeterPoint(this.currentState, me); + + if (tmp != null) + { + current = tmp; + } + } + } + + // Computes the source perimeter point + if (this.sourceConstraint == null && this.previous != null) + { + var next = (this.waypoints != null && this.waypoints.length > 0) ? + this.waypoints[0] : current; + var tmp = this.getSourcePerimeterPoint(this.previous, next, me); + + if (tmp != null) + { + pt2 = tmp; + } + } + } + + // Makes sure the cell under the mousepointer can be detected + // by moving the preview shape away from the mouse. This + // makes sure the preview shape does not prevent the detection + // of the cell under the mousepointer even for slow gestures. + if (this.currentState == null && this.movePreviewAway) + { + var tmp = pt2; + + if (this.edgeState != null && this.edgeState.absolutePoints.length > 2) + { + var tmp2 = this.edgeState.absolutePoints[this.edgeState.absolutePoints.length - 2]; + + if (tmp2 != null) + { + tmp = tmp2; + } + } + + var dx = current.x - tmp.x; + var dy = current.y - tmp.y; + + var len = Math.sqrt(dx * dx + dy * dy); + + if (len == 0) + { + return; + } + + current.x -= dx * 4 / len; + current.y -= dy * 4 / len; + } + + // Creates the preview shape (lazy) + if (this.shape == null) + { + var dx = Math.abs(point.x - this.first.x); + var dy = Math.abs(point.y - this.first.y); + + if (dx > this.graph.tolerance || dy > this.graph.tolerance) + { + this.shape = this.createShape(); + + // Revalidates current connection + this.updateCurrentState(me); + } + } + + // Updates the points in the preview edge + if (this.shape != null) + { + if (this.edgeState != null) + { + this.shape.points = this.edgeState.absolutePoints; + } + else + { + var pts = [pt2]; + + if (this.waypoints != null) + { + pts = pts.concat(this.waypoints); + } + + pts.push(current); + this.shape.points = pts; + } + + this.drawPreview(); + } + + mxEvent.consume(me.getEvent()); + me.consume(); + } + else if(!this.isEnabled() || !this.graph.isEnabled()) + { + this.constraintHandler.reset(); + } + else if (this.previous != this.currentState && this.edgeState == null) + { + this.destroyIcons(this.icons); + this.icons = null; + + // Sets the cursor on the current shape + if (this.currentState != null && this.error == null) + { + this.icons = this.createIcons(this.currentState); + + if (this.icons == null) + { + this.currentState.setCursor(mxConstants.CURSOR_CONNECT); + me.consume(); + } + } + + this.previous = this.currentState; + } + else if (this.previous == this.currentState && this.currentState != null && this.icons == null && + !this.graph.isMouseDown) + { + // Makes sure that no cursors are changed + me.consume(); + } + + if (this.constraintHandler.currentConstraint != null) + { + this.marker.reset(); + } + + if (!this.graph.isMouseDown && this.currentState != null && this.icons != null) + { + var hitsIcon = false; + var target = me.getSource(); + + for (var i = 0; i < this.icons.length && !hitsIcon; i++) + { + hitsIcon = target == this.icons[i].node || target.parentNode == this.icons[i].node; + } + + if (!hitsIcon) + { + this.updateIcons(this.currentState, this.icons, me); + } + } + } + else + { + this.constraintHandler.reset(); + } +}; + +/** + * Function: getTargetPerimeterPoint + * + * Returns the perimeter point for the given target state. + * + * Parameters: + * + * state - <mxCellState> that represents the target cell state. + * me - <mxMouseEvent> that represents the mouse move. + */ +mxConnectionHandler.prototype.getTargetPerimeterPoint = function(state, me) +{ + var result = null; + var view = state.view; + var targetPerimeter = view.getPerimeterFunction(state); + + if (targetPerimeter != null) + { + var next = (this.waypoints != null && this.waypoints.length > 0) ? + this.waypoints[this.waypoints.length - 1] : + new mxPoint(this.previous.getCenterX(), this.previous.getCenterY()); + var tmp = targetPerimeter(view.getPerimeterBounds(state), + this.edgeState, next, false); + + if (tmp != null) + { + result = tmp; + } + } + else + { + result = new mxPoint(state.getCenterX(), state.getCenterY()); + } + + return result; +}; + +/** + * Function: getSourcePerimeterPoint + * + * Hook to update the icon position(s) based on a mouseOver event. This is + * an empty implementation. + * + * Parameters: + * + * state - <mxCellState> that represents the target cell state. + * next - <mxPoint> that represents the next point along the previewed edge. + * me - <mxMouseEvent> that represents the mouse move. + */ +mxConnectionHandler.prototype.getSourcePerimeterPoint = function(state, next, me) +{ + var result = null; + var view = state.view; + var sourcePerimeter = view.getPerimeterFunction(state); + + if (sourcePerimeter != null) + { + var tmp = sourcePerimeter(view.getPerimeterBounds(state), state, next, false); + + if (tmp != null) + { + result = tmp; + } + } + else + { + result = new mxPoint(state.getCenterX(), state.getCenterY()); + } + + return result; +}; + + +/** + * Function: updateIcons + * + * Hook to update the icon position(s) based on a mouseOver event. This is + * an empty implementation. + * + * Parameters: + * + * state - <mxCellState> under the mouse. + * icons - Array of currently displayed icons. + * me - <mxMouseEvent> that contains the mouse event. + */ +mxConnectionHandler.prototype.updateIcons = function(state, icons, me) +{ + // empty +}; + +/** + * Function: isStopEvent + * + * Returns true if the given mouse up event should stop this handler. The + * connection will be created if <error> is null. Note that this is only + * called if <waypointsEnabled> is true. This implemtation returns true + * if there is a cell state in the given event. + */ +mxConnectionHandler.prototype.isStopEvent = function(me) +{ + return me.getState() != null; +}; + +/** + * Function: addWaypoint + * + * Adds the waypoint for the given event to <waypoints>. + */ +mxConnectionHandler.prototype.addWaypointForEvent = function(me) +{ + var point = mxUtils.convertPoint(this.graph.container, me.getX(), me.getY()); + var dx = Math.abs(point.x - this.first.x); + var dy = Math.abs(point.y - this.first.y); + var addPoint = this.waypoints != null || (this.mouseDownCounter > 1 && + (dx > this.graph.tolerance || dy > this.graph.tolerance)); + + if (addPoint) + { + if (this.waypoints == null) + { + this.waypoints = []; + } + + var scale = this.graph.view.scale; + var point = new mxPoint(this.graph.snap(me.getGraphX() / scale) * scale, + this.graph.snap(me.getGraphY() / scale) * scale); + this.waypoints.push(point); + } +}; + +/** + * Function: mouseUp + * + * Handles the event by inserting the new connection. + */ +mxConnectionHandler.prototype.mouseUp = function(sender, me) +{ + if (!me.isConsumed() && this.isConnecting()) + { + if (this.waypointsEnabled && !this.isStopEvent(me)) + { + this.addWaypointForEvent(me); + me.consume(); + + return; + } + + // Inserts the edge if no validation error exists + if (this.error == null) + { + var source = (this.previous != null) ? this.previous.cell : null; + var target = null; + + if (this.constraintHandler.currentConstraint != null && + this.constraintHandler.currentFocus != null) + { + target = this.constraintHandler.currentFocus.cell; + } + + if (target == null && this.marker.hasValidState()) + { + target = this.marker.validState.cell; + } + + this.connect(source, target, me.getEvent(), me.getCell()); + } + else + { + // Selects the source terminal for self-references + if (this.previous != null && this.marker.validState != null && + this.previous.cell == this.marker.validState.cell) + { + this.graph.selectCellForEvent(this.marker.source, evt); + } + + // Displays the error message if it is not an empty string, + // for empty error messages, the event is silently dropped + if (this.error.length > 0) + { + this.graph.validationAlert(this.error); + } + } + + // Redraws the connect icons and resets the handler state + this.destroyIcons(this.icons); + me.consume(); + } + + if (this.first != null) + { + this.reset(); + } + + this.tapAndHoldInProgress = false; + this.tapAndHoldValid = false; +}; + +/** + * Function: reset + * + * Resets the state of this handler. + */ +mxConnectionHandler.prototype.reset = function() +{ + if (this.shape != null) + { + this.shape.destroy(); + this.shape = null; + } + + this.destroyIcons(this.icons); + this.icons = null; + this.marker.reset(); + this.constraintHandler.reset(); + this.selectedIcon = null; + this.edgeState = null; + this.previous = null; + this.error = null; + this.sourceConstraint = null; + this.mouseDownCounter = 0; + this.first = null; + this.icon = null; + + this.fireEvent(new mxEventObject(mxEvent.RESET)); +}; + +/** + * Function: drawPreview + * + * Redraws the preview edge using the color and width returned by + * <getEdgeColor> and <getEdgeWidth>. + */ +mxConnectionHandler.prototype.drawPreview = function() +{ + var valid = this.error == null; + var color = this.getEdgeColor(valid); + + if (this.shape.dialect == mxConstants.DIALECT_SVG) + { + this.shape.innerNode.setAttribute('stroke', color); + } + else + { + this.shape.node.strokecolor = color; + } + + this.shape.strokewidth = this.getEdgeWidth(valid); + this.shape.redraw(); + + // Workaround to force a repaint in AppleWebKit + mxUtils.repaintGraph(this.graph, this.shape.points[1]); +}; + +/** + * Function: getEdgeColor + * + * Returns the color used to draw the preview edge. This returns green if + * there is no edge validation error and red otherwise. + * + * Parameters: + * + * valid - Boolean indicating if the color for a valid edge should be + * returned. + */ +mxConnectionHandler.prototype.getEdgeColor = function(valid) +{ + return (valid) ? mxConstants.VALID_COLOR : mxConstants.INVALID_COLOR; +}; + +/** + * Function: getEdgeWidth + * + * Returns the width used to draw the preview edge. This returns 3 if + * there is no edge validation error and 1 otherwise. + * + * Parameters: + * + * valid - Boolean indicating if the width for a valid edge should be + * returned. + */ +mxConnectionHandler.prototype.getEdgeWidth = function(valid) +{ + return (valid) ? 3 : 1; +}; + +/** + * Function: connect + * + * Connects the given source and target using a new edge. This + * implementation uses <createEdge> to create the edge. + * + * Parameters: + * + * source - <mxCell> that represents the source terminal. + * target - <mxCell> that represents the target terminal. + * evt - Mousedown event of the connect gesture. + * dropTarget - <mxCell> that represents the cell under the mouse when it was + * released. + */ +mxConnectionHandler.prototype.connect = function(source, target, evt, dropTarget) +{ + if (target != null || this.isCreateTarget() || this.graph.allowDanglingEdges) + { + // Uses the common parent of source and target or + // the default parent to insert the edge + var model = this.graph.getModel(); + var edge = null; + + model.beginUpdate(); + try + { + if (source != null && target == null && this.isCreateTarget()) + { + target = this.createTargetVertex(evt, source); + + if (target != null) + { + dropTarget = this.graph.getDropTarget([target], evt, dropTarget); + + // Disables edges as drop targets if the target cell was created + // FIXME: Should not shift if vertex was aligned (same in Java) + if (dropTarget == null || !this.graph.getModel().isEdge(dropTarget)) + { + var pstate = this.graph.getView().getState(dropTarget); + + if (pstate != null) + { + var tmp = model.getGeometry(target); + tmp.x -= pstate.origin.x; + tmp.y -= pstate.origin.y; + } + } + else + { + dropTarget = this.graph.getDefaultParent(); + } + + this.graph.addCell(target, dropTarget); + } + } + + var parent = this.graph.getDefaultParent(); + + if (source != null && target != null && + model.getParent(source) == model.getParent(target) && + model.getParent(model.getParent(source)) != model.getRoot()) + { + parent = model.getParent(source); + + if ((source.geometry != null && source.geometry.relative) && + (target.geometry != null && target.geometry.relative)) + { + parent = model.getParent(parent); + } + } + + // Uses the value of the preview edge state for inserting + // the new edge into the graph + var value = null; + var style = null; + + if (this.edgeState != null) + { + value = this.edgeState.cell.value; + style = this.edgeState.cell.style; + } + + edge = this.insertEdge(parent, null, value, source, target, style); + + if (edge != null) + { + // Updates the connection constraints + this.graph.setConnectionConstraint(edge, source, true, this.sourceConstraint); + this.graph.setConnectionConstraint(edge, target, false, this.constraintHandler.currentConstraint); + + // Uses geometry of the preview edge state + if (this.edgeState != null) + { + model.setGeometry(edge, this.edgeState.cell.geometry); + } + + // Makes sure the edge has a non-null, relative geometry + var geo = model.getGeometry(edge); + + if (geo == null) + { + geo = new mxGeometry(); + geo.relative = true; + + model.setGeometry(edge, geo); + } + + // Uses scaled waypoints in geometry + if (this.waypoints != null && this.waypoints.length > 0) + { + var s = this.graph.view.scale; + var tr = this.graph.view.translate; + geo.points = []; + + for (var i = 0; i < this.waypoints.length; i++) + { + var pt = this.waypoints[i]; + geo.points.push(new mxPoint(pt.x / s - tr.x, pt.y / s - tr.y)); + } + } + + if (target == null) + { + var pt = this.graph.getPointForEvent(evt, false); + pt.x -= this.graph.panDx / this.graph.view.scale; + pt.y -= this.graph.panDy / this.graph.view.scale; + geo.setTerminalPoint(pt, false); + } + + this.fireEvent(new mxEventObject(mxEvent.CONNECT, + 'cell', edge, 'event', evt, 'target', dropTarget)); + } + } + catch (e) + { + mxLog.show(); + mxLog.debug(e.message); + } + finally + { + model.endUpdate(); + } + + if (this.select) + { + this.selectCells(edge, target); + } + } +}; + +/** + * Function: selectCells + * + * Selects the given edge after adding a new connection. The target argument + * contains the target vertex if one has been inserted. + */ +mxConnectionHandler.prototype.selectCells = function(edge, target) +{ + this.graph.setSelectionCell(edge); +}; + +/** + * Function: insertEdge + * + * Creates, inserts and returns the new edge for the given parameters. This + * implementation does only use <createEdge> if <factoryMethod> is defined, + * otherwise <mxGraph.insertEdge> will be used. + */ +mxConnectionHandler.prototype.insertEdge = function(parent, id, value, source, target, style) +{ + if (this.factoryMethod == null) + { + return this.graph.insertEdge(parent, id, value, source, target, style); + } + else + { + var edge = this.createEdge(value, source, target, style); + edge = this.graph.addEdge(edge, parent, source, target); + + return edge; + } +}; + +/** + * Function: createTargetVertex + * + * Hook method for creating new vertices on the fly if no target was + * under the mouse. This is only called if <createTarget> is true and + * returns null. + * + * Parameters: + * + * evt - Mousedown event of the connect gesture. + * source - <mxCell> that represents the source terminal. + */ +mxConnectionHandler.prototype.createTargetVertex = function(evt, source) +{ + // Uses the first non-relative source + var geo = this.graph.getCellGeometry(source); + + while (geo != null && geo.relative) + { + source = this.graph.getModel().getParent(source); + geo = this.graph.getCellGeometry(source); + } + + var clone = this.graph.cloneCells([source])[0]; + var geo = this.graph.getModel().getGeometry(clone); + + if (geo != null) + { + var point = this.graph.getPointForEvent(evt); + geo.x = this.graph.snap(point.x - geo.width / 2) - this.graph.panDx / this.graph.view.scale; + geo.y = this.graph.snap(point.y - geo.height / 2) - this.graph.panDy / this.graph.view.scale; + + // Aligns with source if within certain tolerance + if (this.first != null) + { + var sourceState = this.graph.view.getState(source); + + if (sourceState != null) + { + var tol = this.getAlignmentTolerance(); + + if (Math.abs(this.graph.snap(this.first.x) - + this.graph.snap(point.x)) <= tol) + { + geo.x = sourceState.x; + } + else if (Math.abs(this.graph.snap(this.first.y) - + this.graph.snap(point.y)) <= tol) + { + geo.y = sourceState.y; + } + } + } + } + + return clone; +}; + +/** + * Function: getAlignmentTolerance + * + * Returns the tolerance for aligning new targets to sources. + */ +mxConnectionHandler.prototype.getAlignmentTolerance = function() +{ + return (this.graph.isGridEnabled()) ? + this.graph.gridSize : this.graph.tolerance; +}; + +/** + * Function: createEdge + * + * Creates and returns a new edge using <factoryMethod> if one exists. If + * no factory method is defined, then a new default edge is returned. The + * source and target arguments are informal, the actual connection is + * setup later by the caller of this function. + * + * Parameters: + * + * value - Value to be used for creating the edge. + * source - <mxCell> that represents the source terminal. + * target - <mxCell> that represents the target terminal. + * style - Optional style from the preview edge. + */ +mxConnectionHandler.prototype.createEdge = function(value, source, target, style) +{ + var edge = null; + + // Creates a new edge using the factoryMethod + if (this.factoryMethod != null) + { + edge = this.factoryMethod(source, target, style); + } + + if (edge == null) + { + edge = new mxCell(value || ''); + edge.setEdge(true); + edge.setStyle(style); + + var geo = new mxGeometry(); + geo.relative = true; + edge.setGeometry(geo); + } + + return edge; +}; + +/** + * Function: destroy + * + * Destroys the handler and all its resources and DOM nodes. This should be + * called on all instances. It is called automatically for the built-in + * instance created for each <mxGraph>. + */ +mxConnectionHandler.prototype.destroy = function() +{ + this.graph.removeMouseListener(this); + + if (this.shape != null) + { + this.shape.destroy(); + this.shape = null; + } + + if (this.marker != null) + { + this.marker.destroy(); + this.marker = null; + } + + if (this.constraintHandler != null) + { + this.constraintHandler.destroy(); + this.constraintHandler = null; + } + + if (this.changeHandler != null) + { + this.graph.getModel().removeListener(this.changeHandler); + this.graph.getView().removeListener(this.changeHandler); + this.changeHandler = null; + } + + if (this.drillHandler != null) + { + this.graph.removeListener(this.drillHandler); + this.graph.getView().removeListener(this.drillHandler); + this.drillHandler = null; + } +}; diff --git a/src/js/handler/mxConstraintHandler.js b/src/js/handler/mxConstraintHandler.js new file mode 100644 index 0000000..39b3ab6 --- /dev/null +++ b/src/js/handler/mxConstraintHandler.js @@ -0,0 +1,308 @@ +/** + * $Id: mxConstraintHandler.js,v 1.15 2012-11-01 16:13:41 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxConstraintHandler + * + * Handles constraints on connection targets. This class is in charge of + * showing fixed points when the mouse is over a vertex and handles constraints + * to establish new connections. + * + * Constructor: mxConstraintHandler + * + * Constructs an new constraint handler. + * + * Parameters: + * + * graph - Reference to the enclosing <mxGraph>. + * factoryMethod - Optional function to create the edge. The function takes + * the source and target <mxCell> as the first and second argument and + * returns the <mxCell> that represents the new edge. + */ +function mxConstraintHandler(graph) +{ + this.graph = graph; +}; + +/** + * Variable: pointImage + * + * <mxImage> to be used as the image for fixed connection points. + */ +mxConstraintHandler.prototype.pointImage = new mxImage(mxClient.imageBasePath + '/point.gif', 5, 5); + +/** + * Variable: graph + * + * Reference to the enclosing <mxGraph>. + */ +mxConstraintHandler.prototype.graph = null; + +/** + * Variable: enabled + * + * Specifies if events are handled. Default is true. + */ +mxConstraintHandler.prototype.enabled = true; + +/** + * Variable: highlightColor + * + * Specifies the color for the highlight. Default is <mxConstants.DEFAULT_VALID_COLOR>. + */ +mxConstraintHandler.prototype.highlightColor = mxConstants.DEFAULT_VALID_COLOR; + +/** + * Function: isEnabled + * + * Returns true if events are handled. This implementation + * returns <enabled>. + */ +mxConstraintHandler.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Enables or disables event handling. This implementation + * updates <enabled>. + * + * Parameters: + * + * enabled - Boolean that specifies the new enabled state. + */ +mxConstraintHandler.prototype.setEnabled = function(enabled) +{ + this.enabled = enabled; +}; + +/** + * Function: reset + * + * Resets the state of this handler. + */ +mxConstraintHandler.prototype.reset = function() +{ + if (this.focusIcons != null) + { + for (var i = 0; i < this.focusIcons.length; i++) + { + this.focusIcons[i].destroy(); + } + + this.focusIcons = null; + } + + if (this.focusHighlight != null) + { + this.focusHighlight.destroy(); + this.focusHighlight = null; + } + + this.currentConstraint = null; + this.currentFocusArea = null; + this.currentPoint = null; + this.currentFocus = null; + this.focusPoints = null; +}; + +/** + * Function: getTolerance + * + * Returns the tolerance to be used for intersecting connection points. + */ +mxConstraintHandler.prototype.getTolerance = function() +{ + return this.graph.getTolerance(); +}; + +/** + * Function: getImageForConstraint + * + * Returns the tolerance to be used for intersecting connection points. + */ +mxConstraintHandler.prototype.getImageForConstraint = function(state, constraint, point) +{ + return this.pointImage; +}; + +/** + * Function: isEventIgnored + * + * Returns true if the given <mxMouseEvent> should be ignored in <update>. This + * implementation always returns false. + */ +mxConstraintHandler.prototype.isEventIgnored = function(me, source) +{ + return false; +}; + +/** + * Function: update + * + * Updates the state of this handler based on the given <mxMouseEvent>. + * Source is a boolean indicating if the cell is a source or target. + */ +mxConstraintHandler.prototype.update = function(me, source) +{ + if (this.isEnabled() && !this.isEventIgnored(me)) + { + var tol = this.getTolerance(); + var mouse = new mxRectangle(me.getGraphX() - tol, me.getGraphY() - tol, 2 * tol, 2 * tol); + var connectable = (me.getCell() != null) ? this.graph.isCellConnectable(me.getCell()) : false; + + if ((this.currentFocusArea == null || (!mxUtils.intersects(this.currentFocusArea, mouse) || + (me.getState() != null && this.currentFocus != null && connectable)))) + { + this.currentFocusArea = null; + + if (me.getState() != this.currentFocus) + { + this.currentFocus = null; + this.constraints = (me.getState() != null && connectable) ? + this.graph.getAllConnectionConstraints(me.getState(), source) : null; + + // Only uses cells which have constraints + if (this.constraints != null) + { + this.currentFocus = me.getState(); + this.currentFocusArea = new mxRectangle(me.getState().x, me.getState().y, me.getState().width, me.getState().height); + + if (this.focusIcons != null) + { + for (var i = 0; i < this.focusIcons.length; i++) + { + this.focusIcons[i].destroy(); + } + + this.focusIcons = null; + this.focusPoints = null; + } + + this.focusIcons = []; + this.focusPoints = []; + + for (var i = 0; i < this.constraints.length; i++) + { + var cp = this.graph.getConnectionPoint(me.getState(), this.constraints[i]); + var img = this.getImageForConstraint(me.getState(), this.constraints[i], cp); + + var src = img.src; + var bounds = new mxRectangle(cp.x - img.width / 2, + cp.y - img.height / 2, img.width, img.height); + var icon = new mxImageShape(bounds, src); + icon.dialect = (this.graph.dialect == mxConstants.DIALECT_SVG) ? + mxConstants.DIALECT_SVG : + mxConstants.DIALECT_VML; + icon.init(this.graph.getView().getOverlayPane()); + + // Move the icon behind all other overlays + if (icon.node.previousSibling != null) + { + icon.node.parentNode.insertBefore(icon.node, icon.node.parentNode.firstChild); + } + + var getState = mxUtils.bind(this, function() + { + return (this.currentFocus != null) ? this.currentFocus : me.getState(); + }); + + icon.redraw(); + + mxEvent.redirectMouseEvents(icon.node, this.graph, getState); + this.currentFocusArea.add(icon.bounds); + this.focusIcons.push(icon); + this.focusPoints.push(cp); + } + + this.currentFocusArea.grow(tol); + } + else if (this.focusIcons != null) + { + if (this.focusHighlight != null) + { + this.focusHighlight.destroy(); + this.focusHighlight = null; + } + + for (var i = 0; i < this.focusIcons.length; i++) + { + this.focusIcons[i].destroy(); + } + + this.focusIcons = null; + this.focusPoints = null; + } + } + } + + this.currentConstraint = null; + this.currentPoint = null; + + if (this.focusIcons != null && this.constraints != null && + (me.getState() == null || this.currentFocus == me.getState())) + { + for (var i = 0; i < this.focusIcons.length; i++) + { + if (mxUtils.intersects(this.focusIcons[i].bounds, mouse)) + { + this.currentConstraint = this.constraints[i]; + this.currentPoint = this.focusPoints[i]; + + var tmp = this.focusIcons[i].bounds.clone(); + tmp.grow((mxClient.IS_IE) ? 3 : 2); + + if (mxClient.IS_IE) + { + tmp.width -= 1; + tmp.height -= 1; + } + + if (this.focusHighlight == null) + { + var hl = new mxRectangleShape(tmp, null, this.highlightColor, 3); + hl.dialect = (this.graph.dialect == mxConstants.DIALECT_SVG) ? + mxConstants.DIALECT_SVG : + mxConstants.DIALECT_VML; + hl.init(this.graph.getView().getOverlayPane()); + this.focusHighlight = hl; + + var getState = mxUtils.bind(this, function() + { + return (this.currentFocus != null) ? this.currentFocus : me.getState(); + }); + + mxEvent.redirectMouseEvents(hl.node, this.graph, getState/*, mouseDown*/); + } + else + { + this.focusHighlight.bounds = tmp; + this.focusHighlight.redraw(); + } + + break; + } + } + } + + if (this.currentConstraint == null && + this.focusHighlight != null) + { + this.focusHighlight.destroy(); + this.focusHighlight = null; + } + } +}; + +/** + * Function: destroy + * + * Destroy this handler. + */ +mxConstraintHandler.prototype.destroy = function() +{ + this.reset(); +};
\ No newline at end of file diff --git a/src/js/handler/mxEdgeHandler.js b/src/js/handler/mxEdgeHandler.js new file mode 100644 index 0000000..2028342 --- /dev/null +++ b/src/js/handler/mxEdgeHandler.js @@ -0,0 +1,1529 @@ +/** + * $Id: mxEdgeHandler.js,v 1.178 2012-09-12 09:16:23 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxEdgeHandler + * + * Graph event handler that reconnects edges and modifies control points and + * the edge label location. Uses <mxTerminalMarker> for finding and + * highlighting new source and target vertices. This handler is automatically + * created in <mxGraph.createHandler> for each selected edge. + * + * To enable adding/removing control points, the following code can be used: + * + * (code) + * mxEdgeHandler.prototype.addEnabled = true; + * mxEdgeHandler.prototype.removeEnabled = true; + * (end) + * + * Note: This experimental feature is not recommended for production use. + * + * Constructor: mxEdgeHandler + * + * Constructs an edge handler for the specified <mxCellState>. + * + * Parameters: + * + * state - <mxCellState> of the cell to be handled. + */ +function mxEdgeHandler(state) +{ + if (state != null) + { + this.state = state; + this.init(); + } +}; + +/** + * Variable: graph + * + * Reference to the enclosing <mxGraph>. + */ +mxEdgeHandler.prototype.graph = null; + +/** + * Variable: state + * + * Reference to the <mxCellState> being modified. + */ +mxEdgeHandler.prototype.state = null; + +/** + * Variable: marker + * + * Holds the <mxTerminalMarker> which is used for highlighting terminals. + */ +mxEdgeHandler.prototype.marker = null; + +/** + * Variable: constraintHandler + * + * Holds the <mxConstraintHandler> used for drawing and highlighting + * constraints. + */ +mxEdgeHandler.prototype.constraintHandler = null; + +/** + * Variable: error + * + * Holds the current validation error while a connection is being changed. + */ +mxEdgeHandler.prototype.error = null; + +/** + * Variable: shape + * + * Holds the <mxShape> that represents the preview edge. + */ +mxEdgeHandler.prototype.shape = null; + +/** + * Variable: bends + * + * Holds the <mxShapes> that represent the points. + */ +mxEdgeHandler.prototype.bends = null; + +/** + * Variable: labelShape + * + * Holds the <mxShape> that represents the label position. + */ +mxEdgeHandler.prototype.labelShape = null; + +/** + * Variable: cloneEnabled + * + * Specifies if cloning by control-drag is enabled. Default is true. + */ +mxEdgeHandler.prototype.cloneEnabled = true; + +/** + * Variable: addEnabled + * + * Specifies if adding bends by shift-click is enabled. Default is false. + * Note: This experimental feature is not recommended for production use. + */ +mxEdgeHandler.prototype.addEnabled = false; + +/** + * Variable: removeEnabled + * + * Specifies if removing bends by shift-click is enabled. Default is false. + * Note: This experimental feature is not recommended for production use. + */ +mxEdgeHandler.prototype.removeEnabled = false; + +/** + * Variable: preferHtml + * + * Specifies if bends should be added to the graph container. This is updated + * in <init> based on whether the edge or one of its terminals has an HTML + * label in the container. + */ +mxEdgeHandler.prototype.preferHtml = false; + +/** + * Variable: allowHandleBoundsCheck + * + * Specifies if the bounds of handles should be used for hit-detection in IE + * Default is true. + */ +mxEdgeHandler.prototype.allowHandleBoundsCheck = true; + +/** + * Variable: snapToTerminals + * + * Specifies if waypoints should snap to the routing centers of terminals. + * Default is false. + */ +mxEdgeHandler.prototype.snapToTerminals = false; + +/** + * Variable: crisp + * + * Specifies if the edge handles should be rendered in crisp mode. Default is + * true. + */ +mxEdgeHandler.prototype.crisp = true; + +/** + * Variable: handleImage + * + * Optional <mxImage> to be used as handles. Default is null. + */ +mxEdgeHandler.prototype.handleImage = null; + +/** + * Variable: tolerance + * + * Optional tolerance for hit-detection in <getHandleForEvent>. Default is 0. + */ +mxEdgeHandler.prototype.tolerance = 0; + +/** + * Function: init + * + * Initializes the shapes required for this edge handler. + */ +mxEdgeHandler.prototype.init = function() +{ + this.graph = this.state.view.graph; + this.marker = this.createMarker(); + this.constraintHandler = new mxConstraintHandler(this.graph); + + // Clones the original points from the cell + // and makes sure at least one point exists + this.points = []; + + // Uses the absolute points of the state + // for the initial configuration and preview + this.abspoints = this.getSelectionPoints(this.state); + this.shape = this.createSelectionShape(this.abspoints); + this.shape.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ? + mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG; + this.shape.init(this.graph.getView().getOverlayPane()); + this.shape.node.style.cursor = mxConstants.CURSOR_MOVABLE_EDGE; + + // Event handling + var md = (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown'; + var mm = (mxClient.IS_TOUCH) ? 'touchmove' : 'mousemove'; + var mu = (mxClient.IS_TOUCH) ? 'touchend' : 'mouseup'; + + mxEvent.addListener(this.shape.node, 'dblclick', + mxUtils.bind(this, function(evt) + { + this.graph.dblClick(evt, this.state.cell); + }) + ); + mxEvent.addListener(this.shape.node, md, + mxUtils.bind(this, function(evt) + { + if (this.addEnabled && this.isAddPointEvent(evt)) + { + this.addPoint(this.state, evt); + } + else + { + this.graph.fireMouseEvent(mxEvent.MOUSE_DOWN, + new mxMouseEvent(evt, this.state)); + } + }) + ); + mxEvent.addListener(this.shape.node, mm, + mxUtils.bind(this, function(evt) + { + var cell = this.state.cell; + + // Finds the cell under the mouse if the edge is being connected + // in which case the edge is never highlighted as it cannot + // be its own source or target terminal (transparent preview) + if (this.index != null) + { + var pt = mxUtils.convertPoint(this.graph.container, + mxEvent.getClientX(evt), mxEvent.getClientY(evt)); + cell = this.graph.getCellAt(pt.x, pt.y); + + // Swimlane content area is transparent in this case + if (this.graph.isSwimlane(cell) && this.graph.hitsSwimlaneContent(cell, pt.x, pt.y)) + { + cell = null; + } + } + + this.graph.fireMouseEvent(mxEvent.MOUSE_MOVE, + new mxMouseEvent(evt, this.graph.getView().getState(cell))); + }) + ); + mxEvent.addListener(this.shape.node, mu, + mxUtils.bind(this, function(evt) + { + this.graph.fireMouseEvent(mxEvent.MOUSE_UP, + new mxMouseEvent(evt, this.state)); + }) + ); + + // Updates preferHtml + this.preferHtml = this.state.text != null && + this.state.text.node.parentNode == this.graph.container; + + if (!this.preferHtml) + { + // Checks source terminal + var sourceState = this.state.getVisibleTerminalState(true); + + if (sourceState != null) + { + this.preferHtml = sourceState.text != null && + sourceState.text.node.parentNode == this.graph.container; + } + + if (!this.preferHtml) + { + // Checks target terminal + var targetState = this.state.getVisibleTerminalState(false); + + if (targetState != null) + { + this.preferHtml = targetState.text != null && + targetState.text.node.parentNode == this.graph.container; + } + } + } + + // Creates bends for the non-routed absolute points + // or bends that don't correspond to points + if (this.graph.getSelectionCount() < mxGraphHandler.prototype.maxCells || + mxGraphHandler.prototype.maxCells <= 0) + { + this.bends = this.createBends(); + } + + // Adds a rectangular handle for the label position + this.label = new mxPoint(this.state.absoluteOffset.x, this.state.absoluteOffset.y); + this.labelShape = new mxRectangleShape(new mxRectangle(), + mxConstants.LABEL_HANDLE_FILLCOLOR, + mxConstants.HANDLE_STROKECOLOR); + this.initBend(this.labelShape); + this.labelShape.node.style.cursor = mxConstants.CURSOR_LABEL_HANDLE; + mxEvent.redirectMouseEvents(this.labelShape.node, this.graph, this.state); + + this.redraw(); +}; + +/** + * Function: isAddPointEvent + * + * Returns true if the given event is a trigger to add a new point. This + * implementation returns true if shift is pressed. + */ +mxEdgeHandler.prototype.isAddPointEvent = function(evt) +{ + return mxEvent.isShiftDown(evt); +}; + +/** + * Function: isRemovePointEvent + * + * Returns true if the given event is a trigger to remove a point. This + * implementation returns true if shift is pressed. + */ +mxEdgeHandler.prototype.isRemovePointEvent = function(evt) +{ + return mxEvent.isShiftDown(evt); +}; + +/** + * Function: getSelectionPoints + * + * Returns the list of points that defines the selection stroke. + */ +mxEdgeHandler.prototype.getSelectionPoints = function(state) +{ + return state.absolutePoints; +}; + +/** + * Function: createSelectionShape + * + * Creates the shape used to draw the selection border. + */ +mxEdgeHandler.prototype.createSelectionShape = function(points) +{ + var shape = new mxPolyline(points, this.getSelectionColor()); + shape.strokewidth = this.getSelectionStrokeWidth(); + shape.isDashed = this.isSelectionDashed(); + + return shape; +}; + +/** + * Function: getSelectionColor + * + * Returns <mxConstants.EDGE_SELECTION_COLOR>. + */ +mxEdgeHandler.prototype.getSelectionColor = function() +{ + return mxConstants.EDGE_SELECTION_COLOR; +}; + +/** + * Function: getSelectionStrokeWidth + * + * Returns <mxConstants.EDGE_SELECTION_STROKEWIDTH>. + */ +mxEdgeHandler.prototype.getSelectionStrokeWidth = function() +{ + return mxConstants.EDGE_SELECTION_STROKEWIDTH; +}; + +/** + * Function: isSelectionDashed + * + * Returns <mxConstants.EDGE_SELECTION_DASHED>. + */ +mxEdgeHandler.prototype.isSelectionDashed = function() +{ + return mxConstants.EDGE_SELECTION_DASHED; +}; + +/** + * Function: isConnectableCell + * + * Returns true if the given cell is connectable. This is a hook to + * disable floating connections. This implementation returns true. + */ +mxEdgeHandler.prototype.isConnectableCell = function(cell) +{ + return true; +}; + +/** + * Function: createMarker + * + * Creates and returns the <mxCellMarker> used in <marker>. + */ +mxEdgeHandler.prototype.createMarker = function() +{ + var marker = new mxCellMarker(this.graph); + var self = this; // closure + + // Only returns edges if they are connectable and never returns + // the edge that is currently being modified + marker.getCell = function(me) + { + var cell = mxCellMarker.prototype.getCell.apply(this, arguments); + + if (!self.isConnectableCell(cell)) + { + return null; + } + + var model = self.graph.getModel(); + + if (cell == self.state.cell || (cell != null && + !self.graph.connectableEdges && model.isEdge(cell))) + { + cell = null; + } + + return cell; + }; + + // Sets the highlight color according to validateConnection + marker.isValidState = function(state) + { + var model = self.graph.getModel(); + var other = self.graph.view.getTerminalPort(state, + self.graph.view.getState(model.getTerminal(self.state.cell, + !self.isSource)), !self.isSource); + var otherCell = (other != null) ? other.cell : null; + var source = (self.isSource) ? state.cell : otherCell; + var target = (self.isSource) ? otherCell : state.cell; + + // Updates the error message of the handler + self.error = self.validateConnection(source, target); + + return self.error == null; + }; + + return marker; +}; + +/** + * Function: validateConnection + * + * Returns the error message or an empty string if the connection for the + * given source, target pair is not valid. Otherwise it returns null. This + * implementation uses <mxGraph.getEdgeValidationError>. + * + * Parameters: + * + * source - <mxCell> that represents the source terminal. + * target - <mxCell> that represents the target terminal. + */ +mxEdgeHandler.prototype.validateConnection = function(source, target) +{ + return this.graph.getEdgeValidationError(this.state.cell, source, target); +}; + +/** + * Function: createBends + * + * Creates and returns the bends used for modifying the edge. This is + * typically an array of <mxRectangleShapes>. + */ + mxEdgeHandler.prototype.createBends = function() + { + var cell = this.state.cell; + var bends = []; + + for (var i = 0; i < this.abspoints.length; i++) + { + if (this.isHandleVisible(i)) + { + var source = i == 0; + var target = i == this.abspoints.length - 1; + var terminal = source || target; + + if (terminal || this.graph.isCellBendable(cell)) + { + var bend = this.createHandleShape(i); + this.initBend(bend); + + if (mxClient.IS_TOUCH) + { + bend.node.setAttribute('pointer-events', 'none'); + } + + if (this.isHandleEnabled(i)) + { + if (mxClient.IS_TOUCH) + { + var getState = mxUtils.bind(this, function(evt) + { + var pt = mxUtils.convertPoint(this.graph.container, mxEvent.getClientX(evt), mxEvent.getClientY(evt)); + + return this.graph.view.getState(this.graph.getCellAt(pt.x, pt.y)); + }); + + mxEvent.redirectMouseEvents(bend.node, this.graph, getState); + } + else + { + bend.node.style.cursor = mxConstants.CURSOR_BEND_HANDLE; + mxEvent.redirectMouseEvents(bend.node, this.graph, this.state); + } + } + + bends.push(bend); + + if (!terminal) + { + this.points.push(new mxPoint(0,0)); + bend.node.style.visibility = 'hidden'; + } + } + } + } + + return bends; +}; +/** + * Function: isHandleEnabled + * + * Creates the shape used to display the given bend. + */ +mxEdgeHandler.prototype.isHandleEnabled = function(index) +{ + return true; +}; + +/** + * Function: isHandleVisible + * + * Returns true if the handle at the given index is visible. + */ +mxEdgeHandler.prototype.isHandleVisible = function(index) +{ + return true; +}; + +/** + * Function: createHandleShape + * + * Creates the shape used to display the given bend. Note that the index may be + * null for special cases, such as when called from + * <mxElbowEdgeHandler.createVirtualBend>. + */ +mxEdgeHandler.prototype.createHandleShape = function(index) +{ + if (this.handleImage != null) + { + return new mxImageShape(new mxRectangle(0, 0, this.handleImage.width, this.handleImage.height), this.handleImage.src); + } + else + { + var s = mxConstants.HANDLE_SIZE; + + if (this.preferHtml) + { + s -= 1; + } + + return new mxRectangleShape(new mxRectangle(0, 0, s, s), mxConstants.HANDLE_FILLCOLOR, mxConstants.HANDLE_STROKECOLOR); + } +}; + +/** + * Function: initBend + * + * Helper method to initialize the given bend. + * + * Parameters: + * + * bend - <mxShape> that represents the bend to be initialized. + */ +mxEdgeHandler.prototype.initBend = function(bend) +{ + bend.crisp = this.crisp; + + if (this.preferHtml) + { + bend.dialect = mxConstants.DIALECT_STRICTHTML; + bend.init(this.graph.container); + } + else + { + bend.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ? + mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG; + bend.init(this.graph.getView().getOverlayPane()); + } +}; + +/** + * Function: getHandleForEvent + * + * Returns the index of the handle for the given event. + */ +mxEdgeHandler.prototype.getHandleForEvent = function(me) +{ + // Finds the handle that triggered the event + if (this.bends != null) + { + // Connection highlight may consume events before they reach sizer handle + var tol = this.tolerance; + var hit = (this.allowHandleBoundsCheck && (mxClient.IS_IE || tol > 0)) ? + new mxRectangle(me.getGraphX() - tol, me.getGraphY() - tol, 2 * tol, 2 * tol) : null; + + for (var i = 0; i < this.bends.length; i++) + { + if (me.isSource(this.bends[i]) || (hit != null && + this.bends[i].node.style.visibility != 'hidden' && + mxUtils.intersects(this.bends[i].bounds, hit))) + { + return i; + } + } + } + + if (me.isSource(this.labelShape) || me.isSource(this.state.text)) + { + // Workaround for SELECT element not working in Webkit + if ((!mxClient.IS_SF && !mxClient.IS_GC) || me.getSource().nodeName != 'SELECT') + { + return mxEvent.LABEL_HANDLE; + } + } + + return null; +}; + +/** + * Function: mouseDown + * + * Handles the event by checking if a special element of the handler + * was clicked, in which case the index parameter is non-null. The + * indices may be one of <LABEL_HANDLE> or the number of the respective + * control point. The source and target points are used for reconnecting + * the edge. + */ +mxEdgeHandler.prototype.mouseDown = function(sender, me) +{ + var handle = null; + + // Handles the case where the state in the event points to another + // cell if the cell has a HTML label which sits on top of the handles + // NOTE: Commented out. This should not be required as all HTML labels + // are in order an do not appear behind the handles. + //if (mxClient.IS_SVG || me.getState() == this.state) + { + handle = this.getHandleForEvent(me); + } + + if (handle != null && !me.isConsumed() && this.graph.isEnabled() && + !this.graph.isForceMarqueeEvent(me.getEvent())) + { + if (this.removeEnabled && this.isRemovePointEvent(me.getEvent())) + { + this.removePoint(this.state, handle); + } + else if (handle != mxEvent.LABEL_HANDLE || this.graph.isLabelMovable(me.getCell())) + { + this.start(me.getX(), me.getY(), handle); + } + + me.consume(); + } +}; + +/** + * Function: start + * + * Starts the handling of the mouse gesture. + */ +mxEdgeHandler.prototype.start = function(x, y, index) +{ + this.startX = x; + this.startY = y; + + this.isSource = (this.bends == null) ? false : index == 0; + this.isTarget = (this.bends == null) ? false : index == this.bends.length - 1; + this.isLabel = index == mxEvent.LABEL_HANDLE; + + if (this.isSource || this.isTarget) + { + var cell = this.state.cell; + var terminal = this.graph.model.getTerminal(cell, this.isSource); + + if ((terminal == null && this.graph.isTerminalPointMovable(cell, this.isSource)) || + (terminal != null && this.graph.isCellDisconnectable(cell, terminal, this.isSource))) + { + this.index = index; + } + } + else + { + this.index = index; + } +}; + +/** + * Function: clonePreviewState + * + * Returns a clone of the current preview state for the given point and terminal. + */ +mxEdgeHandler.prototype.clonePreviewState = function(point, terminal) +{ + return this.state.clone(); +}; + +/** + * Function: getSnapToTerminalTolerance + * + * Returns the tolerance for the guides. Default value is + * gridSize * scale / 2. + */ +mxEdgeHandler.prototype.getSnapToTerminalTolerance = function() +{ + return this.graph.gridSize * this.graph.view.scale / 2; +}; + +/** + * Function: getPointForEvent + * + * Returns the point for the given event. + */ +mxEdgeHandler.prototype.getPointForEvent = function(me) +{ + var point = new mxPoint(me.getGraphX(), me.getGraphY()); + + var tt = this.getSnapToTerminalTolerance(); + var view = this.graph.getView(); + var overrideX = false; + var overrideY = false; + + if (this.snapToTerminals && tt > 0) + { + function snapToPoint(pt) + { + if (pt != null) + { + var x = pt.x; + + if (Math.abs(point.x - x) < tt) + { + point.x = x; + overrideX = true; + } + + var y = pt.y; + + if (Math.abs(point.y - y) < tt) + { + point.y = y; + overrideY = true; + } + } + } + + // Temporary function + function snapToTerminal(terminal) + { + if (terminal != null) + { + snapToPoint.call(this, new mxPoint(view.getRoutingCenterX(terminal), + view.getRoutingCenterY(terminal))); + } + }; + + snapToTerminal.call(this, this.state.getVisibleTerminalState(true)); + snapToTerminal.call(this, this.state.getVisibleTerminalState(false)); + + if (this.abspoints != null) + { + for (var i = 0; i < this.abspoints; i++) + { + if (i != this.index) + { + snapToPoint.call(this, this.abspoints[i]); + } + } + } + } + + if (this.graph.isGridEnabledEvent(me.getEvent())) + { + var scale = view.scale; + var tr = view.translate; + + if (!overrideX) + { + point.x = (this.graph.snap(point.x / scale - tr.x) + tr.x) * scale; + } + + if (!overrideY) + { + point.y = (this.graph.snap(point.y / scale - tr.y) + tr.y) * scale; + } + } + + return point; +}; + +/** + * Function: getPreviewTerminalState + * + * Updates the given preview state taking into account the state of the constraint handler. + */ +mxEdgeHandler.prototype.getPreviewTerminalState = function(me) +{ + this.constraintHandler.update(me, this.isSource); + this.marker.process(me); + var currentState = this.marker.getValidState(); + var result = null; + + if (this.constraintHandler.currentFocus != null && + this.constraintHandler.currentConstraint != null) + { + this.marker.reset(); + } + + if (currentState != null) + { + result = currentState; + } + else if (this.constraintHandler.currentConstraint != null && + this.constraintHandler.currentFocus != null) + { + result = this.constraintHandler.currentFocus; + } + + return result; +}; + +/** + * Function: getPreviewPoints + * + * Updates the given preview state taking into account the state of the constraint handler. + */ +mxEdgeHandler.prototype.getPreviewPoints = function(point) +{ + var geometry = this.graph.getCellGeometry(this.state.cell); + var points = (geometry.points != null) ? geometry.points.slice() : null; + + if (!this.isSource && !this.isTarget) + { + this.convertPoint(point, false); + + if (points == null) + { + points = [point]; + } + else + { + points[this.index - 1] = point; + } + } + else if (this.graph.resetEdgesOnConnect) + { + points = null; + } + + return points; +}; + +/** + * Function: updatePreviewState + * + * Updates the given preview state taking into account the state of the constraint handler. + */ +mxEdgeHandler.prototype.updatePreviewState = function(edge, point, terminalState) +{ + // Computes the points for the edge style and terminals + var sourceState = (this.isSource) ? terminalState : this.state.getVisibleTerminalState(true); + var targetState = (this.isTarget) ? terminalState : this.state.getVisibleTerminalState(false); + + var sourceConstraint = this.graph.getConnectionConstraint(edge, sourceState, true); + var targetConstraint = this.graph.getConnectionConstraint(edge, targetState, false); + + var constraint = this.constraintHandler.currentConstraint; + + if (constraint == null) + { + constraint = new mxConnectionConstraint(); + } + + if (this.isSource) + { + sourceConstraint = constraint; + } + else if (this.isTarget) + { + targetConstraint = constraint; + } + + if (!this.isSource || sourceState != null) + { + edge.view.updateFixedTerminalPoint(edge, sourceState, true, sourceConstraint); + } + + if (!this.isTarget || targetState != null) + { + edge.view.updateFixedTerminalPoint(edge, targetState, false, targetConstraint); + } + + if ((this.isSource || this.isTarget) && terminalState == null) + { + edge.setAbsoluteTerminalPoint(point, this.isSource); + + if (this.marker.getMarkedState() == null) + { + this.error = (this.graph.allowDanglingEdges) ? null : ''; + } + } + + edge.view.updatePoints(edge, this.points, sourceState, targetState); + edge.view.updateFloatingTerminalPoints(edge, sourceState, targetState); +}; + +/** + * Function: mouseMove + * + * Handles the event by updating the preview. + */ +mxEdgeHandler.prototype.mouseMove = function(sender, me) +{ + if (this.index != null && this.marker != null) + { + var point = this.getPointForEvent(me); + + if (this.isLabel) + { + this.label.x = point.x; + this.label.y = point.y; + } + else + { + this.points = this.getPreviewPoints(point); + var terminalState = (this.isSource || this.isTarget) ? this.getPreviewTerminalState(me) : null; + var clone = this.clonePreviewState(point, (terminalState != null) ? terminalState.cell : null); + this.updatePreviewState(clone, point, terminalState); + + // Sets the color of the preview to valid or invalid, updates the + // points of the preview and redraws + var color = (this.error == null) ? this.marker.validColor : + this.marker.invalidColor; + this.setPreviewColor(color); + this.abspoints = clone.absolutePoints; + this.active = true; + } + + this.drawPreview(); + mxEvent.consume(me.getEvent()); + me.consume(); + } + // Workaround for disabling the connect highlight when over handle + else if (mxClient.IS_IE && this.getHandleForEvent(me) != null) + { + me.consume(false); + } +}; + +/** + * Function: mouseUp + * + * Handles the event to applying the previewed changes on the edge by + * using <moveLabel>, <connect> or <changePoints>. + */ +mxEdgeHandler.prototype.mouseUp = function(sender, me) +{ + if (this.index != null && this.marker != null) + { + var edge = this.state.cell; + + // Ignores event if mouse has not been moved + if (me.getX() != this.startX || me.getY() != this.startY) + { + // Displays the reason for not carriying out the change + // if there is an error message with non-zero length + if (this.error != null) + { + if (this.error.length > 0) + { + this.graph.validationAlert(this.error); + } + } + else if (this.isLabel) + { + this.moveLabel(this.state, this.label.x, this.label.y); + } + else if (this.isSource || this.isTarget) + { + var terminal = null; + + if (this.constraintHandler.currentConstraint != null && + this.constraintHandler.currentFocus != null) + { + terminal = this.constraintHandler.currentFocus.cell; + } + + if (terminal == null && this.marker.hasValidState()) + { + terminal = this.marker.validState.cell; + } + + if (terminal != null) + { + edge = this.connect(edge, terminal, this.isSource, + this.graph.isCloneEvent(me.getEvent()) && this.cloneEnabled && + this.graph.isCellsCloneable(), me); + } + else if (this.graph.isAllowDanglingEdges()) + { + var pt = this.abspoints[(this.isSource) ? 0 : this.abspoints.length - 1]; + pt.x = pt.x / this.graph.view.scale - this.graph.view.translate.x; + pt.y = pt.y / this.graph.view.scale - this.graph.view.translate.y; + + var pstate = this.graph.getView().getState( + this.graph.getModel().getParent(edge)); + + if (pstate != null) + { + pt.x -= pstate.origin.x; + pt.y -= pstate.origin.y; + } + + pt.x -= this.graph.panDx / this.graph.view.scale; + pt.y -= this.graph.panDy / this.graph.view.scale; + + // Destroys and rectreates this handler + this.changeTerminalPoint(edge, pt, this.isSource); + } + } + else if (this.active) + { + this.changePoints(edge, this.points); + } + else + { + this.graph.getView().invalidate(this.state.cell); + this.graph.getView().revalidate(this.state.cell); + } + } + + // Resets the preview color the state of the handler if this + // handler has not been recreated + if (this.marker != null) + { + this.reset(); + + // Updates the selection if the edge has been cloned + if (edge != this.state.cell) + { + this.graph.setSelectionCell(edge); + } + } + + me.consume(); + } +}; + +/** + * Function: reset + * + * Resets the state of this handler. + */ +mxEdgeHandler.prototype.reset = function() +{ + this.error = null; + this.index = null; + this.label = null; + this.points = null; + this.active = false; + this.isLabel = false; + this.isSource = false; + this.isTarget = false; + this.marker.reset(); + this.constraintHandler.reset(); + this.setPreviewColor(mxConstants.EDGE_SELECTION_COLOR); + this.redraw(); +}; + +/** + * Function: setPreviewColor + * + * Sets the color of the preview to the given value. + */ +mxEdgeHandler.prototype.setPreviewColor = function(color) +{ + if (this.shape != null && this.shape.node != null) + { + if (this.shape.dialect == mxConstants.DIALECT_SVG) + { + this.shape.innerNode.setAttribute('stroke', color); + } + else + { + this.shape.node.strokecolor = color; + } + } +}; + +/** + * Function: convertPoint + * + * Converts the given point in-place from screen to unscaled, untranslated + * graph coordinates and applies the grid. Returns the given, modified + * point instance. + * + * Parameters: + * + * point - <mxPoint> to be converted. + * gridEnabled - Boolean that specifies if the grid should be applied. + */ +mxEdgeHandler.prototype.convertPoint = function(point, gridEnabled) +{ + var scale = this.graph.getView().getScale(); + var tr = this.graph.getView().getTranslate(); + + if (gridEnabled) + { + point.x = this.graph.snap(point.x); + point.y = this.graph.snap(point.y); + } + + point.x = Math.round(point.x / scale - tr.x); + point.y = Math.round(point.y / scale - tr.y); + + var pstate = this.graph.getView().getState( + this.graph.getModel().getParent(this.state.cell)); + + if (pstate != null) + { + point.x -= pstate.origin.x; + point.y -= pstate.origin.y; + } + + return point; +}; + +/** + * Function: moveLabel + * + * Changes the coordinates for the label of the given edge. + * + * Parameters: + * + * edge - <mxCell> that represents the edge. + * x - Integer that specifies the x-coordinate of the new location. + * y - Integer that specifies the y-coordinate of the new location. + */ +mxEdgeHandler.prototype.moveLabel = function(edgeState, x, y) +{ + var model = this.graph.getModel(); + var geometry = model.getGeometry(edgeState.cell); + + if (geometry != null) + { + geometry = geometry.clone(); + + // Resets the relative location stored inside the geometry + var pt = this.graph.getView().getRelativePoint(edgeState, x, y); + geometry.x = pt.x; + geometry.y = pt.y; + + // Resets the offset inside the geometry to find the offset + // from the resulting point + var scale = this.graph.getView().scale; + geometry.offset = new mxPoint(0, 0); + var pt = this.graph.view.getPoint(edgeState, geometry); + geometry.offset = new mxPoint((x - pt.x) / scale, (y - pt.y) / scale); + + model.setGeometry(edgeState.cell, geometry); + } +}; + +/** + * Function: connect + * + * Changes the terminal or terminal point of the given edge in the graph + * model. + * + * Parameters: + * + * edge - <mxCell> that represents the edge to be reconnected. + * terminal - <mxCell> that represents the new terminal. + * isSource - Boolean indicating if the new terminal is the source or + * target terminal. + * isClone - Boolean indicating if the new connection should be a clone of + * the old edge. + * me - <mxMouseEvent> that contains the mouse up event. + */ +mxEdgeHandler.prototype.connect = function(edge, terminal, isSource, isClone, me) +{ + var model = this.graph.getModel(); + var parent = model.getParent(edge); + + model.beginUpdate(); + try + { + // Clones and adds the cell + if (isClone) + { + var clone = edge.clone(); + model.add(parent, clone, model.getChildCount(parent)); + + var other = model.getTerminal(edge, !isSource); + this.graph.connectCell(clone, other, !isSource); + + edge = clone; + } + + var constraint = this.constraintHandler.currentConstraint; + + if (constraint == null) + { + constraint = new mxConnectionConstraint(); + } + + this.graph.connectCell(edge, terminal, isSource, constraint); + } + finally + { + model.endUpdate(); + } + + return edge; +}; + +/** + * Function: changeTerminalPoint + * + * Changes the terminal point of the given edge. + */ +mxEdgeHandler.prototype.changeTerminalPoint = function(edge, point, isSource) +{ + var model = this.graph.getModel(); + var geo = model.getGeometry(edge); + + if (geo != null) + { + model.beginUpdate(); + try + { + geo = geo.clone(); + geo.setTerminalPoint(point, isSource); + model.setGeometry(edge, geo); + this.graph.connectCell(edge, null, isSource, new mxConnectionConstraint()); + } + finally + { + model.endUpdate(); + } + } +}; + +/** + * Function: changePoints + * + * Changes the control points of the given edge in the graph model. + */ +mxEdgeHandler.prototype.changePoints = function(edge, points) +{ + var model = this.graph.getModel(); + var geo = model.getGeometry(edge); + + if (geo != null) + { + geo = geo.clone(); + geo.points = points; + + model.setGeometry(edge, geo); + } +}; + +/** + * Function: addPoint + * + * Adds a control point for the given state and event. + */ +mxEdgeHandler.prototype.addPoint = function(state, evt) +{ + var geo = this.graph.getCellGeometry(state.cell); + + if (geo != null) + { + geo = geo.clone(); + var pt = mxUtils.convertPoint(this.graph.container, mxEvent.getClientX(evt), + mxEvent.getClientY(evt)); + var index = mxUtils.findNearestSegment(state, pt.x, pt.y); + var gridEnabled = this.graph.isGridEnabledEvent(evt); + this.convertPoint(pt, gridEnabled); + + if (geo.points == null) + { + geo.points = [pt]; + } + else + { + geo.points.splice(index, 0, pt); + } + + this.graph.getModel().setGeometry(state.cell, geo); + this.destroy(); + this.init(); + mxEvent.consume(evt); + } +}; + +/** + * Function: removePoint + * + * Removes the control point at the given index from the given state. + */ +mxEdgeHandler.prototype.removePoint = function(state, index) +{ + if (index > 0 && index < this.abspoints.length - 1) + { + var geo = this.graph.getCellGeometry(this.state.cell); + + if (geo != null && + geo.points != null) + { + geo = geo.clone(); + geo.points.splice(index - 1, 1); + this.graph.getModel().setGeometry(state.cell, geo); + this.destroy(); + this.init(); + } + } +}; + +/** + * Function: getHandleFillColor + * + * Returns the fillcolor for the handle at the given index. + */ +mxEdgeHandler.prototype.getHandleFillColor = function(index) +{ + var isSource = index == 0; + var cell = this.state.cell; + var terminal = this.graph.getModel().getTerminal(cell, isSource); + var color = mxConstants.HANDLE_FILLCOLOR; + + if ((terminal != null && !this.graph.isCellDisconnectable(cell, terminal, isSource)) || + (terminal == null && !this.graph.isTerminalPointMovable(cell, isSource))) + { + color = mxConstants.LOCKED_HANDLE_FILLCOLOR; + } + else if (terminal != null && this.graph.isCellDisconnectable(cell, terminal, isSource)) + { + color = mxConstants.CONNECT_HANDLE_FILLCOLOR; + } + + return color; +}; + +/** + * Function: redraw + * + * Redraws the preview, and the bends- and label control points. + */ +mxEdgeHandler.prototype.redraw = function() +{ + this.abspoints = this.state.absolutePoints.slice(); + var cell = this.state.cell; + + // Updates the handle for the label position + var s = mxConstants.LABEL_HANDLE_SIZE; + + this.label = new mxPoint(this.state.absoluteOffset.x, this.state.absoluteOffset.y); + this.labelShape.bounds = new mxRectangle(this.label.x - s / 2, + this.label.y - s / 2, s, s); + this.labelShape.redraw(); + + // Shows or hides the label handle depending on the label + var lab = this.graph.getLabel(cell); + + if (lab != null && lab.length > 0 && this.graph.isLabelMovable(cell)) + { + this.labelShape.node.style.visibility = 'visible'; + } + else + { + this.labelShape.node.style.visibility = 'hidden'; + } + + if (this.bends != null && this.bends.length > 0) + { + var n = this.abspoints.length - 1; + + var p0 = this.abspoints[0]; + var x0 = this.abspoints[0].x; + var y0 = this.abspoints[0].y; + + var b = this.bends[0].bounds; + this.bends[0].bounds = new mxRectangle(x0 - b.width / 2, y0 - b.height / 2, b.width, b.height); + this.bends[0].fill = this.getHandleFillColor(0); + this.bends[0].reconfigure(); + this.bends[0].redraw(); + + var pe = this.abspoints[n]; + var xn = this.abspoints[n].x; + var yn = this.abspoints[n].y; + + var bn = this.bends.length - 1; + b = this.bends[bn].bounds; + this.bends[bn].bounds = new mxRectangle(xn - b.width / 2, yn - b.height / 2, b.width, b.height); + this.bends[bn].fill = this.getHandleFillColor(bn); + this.bends[bn].reconfigure(); + this.bends[bn].redraw(); + + this.redrawInnerBends(p0, pe); + } + + this.drawPreview(); +}; + +/** + * Function: redrawInnerBends + * + * Updates and redraws the inner bends. + * + * Parameters: + * + * p0 - <mxPoint> that represents the location of the first point. + * pe - <mxPoint> that represents the location of the last point. + */ +mxEdgeHandler.prototype.redrawInnerBends = function(p0, pe) +{ + var g = this.graph.getModel().getGeometry(this.state.cell); + var pts = g.points; + + if (pts != null) + { + if (this.points == null) + { + this.points = []; + } + + for (var i = 1; i < this.bends.length-1; i++) + { + if (this.bends[i] != null) + { + if (this.abspoints[i] != null) + { + var x = this.abspoints[i].x; + var y = this.abspoints[i].y; + + var b = this.bends[i].bounds; + this.bends[i].node.style.visibility = 'visible'; + this.bends[i].bounds = new mxRectangle(x - b.width / 2, y - b.height / 2, b.width, b.height); + this.bends[i].redraw(); + + this.points[i - 1] = pts[i - 1]; + } + else + { + this.bends[i].destroy(); + this.bends[i] = null; + } + } + } + } +}; + +/** + * Function: drawPreview + * + * Redraws the preview. + */ +mxEdgeHandler.prototype.drawPreview = function() +{ + if (this.isLabel) + { + var s = mxConstants.LABEL_HANDLE_SIZE; + + var bounds = new mxRectangle(this.label.x - s / 2, this.label.y - s / 2, s, s); + this.labelShape.bounds = bounds; + this.labelShape.redraw(); + } + else + { + this.shape.points = this.abspoints; + this.shape.redraw(); + } + + // Workaround to force a repaint in AppleWebKit + mxUtils.repaintGraph(this.graph, this.shape.points[this.shape.points.length - 1]); +}; + +/** + * Function: destroy + * + * Destroys the handler and all its resources and DOM nodes. This does + * normally not need to be called as handlers are destroyed automatically + * when the corresponding cell is deselected. + */ +mxEdgeHandler.prototype.destroy = function() +{ + if (this.marker != null) + { + this.marker.destroy(); + this.marker = null; + } + + if (this.shape != null) + { + this.shape.destroy(); + this.shape = null; + } + + if (this.labelShape != null) + { + this.labelShape.destroy(); + this.labelShape = null; + } + + if (this.constraintHandler != null) + { + this.constraintHandler.destroy(); + this.constraintHandler = null; + } + + // Destroy the control points for the bends + if (this.bends != null) + { + for (var i = 0; i < this.bends.length; i++) + { + if (this.bends[i] != null) + { + this.bends[i].destroy(); + this.bends[i] = null; + } + } + } +}; diff --git a/src/js/handler/mxEdgeSegmentHandler.js b/src/js/handler/mxEdgeSegmentHandler.js new file mode 100644 index 0000000..e14fde0 --- /dev/null +++ b/src/js/handler/mxEdgeSegmentHandler.js @@ -0,0 +1,284 @@ +/** + * $Id: mxEdgeSegmentHandler.js,v 1.14 2012-12-17 13:22:49 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +function mxEdgeSegmentHandler(state) +{ + if (state != null) + { + this.state = state; + this.init(); + } +}; + +/** + * Extends mxEdgeHandler. + */ +mxEdgeSegmentHandler.prototype = new mxElbowEdgeHandler(); +mxEdgeSegmentHandler.prototype.constructor = mxEdgeSegmentHandler; + +/** + * Function: getPreviewPoints + * + * Updates the given preview state taking into account the state of the constraint handler. + */ +mxEdgeSegmentHandler.prototype.getPreviewPoints = function(point) +{ + if (this.isSource || this.isTarget) + { + return mxElbowEdgeHandler.prototype.getPreviewPoints.apply(this, arguments); + } + else + { + this.convertPoint(point, false); + var pts = this.state.absolutePoints; + var last = pts[0].clone(); + this.convertPoint(last, false); + var result = []; + + for (var i = 1; i < pts.length; i++) + { + var pt = pts[i].clone(); + this.convertPoint(pt, false); + + if (i == this.index) + { + if (last.x == pt.x) + { + last.x = point.x; + pt.x = point.x; + } + else + { + last.y = point.y; + pt.y = point.y; + } + } + + if (i < pts.length - 1) + { + result.push(pt); + } + + last = pt; + } + + if (result.length == 1) + { + var view = this.state.view; + var source = this.state.getVisibleTerminalState(true); + var target = this.state.getVisibleTerminalState(false); + + if (target != null & source != null) + { + var dx = this.state.origin.x; + var dy = this.state.origin.y; + + if (mxUtils.contains(target, result[0].x + dx, result[0].y + dy)) + { + if (pts[1].y == pts[2].y) + { + result[0].y = view.getRoutingCenterY(source) - dy; + } + else + { + result[0].x = view.getRoutingCenterX(source) - dx; + } + } + else if (mxUtils.contains(source, result[0].x + dx, result[0].y + dy)) + { + if (pts[1].y == pts[0].y) + { + result[0].y = view.getRoutingCenterY(target) - dy; + } + else + { + result[0].x = view.getRoutingCenterX(target) - dx; + } + } + } + } + else if (result.length == 0) + { + result = [point]; + } + + return result; + } +}; + +/** + * Function: createBends + * + * Adds custom bends for the center of each segment. + */ +mxEdgeSegmentHandler.prototype.createBends = function() +{ + var bends = []; + + // Source + var bend = this.createHandleShape(0); + + this.initBend(bend); + bend.node.style.cursor = mxConstants.CURSOR_BEND_HANDLE; + mxEvent.redirectMouseEvents(bend.node, this.graph, this.state); + bends.push(bend); + + if (mxClient.IS_TOUCH) + { + bend.node.setAttribute('pointer-events', 'none'); + } + + var pts = this.state.absolutePoints; + + // Waypoints (segment handles) + if (this.graph.isCellBendable(this.state.cell)) + { + if (this.points == null) + { + this.points = []; + } + + for (var i = 0; i < pts.length - 1; i++) + { + var bend = this.createVirtualBend(); + bends.push(bend); + var horizontal = pts[i].x - pts[i + 1].x == 0; + bend.node.style.cursor = (horizontal) ? 'col-resize' : 'row-resize'; + this.points.push(new mxPoint(0,0)); + + if (mxClient.IS_TOUCH) + { + bend.node.setAttribute('pointer-events', 'none'); + } + } + } + + // Target + var bend = this.createHandleShape(pts.length); + + this.initBend(bend); + bend.node.style.cursor = mxConstants.CURSOR_BEND_HANDLE; + mxEvent.redirectMouseEvents(bend.node, this.graph, this.state); + bends.push(bend); + + if (mxClient.IS_TOUCH) + { + bend.node.setAttribute('pointer-events', 'none'); + } + + return bends; +}; + + +/** + * Function: redrawInnerBends + * + * Updates the position of the custom bends. + */ +mxEdgeSegmentHandler.prototype.redrawInnerBends = function(p0, pe) +{ + if (this.graph.isCellBendable(this.state.cell)) + { + var s = mxConstants.HANDLE_SIZE; + var pts = this.state.absolutePoints; + + if (pts != null && pts.length > 1) + { + for (var i = 0; i < this.state.absolutePoints.length - 1; i++) + { + if (this.bends[i + 1] != null) + { + var p0 = pts[i]; + var pe = pts[i + 1]; + var pt = new mxPoint(p0.x + (pe.x - p0.x) / 2, p0.y + (pe.y - p0.y) / 2); + this.bends[i+1].bounds = new mxRectangle(pt.x - s / 2, pt.y - s / 2, s, s); + this.bends[i+1].reconfigure(); + this.bends[i+1].redraw(); + } + } + } + } +}; + +/** + * Function: connect + * + * Calls <refresh> after <mxEdgeHandler.connect>. + */ +mxEdgeSegmentHandler.prototype.connect = function(edge, terminal, isSource, isClone, me) +{ + mxEdgeHandler.prototype.connect.apply(this, arguments); + this.refresh(); +}; + +/** + * Function: changeTerminalPoint + * + * Calls <refresh> after <mxEdgeHandler.changeTerminalPoint>. + */ +mxEdgeSegmentHandler.prototype.changeTerminalPoint = function(edge, point, isSource) +{ + mxEdgeHandler.prototype.changeTerminalPoint.apply(this, arguments); + this.refresh(); +}; + +/** + * Function: changePoints + * + * Changes the points of the given edge to reflect the current state of the handler. + */ +mxEdgeSegmentHandler.prototype.changePoints = function(edge, points) +{ + points = []; + var pts = this.abspoints; + + if (pts.length > 1) + { + var pt0 = pts[0]; + var pt1 = pts[1]; + + for (var i = 2; i < pts.length; i++) + { + var pt2 = pts[i]; + + if ((Math.round(pt0.x) != Math.round(pt1.x) || + Math.round(pt1.x) != Math.round(pt2.x)) && + (Math.round(pt0.y) != Math.round(pt1.y) || + Math.round(pt1.y) != Math.round(pt2.y))) + { + pt0 = pt1; + pt1 = pt1.clone(); + this.convertPoint(pt1, false); + points.push(pt1); + } + + pt1 = pt2; + } + } + + mxElbowEdgeHandler.prototype.changePoints.apply(this, arguments); + this.refresh(); +}; + +/** + * Function: refresh + * + * Refreshes the bends of this handler. + */ +mxEdgeSegmentHandler.prototype.refresh = function() +{ + if (this.bends != null) + { + for (var i = 0; i < this.bends.length; i++) + { + if (this.bends[i] != null) + { + this.bends[i].destroy(); + this.bends[i] = null; + } + } + + this.bends = this.createBends(); + } +}; diff --git a/src/js/handler/mxElbowEdgeHandler.js b/src/js/handler/mxElbowEdgeHandler.js new file mode 100644 index 0000000..85fbb06 --- /dev/null +++ b/src/js/handler/mxElbowEdgeHandler.js @@ -0,0 +1,248 @@ +/** + * $Id: mxElbowEdgeHandler.js,v 1.43 2012-01-06 13:06:01 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxElbowEdgeHandler + * + * Graph event handler that reconnects edges and modifies control points and + * the edge label location. Uses <mxTerminalMarker> for finding and + * highlighting new source and target vertices. This handler is automatically + * created in <mxGraph.createHandler>. It extends <mxEdgeHandler>. + * + * Constructor: mxEdgeHandler + * + * Constructs an edge handler for the specified <mxCellState>. + * + * Parameters: + * + * state - <mxCellState> of the cell to be modified. + */ +function mxElbowEdgeHandler(state) +{ + if (state != null) + { + this.state = state; + this.init(); + } +}; + +/** + * Extends mxEdgeHandler. + */ +mxElbowEdgeHandler.prototype = new mxEdgeHandler(); +mxElbowEdgeHandler.prototype.constructor = mxElbowEdgeHandler; + +/** + * Specifies if a double click on the middle handle should call + * <mxGraph.flipEdge>. Default is true. + */ +mxElbowEdgeHandler.prototype.flipEnabled = true; + +/** + * Variable: doubleClickOrientationResource + * + * Specifies the resource key for the tooltip to be displayed on the single + * control point for routed edges. If the resource for this key does not + * exist then the value is used as the error message. Default is + * 'doubleClickOrientation'. + */ +mxElbowEdgeHandler.prototype.doubleClickOrientationResource = + (mxClient.language != 'none') ? 'doubleClickOrientation' : ''; + +/** + * Function: createBends + * + * Overrides <mxEdgeHandler.createBends> to create custom bends. + */ + mxElbowEdgeHandler.prototype.createBends = function() + { + var bends = []; + + // Source + var bend = this.createHandleShape(0); + + this.initBend(bend); + bend.node.style.cursor = mxConstants.CURSOR_BEND_HANDLE; + mxEvent.redirectMouseEvents(bend.node, this.graph, this.state); + bends.push(bend); + + if (mxClient.IS_TOUCH) + { + bend.node.setAttribute('pointer-events', 'none'); + } + + // Virtual + bends.push(this.createVirtualBend()); + this.points.push(new mxPoint(0,0)); + + // Target + bend = this.createHandleShape(2); + + this.initBend(bend); + bend.node.style.cursor = mxConstants.CURSOR_BEND_HANDLE; + mxEvent.redirectMouseEvents(bend.node, this.graph, this.state); + bends.push(bend); + + if (mxClient.IS_TOUCH) + { + bend.node.setAttribute('pointer-events', 'none'); + } + + return bends; + }; + +/** + * Function: createVirtualBend + * + * Creates a virtual bend that supports double clicking and calls + * <mxGraph.flipEdge>. + */ +mxElbowEdgeHandler.prototype.createVirtualBend = function() +{ + var bend = this.createHandleShape(); + this.initBend(bend); + + var crs = this.getCursorForBend(); + bend.node.style.cursor = crs; + + // Double-click changes edge style + var dblClick = mxUtils.bind(this, function(evt) + { + if (!mxEvent.isConsumed(evt) && + this.flipEnabled) + { + this.graph.flipEdge(this.state.cell, evt); + mxEvent.consume(evt); + } + }); + + mxEvent.redirectMouseEvents(bend.node, this.graph, this.state, + null, null, null, dblClick); + + if (!this.graph.isCellBendable(this.state.cell)) + { + bend.node.style.visibility = 'hidden'; + } + + return bend; +}; + +/** + * Function: getCursorForBend + * + * Returns the cursor to be used for the bend. + */ +mxElbowEdgeHandler.prototype.getCursorForBend = function() +{ + return (this.state.style[mxConstants.STYLE_EDGE] == mxEdgeStyle.TopToBottom || + this.state.style[mxConstants.STYLE_EDGE] == mxConstants.EDGESTYLE_TOPTOBOTTOM || + ((this.state.style[mxConstants.STYLE_EDGE] == mxEdgeStyle.ElbowConnector || + this.state.style[mxConstants.STYLE_EDGE] == mxConstants.EDGESTYLE_ELBOW)&& + this.state.style[mxConstants.STYLE_ELBOW] == mxConstants.ELBOW_VERTICAL)) ? + 'row-resize' : 'col-resize'; +}; + +/** + * Function: getTooltipForNode + * + * Returns the tooltip for the given node. + */ +mxElbowEdgeHandler.prototype.getTooltipForNode = function(node) +{ + var tip = null; + + if (this.bends != null && + this.bends[1] != null && + (node == this.bends[1].node || + node.parentNode == this.bends[1].node)) + { + tip = this.doubleClickOrientationResource; + tip = mxResources.get(tip) || tip; // translate + } + + return tip; +}; + +/** + * Function: convertPoint + * + * Converts the given point in-place from screen to unscaled, untranslated + * graph coordinates and applies the grid. + * + * Parameters: + * + * point - <mxPoint> to be converted. + * gridEnabled - Boolean that specifies if the grid should be applied. + */ +mxElbowEdgeHandler.prototype.convertPoint = function(point, gridEnabled) +{ + var scale = this.graph.getView().getScale(); + var tr = this.graph.getView().getTranslate(); + var origin = this.state.origin; + + if (gridEnabled) + { + point.x = this.graph.snap(point.x); + point.y = this.graph.snap(point.y); + } + + point.x = Math.round(point.x / scale - tr.x - origin.x); + point.y = Math.round(point.y / scale - tr.y - origin.y); +}; + +/** + * Function: redrawInnerBends + * + * Updates and redraws the inner bends. + * + * Parameters: + * + * p0 - <mxPoint> that represents the location of the first point. + * pe - <mxPoint> that represents the location of the last point. + */ +mxElbowEdgeHandler.prototype.redrawInnerBends = function(p0, pe) +{ + var g = this.graph.getModel().getGeometry(this.state.cell); + var pts = g.points; + + var pt = (pts != null) ? pts[0] : null; + + if (pt == null) + { + pt = new mxPoint(p0.x + (pe.x - p0.x) / 2, p0.y + (pe.y - p0.y) / 2); + } + else + { + pt = new mxPoint(this.graph.getView().scale*(pt.x + + this.graph.getView().translate.x + this.state.origin.x), + this.graph.getView().scale*(pt.y + this.graph.getView().translate.y + + this.state.origin.y)); + } + + // Makes handle slightly bigger if the yellow label handle + // exists and intersects this green handle + var b = this.bends[1].bounds; + var w = b.width; + var h = b.height; + + if (this.handleImage == null) + { + w = mxConstants.HANDLE_SIZE; + h = mxConstants.HANDLE_SIZE; + } + + var bounds = new mxRectangle(pt.x - w / 2, pt.y - h / 2, w, h); + + if (this.handleImage == null && this.labelShape.node.style.visibility != 'hidden' && + mxUtils.intersects(bounds, this.labelShape.bounds)) + { + w += 3; + h += 3; + bounds = new mxRectangle(pt.x - w / 2, pt.y - h / 2, w, h); + } + + this.bends[1].bounds = bounds; + this.bends[1].reconfigure(); + this.bends[1].redraw(); +}; diff --git a/src/js/handler/mxGraphHandler.js b/src/js/handler/mxGraphHandler.js new file mode 100644 index 0000000..57e27a1 --- /dev/null +++ b/src/js/handler/mxGraphHandler.js @@ -0,0 +1,916 @@ +/** + * $Id: mxGraphHandler.js,v 1.129 2012-04-13 12:53:30 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxGraphHandler + * + * Graph event handler that handles selection. Individual cells are handled + * separately using <mxVertexHandler> or one of the edge handlers. These + * handlers are created using <mxGraph.createHandler> in + * <mxGraphSelectionModel.cellAdded>. + * + * To avoid the container to scroll a moved cell into view, set + * <scrollAfterMove> to false. + * + * Constructor: mxGraphHandler + * + * Constructs an event handler that creates handles for the + * selection cells. + * + * Parameters: + * + * graph - Reference to the enclosing <mxGraph>. + */ +function mxGraphHandler(graph) +{ + this.graph = graph; + this.graph.addMouseListener(this); + + // Repaints the handler after autoscroll + this.panHandler = mxUtils.bind(this, function() + { + this.updatePreviewShape(); + }); + + this.graph.addListener(mxEvent.PAN, this.panHandler); +}; + +/** + * Variable: graph + * + * Reference to the enclosing <mxGraph>. + */ +mxGraphHandler.prototype.graph = null; + +/** + * Variable: maxCells + * + * Defines the maximum number of cells to paint subhandles + * for. Default is 50 for Firefox and 20 for IE. Set this + * to 0 if you want an unlimited number of handles to be + * displayed. This is only recommended if the number of + * cells in the graph is limited to a small number, eg. + * 500. + */ +mxGraphHandler.prototype.maxCells = (mxClient.IS_IE) ? 20 : 50; + +/** + * Variable: enabled + * + * Specifies if events are handled. Default is true. + */ +mxGraphHandler.prototype.enabled = true; + +/** + * Variable: highlightEnabled + * + * Specifies if drop targets under the mouse should be enabled. Default is + * true. + */ +mxGraphHandler.prototype.highlightEnabled = true; + +/** + * Variable: cloneEnabled + * + * Specifies if cloning by control-drag is enabled. Default is true. + */ +mxGraphHandler.prototype.cloneEnabled = true; + +/** + * Variable: moveEnabled + * + * Specifies if moving is enabled. Default is true. + */ +mxGraphHandler.prototype.moveEnabled = true; + +/** + * Variable: guidesEnabled + * + * Specifies if other cells should be used for snapping the right, center or + * left side of the current selection. Default is false. + */ +mxGraphHandler.prototype.guidesEnabled = false; + +/** + * Variable: guide + * + * Holds the <mxGuide> instance that is used for alignment. + */ +mxGraphHandler.prototype.guide = null; + +/** + * Variable: currentDx + * + * Stores the x-coordinate of the current mouse move. + */ +mxGraphHandler.prototype.currentDx = null; + +/** + * Variable: currentDy + * + * Stores the y-coordinate of the current mouse move. + */ +mxGraphHandler.prototype.currentDy = null; + +/** + * Variable: updateCursor + * + * Specifies if a move cursor should be shown if the mouse is ove a movable + * cell. Default is true. + */ +mxGraphHandler.prototype.updateCursor = true; + +/** + * Variable: selectEnabled + * + * Specifies if selecting is enabled. Default is true. + */ +mxGraphHandler.prototype.selectEnabled = true; + +/** + * Variable: removeCellsFromParent + * + * Specifies if cells may be moved out of their parents. Default is true. + */ +mxGraphHandler.prototype.removeCellsFromParent = true; + +/** + * Variable: connectOnDrop + * + * Specifies if drop events are interpreted as new connections if no other + * drop action is defined. Default is false. + */ +mxGraphHandler.prototype.connectOnDrop = false; + +/** + * Variable: scrollOnMove + * + * Specifies if the view should be scrolled so that a moved cell is + * visible. Default is true. + */ +mxGraphHandler.prototype.scrollOnMove = true; + +/** + * Variable: minimumSize + * + * Specifies the minimum number of pixels for the width and height of a + * selection border. Default is 6. + */ +mxGraphHandler.prototype.minimumSize = 6; + +/** + * Variable: previewColor + * + * Specifies the color of the preview shape. Default is black. + */ +mxGraphHandler.prototype.previewColor = 'black'; + +/** + * Variable: htmlPreview + * + * Specifies if the graph container should be used for preview. If this is used + * then drop target detection relies entirely on <mxGraph.getCellAt> because + * the HTML preview does not "let events through". Default is false. + */ +mxGraphHandler.prototype.htmlPreview = false; + +/** + * Variable: shape + * + * Reference to the <mxShape> that represents the preview. + */ +mxGraphHandler.prototype.shape = null; + +/** + * Variable: scaleGrid + * + * Specifies if the grid should be scaled. Default is false. + */ +mxGraphHandler.prototype.scaleGrid = false; + +/** + * Variable: crisp + * + * Specifies if the move preview should be rendered in crisp mode if applicable. + * Default is true. + */ +mxGraphHandler.prototype.crisp = true; + +/** + * Function: isEnabled + * + * Returns <enabled>. + */ +mxGraphHandler.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Sets <enabled>. + */ +mxGraphHandler.prototype.setEnabled = function(value) +{ + this.enabled = value; +}; + +/** + * Function: isCloneEnabled + * + * Returns <cloneEnabled>. + */ +mxGraphHandler.prototype.isCloneEnabled = function() +{ + return this.cloneEnabled; +}; + +/** + * Function: setCloneEnabled + * + * Sets <cloneEnabled>. + * + * Parameters: + * + * value - Boolean that specifies the new clone enabled state. + */ +mxGraphHandler.prototype.setCloneEnabled = function(value) +{ + this.cloneEnabled = value; +}; + +/** + * Function: isMoveEnabled + * + * Returns <moveEnabled>. + */ +mxGraphHandler.prototype.isMoveEnabled = function() +{ + return this.moveEnabled; +}; + +/** + * Function: setMoveEnabled + * + * Sets <moveEnabled>. + */ +mxGraphHandler.prototype.setMoveEnabled = function(value) +{ + this.moveEnabled = value; +}; + +/** + * Function: isSelectEnabled + * + * Returns <selectEnabled>. + */ +mxGraphHandler.prototype.isSelectEnabled = function() +{ + return this.selectEnabled; +}; + +/** + * Function: setSelectEnabled + * + * Sets <selectEnabled>. + */ +mxGraphHandler.prototype.setSelectEnabled = function(value) +{ + this.selectEnabled = value; +}; + +/** + * Function: isRemoveCellsFromParent + * + * Returns <removeCellsFromParent>. + */ +mxGraphHandler.prototype.isRemoveCellsFromParent = function() +{ + return this.removeCellsFromParent; +}; + +/** + * Function: setRemoveCellsFromParent + * + * Sets <removeCellsFromParent>. + */ +mxGraphHandler.prototype.setRemoveCellsFromParent = function(value) +{ + this.removeCellsFromParent = value; +}; + +/** + * Function: getInitialCellForEvent + * + * Hook to return initial cell for the given event. + */ +mxGraphHandler.prototype.getInitialCellForEvent = function(me) +{ + return me.getCell(); +}; + +/** + * Function: isDelayedSelection + * + * Hook to return true for delayed selections. + */ +mxGraphHandler.prototype.isDelayedSelection = function(cell) +{ + return this.graph.isCellSelected(cell); +}; + +/** + * Function: mouseDown + * + * Handles the event by selecing the given cell and creating a handle for + * it. By consuming the event all subsequent events of the gesture are + * redirected to this handler. + */ +mxGraphHandler.prototype.mouseDown = function(sender, me) +{ + if (!me.isConsumed() && this.isEnabled() && this.graph.isEnabled() && + !this.graph.isForceMarqueeEvent(me.getEvent()) && me.getState() != null) + { + var cell = this.getInitialCellForEvent(me); + this.cell = null; + this.delayedSelection = this.isDelayedSelection(cell); + + if (this.isSelectEnabled() && !this.delayedSelection) + { + this.graph.selectCellForEvent(cell, me.getEvent()); + } + + if (this.isMoveEnabled()) + { + var model = this.graph.model; + var geo = model.getGeometry(cell); + + if (this.graph.isCellMovable(cell) && ((!model.isEdge(cell) || this.graph.getSelectionCount() > 1 || + (geo.points != null && geo.points.length > 0) || model.getTerminal(cell, true) == null || + model.getTerminal(cell, false) == null) || this.graph.allowDanglingEdges || + (this.graph.isCloneEvent(me.getEvent()) && this.graph.isCellsCloneable()))) + { + this.start(cell, me.getX(), me.getY()); + } + + this.cellWasClicked = true; + + // Workaround for SELECT element not working in Webkit, this blocks moving + // of the cell if the select element is clicked in Safari which is needed + // because Safari doesn't seem to route the subsequent mouseUp event via + // this handler which leads to an inconsistent state (no reset called). + // Same for cellWasClicked which will block clearing the selection when + // clicking the background after clicking on the SELECT element in Safari. + if ((!mxClient.IS_SF && !mxClient.IS_GC) || me.getSource().nodeName != 'SELECT') + { + me.consume(); + } + else if (mxClient.IS_SF && me.getSource().nodeName == 'SELECT') + { + this.cellWasClicked = false; + this.first = null; + } + } + } +}; + +/** + * Function: getGuideStates + * + * Creates an array of cell states which should be used as guides. + */ +mxGraphHandler.prototype.getGuideStates = function() +{ + var parent = this.graph.getDefaultParent(); + var model = this.graph.getModel(); + + var filter = mxUtils.bind(this, function(cell) + { + return this.graph.view.getState(cell) != null && + model.isVertex(cell) && + model.getGeometry(cell) != null && + !model.getGeometry(cell).relative; + }); + + return this.graph.view.getCellStates(model.filterDescendants(filter, parent)); +}; + +/** + * Function: getCells + * + * Returns the cells to be modified by this handler. This implementation + * returns all selection cells that are movable, or the given initial cell if + * the given cell is not selected and movable. This handles the case of moving + * unselectable or unselected cells. + * + * Parameters: + * + * initialCell - <mxCell> that triggered this handler. + */ +mxGraphHandler.prototype.getCells = function(initialCell) +{ + if (!this.delayedSelection && this.graph.isCellMovable(initialCell)) + { + return [initialCell]; + } + else + { + return this.graph.getMovableCells(this.graph.getSelectionCells()); + } +}; + +/** + * Function: getPreviewBounds + * + * Returns the <mxRectangle> used as the preview bounds for + * moving the given cells. + */ +mxGraphHandler.prototype.getPreviewBounds = function(cells) +{ + var bounds = this.graph.getView().getBounds(cells); + + if (bounds != null) + { + if (bounds.width < this.minimumSize) + { + var dx = this.minimumSize - bounds.width; + bounds.x -= dx / 2; + bounds.width = this.minimumSize; + } + + if (bounds.height < this.minimumSize) + { + var dy = this.minimumSize - bounds.height; + bounds.y -= dy / 2; + bounds.height = this.minimumSize; + } + } + + return bounds; +}; + +/** + * Function: createPreviewShape + * + * Creates the shape used to draw the preview for the given bounds. + */ +mxGraphHandler.prototype.createPreviewShape = function(bounds) +{ + var shape = new mxRectangleShape(bounds, null, this.previewColor); + shape.isDashed = true; + shape.crisp = this.crisp; + + if (this.htmlPreview) + { + shape.dialect = mxConstants.DIALECT_STRICTHTML; + shape.init(this.graph.container); + } + else + { + // Makes sure to use either VML or SVG shapes in order to implement + // event-transparency on the background area of the rectangle since + // HTML shapes do not let mouseevents through even when transparent + shape.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ? + mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG; + shape.init(this.graph.getView().getOverlayPane()); + + // Event-transparency + if (shape.dialect == mxConstants.DIALECT_SVG) + { + shape.node.setAttribute('style', 'pointer-events:none;'); + } + else + { + shape.node.style.background = ''; + } + } + + return shape; +}; + +/** + * Function: start + * + * Starts the handling of the mouse gesture. + */ +mxGraphHandler.prototype.start = function(cell, x, y) +{ + this.cell = cell; + this.first = mxUtils.convertPoint(this.graph.container, x, y); + this.cells = this.getCells(this.cell); + this.bounds = this.getPreviewBounds(this.cells); + + if (this.guidesEnabled) + { + this.guide = new mxGuide(this.graph, this.getGuideStates()); + } +}; + +/** + * Function: useGuidesForEvent + * + * Returns true if the guides should be used for the given <mxMouseEvent>. + * This implementation returns <mxGuide.isEnabledForEvent>. + */ +mxGraphHandler.prototype.useGuidesForEvent = function(me) +{ + return (this.guide != null) ? this.guide.isEnabledForEvent(me.getEvent()) : true; +}; + + +/** + * Function: snap + * + * Snaps the given vector to the grid and returns the given mxPoint instance. + */ +mxGraphHandler.prototype.snap = function(vector) +{ + var scale = (this.scaleGrid) ? this.graph.view.scale : 1; + + vector.x = this.graph.snap(vector.x / scale) * scale; + vector.y = this.graph.snap(vector.y / scale) * scale; + + return vector; +}; + +/** + * Function: mouseMove + * + * Handles the event by highlighting possible drop targets and updating the + * preview. + */ +mxGraphHandler.prototype.mouseMove = function(sender, me) +{ + var graph = this.graph; + + if (!me.isConsumed() && graph.isMouseDown && this.cell != null && + this.first != null && this.bounds != null) + { + var point = mxUtils.convertPoint(graph.container, me.getX(), me.getY()); + var dx = point.x - this.first.x; + var dy = point.y - this.first.y; + var tol = graph.tolerance; + + if (this.shape!= null || Math.abs(dx) > tol || Math.abs(dy) > tol) + { + // Highlight is used for highlighting drop targets + if (this.highlight == null) + { + this.highlight = new mxCellHighlight(this.graph, + mxConstants.DROP_TARGET_COLOR, 3); + } + + if (this.shape == null) + { + this.shape = this.createPreviewShape(this.bounds); + } + + var gridEnabled = graph.isGridEnabledEvent(me.getEvent()); + var hideGuide = true; + + if (this.guide != null && this.useGuidesForEvent(me)) + { + var delta = this.guide.move(this.bounds, new mxPoint(dx, dy), gridEnabled); + hideGuide = false; + dx = delta.x; + dy = delta.y; + } + else if (gridEnabled) + { + var trx = graph.getView().translate; + var scale = graph.getView().scale; + + var tx = this.bounds.x - (graph.snap(this.bounds.x / scale - trx.x) + trx.x) * scale; + var ty = this.bounds.y - (graph.snap(this.bounds.y / scale - trx.y) + trx.y) * scale; + var v = this.snap(new mxPoint(dx, dy)); + + dx = v.x - tx; + dy = v.y - ty; + } + + if (this.guide != null && hideGuide) + { + this.guide.hide(); + } + + // Constrained movement if shift key is pressed + if (graph.isConstrainedEvent(me.getEvent())) + { + if (Math.abs(dx) > Math.abs(dy)) + { + dy = 0; + } + else + { + dx = 0; + } + } + + this.currentDx = dx; + this.currentDy = dy; + this.updatePreviewShape(); + + var target = null; + var cell = me.getCell(); + + if (graph.isDropEnabled() && this.highlightEnabled) + { + // Contains a call to getCellAt to find the cell under the mouse + target = graph.getDropTarget(this.cells, me.getEvent(), cell); + } + + // Checks if parent is dropped into child + var parent = target; + var model = graph.getModel(); + + while (parent != null && parent != this.cells[0]) + { + parent = model.getParent(parent); + } + + var clone = graph.isCloneEvent(me.getEvent()) && graph.isCellsCloneable() && this.isCloneEnabled(); + var state = graph.getView().getState(target); + var highlight = false; + + if (state != null && parent == null && (model.getParent(this.cell) != target || clone)) + { + if (this.target != target) + { + this.target = target; + this.setHighlightColor(mxConstants.DROP_TARGET_COLOR); + } + + highlight = true; + } + else + { + this.target = null; + + if (this.connectOnDrop && cell != null && this.cells.length == 1 && + graph.getModel().isVertex(cell) && graph.isCellConnectable(cell)) + { + state = graph.getView().getState(cell); + + if (state != null) + { + var error = graph.getEdgeValidationError(null, this.cell, cell); + var color = (error == null) ? + mxConstants.VALID_COLOR : + mxConstants.INVALID_CONNECT_TARGET_COLOR; + this.setHighlightColor(color); + highlight = true; + } + } + } + + if (state != null && highlight) + { + this.highlight.highlight(state); + } + else + { + this.highlight.hide(); + } + } + + me.consume(); + + // Cancels the bubbling of events to the container so + // that the droptarget is not reset due to an mouseMove + // fired on the container with no associated state. + mxEvent.consume(me.getEvent()); + } + else if ((this.isMoveEnabled() || this.isCloneEnabled()) && this.updateCursor && + !me.isConsumed() && me.getState() != null && !graph.isMouseDown) + { + var cursor = graph.getCursorForCell(me.getCell()); + + if (cursor == null && graph.isEnabled() && graph.isCellMovable(me.getCell())) + { + if (graph.getModel().isEdge(me.getCell())) + { + cursor = mxConstants.CURSOR_MOVABLE_EDGE; + } + else + { + cursor = mxConstants.CURSOR_MOVABLE_VERTEX; + } + } + + me.getState().setCursor(cursor); + me.consume(); + } +}; + +/** + * Function: updatePreviewShape + * + * Updates the bounds of the preview shape. + */ +mxGraphHandler.prototype.updatePreviewShape = function() +{ + if (this.shape != null) + { + this.shape.bounds = new mxRectangle(this.bounds.x + this.currentDx - this.graph.panDx, + this.bounds.y + this.currentDy - this.graph.panDy, this.bounds.width, this.bounds.height); + this.shape.redraw(); + } +}; + +/** + * Function: setHighlightColor + * + * Sets the color of the rectangle used to highlight drop targets. + * + * Parameters: + * + * color - String that represents the new highlight color. + */ +mxGraphHandler.prototype.setHighlightColor = function(color) +{ + if (this.highlight != null) + { + this.highlight.setHighlightColor(color); + } +}; + +/** + * Function: mouseUp + * + * Handles the event by applying the changes to the selection cells. + */ +mxGraphHandler.prototype.mouseUp = function(sender, me) +{ + if (!me.isConsumed()) + { + var graph = this.graph; + + if (this.cell != null && this.first != null && this.shape != null && + this.currentDx != null && this.currentDy != null) + { + var scale = graph.getView().scale; + var clone = graph.isCloneEvent(me.getEvent()) && graph.isCellsCloneable() && this.isCloneEnabled(); + var dx = this.currentDx / scale; + var dy = this.currentDy / scale; + + var cell = me.getCell(); + + if (this.connectOnDrop && this.target == null && cell != null && graph.getModel().isVertex(cell) && + graph.isCellConnectable(cell) && graph.isEdgeValid(null, this.cell, cell)) + { + graph.connectionHandler.connect(this.cell, cell, me.getEvent()); + } + else + { + var target = this.target; + + if (graph.isSplitEnabled() && graph.isSplitTarget(target, this.cells, me.getEvent())) + { + graph.splitEdge(target, this.cells, null, dx, dy); + } + else + { + this.moveCells(this.cells, dx, dy, clone, this.target, me.getEvent()); + } + } + } + else if (this.isSelectEnabled() && this.delayedSelection && this.cell != null) + { + this.selectDelayed(me); + } + } + + // Consumes the event if a cell was initially clicked + if (this.cellWasClicked) + { + me.consume(); + } + + this.reset(); +}; + +/** + * Function: selectDelayed + * + * Implements the delayed selection for the given mouse event. + */ +mxGraphHandler.prototype.selectDelayed = function(me) +{ + this.graph.selectCellForEvent(this.cell, me.getEvent()); +}; + +/** + * Function: reset + * + * Resets the state of this handler. + */ +mxGraphHandler.prototype.reset = function() +{ + this.destroyShapes(); + this.cellWasClicked = false; + this.delayedSelection = false; + this.currentDx = null; + this.currentDy = null; + this.guides = null; + this.first = null; + this.cell = null; + this.target = null; +}; + +/** + * Function: shouldRemoveCellsFromParent + * + * Returns true if the given cells should be removed from the parent for the specified + * mousereleased event. + */ +mxGraphHandler.prototype.shouldRemoveCellsFromParent = function(parent, cells, evt) +{ + if (this.graph.getModel().isVertex(parent)) + { + var pState = this.graph.getView().getState(parent); + var pt = mxUtils.convertPoint(this.graph.container, + mxEvent.getClientX(evt), mxEvent.getClientY(evt)); + + return pState != null && !mxUtils.contains(pState, pt.x, pt.y); + } + + return false; +}; + +/** + * Function: moveCells + * + * Moves the given cells by the specified amount. + */ +mxGraphHandler.prototype.moveCells = function(cells, dx, dy, clone, target, evt) +{ + if (clone) + { + cells = this.graph.getCloneableCells(cells); + } + + // Removes cells from parent + if (target == null && this.isRemoveCellsFromParent() && + this.shouldRemoveCellsFromParent(this.graph.getModel().getParent(this.cell), cells, evt)) + { + target = this.graph.getDefaultParent(); + } + + // Passes all selected cells in order to correctly clone or move into + // the target cell. The method checks for each cell if its movable. + cells = this.graph.moveCells(cells, dx - this.graph.panDx / this.graph.view.scale, + dy - this.graph.panDy / this.graph.view.scale, clone, target, evt); + + if (this.isSelectEnabled() && this.scrollOnMove) + { + this.graph.scrollCellToVisible(cells[0]); + } + + // Selects the new cells if cells have been cloned + if (clone) + { + this.graph.setSelectionCells(cells); + } +}; + +/** + * Function: destroyShapes + * + * Destroy the preview and highlight shapes. + */ +mxGraphHandler.prototype.destroyShapes = function() +{ + // Destroys the preview dashed rectangle + if (this.shape != null) + { + this.shape.destroy(); + this.shape = null; + } + + if (this.guide != null) + { + this.guide.destroy(); + this.guide = null; + } + + // Destroys the drop target highlight + if (this.highlight != null) + { + this.highlight.destroy(); + this.highlight = null; + } +}; + +/** + * Function: destroy + * + * Destroys the handler and all its resources and DOM nodes. + */ +mxGraphHandler.prototype.destroy = function() +{ + this.graph.removeMouseListener(this); + this.graph.removeListener(this.panHandler); + this.destroyShapes(); +}; diff --git a/src/js/handler/mxKeyHandler.js b/src/js/handler/mxKeyHandler.js new file mode 100644 index 0000000..cc07e51 --- /dev/null +++ b/src/js/handler/mxKeyHandler.js @@ -0,0 +1,402 @@ +/** + * $Id: mxKeyHandler.js,v 1.48 2012-03-30 08:30:41 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxKeyHandler + * + * Event handler that listens to keystroke events. This is not a singleton, + * however, it is normally only required once if the target is the document + * element (default). + * + * This handler installs a key event listener in the topmost DOM node and + * processes all events that originate from descandants of <mxGraph.container> + * or from the topmost DOM node. The latter means that all unhandled keystrokes + * are handled by this object regardless of the focused state of the <graph>. + * + * Example: + * + * The following example creates a key handler that listens to the delete key + * (46) and deletes the selection cells if the graph is enabled. + * + * (code) + * var keyHandler = new mxKeyHandler(graph); + * keyHandler.bindKey(46, function(evt) + * { + * if (graph.isEnabled()) + * { + * graph.removeCells(); + * } + * }); + * (end) + * + * Keycodes: + * + * See http://tinyurl.com/yp8jgl or http://tinyurl.com/229yqw for a list of + * keycodes or install a key event listener into the document element and print + * the key codes of the respective events to the console. + * + * To support the Command key and the Control key on the Mac, the following + * code can be used. + * + * (code) + * keyHandler.getFunction = function(evt) + * { + * if (evt != null) + * { + * return (mxEvent.isControlDown(evt) || (mxClient.IS_MAC && evt.metaKey)) ? this.controlKeys[evt.keyCode] : this.normalKeys[evt.keyCode]; + * } + * + * return null; + * }; + * (end) + * + * Constructor: mxKeyHandler + * + * Constructs an event handler that executes functions bound to specific + * keystrokes. + * + * Parameters: + * + * graph - Reference to the associated <mxGraph>. + * target - Optional reference to the event target. If null, the document + * element is used as the event target, that is, the object where the key + * event listener is installed. + */ +function mxKeyHandler(graph, target) +{ + if (graph != null) + { + this.graph = graph; + this.target = target || document.documentElement; + + // Creates the arrays to map from keycodes to functions + this.normalKeys = []; + this.shiftKeys = []; + this.controlKeys = []; + this.controlShiftKeys = []; + + // Installs the keystroke listener in the target + mxEvent.addListener(this.target, "keydown", + mxUtils.bind(this, function(evt) + { + this.keyDown(evt); + }) + ); + + // Automatically deallocates memory in IE + if (mxClient.IS_IE) + { + mxEvent.addListener(window, 'unload', + mxUtils.bind(this, function() + { + this.destroy(); + }) + ); + } + } +}; + +/** + * Variable: graph + * + * Reference to the <mxGraph> associated with this handler. + */ +mxKeyHandler.prototype.graph = null; + +/** + * Variable: target + * + * Reference to the target DOM, that is, the DOM node where the key event + * listeners are installed. + */ +mxKeyHandler.prototype.target = null; + +/** + * Variable: normalKeys + * + * Maps from keycodes to functions for non-pressed control keys. + */ +mxKeyHandler.prototype.normalKeys = null; + +/** + * Variable: shiftKeys + * + * Maps from keycodes to functions for pressed shift keys. + */ +mxKeyHandler.prototype.shiftKeys = null; + +/** + * Variable: controlKeys + * + * Maps from keycodes to functions for pressed control keys. + */ +mxKeyHandler.prototype.controlKeys = null; + +/** + * Variable: controlShiftKeys + * + * Maps from keycodes to functions for pressed control and shift keys. + */ +mxKeyHandler.prototype.controlShiftKeys = null; + +/** + * Variable: enabled + * + * Specifies if events are handled. Default is true. + */ +mxKeyHandler.prototype.enabled = true; + +/** + * Function: isEnabled + * + * Returns true if events are handled. This implementation returns + * <enabled>. + */ +mxKeyHandler.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Enables or disables event handling by updating <enabled>. + * + * Parameters: + * + * enabled - Boolean that specifies the new enabled state. + */ +mxKeyHandler.prototype.setEnabled = function(enabled) +{ + this.enabled = enabled; +}; + +/** + * Function: bindKey + * + * Binds the specified keycode to the given function. This binding is used + * if the control key is not pressed. + * + * Parameters: + * + * code - Integer that specifies the keycode. + * funct - JavaScript function that takes the key event as an argument. + */ +mxKeyHandler.prototype.bindKey = function(code, funct) +{ + this.normalKeys[code] = funct; +}; + +/** + * Function: bindShiftKey + * + * Binds the specified keycode to the given function. This binding is used + * if the shift key is pressed. + * + * Parameters: + * + * code - Integer that specifies the keycode. + * funct - JavaScript function that takes the key event as an argument. + */ +mxKeyHandler.prototype.bindShiftKey = function(code, funct) +{ + this.shiftKeys[code] = funct; +}; + +/** + * Function: bindControlKey + * + * Binds the specified keycode to the given function. This binding is used + * if the control key is pressed. + * + * Parameters: + * + * code - Integer that specifies the keycode. + * funct - JavaScript function that takes the key event as an argument. + */ +mxKeyHandler.prototype.bindControlKey = function(code, funct) +{ + this.controlKeys[code] = funct; +}; + +/** + * Function: bindControlShiftKey + * + * Binds the specified keycode to the given function. This binding is used + * if the control and shift key are pressed. + * + * Parameters: + * + * code - Integer that specifies the keycode. + * funct - JavaScript function that takes the key event as an argument. + */ +mxKeyHandler.prototype.bindControlShiftKey = function(code, funct) +{ + this.controlShiftKeys[code] = funct; +}; + +/** + * Function: isControlDown + * + * Returns true if the control key is pressed. This uses <mxEvent.isControlDown>. + * + * Parameters: + * + * evt - Key event whose control key pressed state should be returned. + */ +mxKeyHandler.prototype.isControlDown = function(evt) +{ + return mxEvent.isControlDown(evt); +}; + +/** + * Function: getFunction + * + * Returns the function associated with the given key event or null if no + * function is associated with the given event. + * + * Parameters: + * + * evt - Key event whose associated function should be returned. + */ +mxKeyHandler.prototype.getFunction = function(evt) +{ + if (evt != null) + { + if (this.isControlDown(evt)) + { + if (mxEvent.isShiftDown(evt)) + { + return this.controlShiftKeys[evt.keyCode]; + } + else + { + return this.controlKeys[evt.keyCode]; + } + } + else + { + if (mxEvent.isShiftDown(evt)) + { + return this.shiftKeys[evt.keyCode]; + } + else + { + return this.normalKeys[evt.keyCode]; + } + } + } + + return null; +}; + +/** + * Function: isGraphEvent + * + * Returns true if the event should be processed by this handler, that is, + * if the event source is either the target, one of its direct children, a + * descendant of the <mxGraph.container>, or the <mxGraph.cellEditor> of the + * <graph>. + * + * Parameters: + * + * evt - Key event that represents the keystroke. + */ +mxKeyHandler.prototype.isGraphEvent = function(evt) +{ + var source = mxEvent.getSource(evt); + + // Accepts events from the target object or + // in-place editing inside graph + if ((source == this.target || source.parentNode == this.target) || + (this.graph.cellEditor != null && source == this.graph.cellEditor.textarea)) + { + return true; + } + + // Accepts events from inside the container + var elt = source; + + while (elt != null) + { + if (elt == this.graph.container) + { + return true; + } + + elt = elt.parentNode; + } + + return false; +}; + +/** + * Function: keyDown + * + * Handles the event by invoking the function bound to the respective + * keystroke if <mxGraph.isEnabled>, <isEnabled> and <isGraphEvent> all + * return true for the given event and <mxGraph.isEditing> returns false. + * If the graph is editing only the <enter> and <escape> cases are handled + * by calling the respective hooks. + * + * Parameters: + * + * evt - Key event that represents the keystroke. + */ +mxKeyHandler.prototype.keyDown = function(evt) +{ + if (this.graph.isEnabled() && !mxEvent.isConsumed(evt) && + this.isGraphEvent(evt) && this.isEnabled()) + { + // Cancels the editing if escape is pressed + if (evt.keyCode == 27 /* Escape */) + { + this.escape(evt); + } + + // Invokes the function for the keystroke + else if (!this.graph.isEditing()) + { + var boundFunction = this.getFunction(evt); + + if (boundFunction != null) + { + boundFunction(evt); + mxEvent.consume(evt); + } + } + } +}; + +/** + * Function: escape + * + * Hook to process ESCAPE keystrokes. This implementation invokes + * <mxGraph.stopEditing> to cancel the current editing, connecting + * and/or other ongoing modifications. + * + * Parameters: + * + * evt - Key event that represents the keystroke. Possible keycode in this + * case is 27 (ESCAPE). + */ +mxKeyHandler.prototype.escape = function(evt) +{ + if (this.graph.isEscapeEnabled()) + { + this.graph.escape(evt); + } +}; + +/** + * Function: destroy + * + * Destroys the handler and all its references into the DOM. This does + * normally not need to be called, it is called automatically when the + * window unloads (in IE). + */ +mxKeyHandler.prototype.destroy = function() +{ + this.target = null; +}; diff --git a/src/js/handler/mxPanningHandler.js b/src/js/handler/mxPanningHandler.js new file mode 100644 index 0000000..b388144 --- /dev/null +++ b/src/js/handler/mxPanningHandler.js @@ -0,0 +1,390 @@ +/** + * $Id: mxPanningHandler.js,v 1.79 2012-07-17 14:37:41 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxPanningHandler + * + * Event handler that pans and creates popupmenus. To use the left + * mousebutton for panning without interfering with cell moving and + * resizing, use <isUseLeftButton> and <isIgnoreCell>. For grid size + * steps while panning, use <useGrid>. This handler is built-into + * <mxGraph.panningHandler> and enabled using <mxGraph.setPanning>. + * + * Constructor: mxPanningHandler + * + * Constructs an event handler that creates a <mxPopupMenu> + * and pans the graph. + * + * Event: mxEvent.PAN_START + * + * Fires when the panning handler changes its <active> state to true. The + * <code>event</code> property contains the corresponding <mxMouseEvent>. + * + * Event: mxEvent.PAN + * + * Fires while handle is processing events. The <code>event</code> property contains + * the corresponding <mxMouseEvent>. + * + * Event: mxEvent.PAN_END + * + * Fires when the panning handler changes its <active> state to false. The + * <code>event</code> property contains the corresponding <mxMouseEvent>. + */ +function mxPanningHandler(graph, factoryMethod) +{ + if (graph != null) + { + this.graph = graph; + this.factoryMethod = factoryMethod; + this.graph.addMouseListener(this); + this.init(); + } +}; + +/** + * Extends mxPopupMenu. + */ +mxPanningHandler.prototype = new mxPopupMenu(); +mxPanningHandler.prototype.constructor = mxPanningHandler; + +/** + * Variable: graph + * + * Reference to the enclosing <mxGraph>. + */ +mxPanningHandler.prototype.graph = null; + +/** + * Variable: usePopupTrigger + * + * Specifies if the <isPopupTrigger> should also be used for panning. To + * avoid conflicts, the panning is only activated if the mouse was moved + * more than <mxGraph.tolerance>, otherwise, a single click is assumed + * and the popupmenu is displayed. Default is true. + */ +mxPanningHandler.prototype.usePopupTrigger = true; + +/** + * Variable: useLeftButtonForPanning + * + * Specifies if panning should be active for the left mouse button. + * Setting this to true may conflict with <mxRubberband>. Default is false. + */ +mxPanningHandler.prototype.useLeftButtonForPanning = false; + +/** + * Variable: selectOnPopup + * + * Specifies if cells should be selected if a popupmenu is displayed for + * them. Default is true. + */ +mxPanningHandler.prototype.selectOnPopup = true; + +/** + * Variable: clearSelectionOnBackground + * + * Specifies if cells should be deselected if a popupmenu is displayed for + * the diagram background. Default is true. + */ +mxPanningHandler.prototype.clearSelectionOnBackground = true; + +/** + * Variable: ignoreCell + * + * Specifies if panning should be active even if there is a cell under the + * mousepointer. Default is false. + */ +mxPanningHandler.prototype.ignoreCell = false; + +/** + * Variable: previewEnabled + * + * Specifies if the panning should be previewed. Default is true. + */ +mxPanningHandler.prototype.previewEnabled = true; + +/** + * Variable: useGrid + * + * Specifies if the panning steps should be aligned to the grid size. + * Default is false. + */ +mxPanningHandler.prototype.useGrid = false; + +/** + * Variable: panningEnabled + * + * Specifies if panning should be enabled. Default is true. + */ +mxPanningHandler.prototype.panningEnabled = true; + +/** + * Function: isPanningEnabled + * + * Returns <panningEnabled>. + */ +mxPanningHandler.prototype.isPanningEnabled = function() +{ + return this.panningEnabled; +}; + +/** + * Function: setPanningEnabled + * + * Sets <panningEnabled>. + */ +mxPanningHandler.prototype.setPanningEnabled = function(value) +{ + this.panningEnabled = value; +}; + +/** + * Function: init + * + * Initializes the shapes required for this vertex handler. + */ +mxPanningHandler.prototype.init = function() +{ + // Supercall + mxPopupMenu.prototype.init.apply(this); + + // Hides the tooltip if the mouse is over + // the context menu + mxEvent.addListener(this.div, (mxClient.IS_TOUCH) ? 'touchmove' : 'mousemove', + mxUtils.bind(this, function(evt) + { + this.graph.tooltipHandler.hide(); + }) + ); +}; + +/** + * Function: isPanningTrigger + * + * Returns true if the given event is a panning trigger for the optional + * given cell. This returns true if control-shift is pressed or if + * <usePopupTrigger> is true and the event is a popup trigger. + */ +mxPanningHandler.prototype.isPanningTrigger = function(me) +{ + var evt = me.getEvent(); + + return (this.useLeftButtonForPanning && (this.ignoreCell || me.getState() == null) && + mxEvent.isLeftMouseButton(evt)) || (mxEvent.isControlDown(evt) && + mxEvent.isShiftDown(evt)) || (this.usePopupTrigger && + mxEvent.isPopupTrigger(evt)); +}; + +/** + * Function: mouseDown + * + * Handles the event by initiating the panning. By consuming the event all + * subsequent events of the gesture are redirected to this handler. + */ +mxPanningHandler.prototype.mouseDown = function(sender, me) +{ + if (!me.isConsumed() && this.isEnabled()) + { + // Hides the popupmenu if is is being displayed + this.hideMenu(); + + this.dx0 = -this.graph.container.scrollLeft; + this.dy0 = -this.graph.container.scrollTop; + + // Checks the event triggers to panning and popupmenu + this.popupTrigger = this.isPopupTrigger(me); + this.panningTrigger = this.isPanningEnabled() && + this.isPanningTrigger(me); + + // Stores the location of the trigger event + this.startX = me.getX(); + this.startY = me.getY(); + + // Displays popup menu on Mac after the mouse was released + if (this.panningTrigger) + { + this.consumePanningTrigger(me); + } + } +}; + +/** + * Function: consumePanningTrigger + * + * Consumes the given <mxMouseEvent> if it was a panning trigger in + * <mouseDown>. The default is to invoke <mxMouseEvent.consume>. Note that this + * will block any further event processing. If you haven't disabled built-in + * context menus and require immediate selection of the cell on mouseDown in + * Safari and/or on the Mac, then use the following code: + * + * (code) + * mxPanningHandler.prototype.consumePanningTrigger = function(me) + * { + * if (me.evt.preventDefault) + * { + * me.evt.preventDefault(); + * } + * + * // Stops event processing in IE + * me.evt.returnValue = false; + * + * // Sets local consumed state + * if (!mxClient.IS_SF && !mxClient.IS_MAC) + * { + * me.consumed = true; + * } + * }; + * (end) + */ +mxPanningHandler.prototype.consumePanningTrigger = function(me) +{ + me.consume(); +}; + +/** + * Function: mouseMove + * + * Handles the event by updating the panning on the graph. + */ +mxPanningHandler.prototype.mouseMove = function(sender, me) +{ + var dx = me.getX() - this.startX; + var dy = me.getY() - this.startY; + + if (this.active) + { + if (this.previewEnabled) + { + // Applies the grid to the panning steps + if (this.useGrid) + { + dx = this.graph.snap(dx); + dy = this.graph.snap(dy); + } + + this.graph.panGraph(dx + this.dx0, dy + this.dy0); + } + + this.fireEvent(new mxEventObject(mxEvent.PAN, 'event', me)); + me.consume(); + } + else if (this.panningTrigger) + { + var tmp = this.active; + + // Panning is activated only if the mouse is moved + // beyond the graph tolerance + this.active = Math.abs(dx) > this.graph.tolerance || + Math.abs(dy) > this.graph.tolerance; + + if (!tmp && this.active) + { + this.fireEvent(new mxEventObject(mxEvent.PAN_START, 'event', me)); + } + } +}; + +/** + * Function: mouseUp + * + * Handles the event by setting the translation on the view or showing the + * popupmenu. + */ +mxPanningHandler.prototype.mouseUp = function(sender, me) +{ + // Shows popup menu if mouse was not moved + var dx = Math.abs(me.getX() - this.startX); + var dy = Math.abs(me.getY() - this.startY); + + if (this.active) + { + if (!this.graph.useScrollbarsForPanning || !mxUtils.hasScrollbars(this.graph.container)) + { + dx = me.getX() - this.startX; + dy = me.getY() - this.startY; + + // Applies the grid to the panning steps + if (this.useGrid) + { + dx = this.graph.snap(dx); + dy = this.graph.snap(dy); + } + + var scale = this.graph.getView().scale; + var t = this.graph.getView().translate; + + this.graph.panGraph(0, 0); + this.panGraph(t.x + dx / scale, t.y + dy / scale); + } + + this.active = false; + this.fireEvent(new mxEventObject(mxEvent.PAN_END, 'event', me)); + me.consume(); + } + else if (this.popupTrigger) + { + if (dx < this.graph.tolerance && dy < this.graph.tolerance) + { + var cell = this.getCellForPopupEvent(me); + + // Selects the cell for which the context menu is being displayed + if (this.graph.isEnabled() && this.selectOnPopup && + cell != null && !this.graph.isCellSelected(cell)) + { + this.graph.setSelectionCell(cell); + } + else if (this.clearSelectionOnBackground && cell == null) + { + this.graph.clearSelection(); + } + + // Hides the tooltip if there is one + this.graph.tooltipHandler.hide(); + var origin = mxUtils.getScrollOrigin(); + var point = new mxPoint(me.getX() + origin.x, + me.getY() + origin.y); + + // Menu is shifted by 1 pixel so that the mouse up event + // is routed via the underlying shape instead of the DIV + this.popup(point.x + 1, point.y + 1, cell, me.getEvent()); + me.consume(); + } + } + + this.panningTrigger = false; + this.popupTrigger = false; +}; + +/** + * Function: getCellForPopupEvent + * + * Hook to return the cell for the mouse up popup trigger handling. + */ +mxPanningHandler.prototype.getCellForPopupEvent = function(me) +{ + return me.getCell(); +}; + +/** + * Function: panGraph + * + * Pans <graph> by the given amount. + */ +mxPanningHandler.prototype.panGraph = function(dx, dy) +{ + this.graph.getView().setTranslate(dx, dy); +}; + +/** + * Function: destroy + * + * Destroys the handler and all its resources and DOM nodes. + */ +mxPanningHandler.prototype.destroy = function() +{ + this.graph.removeMouseListener(this); + + // Supercall + mxPopupMenu.prototype.destroy.apply(this); +}; diff --git a/src/js/handler/mxRubberband.js b/src/js/handler/mxRubberband.js new file mode 100644 index 0000000..f9e7187 --- /dev/null +++ b/src/js/handler/mxRubberband.js @@ -0,0 +1,348 @@ +/** + * $Id: mxRubberband.js,v 1.48 2012-04-13 12:53:30 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxRubberband + * + * Event handler that selects rectangular regions. This is not built-into + * <mxGraph>. To enable rubberband selection in a graph, use the following code. + * + * Example: + * + * (code) + * var rubberband = new mxRubberband(graph); + * (end) + * + * Constructor: mxRubberband + * + * Constructs an event handler that selects rectangular regions in the graph + * using rubberband selection. + */ +function mxRubberband(graph) +{ + if (graph != null) + { + this.graph = graph; + this.graph.addMouseListener(this); + + // Repaints the marquee after autoscroll + this.panHandler = mxUtils.bind(this, function() + { + this.repaint(); + }); + + this.graph.addListener(mxEvent.PAN, this.panHandler); + + // Automatic deallocation of memory + if (mxClient.IS_IE) + { + mxEvent.addListener(window, 'unload', + mxUtils.bind(this, function() + { + this.destroy(); + }) + ); + } + } +}; + +/** + * Variable: defaultOpacity + * + * Specifies the default opacity to be used for the rubberband div. Default + * is 20. + */ +mxRubberband.prototype.defaultOpacity = 20; + +/** + * Variable: enabled + * + * Specifies if events are handled. Default is true. + */ +mxRubberband.prototype.enabled = true; + +/** + * Variable: div + * + * Holds the DIV element which is currently visible. + */ +mxRubberband.prototype.div = null; + +/** + * Variable: sharedDiv + * + * Holds the DIV element which is used to display the rubberband. + */ +mxRubberband.prototype.sharedDiv = null; + +/** + * Variable: currentX + * + * Holds the value of the x argument in the last call to <update>. + */ +mxRubberband.prototype.currentX = 0; + +/** + * Variable: currentY + * + * Holds the value of the y argument in the last call to <update>. + */ +mxRubberband.prototype.currentY = 0; + +/** + * Function: isEnabled + * + * Returns true if events are handled. This implementation returns + * <enabled>. + */ +mxRubberband.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Enables or disables event handling. This implementation updates + * <enabled>. + */ +mxRubberband.prototype.setEnabled = function(enabled) +{ + this.enabled = enabled; +}; + +/** + * Function: mouseDown + * + * Handles the event by initiating a rubberband selection. By consuming the + * event all subsequent events of the gesture are redirected to this + * handler. + */ +mxRubberband.prototype.mouseDown = function(sender, me) +{ + if (!me.isConsumed() && this.isEnabled() && this.graph.isEnabled() && + (this.graph.isForceMarqueeEvent(me.getEvent()) || me.getState() == null)) + { + var offset = mxUtils.getOffset(this.graph.container); + var origin = mxUtils.getScrollOrigin(this.graph.container); + origin.x -= offset.x; + origin.y -= offset.y; + this.start(me.getX() + origin.x, me.getY() + origin.y); + + // Workaround for rubberband stopping if the mouse leaves the + // graph container in Firefox. + if (mxClient.IS_NS && !mxClient.IS_SF && !mxClient.IS_GC) + { + var container = this.graph.container; + + function createMouseEvent(evt) + { + var me = new mxMouseEvent(evt); + var pt = mxUtils.convertPoint(container, me.getX(), me.getY()); + + me.graphX = pt.x; + me.graphY = pt.y; + + return me; + }; + + this.dragHandler = mxUtils.bind(this, function(evt) + { + this.mouseMove(this.graph, createMouseEvent(evt)); + }); + + this.dropHandler = mxUtils.bind(this, function(evt) + { + this.mouseUp(this.graph, createMouseEvent(evt)); + }); + + mxEvent.addListener(document, 'mousemove', this.dragHandler); + mxEvent.addListener(document, 'mouseup', this.dropHandler); + } + + // Does not prevent the default for this event so that the + // event processing chain is still executed even if we start + // rubberbanding. This is required eg. in ExtJs to hide the + // current context menu. In mouseMove we'll make sure we're + // not selecting anything while we're rubberbanding. + me.consume(false); + } +}; + +/** + * Function: start + * + * Sets the start point for the rubberband selection. + */ +mxRubberband.prototype.start = function(x, y) +{ + this.first = new mxPoint(x, y); +}; + +/** + * Function: mouseMove + * + * Handles the event by updating therubberband selection. + */ +mxRubberband.prototype.mouseMove = function(sender, me) +{ + if (!me.isConsumed() && this.first != null) + { + var origin = mxUtils.getScrollOrigin(this.graph.container); + var offset = mxUtils.getOffset(this.graph.container); + origin.x -= offset.x; + origin.y -= offset.y; + var x = me.getX() + origin.x; + var y = me.getY() + origin.y; + var dx = this.first.x - x; + var dy = this.first.y - y; + var tol = this.graph.tolerance; + + if (this.div != null || Math.abs(dx) > tol || Math.abs(dy) > tol) + { + if (this.div == null) + { + this.div = this.createShape(); + } + + // Clears selection while rubberbanding. This is required because + // the event is not consumed in mouseDown. + mxUtils.clearSelection(); + + this.update(x, y); + me.consume(); + } + } +}; + +/** + * Function: createShape + * + * Creates the rubberband selection shape. + */ +mxRubberband.prototype.createShape = function() +{ + if (this.sharedDiv == null) + { + this.sharedDiv = document.createElement('div'); + this.sharedDiv.className = 'mxRubberband'; + mxUtils.setOpacity(this.sharedDiv, this.defaultOpacity); + } + + this.graph.container.appendChild(this.sharedDiv); + + return this.sharedDiv; +}; + +/** + * Function: mouseUp + * + * Handles the event by selecting the region of the rubberband using + * <mxGraph.selectRegion>. + */ +mxRubberband.prototype.mouseUp = function(sender, me) +{ + var execute = this.div != null; + this.reset(); + + if (execute) + { + var rect = new mxRectangle(this.x, this.y, this.width, this.height); + this.graph.selectRegion(rect, me.getEvent()); + me.consume(); + } +}; + +/** + * Function: reset + * + * Resets the state of the rubberband selection. + */ +mxRubberband.prototype.reset = function() +{ + if (this.div != null) + { + this.div.parentNode.removeChild(this.div); + } + + if (this.dragHandler != null) + { + mxEvent.removeListener(document, 'mousemove', this.dragHandler); + this.dragHandler = null; + } + + if (this.dropHandler != null) + { + mxEvent.removeListener(document, 'mouseup', this.dropHandler); + this.dropHandler = null; + } + + this.currentX = 0; + this.currentY = 0; + this.first = null; + this.div = null; +}; + +/** + * Function: update + * + * Sets <currentX> and <currentY> and calls <repaint>. + */ +mxRubberband.prototype.update = function(x, y) +{ + this.currentX = x; + this.currentY = y; + + this.repaint(); +}; + +/** + * Function: repaint + * + * Computes the bounding box and updates the style of the <div>. + */ +mxRubberband.prototype.repaint = function() +{ + if (this.div != null) + { + var x = this.currentX - this.graph.panDx; + var y = this.currentY - this.graph.panDy; + + this.x = Math.min(this.first.x, x); + this.y = Math.min(this.first.y, y); + this.width = Math.max(this.first.x, x) - this.x; + this.height = Math.max(this.first.y, y) - this.y; + + var dx = (mxClient.IS_VML) ? this.graph.panDx : 0; + var dy = (mxClient.IS_VML) ? this.graph.panDy : 0; + + this.div.style.left = (this.x + dx) + 'px'; + this.div.style.top = (this.y + dy) + 'px'; + this.div.style.width = Math.max(1, this.width) + 'px'; + this.div.style.height = Math.max(1, this.height) + 'px'; + } +}; + +/** + * Function: destroy + * + * Destroys the handler and all its resources and DOM nodes. This does + * normally not need to be called, it is called automatically when the + * window unloads. + */ +mxRubberband.prototype.destroy = function() +{ + if (!this.destroyed) + { + this.destroyed = true; + this.graph.removeMouseListener(this); + this.graph.removeListener(this.panHandler); + this.reset(); + + if (this.sharedDiv != null) + { + this.sharedDiv = null; + } + } +}; diff --git a/src/js/handler/mxSelectionCellsHandler.js b/src/js/handler/mxSelectionCellsHandler.js new file mode 100644 index 0000000..800d718 --- /dev/null +++ b/src/js/handler/mxSelectionCellsHandler.js @@ -0,0 +1,260 @@ +/** + * $Id: mxSelectionCellsHandler.js,v 1.5 2012-08-10 11:35:06 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxSelectionCellsHandler + * + * An event handler that manages cell handlers and invokes their mouse event + * processing functions. + * + * Group: Events + * + * Event: mxEvent.ADD + * + * Fires if a cell has been added to the selection. The <code>state</code> + * property contains the <mxCellState> that has been added. + * + * Event: mxEvent.REMOVE + * + * Fires if a cell has been remove from the selection. The <code>state</code> + * property contains the <mxCellState> that has been removed. + * + * Parameters: + * + * graph - Reference to the enclosing <mxGraph>. + */ +function mxSelectionCellsHandler(graph) +{ + this.graph = graph; + this.handlers = new mxDictionary(); + this.graph.addMouseListener(this); + + this.refreshHandler = mxUtils.bind(this, function(sender, evt) + { + if (this.isEnabled()) + { + this.refresh(); + } + }); + + this.graph.getSelectionModel().addListener(mxEvent.CHANGE, this.refreshHandler); + this.graph.getModel().addListener(mxEvent.CHANGE, this.refreshHandler); + this.graph.getView().addListener(mxEvent.SCALE, this.refreshHandler); + this.graph.getView().addListener(mxEvent.TRANSLATE, this.refreshHandler); + this.graph.getView().addListener(mxEvent.SCALE_AND_TRANSLATE, this.refreshHandler); + this.graph.getView().addListener(mxEvent.DOWN, this.refreshHandler); + this.graph.getView().addListener(mxEvent.UP, this.refreshHandler); +}; + +/** + * Extends mxEventSource. + */ +mxSelectionCellsHandler.prototype = new mxEventSource(); +mxSelectionCellsHandler.prototype.constructor = mxSelectionCellsHandler; + +/** + * Variable: graph + * + * Reference to the enclosing <mxGraph>. + */ +mxSelectionCellsHandler.prototype.graph = null; + +/** + * Variable: enabled + * + * Specifies if events are handled. Default is true. + */ +mxSelectionCellsHandler.prototype.enabled = true; + +/** + * Variable: refreshHandler + * + * Keeps a reference to an event listener for later removal. + */ +mxSelectionCellsHandler.prototype.refreshHandler = null; + +/** + * Variable: maxHandlers + * + * Defines the maximum number of handlers to paint individually. Default is 100. + */ +mxSelectionCellsHandler.prototype.maxHandlers = 100; + +/** + * Variable: handlers + * + * <mxDictionary> that maps from cells to handlers. + */ +mxSelectionCellsHandler.prototype.handlers = null; + +/** + * Function: isEnabled + * + * Returns <enabled>. + */ +mxSelectionCellsHandler.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Sets <enabled>. + */ +mxSelectionCellsHandler.prototype.setEnabled = function(value) +{ + this.enabled = value; +}; + +/** + * Function: getHandler + * + * Returns the handler for the given cell. + */ +mxSelectionCellsHandler.prototype.getHandler = function(cell) +{ + return this.handlers.get(cell); +}; + +/** + * Function: reset + * + * Resets all handlers. + */ +mxSelectionCellsHandler.prototype.reset = function() +{ + this.handlers.visit(function(key, handler) + { + handler.reset.apply(handler); + }); +}; + +/** + * Function: refresh + * + * Reloads or updates all handlers. + */ +mxSelectionCellsHandler.prototype.refresh = function() +{ + // Removes all existing handlers + var oldHandlers = this.handlers; + this.handlers = new mxDictionary(); + + // Creates handles for all selection cells + var tmp = this.graph.getSelectionCells(); + + for (var i = 0; i < tmp.length; i++) + { + var state = this.graph.view.getState(tmp[i]); + + if (state != null) + { + var handler = oldHandlers.remove(tmp[i]); + + if (handler != null) + { + if (handler.state != state) + { + handler.destroy(); + handler = null; + } + else + { + handler.redraw(); + } + } + + if (handler == null) + { + handler = this.graph.createHandler(state); + this.fireEvent(new mxEventObject(mxEvent.ADD, 'state', state)); + } + + if (handler != null) + { + this.handlers.put(tmp[i], handler); + } + } + } + + // Destroys all unused handlers + oldHandlers.visit(mxUtils.bind(this, function(key, handler) + { + this.fireEvent(new mxEventObject(mxEvent.REMOVE, 'state', handler.state)); + handler.destroy(); + })); +}; + +/** + * Function: mouseDown + * + * Redirects the given event to the handlers. + */ +mxSelectionCellsHandler.prototype.mouseDown = function(sender, me) +{ + if (this.graph.isEnabled() && this.isEnabled()) + { + var args = [sender, me]; + + this.handlers.visit(function(key, handler) + { + handler.mouseDown.apply(handler, args); + }); + } +}; + +/** + * Function: mouseMove + * + * Redirects the given event to the handlers. + */ +mxSelectionCellsHandler.prototype.mouseMove = function(sender, me) +{ + if (this.graph.isEnabled() && this.isEnabled()) + { + var args = [sender, me]; + + this.handlers.visit(function(key, handler) + { + handler.mouseMove.apply(handler, args); + }); + } +}; + +/** + * Function: mouseUp + * + * Redirects the given event to the handlers. + */ +mxSelectionCellsHandler.prototype.mouseUp = function(sender, me) +{ + if (this.graph.isEnabled() && this.isEnabled()) + { + var args = [sender, me]; + + this.handlers.visit(function(key, handler) + { + handler.mouseUp.apply(handler, args); + }); + } +}; + +/** + * Function: destroy + * + * Destroys the handler and all its resources and DOM nodes. + */ +mxSelectionCellsHandler.prototype.destroy = function() +{ + this.graph.removeMouseListener(this); + + if (this.refreshHandler != null) + { + this.graph.getSelectionModel().removeListener(this.refreshHandler); + this.graph.getModel().removeListener(this.refreshHandler); + this.graph.getView().removeListener(this.refreshHandler); + this.refreshHandler = null; + } +}; diff --git a/src/js/handler/mxTooltipHandler.js b/src/js/handler/mxTooltipHandler.js new file mode 100644 index 0000000..4e34a13 --- /dev/null +++ b/src/js/handler/mxTooltipHandler.js @@ -0,0 +1,317 @@ +/** + * $Id: mxTooltipHandler.js,v 1.51 2011-03-31 10:11:17 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxTooltipHandler + * + * Graph event handler that displays tooltips. <mxGraph.getTooltip> is used to + * get the tooltip for a cell or handle. This handler is built-into + * <mxGraph.tooltipHandler> and enabled using <mxGraph.setTooltips>. + * + * Example: + * + * (code> + * new mxTooltipHandler(graph); + * (end) + * + * Constructor: mxTooltipHandler + * + * Constructs an event handler that displays tooltips with the specified + * delay (in milliseconds). If no delay is specified then a default delay + * of 500 ms (0.5 sec) is used. + * + * Parameters: + * + * graph - Reference to the enclosing <mxGraph>. + * delay - Optional delay in milliseconds. + */ +function mxTooltipHandler(graph, delay) +{ + if (graph != null) + { + this.graph = graph; + this.delay = delay || 500; + this.graph.addMouseListener(this); + } +}; + +/** + * Variable: zIndex + * + * Specifies the zIndex for the tooltip and its shadow. Default is 10005. + */ +mxTooltipHandler.prototype.zIndex = 10005; + +/** + * Variable: graph + * + * Reference to the enclosing <mxGraph>. + */ +mxTooltipHandler.prototype.graph = null; + +/** + * Variable: delay + * + * Delay to show the tooltip in milliseconds. Default is 500. + */ +mxTooltipHandler.prototype.delay = null; + +/** + * Variable: hideOnHover + * + * Specifies if the tooltip should be hidden if the mouse is moved over the + * current cell. Default is false. + */ +mxTooltipHandler.prototype.hideOnHover = false; + +/** + * Variable: enabled + * + * Specifies if events are handled. Default is true. + */ +mxTooltipHandler.prototype.enabled = true; + +/** + * Function: isEnabled + * + * Returns true if events are handled. This implementation + * returns <enabled>. + */ +mxTooltipHandler.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Enables or disables event handling. This implementation + * updates <enabled>. + */ +mxTooltipHandler.prototype.setEnabled = function(enabled) +{ + this.enabled = enabled; +}; + +/** + * Function: isHideOnHover + * + * Returns <hideOnHover>. + */ +mxTooltipHandler.prototype.isHideOnHover = function() +{ + return this.hideOnHover; +}; + +/** + * Function: setHideOnHover + * + * Sets <hideOnHover>. + */ +mxTooltipHandler.prototype.setHideOnHover = function(value) +{ + this.hideOnHover = value; +}; + +/** + * Function: init + * + * Initializes the DOM nodes required for this tooltip handler. + */ +mxTooltipHandler.prototype.init = function() +{ + if (document.body != null) + { + this.div = document.createElement('div'); + this.div.className = 'mxTooltip'; + this.div.style.visibility = 'hidden'; + this.div.style.zIndex = this.zIndex; + + document.body.appendChild(this.div); + + mxEvent.addListener(this.div, 'mousedown', + mxUtils.bind(this, function(evt) + { + this.hideTooltip(); + }) + ); + } +}; + +/** + * Function: mouseDown + * + * Handles the event by initiating a rubberband selection. By consuming the + * event all subsequent events of the gesture are redirected to this + * handler. + */ +mxTooltipHandler.prototype.mouseDown = function(sender, me) +{ + this.reset(me, false); + this.hideTooltip(); +}; + +/** + * Function: mouseMove + * + * Handles the event by updating the rubberband selection. + */ +mxTooltipHandler.prototype.mouseMove = function(sender, me) +{ + if (me.getX() != this.lastX || me.getY() != this.lastY) + { + this.reset(me, true); + + if (this.isHideOnHover() || me.getState() != this.state || (me.getSource() != this.node && + (!this.stateSource || (me.getState() != null && this.stateSource == + (me.isSource(me.getState().shape) || !me.isSource(me.getState().text)))))) + { + this.hideTooltip(); + } + } + + this.lastX = me.getX(); + this.lastY = me.getY(); +}; + +/** + * Function: mouseUp + * + * Handles the event by resetting the tooltip timer or hiding the existing + * tooltip. + */ +mxTooltipHandler.prototype.mouseUp = function(sender, me) +{ + this.reset(me, true); + this.hideTooltip(); +}; + + +/** + * Function: resetTimer + * + * Resets the timer. + */ +mxTooltipHandler.prototype.resetTimer = function() +{ + if (this.thread != null) + { + window.clearTimeout(this.thread); + this.thread = null; + } +}; + +/** + * Function: reset + * + * Resets and/or restarts the timer to trigger the display of the tooltip. + */ +mxTooltipHandler.prototype.reset = function(me, restart) +{ + this.resetTimer(); + + if (restart && this.isEnabled() && me.getState() != null && (this.div == null || + this.div.style.visibility == 'hidden')) + { + var state = me.getState(); + var node = me.getSource(); + var x = me.getX(); + var y = me.getY(); + var stateSource = me.isSource(state.shape) || me.isSource(state.text); + + this.thread = window.setTimeout(mxUtils.bind(this, function() + { + if (!this.graph.isEditing() && !this.graph.panningHandler.isMenuShowing()) + { + // Uses information from inside event cause using the event at + // this (delayed) point in time is not possible in IE as it no + // longer contains the required information (member not found) + var tip = this.graph.getTooltip(state, node, x, y); + this.show(tip, x, y); + this.state = state; + this.node = node; + this.stateSource = stateSource; + } + }), this.delay); + } +}; + +/** + * Function: hide + * + * Hides the tooltip and resets the timer. + */ +mxTooltipHandler.prototype.hide = function() +{ + this.resetTimer(); + this.hideTooltip(); +}; + +/** + * Function: hideTooltip + * + * Hides the tooltip. + */ +mxTooltipHandler.prototype.hideTooltip = function() +{ + if (this.div != null) + { + this.div.style.visibility = 'hidden'; + } +}; + +/** + * Function: show + * + * Shows the tooltip for the specified cell and optional index at the + * specified location (with a vertical offset of 10 pixels). + */ +mxTooltipHandler.prototype.show = function(tip, x, y) +{ + if (tip != null && tip.length > 0) + { + // Initializes the DOM nodes if required + if (this.div == null) + { + this.init(); + } + + var origin = mxUtils.getScrollOrigin(); + + this.div.style.left = (x + origin.x) + 'px'; + this.div.style.top = (y + mxConstants.TOOLTIP_VERTICAL_OFFSET + + origin.y) + 'px'; + + if (!mxUtils.isNode(tip)) + { + this.div.innerHTML = tip.replace(/\n/g, '<br>'); + } + else + { + this.div.innerHTML = ''; + this.div.appendChild(tip); + } + + this.div.style.visibility = ''; + mxUtils.fit(this.div); + } +}; + +/** + * Function: destroy + * + * Destroys the handler and all its resources and DOM nodes. + */ +mxTooltipHandler.prototype.destroy = function() +{ + this.graph.removeMouseListener(this); + mxEvent.release(this.div); + + if (this.div != null && this.div.parentNode != null) + { + this.div.parentNode.removeChild(this.div); + } + + this.div = null; +}; diff --git a/src/js/handler/mxVertexHandler.js b/src/js/handler/mxVertexHandler.js new file mode 100644 index 0000000..0b12e27 --- /dev/null +++ b/src/js/handler/mxVertexHandler.js @@ -0,0 +1,753 @@ +/** + * $Id: mxVertexHandler.js,v 1.107 2012-11-20 09:06:07 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxVertexHandler + * + * Event handler for resizing cells. This handler is automatically created in + * <mxGraph.createHandler>. + * + * Constructor: mxVertexHandler + * + * Constructs an event handler that allows to resize vertices + * and groups. + * + * Parameters: + * + * state - <mxCellState> of the cell to be resized. + */ +function mxVertexHandler(state) +{ + if (state != null) + { + this.state = state; + this.init(); + } +}; + +/** + * Variable: graph + * + * Reference to the enclosing <mxGraph>. + */ +mxVertexHandler.prototype.graph = null; + +/** + * Variable: state + * + * Reference to the <mxCellState> being modified. + */ +mxVertexHandler.prototype.state = null; + +/** + * Variable: singleSizer + * + * Specifies if only one sizer handle at the bottom, right corner should be + * used. Default is false. + */ +mxVertexHandler.prototype.singleSizer = false; + +/** + * Variable: index + * + * Holds the index of the current handle. + */ +mxVertexHandler.prototype.index = null; + +/** + * Variable: allowHandleBoundsCheck + * + * Specifies if the bounds of handles should be used for hit-detection in IE + * Default is true. + */ +mxVertexHandler.prototype.allowHandleBoundsCheck = true; + +/** + * Variable: crisp + * + * Specifies if the selection bounds and handles should be rendered in crisp + * mode. Default is true. + */ +mxVertexHandler.prototype.crisp = true; + +/** + * Variable: handleImage + * + * Optional <mxImage> to be used as handles. Default is null. + */ +mxVertexHandler.prototype.handleImage = null; + +/** + * Variable: tolerance + * + * Optional tolerance for hit-detection in <getHandleForEvent>. Default is 0. + */ +mxVertexHandler.prototype.tolerance = 0; + +/** + * Function: init + * + * Initializes the shapes required for this vertex handler. + */ +mxVertexHandler.prototype.init = function() +{ + this.graph = this.state.view.graph; + this.selectionBounds = this.getSelectionBounds(this.state); + this.bounds = new mxRectangle(this.selectionBounds.x, this.selectionBounds.y, + this.selectionBounds.width, this.selectionBounds.height); + this.selectionBorder = this.createSelectionShape(this.bounds); + this.selectionBorder.dialect = + (this.graph.dialect != mxConstants.DIALECT_SVG) ? + mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG; + this.selectionBorder.init(this.graph.getView().getOverlayPane()); + + // Event-transparency + if (this.selectionBorder.dialect == mxConstants.DIALECT_SVG) + { + this.selectionBorder.node.setAttribute('pointer-events', 'none'); + } + else + { + this.selectionBorder.node.style.background = ''; + } + + if (this.graph.isCellMovable(this.state.cell)) + { + this.selectionBorder.node.style.cursor = mxConstants.CURSOR_MOVABLE_VERTEX; + } + + mxEvent.redirectMouseEvents(this.selectionBorder.node, this.graph, this.state); + + // Adds the sizer handles + if (mxGraphHandler.prototype.maxCells <= 0 || + this.graph.getSelectionCount() < mxGraphHandler.prototype.maxCells) + { + var resizable = this.graph.isCellResizable(this.state.cell); + this.sizers = []; + + if (resizable || (this.graph.isLabelMovable(this.state.cell) && + this.state.width >= 2 && this.state.height >= 2)) + { + var i = 0; + + if (resizable) + { + if (!this.singleSizer) + { + this.sizers.push(this.createSizer('nw-resize', i++)); + this.sizers.push(this.createSizer('n-resize', i++)); + this.sizers.push(this.createSizer('ne-resize', i++)); + this.sizers.push(this.createSizer('w-resize', i++)); + this.sizers.push(this.createSizer('e-resize', i++)); + this.sizers.push(this.createSizer('sw-resize', i++)); + this.sizers.push(this.createSizer('s-resize', i++)); + } + + this.sizers.push(this.createSizer('se-resize', i++)); + } + + var geo = this.graph.model.getGeometry(this.state.cell); + + if (geo != null && !geo.relative && !this.graph.isSwimlane(this.state.cell) && + this.graph.isLabelMovable(this.state.cell)) + { + // Marks this as the label handle for getHandleForEvent + this.labelShape = this.createSizer(mxConstants.CURSOR_LABEL_HANDLE, + mxEvent.LABEL_HANDLE, mxConstants.LABEL_HANDLE_SIZE, + mxConstants.LABEL_HANDLE_FILLCOLOR); + this.sizers.push(this.labelShape); + } + } + else if (this.graph.isCellMovable(this.state.cell) && !this.graph.isCellResizable(this.state.cell) && + this.state.width < 2 && this.state.height < 2) + { + this.labelShape = this.createSizer(mxConstants.CURSOR_MOVABLE_VERTEX, + null, null, mxConstants.LABEL_HANDLE_FILLCOLOR); + this.sizers.push(this.labelShape); + } + } + + this.redraw(); +}; + +/** + * Function: getSelectionBounds + * + * Returns the mxRectangle that defines the bounds of the selection + * border. + */ +mxVertexHandler.prototype.getSelectionBounds = function(state) +{ + return new mxRectangle(state.x, state.y, state.width, state.height); +}; + +/** + * Function: createSelectionShape + * + * Creates the shape used to draw the selection border. + */ +mxVertexHandler.prototype.createSelectionShape = function(bounds) +{ + var shape = new mxRectangleShape(bounds, null, this.getSelectionColor()); + shape.strokewidth = this.getSelectionStrokeWidth(); + shape.isDashed = this.isSelectionDashed(); + shape.crisp = this.crisp; + + return shape; +}; + +/** + * Function: getSelectionColor + * + * Returns <mxConstants.VERTEX_SELECTION_COLOR>. + */ +mxVertexHandler.prototype.getSelectionColor = function() +{ + return mxConstants.VERTEX_SELECTION_COLOR; +}; + +/** + * Function: getSelectionStrokeWidth + * + * Returns <mxConstants.VERTEX_SELECTION_STROKEWIDTH>. + */ +mxVertexHandler.prototype.getSelectionStrokeWidth = function() +{ + return mxConstants.VERTEX_SELECTION_STROKEWIDTH; +}; + +/** + * Function: isSelectionDashed + * + * Returns <mxConstants.VERTEX_SELECTION_DASHED>. + */ +mxVertexHandler.prototype.isSelectionDashed = function() +{ + return mxConstants.VERTEX_SELECTION_DASHED; +}; + +/** + * Function: createSizer + * + * Creates a sizer handle for the specified cursor and index and returns + * the new <mxRectangleShape> that represents the handle. + */ +mxVertexHandler.prototype.createSizer = function(cursor, index, size, fillColor) +{ + size = size || mxConstants.HANDLE_SIZE; + + var bounds = new mxRectangle(0, 0, size, size); + var sizer = this.createSizerShape(bounds, index, fillColor); + + if (this.state.text != null && this.state.text.node.parentNode == this.graph.container) + { + sizer.bounds.height -= 1; + sizer.bounds.width -= 1; + sizer.dialect = mxConstants.DIALECT_STRICTHTML; + sizer.init(this.graph.container); + } + else + { + sizer.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ? + mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG; + sizer.init(this.graph.getView().getOverlayPane()); + } + + mxEvent.redirectMouseEvents(sizer.node, this.graph, this.state); + + if (this.graph.isEnabled()) + { + sizer.node.style.cursor = cursor; + } + + if (!this.isSizerVisible(index)) + { + sizer.node.style.visibility = 'hidden'; + } + + return sizer; +}; + +/** + * Function: isSizerVisible + * + * Returns true if the sizer for the given index is visible. + * This returns true for all given indices. + */ +mxVertexHandler.prototype.isSizerVisible = function(index) +{ + return true; +}; + +/** + * Function: createSizerShape + * + * Creates the shape used for the sizer handle for the specified bounds and + * index. + */ +mxVertexHandler.prototype.createSizerShape = function(bounds, index, fillColor) +{ + if (this.handleImage != null) + { + bounds.width = this.handleImage.width; + bounds.height = this.handleImage.height; + + return new mxImageShape(bounds, this.handleImage.src); + } + else + { + var shape = new mxRectangleShape(bounds, + fillColor || mxConstants.HANDLE_FILLCOLOR, + mxConstants.HANDLE_STROKECOLOR); + shape.crisp = this.crisp; + + return shape; + } +}; + +/** + * Function: createBounds + * + * Helper method to create an <mxRectangle> around the given centerpoint + * with a width and height of 2*s or 6, if no s is given. + */ +mxVertexHandler.prototype.moveSizerTo = function(shape, x, y) +{ + if (shape != null) + { + shape.bounds.x = x - shape.bounds.width / 2; + shape.bounds.y = y - shape.bounds.height / 2; + shape.redraw(); + } +}; + +/** + * Function: getHandleForEvent + * + * Returns the index of the handle for the given event. This returns the index + * of the sizer from where the event originated or <mxEvent.LABEL_INDEX>. + */ +mxVertexHandler.prototype.getHandleForEvent = function(me) +{ + if (me.isSource(this.labelShape)) + { + return mxEvent.LABEL_HANDLE; + } + + if (this.sizers != null) + { + // Connection highlight may consume events before they reach sizer handle + var tol = this.tolerance; + var hit = (this.allowHandleBoundsCheck && (mxClient.IS_IE || tol > 0)) ? + new mxRectangle(me.getGraphX() - tol, me.getGraphY() - tol, 2 * tol, 2 * tol) : null; + + for (var i = 0; i < this.sizers.length; i++) + { + if (me.isSource(this.sizers[i]) || (hit != null && + this.sizers[i].node.style.visibility != 'hidden' && + mxUtils.intersects(this.sizers[i].bounds, hit))) + { + return i; + } + } + } + + return null; +}; + +/** + * Function: mouseDown + * + * Handles the event if a handle has been clicked. By consuming the + * event all subsequent events of the gesture are redirected to this + * handler. + */ +mxVertexHandler.prototype.mouseDown = function(sender, me) +{ + if (!me.isConsumed() && this.graph.isEnabled() && !this.graph.isForceMarqueeEvent(me.getEvent()) && + (this.tolerance > 0 || me.getState() == this.state)) + { + var handle = this.getHandleForEvent(me); + + if (handle != null) + { + this.start(me.getX(), me.getY(), handle); + me.consume(); + } + } +}; + +/** + * Function: start + * + * Starts the handling of the mouse gesture. + */ +mxVertexHandler.prototype.start = function(x, y, index) +{ + var pt = mxUtils.convertPoint(this.graph.container, x, y); + this.startX = pt.x; + this.startY = pt.y; + this.index = index; + + // Creates a preview that can be on top of any HTML label + this.selectionBorder.node.style.visibility = 'hidden'; + this.preview = this.createSelectionShape(this.bounds); + + if (this.state.text != null && this.state.text.node.parentNode == this.graph.container) + { + this.preview.dialect = mxConstants.DIALECT_STRICTHTML; + this.preview.init(this.graph.container); + } + else + { + this.preview.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ? + mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG; + this.preview.init(this.graph.view.getOverlayPane()); + } +}; + +/** + * Function: mouseMove + * + * Handles the event by updating the preview. + */ +mxVertexHandler.prototype.mouseMove = function(sender, me) +{ + if (!me.isConsumed() && this.index != null) + { + var point = new mxPoint(me.getGraphX(), me.getGraphY()); + var gridEnabled = this.graph.isGridEnabledEvent(me.getEvent()); + var scale = this.graph.getView().scale; + + if (this.index == mxEvent.LABEL_HANDLE) + { + if (gridEnabled) + { + point.x = this.graph.snap(point.x / scale) * scale; + point.y = this.graph.snap(point.y / scale) * scale; + } + + this.moveSizerTo(this.sizers[this.sizers.length - 1], point.x, point.y); + me.consume(); + } + else if (this.index != null) + { + var dx = point.x - this.startX; + var dy = point.y - this.startY; + var tr = this.graph.view.translate; + this.bounds = this.union(this.selectionBounds, dx, dy, this.index, gridEnabled, scale, tr); + this.drawPreview(); + me.consume(); + } + } + // Workaround for disabling the connect highlight when over handle + else if (this.getHandleForEvent(me) != null) + { + me.consume(false); + } +}; + +/** + * Function: mouseUp + * + * Handles the event by applying the changes to the geometry. + */ +mxVertexHandler.prototype.mouseUp = function(sender, me) +{ + if (!me.isConsumed() && this.index != null && this.state != null) + { + var point = new mxPoint(me.getGraphX(), me.getGraphY()); + var scale = this.graph.getView().scale; + + var gridEnabled = this.graph.isGridEnabledEvent(me.getEvent()); + var dx = (point.x - this.startX) / scale; + var dy = (point.y - this.startY) / scale; + + this.resizeCell(this.state.cell, dx, dy, this.index, gridEnabled); + this.reset(); + me.consume(); + } +}; + +/** + * Function: reset + * + * Resets the state of this handler. + */ +mxVertexHandler.prototype.reset = function() +{ + this.index = null; + + if (this.preview != null) + { + this.preview.destroy(); + this.preview = null; + } + + // Checks if handler has been destroyed + if (this.selectionBorder != null) + { + this.selectionBounds = this.getSelectionBounds(this.state); + this.selectionBorder.node.style.visibility = 'visible'; + this.bounds = new mxRectangle(this.selectionBounds.x, this.selectionBounds.y, + this.selectionBounds.width, this.selectionBounds.height); + this.drawPreview(); + } +}; + +/** + * Function: resizeCell + * + * Uses the given vector to change the bounds of the given cell + * in the graph using <mxGraph.resizeCell>. + */ +mxVertexHandler.prototype.resizeCell = function(cell, dx, dy, index, gridEnabled) +{ + var geo = this.graph.model.getGeometry(cell); + + if (index == mxEvent.LABEL_HANDLE) + { + var scale = this.graph.view.scale; + dx = (this.labelShape.bounds.getCenterX() - this.startX) / scale; + dy = (this.labelShape.bounds.getCenterY() - this.startY) / scale; + + geo = geo.clone(); + + if (geo.offset == null) + { + geo.offset = new mxPoint(dx, dy); + } + else + { + geo.offset.x += dx; + geo.offset.y += dy; + } + + this.graph.model.setGeometry(cell, geo); + } + else + { + var bounds = this.union(geo, dx, dy, index, gridEnabled, 1, new mxPoint(0, 0)); + this.graph.resizeCell(cell, bounds); + } +}; + +/** + * Function: union + * + * Returns the union of the given bounds and location for the specified + * handle index. + * + * To override this to limit the size of vertex via a minWidth/-Height style, + * the following code can be used. + * + * (code) + * var vertexHandlerUnion = mxVertexHandler.prototype.union; + * mxVertexHandler.prototype.union = function(bounds, dx, dy, index, gridEnabled, scale, tr) + * { + * var result = vertexHandlerUnion.apply(this, arguments); + * + * result.width = Math.max(result.width, mxUtils.getNumber(this.state.style, 'minWidth', 0)); + * result.height = Math.max(result.height, mxUtils.getNumber(this.state.style, 'minHeight', 0)); + * + * return result; + * }; + * (end) + * + * The minWidth/-Height style can then be used as follows: + * + * (code) + * graph.insertVertex(parent, null, 'Hello,', 20, 20, 80, 30, 'minWidth=100;minHeight=100;'); + * (end) + */ +mxVertexHandler.prototype.union = function(bounds, dx, dy, index, gridEnabled, scale, tr) +{ + if (this.singleSizer) + { + var x = bounds.x + bounds.width + dx; + var y = bounds.y + bounds.height + dy; + + if (gridEnabled) + { + x = this.graph.snap(x / scale) * scale; + y = this.graph.snap(y / scale) * scale; + } + + var rect = new mxRectangle(bounds.x, bounds.y, 0, 0); + rect.add(new mxRectangle(x, y, 0, 0)); + + return rect; + } + else + { + var left = bounds.x - tr.x * scale; + var right = left + bounds.width; + var top = bounds.y - tr.y * scale; + var bottom = top + bounds.height; + + if (index > 4 /* Bottom Row */) + { + bottom = bottom + dy; + + if (gridEnabled) + { + bottom = this.graph.snap(bottom / scale) * scale; + } + } + else if (index < 3 /* Top Row */) + { + top = top + dy; + + if (gridEnabled) + { + top = this.graph.snap(top / scale) * scale; + } + } + + if (index == 0 || index == 3 || index == 5 /* Left */) + { + left += dx; + + if (gridEnabled) + { + left = this.graph.snap(left / scale) * scale; + } + } + else if (index == 2 || index == 4 || index == 7 /* Right */) + { + right += dx; + + if (gridEnabled) + { + right = this.graph.snap(right / scale) * scale; + } + } + + var width = right - left; + var height = bottom - top; + + // Flips over left side + if (width < 0) + { + left += width; + width = Math.abs(width); + } + + // Flips over top side + if (height < 0) + { + top += height; + height = Math.abs(height); + } + + return new mxRectangle(left + tr.x * scale, top + tr.y * scale, width, height); + } +}; + +/** + * Function: redraw + * + * Redraws the handles and the preview. + */ +mxVertexHandler.prototype.redraw = function() +{ + this.selectionBounds = this.getSelectionBounds(this.state); + this.bounds = new mxRectangle(this.selectionBounds.x, this.selectionBounds.y, + this.selectionBounds.width, this.selectionBounds.height); + + if (this.sizers != null) + { + var s = this.state; + var r = s.x + s.width; + var b = s.y + s.height; + + if (this.singleSizer) + { + this.moveSizerTo(this.sizers[0], r, b); + } + else + { + var cx = s.x + s.width / 2; + var cy = s.y + s.height / 2; + + if (this.sizers.length > 1) + { + this.moveSizerTo(this.sizers[0], s.x, s.y); + this.moveSizerTo(this.sizers[1], cx, s.y); + this.moveSizerTo(this.sizers[2], r, s.y); + this.moveSizerTo(this.sizers[3], s.x, cy); + this.moveSizerTo(this.sizers[4], r, cy); + this.moveSizerTo(this.sizers[5], s.x, b); + this.moveSizerTo(this.sizers[6], cx, b); + this.moveSizerTo(this.sizers[7], r, b); + this.moveSizerTo(this.sizers[8], + cx + s.absoluteOffset.x, + cy + s.absoluteOffset.y); + } + else if (this.state.width >= 2 && this.state.height >= 2) + { + this.moveSizerTo(this.sizers[0], + cx + s.absoluteOffset.x, + cy + s.absoluteOffset.y); + } + else + { + this.moveSizerTo(this.sizers[0], s.x, s.y); + } + } + } + + this.drawPreview(); +}; + +/** + * Function: drawPreview + * + * Redraws the preview. + */ +mxVertexHandler.prototype.drawPreview = function() +{ + if (this.preview != null) + { + this.preview.bounds = this.bounds; + + if (this.preview.node.parentNode == this.graph.container) + { + this.preview.bounds.width = Math.max(0, this.preview.bounds.width - 1); + this.preview.bounds.height = Math.max(0, this.preview.bounds.height - 1); + } + + this.preview.redraw(); + } + + this.selectionBorder.bounds = this.bounds; + this.selectionBorder.redraw(); +}; + +/** + * Function: destroy + * + * Destroys the handler and all its resources and DOM nodes. + */ +mxVertexHandler.prototype.destroy = function() +{ + if (this.preview != null) + { + this.preview.destroy(); + this.preview = null; + } + + this.selectionBorder.destroy(); + this.selectionBorder = null; + this.labelShape = null; + + if (this.sizers != null) + { + for (var i = 0; i < this.sizers.length; i++) + { + this.sizers[i].destroy(); + this.sizers[i] = null; + } + } +}; diff --git a/src/js/index.txt b/src/js/index.txt new file mode 100644 index 0000000..f3631d6 --- /dev/null +++ b/src/js/index.txt @@ -0,0 +1,316 @@ +Document: API Specification + +Overview: + + This JavaScript library is divided into 8 packages. The top-level <mxClient> + class includes (or dynamically imports) everything else. The current version + is stored in <mxClient.VERSION>. + + The *editor* package provides the classes required to implement a diagram + editor. The main class in this package is <mxEditor>. + + The *view* and *model* packages implement the graph component, represented + by <mxGraph>. It refers to a <mxGraphModel> which contains <mxCell>s and + caches the state of the cells in a <mxGraphView>. The cells are painted + using a <mxCellRenderer> based on the appearance defined in <mxStylesheet>. + Undo history is implemented in <mxUndoManager>. To display an icon on the + graph, <mxCellOverlay> may be used. Validation rules are defined with + <mxMultiplicity>. + + The *handler*, *layout* and *shape* packages contain event listeners, + layout algorithms and shapes, respectively. The graph event listeners + include <mxRubberband> for rubberband selection, <mxTooltipHandler> + for tooltips and <mxGraphHandler> for basic cell modifications. + <mxCompactTreeLayout> implements a tree layout algorithm, and the + shape package provides various shapes, which are subclasses of + <mxShape>. + + The *util* package provides utility classes including <mxClipboard> for + copy-paste, <mxDatatransfer> for drag-and-drop, <mxConstants> for keys and + values of stylesheets, <mxEvent> and <mxUtils> for cross-browser + event-handling and general purpose functions, <mxResources> for + internationalization and <mxLog> for console output. + + The *io* package implements a generic <mxObjectCodec> for turning + JavaScript objects into XML. The main class is <mxCodec>. + <mxCodecRegistry> is the global registry for custom codecs. + +Events: + + There are three different types of events, namely native DOM events, + <mxEventObjects> which are fired in an <mxEventSource>, and <mxMouseEvents> + which are fired in <mxGraph>. + + Some helper methods for handling native events are provided in <mxEvent>. It + also takes care of resolving cycles between DOM nodes and JavaScript event + handlers, which can lead to memory leaks in IE6. + + Most custom events in mxGraph are implemented using <mxEventSource>. Its + listeners are functions that take a sender and <mxEventObject>. Additionally, + the <mxGraph> class fires special <mxMouseEvents> which are handled using + mouse listeners, which are objects that provide a mousedown, mousemove and + mouseup method. + + Events in <mxEventSource> are fired using <mxEventSource.fireEvent>. + Listeners are added and removed using <mxEventSource.addListener> and + <mxEventSource.removeListener>. <mxMouseEvents> in <mxGraph> are fired using + <mxGraph.fireMouseEvent>. Listeners are added and removed using + <mxGraph.addMouseListener> and <mxGraph.removeMouseListener>, respectively. + +Key bindings: + + The following key bindings are defined for mouse events in the client across + all browsers and platforms: + + - Control-Drag: Duplicates (clones) selected cells + - Shift-Rightlick: Shows the context menu + - Alt-Click: Forces rubberband (aka. marquee) + - Control-Select: Toggles the selection state + - Shift-Drag: Constrains the offset to one direction + - Shift-Control-Drag: Panning (also Shift-Rightdrag) + +Configuration: + + The following global variables may be defined before the client is loaded to + specify its language or base path, respectively. + + - mxBasePath: Specifies the path in <mxClient.basePath>. + - mxImageBasePath: Specifies the path in <mxClient.imageBasePath>. + - mxLanguage: Specifies the language for resources in <mxClient.language>. + - mxDefaultLanguage: Specifies the default language in <mxClient.defaultLanguage>. + - mxLoadResources: Specifies if any resources should be loaded. Default is true. + - mxLoadStylesheets: Specifies if any stylesheets should be loaded. Default is true. + +Reserved Words: + + The mx prefix is used for all classes and objects in mxGraph. The mx prefix + can be seen as the global namespace for all JavaScript code in mxGraph. The + following fieldnames should not be used in objects. + + - *mxObjectId*: If the object is used with mxObjectIdentity + - *as*: If the object is a field of another object + - *id*: If the object is an idref in a codec + - *mxListenerList*: Added to DOM nodes when used with <mxEvent> + - *window._mxDynamicCode*: Temporarily used to load code in Safari and Chrome + (see <mxClient.include>). + - *_mxJavaScriptExpression*: Global variable that is temporarily used to + evaluate code in Safari, Opera, Firefox 3 and IE (see <mxUtils.eval>). + +Files: + + The library contains these relative filenames. All filenames are relative + to <mxClient.basePath>. + +Built-in Images: + + All images are loaded from the <mxClient.imageBasePath>, + which you can change to reflect your environment. The image variables can + also be changed individually. + + - mxGraph.prototype.collapsedImage + - mxGraph.prototype.expandedImage + - mxGraph.prototype.warningImage + - mxWindow.prototype.closeImage + - mxWindow.prototype.minimizeImage + - mxWindow.prototype.normalizeImage + - mxWindow.prototype.maximizeImage + - mxWindow.prototype.resizeImage + - mxPopupMenu.prototype.submenuImage + - mxUtils.errorImage + - mxConstraintHandler.prototype.pointImage + + The basename of the warning image (images/warning without extension) used in + <mxGraph.setCellWarning> is defined in <mxGraph.warningImage>. + +Resources: + + The <mxEditor> and <mxGraph> classes add the following resources to + <mxResources> at class loading time: + + - resources/editor*.properties + - resources/graph*.properties + + By default, the library ships with English and German resource files. + +Images: + + Recommendations for using images. Use GIF images (256 color palette) in HTML + elements (such as the toolbar and context menu), and PNG images (24 bit) for + all images which appear inside the graph component. + + - For PNG images inside HTML elements, Internet Explorer will ignore any + transparency information. + - For GIF images inside the graph, Firefox on the Mac will display strange + colors. Furthermore, only the first image for animated GIFs is displayed + on the Mac. + + For faster image rendering during application runtime, images can be + prefetched using the following code: + + (code) + var image = new Image(); + image.src = url_to_image; + (end) + +Deployment: + + The client is added to the page using the following script tag inside the + head of a document: + + (code) + <script type="text/javascript" src="js/mxClient.js"></script> + (end) + + The deployment version of the mxClient.js file contains all required code + in a single file. For deployment, the complete javascript/src directory is + required. + +Source Code: + + If you are a source code customer and you wish to develop using the + full source code, the commented source code is shipped in the + javascript/devel/source.zip file. It contains one file for each class + in mxGraph. To use the source code the source.zip file must be + uncompressed and the mxClient.js URL in the HTML page must be changed + to reference the uncompressed mxClient.js from the source.zip file. + +Compression: + + When using Apache2 with mod_deflate, you can use the following directive + in src/js/.htaccess to speedup the loading of the JavaScript sources: + + (code) + SetOutputFilter DEFLATE + (end) + +Classes: + + There are two types of "classes" in mxGraph: classes and singletons (where + only one instance exists). Singletons are mapped to global objects where the + variable name equals the classname. For example mxConstants is an object with + all the constants defined as object fields. Normal classes are mapped to a + constructor function and a prototype which defines the instance fields and + methods. For example, <mxEditor> is a function and mxEditor.prototype is the + prototype for the object that the mxEditor function creates. The mx prefix is + a convention that is used for all classes in the mxGraph package to avoid + conflicts with other objects in the global namespace. + +Subclassing: + + For subclassing, the superclass must provide a constructor that is either + parameterless or handles an invocation with no arguments. Furthermore, the + special constructor field must be redefined after extending the prototype. + For example, the superclass of mxEditor is <mxEventSource>. This is + represented in JavaScript by first "inheriting" all fields and methods from + the superclass by assigning the prototype to an instance of the superclass, + eg. mxEditor.prototype = new mxEventSource() and redefining the constructor + field using mxEditor.prototype.constructor = mxEditor. The latter rule is + applied so that the type of an object can be retrieved via the name of it’s + constructor using mxUtils.getFunctionName(obj.constructor). + +Constructor: + + For subclassing in mxGraph, the same scheme should be applied. For example, + for subclassing the <mxGraph> class, first a constructor must be defined for + the new class. The constructor calls the super constructor with any arguments + that it may have using the call function on the mxGraph function object, + passing along explitely each argument: + + (code) + function MyGraph(container) + { + mxGraph.call(this, container); + } + (end) + + The prototype of MyGraph inherits from mxGraph as follows. As usual, the + constructor is redefined after extending the superclass: + + (code) + MyGraph.prototype = new mxGraph(); + MyGraph.prototype.constructor = MyGraph; + (end) + + You may want to define the codec associated for the class after the above + code. This code will be executed at class loading time and makes sure the + same codec is used to encode instances of mxGraph and MyGraph. + + (code) + var codec = mxCodecRegistry.getCodec(mxGraph); + codec.template = new MyGraph(); + mxCodecRegistry.register(codec); + (end) + +Functions: + + In the prototype for MyGraph, functions of mxGraph can then be extended as + follows. + + (code) + MyGraph.prototype.isCellSelectable = function(cell) + { + var selectable = mxGraph.prototype.isSelectable.apply(this, arguments); + + var geo = this.model.getGeometry(cell); + return selectable && (geo == null || !geo.relative); + } + (end) + + The supercall in the first line is optional. It is done using the apply + function on the isSelectable function object of the mxGraph prototype, using + the special this and arguments variables as parameters. Calls to the + superclass function are only possible if the function is not replaced in the + superclass as follows, which is another way of “subclassing” in JavaScript. + + (code) + mxGraph.prototype.isCellSelectable = function(cell) + { + var geo = this.model.getGeometry(cell); + return selectable && + (geo == null || + !geo.relative); + } + (end) + + The above scheme is useful if a function definition needs to be replaced + completely. + + In order to add new functions and fields to the subclass, the following code + is used. The example below adds a new function to return the XML + representation of the graph model: + + (code) + MyGraph.prototype.getXml = function() + { + var enc = new mxCodec(); + return enc.encode(this.getModel()); + } + (end) + +Variables: + + Likewise, a new field is declared and defined as follows. + + (code) + MyGraph.prototype.myField = 'Hello, World!'; + (end) + + Note that the value assigned to myField is created only once, that is, all + instances of MyGraph share the same value. If you require instance-specific + values, then the field must be defined in the constructor instead. + + (code) + function MyGraph(container) + { + mxGraph.call(this, container); + + this.myField = new Array(); + } + (end) + + Finally, a new instance of MyGraph is created using the following code, where + container is a DOM node that acts as a container for the graph view: + + (code) + var graph = new MyGraph(container); + (end) diff --git a/src/js/io/mxCellCodec.js b/src/js/io/mxCellCodec.js new file mode 100644 index 0000000..cbcd651 --- /dev/null +++ b/src/js/io/mxCellCodec.js @@ -0,0 +1,170 @@ +/** + * $Id: mxCellCodec.js,v 1.22 2010-10-21 07:12:31 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +mxCodecRegistry.register(function() +{ + /** + * Class: mxCellCodec + * + * Codec for <mxCell>s. This class is created and registered + * dynamically at load time and used implicitely via <mxCodec> + * and the <mxCodecRegistry>. + * + * Transient Fields: + * + * - children + * - edges + * - overlays + * - mxTransient + * + * Reference Fields: + * + * - parent + * - source + * - target + * + * Transient fields can be added using the following code: + * + * mxCodecRegistry.getCodec(mxCell).exclude.push('name_of_field'); + */ + var codec = new mxObjectCodec(new mxCell(), + ['children', 'edges', 'overlays', 'mxTransient'], + ['parent', 'source', 'target']); + + /** + * Function: isCellCodec + * + * Returns true since this is a cell codec. + */ + codec.isCellCodec = function() + { + return true; + }; + + /** + * Function: isExcluded + * + * Excludes user objects that are XML nodes. + */ + codec.isExcluded = function(obj, attr, value, isWrite) + { + return mxObjectCodec.prototype.isExcluded.apply(this, arguments) || + (isWrite && attr == 'value' && + value.nodeType == mxConstants.NODETYPE_ELEMENT); + }; + + /** + * Function: afterEncode + * + * Encodes an <mxCell> and wraps the XML up inside the + * XML of the user object (inversion). + */ + codec.afterEncode = function(enc, obj, node) + { + if (obj.value != null && + obj.value.nodeType == mxConstants.NODETYPE_ELEMENT) + { + // Wraps the graphical annotation up in the user object (inversion) + // by putting the result of the default encoding into a clone of the + // user object (node type 1) and returning this cloned user object. + var tmp = node; + node = (mxClient.IS_IE) ? + obj.value.cloneNode(true) : + enc.document.importNode(obj.value, true); + node.appendChild(tmp); + + // Moves the id attribute to the outermost XML node, namely the + // node which denotes the object boundaries in the file. + var id = tmp.getAttribute('id'); + node.setAttribute('id', id); + tmp.removeAttribute('id'); + } + + return node; + }; + + /** + * Function: beforeDecode + * + * Decodes an <mxCell> and uses the enclosing XML node as + * the user object for the cell (inversion). + */ + codec.beforeDecode = function(dec, node, obj) + { + var inner = node; + var classname = this.getName(); + + if (node.nodeName != classname) + { + // Passes the inner graphical annotation node to the + // object codec for further processing of the cell. + var tmp = node.getElementsByTagName(classname)[0]; + + if (tmp != null && + tmp.parentNode == node) + { + mxUtils.removeWhitespace(tmp, true); + mxUtils.removeWhitespace(tmp, false); + tmp.parentNode.removeChild(tmp); + inner = tmp; + } + else + { + inner = null; + } + + // Creates the user object out of the XML node + obj.value = node.cloneNode(true); + var id = obj.value.getAttribute('id'); + + if (id != null) + { + obj.setId(id); + obj.value.removeAttribute('id'); + } + } + else + { + // Uses ID from XML file as ID for cell in model + obj.setId(node.getAttribute('id')); + } + + // Preprocesses and removes all Id-references in order to use the + // correct encoder (this) for the known references to cells (all). + if (inner != null) + { + for (var i = 0; i < this.idrefs.length; i++) + { + var attr = this.idrefs[i]; + var ref = inner.getAttribute(attr); + + if (ref != null) + { + inner.removeAttribute(attr); + var object = dec.objects[ref] || dec.lookup(ref); + + if (object == null) + { + // Needs to decode forward reference + var element = dec.getElementById(ref); + + if (element != null) + { + var decoder = mxCodecRegistry.codecs[element.nodeName] || this; + object = decoder.decode(dec, element); + } + } + + obj[attr] = object; + } + } + } + + return inner; + }; + + // Returns the codec into the registry + return codec; + +}()); diff --git a/src/js/io/mxChildChangeCodec.js b/src/js/io/mxChildChangeCodec.js new file mode 100644 index 0000000..deeb57b --- /dev/null +++ b/src/js/io/mxChildChangeCodec.js @@ -0,0 +1,149 @@ +/** + * $Id: mxChildChangeCodec.js,v 1.12 2010-09-15 14:38:52 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +mxCodecRegistry.register(function() +{ + /** + * Class: mxChildChangeCodec + * + * Codec for <mxChildChange>s. This class is created and registered + * dynamically at load time and used implicitely via <mxCodec> and + * the <mxCodecRegistry>. + * + * Transient Fields: + * + * - model + * - previous + * - previousIndex + * - child + * + * Reference Fields: + * + * - parent + */ + var codec = new mxObjectCodec(new mxChildChange(), + ['model', 'child', 'previousIndex'], + ['parent', 'previous']); + + /** + * Function: isReference + * + * Returns true for the child attribute if the child + * cell had a previous parent or if we're reading the + * child as an attribute rather than a child node, in + * which case it's always a reference. + */ + codec.isReference = function(obj, attr, value, isWrite) + { + if (attr == 'child' && + (obj.previous != null || + !isWrite)) + { + return true; + } + + return mxUtils.indexOf(this.idrefs, attr) >= 0; + }; + + /** + * Function: afterEncode + * + * Encodes the child recusively and adds the result + * to the given node. + */ + codec.afterEncode = function(enc, obj, node) + { + if (this.isReference(obj, 'child', obj.child, true)) + { + // Encodes as reference (id) + node.setAttribute('child', enc.getId(obj.child)); + } + else + { + // At this point, the encoder is no longer able to know which cells + // are new, so we have to encode the complete cell hierarchy and + // ignore the ones that are already there at decoding time. Note: + // This can only be resolved by moving the notify event into the + // execute of the edit. + enc.encodeCell(obj.child, node); + } + + return node; + }; + + /** + * Function: beforeDecode + * + * Decodes the any child nodes as using the respective + * codec from the registry. + */ + codec.beforeDecode = function(dec, node, obj) + { + if (node.firstChild != null && + node.firstChild.nodeType == mxConstants.NODETYPE_ELEMENT) + { + // Makes sure the original node isn't modified + node = node.cloneNode(true); + + var tmp = node.firstChild; + obj.child = dec.decodeCell(tmp, false); + + var tmp2 = tmp.nextSibling; + tmp.parentNode.removeChild(tmp); + tmp = tmp2; + + while (tmp != null) + { + tmp2 = tmp.nextSibling; + + if (tmp.nodeType == mxConstants.NODETYPE_ELEMENT) + { + // Ignores all existing cells because those do not need to + // be re-inserted into the model. Since the encoded version + // of these cells contains the new parent, this would leave + // to an inconsistent state on the model (ie. a parent + // change without a call to parentForCellChanged). + var id = tmp.getAttribute('id'); + + if (dec.lookup(id) == null) + { + dec.decodeCell(tmp); + } + } + + tmp.parentNode.removeChild(tmp); + tmp = tmp2; + } + } + else + { + var childRef = node.getAttribute('child'); + obj.child = dec.getObject(childRef); + } + + return node; + }; + + /** + * Function: afterDecode + * + * Restores object state in the child change. + */ + codec.afterDecode = function(dec, node, obj) + { + // Cells are encoded here after a complete transaction so the previous + // parent must be restored on the cell for the case where the cell was + // added. This is needed for the local model to identify the cell as a + // new cell and register the ID. + obj.child.parent = obj.previous; + obj.previous = obj.parent; + obj.previousIndex = obj.index; + + return obj; + }; + + // Returns the codec into the registry + return codec; + +}()); diff --git a/src/js/io/mxCodec.js b/src/js/io/mxCodec.js new file mode 100644 index 0000000..b8bfc6a --- /dev/null +++ b/src/js/io/mxCodec.js @@ -0,0 +1,531 @@ +/** + * $Id: mxCodec.js,v 1.48 2012-01-04 10:01:16 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxCodec + * + * XML codec for JavaScript object graphs. See <mxObjectCodec> for a + * description of the general encoding/decoding scheme. This class uses the + * codecs registered in <mxCodecRegistry> for encoding/decoding each object. + * + * References: + * + * In order to resolve references, especially forward references, the mxCodec + * constructor must be given the document that contains the referenced + * elements. + * + * Examples: + * + * The following code is used to encode a graph model. + * + * (code) + * var encoder = new mxCodec(); + * var result = encoder.encode(graph.getModel()); + * var xml = mxUtils.getXml(result); + * (end) + * + * Example: + * + * Using the following code, the selection cells of a graph are encoded and the + * output is displayed in a dialog box. + * + * (code) + * var enc = new mxCodec(); + * var cells = graph.getSelectionCells(); + * mxUtils.alert(mxUtils.getPrettyXml(enc.encode(cells))); + * (end) + * + * Newlines in the XML can be coverted to <br>, in which case a '<br>' argument + * must be passed to <mxUtils.getXml> as the second argument. + * + * Example: + * + * Using the code below, an XML document is decodec into an existing model. The + * document may be obtained using one of the functions in mxUtils for loading + * an XML file, eg. <mxUtils.get>, or using <mxUtils.parseXml> for parsing an + * XML string. + * + * (code) + * var decoder = new mxCodec(doc) + * decoder.decode(doc.documentElement, graph.getModel()); + * (end) + * + * Debugging: + * + * For debugging i/o you can use the following code to get the sequence of + * encoded objects: + * + * (code) + * var oldEncode = mxCodec.prototype.encode; + * mxCodec.prototype.encode = function(obj) + * { + * mxLog.show(); + * mxLog.debug('mxCodec.encode: obj='+mxUtils.getFunctionName(obj.constructor)); + * + * return oldEncode.apply(this, arguments); + * }; + * (end) + * + * Constructor: mxCodec + * + * Constructs an XML encoder/decoder for the specified + * owner document. + * + * Parameters: + * + * document - Optional XML document that contains the data. + * If no document is specified then a new document is created + * using <mxUtils.createXmlDocument>. + */ +function mxCodec(document) +{ + this.document = document || mxUtils.createXmlDocument(); + this.objects = []; +}; + +/** + * Variable: document + * + * The owner document of the codec. + */ +mxCodec.prototype.document = null; + +/** + * Variable: objects + * + * Maps from IDs to objects. + */ +mxCodec.prototype.objects = null; + +/** + * Variable: encodeDefaults + * + * Specifies if default values should be encoded. Default is false. + */ +mxCodec.prototype.encodeDefaults = false; + + +/** + * Function: putObject + * + * Assoiates the given object with the given ID and returns the given object. + * + * Parameters + * + * id - ID for the object to be associated with. + * obj - Object to be associated with the ID. + */ +mxCodec.prototype.putObject = function(id, obj) +{ + this.objects[id] = obj; + + return obj; +}; + +/** + * Function: getObject + * + * Returns the decoded object for the element with the specified ID in + * <document>. If the object is not known then <lookup> is used to find an + * object. If no object is found, then the element with the respective ID + * from the document is parsed using <decode>. + */ +mxCodec.prototype.getObject = function(id) +{ + var obj = null; + + if (id != null) + { + obj = this.objects[id]; + + if (obj == null) + { + obj = this.lookup(id); + + if (obj == null) + { + var node = this.getElementById(id); + + if (node != null) + { + obj = this.decode(node); + } + } + } + } + + return obj; +}; + +/** + * Function: lookup + * + * Hook for subclassers to implement a custom lookup mechanism for cell IDs. + * This implementation always returns null. + * + * Example: + * + * (code) + * var codec = new mxCodec(); + * codec.lookup = function(id) + * { + * return model.getCell(id); + * }; + * (end) + * + * Parameters: + * + * id - ID of the object to be returned. + */ +mxCodec.prototype.lookup = function(id) +{ + return null; +}; + +/** + * Function: getElementById + * + * Returns the element with the given ID from <document>. The optional attr + * argument specifies the name of the ID attribute. Default is "id". The + * XPath expression used to find the element is //*[@attr='arg'] where attr is + * the name of the ID attribute and arg is the given id. + * + * Parameters: + * + * id - String that contains the ID. + * attr - Optional string for the attributename. Default is "id". + */ +mxCodec.prototype.getElementById = function(id, attr) +{ + attr = (attr != null) ? attr : 'id'; + + return mxUtils.findNodeByAttribute(this.document.documentElement, attr, id); +}; + +/** + * Function: getId + * + * Returns the ID of the specified object. This implementation + * calls <reference> first and if that returns null handles + * the object as an <mxCell> by returning their IDs using + * <mxCell.getId>. If no ID exists for the given cell, then + * an on-the-fly ID is generated using <mxCellPath.create>. + * + * Parameters: + * + * obj - Object to return the ID for. + */ +mxCodec.prototype.getId = function(obj) +{ + var id = null; + + if (obj != null) + { + id = this.reference(obj); + + if (id == null && obj instanceof mxCell) + { + id = obj.getId(); + + if (id == null) + { + // Uses an on-the-fly Id + id = mxCellPath.create(obj); + + if (id.length == 0) + { + id = 'root'; + } + } + } + } + + return id; +}; + +/** + * Function: reference + * + * Hook for subclassers to implement a custom method + * for retrieving IDs from objects. This implementation + * always returns null. + * + * Example: + * + * (code) + * var codec = new mxCodec(); + * codec.reference = function(obj) + * { + * return obj.getCustomId(); + * }; + * (end) + * + * Parameters: + * + * obj - Object whose ID should be returned. + */ +mxCodec.prototype.reference = function(obj) +{ + return null; +}; + +/** + * Function: encode + * + * Encodes the specified object and returns the resulting + * XML node. + * + * Parameters: + * + * obj - Object to be encoded. + */ +mxCodec.prototype.encode = function(obj) +{ + var node = null; + + if (obj != null && obj.constructor != null) + { + var enc = mxCodecRegistry.getCodec(obj.constructor); + + if (enc != null) + { + node = enc.encode(this, obj); + } + else + { + if (mxUtils.isNode(obj)) + { + node = (mxClient.IS_IE) ? obj.cloneNode(true) : + this.document.importNode(obj, true); + } + else + { + mxLog.warn('mxCodec.encode: No codec for '+ + mxUtils.getFunctionName(obj.constructor)); + } + } + } + + return node; +}; + +/** + * Function: decode + * + * Decodes the given XML node. The optional "into" + * argument specifies an existing object to be + * used. If no object is given, then a new instance + * is created using the constructor from the codec. + * + * The function returns the passed in object or + * the new instance if no object was given. + * + * Parameters: + * + * node - XML node to be decoded. + * into - Optional object to be decodec into. + */ +mxCodec.prototype.decode = function(node, into) +{ + var obj = null; + + if (node != null && node.nodeType == mxConstants.NODETYPE_ELEMENT) + { + var ctor = null; + + try + { + ctor = eval(node.nodeName); + } + catch (err) + { + // ignore + } + + try + { + var dec = mxCodecRegistry.getCodec(ctor); + + if (dec != null) + { + obj = dec.decode(this, node, into); + } + else + { + obj = node.cloneNode(true); + obj.removeAttribute('as'); + } + } + catch (err) + { + mxLog.debug('Cannot decode '+node.nodeName+': '+err.message); + } + } + + return obj; +}; + +/** + * Function: encodeCell + * + * Encoding of cell hierarchies is built-into the core, but + * is a higher-level function that needs to be explicitely + * used by the respective object encoders (eg. <mxModelCodec>, + * <mxChildChangeCodec> and <mxRootChangeCodec>). This + * implementation writes the given cell and its children as a + * (flat) sequence into the given node. The children are not + * encoded if the optional includeChildren is false. The + * function is in charge of adding the result into the + * given node and has no return value. + * + * Parameters: + * + * cell - <mxCell> to be encoded. + * node - Parent XML node to add the encoded cell into. + * includeChildren - Optional boolean indicating if the + * function should include all descendents. Default is true. + */ +mxCodec.prototype.encodeCell = function(cell, node, includeChildren) +{ + node.appendChild(this.encode(cell)); + + if (includeChildren == null || includeChildren) + { + var childCount = cell.getChildCount(); + + for (var i = 0; i < childCount; i++) + { + this.encodeCell(cell.getChildAt(i), node); + } + } +}; + +/** + * Function: isCellCodec + * + * Returns true if the given codec is a cell codec. This uses + * <mxCellCodec.isCellCodec> to check if the codec is of the + * given type. + */ +mxCodec.prototype.isCellCodec = function(codec) +{ + if (codec != null && typeof(codec.isCellCodec) == 'function') + { + return codec.isCellCodec(); + } + + return false; +}; + +/** + * Function: decodeCell + * + * Decodes cells that have been encoded using inversion, ie. + * where the user object is the enclosing node in the XML, + * and restores the group and graph structure in the cells. + * Returns a new <mxCell> instance that represents the + * given node. + * + * Parameters: + * + * node - XML node that contains the cell data. + * restoreStructures - Optional boolean indicating whether + * the graph structure should be restored by calling insert + * and insertEdge on the parent and terminals, respectively. + * Default is true. + */ +mxCodec.prototype.decodeCell = function(node, restoreStructures) +{ + restoreStructures = (restoreStructures != null) ? restoreStructures : true; + var cell = null; + + if (node != null && node.nodeType == mxConstants.NODETYPE_ELEMENT) + { + // Tries to find a codec for the given node name. If that does + // not return a codec then the node is the user object (an XML node + // that contains the mxCell, aka inversion). + var decoder = mxCodecRegistry.getCodec(node.nodeName); + + // Tries to find the codec for the cell inside the user object. + // This assumes all node names inside the user object are either + // not registered or they correspond to a class for cells. + if (!this.isCellCodec(decoder)) + { + var child = node.firstChild; + + while (child != null && !this.isCellCodec(decoder)) + { + decoder = mxCodecRegistry.getCodec(child.nodeName); + child = child.nextSibling; + } + } + + if (!this.isCellCodec(decoder)) + { + decoder = mxCodecRegistry.getCodec(mxCell); + } + + cell = decoder.decode(this, node); + + if (restoreStructures) + { + this.insertIntoGraph(cell); + } + } + + return cell; +}; + +/** + * Function: insertIntoGraph + * + * Inserts the given cell into its parent and terminal cells. + */ +mxCodec.prototype.insertIntoGraph = function(cell) +{ + var parent = cell.parent; + var source = cell.getTerminal(true); + var target = cell.getTerminal(false); + + // Fixes possible inconsistencies during insert into graph + cell.setTerminal(null, false); + cell.setTerminal(null, true); + cell.parent = null; + + if (parent != null) + { + parent.insert(cell); + } + + if (source != null) + { + source.insertEdge(cell, true); + } + + if (target != null) + { + target.insertEdge(cell, false); + } +}; + +/** + * Function: setAttribute + * + * Sets the attribute on the specified node to value. This is a + * helper method that makes sure the attribute and value arguments + * are not null. + * + * Parameters: + * + * node - XML node to set the attribute for. + * attributes - Attributename to be set. + * value - New value of the attribute. + */ +mxCodec.prototype.setAttribute = function(node, attribute, value) +{ + if (attribute != null && value != null) + { + node.setAttribute(attribute, value); + } +}; diff --git a/src/js/io/mxCodecRegistry.js b/src/js/io/mxCodecRegistry.js new file mode 100644 index 0000000..a0a0f20 --- /dev/null +++ b/src/js/io/mxCodecRegistry.js @@ -0,0 +1,137 @@ +/** + * $Id: mxCodecRegistry.js,v 1.12 2010-04-30 13:18:21 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +var mxCodecRegistry = +{ + /** + * Class: mxCodecRegistry + * + * Singleton class that acts as a global registry for codecs. + * + * Adding an <mxCodec>: + * + * 1. Define a default codec with a new instance of the + * object to be handled. + * + * (code) + * var codec = new mxObjectCodec(new mxGraphModel()); + * (end) + * + * 2. Define the functions required for encoding and decoding + * objects. + * + * (code) + * codec.encode = function(enc, obj) { ... } + * codec.decode = function(dec, node, into) { ... } + * (end) + * + * 3. Register the codec in the <mxCodecRegistry>. + * + * (code) + * mxCodecRegistry.register(codec); + * (end) + * + * <mxObjectCodec.decode> may be used to either create a new + * instance of an object or to configure an existing instance, + * in which case the into argument points to the existing + * object. In this case, we say the codec "configures" the + * object. + * + * Variable: codecs + * + * Maps from constructor names to codecs. + */ + codecs: [], + + /** + * Variable: aliases + * + * Maps from classnames to codecnames. + */ + aliases: [], + + /** + * Function: register + * + * Registers a new codec and associates the name of the template + * constructor in the codec with the codec object. + * + * Parameters: + * + * codec - <mxObjectCodec> to be registered. + */ + register: function(codec) + { + if (codec != null) + { + var name = codec.getName(); + mxCodecRegistry.codecs[name] = codec; + + var classname = mxUtils.getFunctionName(codec.template.constructor); + + if (classname != name) + { + mxCodecRegistry.addAlias(classname, name); + } + } + + return codec; + }, + + /** + * Function: addAlias + * + * Adds an alias for mapping a classname to a codecname. + */ + addAlias: function(classname, codecname) + { + mxCodecRegistry.aliases[classname] = codecname; + }, + + /** + * Function: getCodec + * + * Returns a codec that handles objects that are constructed + * using the given constructor. + * + * Parameters: + * + * ctor - JavaScript constructor function. + */ + getCodec: function(ctor) + { + var codec = null; + + if (ctor != null) + { + var name = mxUtils.getFunctionName(ctor); + var tmp = mxCodecRegistry.aliases[name]; + + if (tmp != null) + { + name = tmp; + } + + codec = mxCodecRegistry.codecs[name]; + + // Registers a new default codec for the given constructor + // if no codec has been previously defined. + if (codec == null) + { + try + { + codec = new mxObjectCodec(new ctor()); + mxCodecRegistry.register(codec); + } + catch (e) + { + // ignore + } + } + } + + return codec; + } + +}; diff --git a/src/js/io/mxDefaultKeyHandlerCodec.js b/src/js/io/mxDefaultKeyHandlerCodec.js new file mode 100644 index 0000000..628524a --- /dev/null +++ b/src/js/io/mxDefaultKeyHandlerCodec.js @@ -0,0 +1,88 @@ +/** + * $Id: mxDefaultKeyHandlerCodec.js,v 1.5 2010-01-02 09:45:15 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +mxCodecRegistry.register(function() +{ + /** + * Class: mxDefaultKeyHandlerCodec + * + * Custom codec for configuring <mxDefaultKeyHandler>s. This class is created + * and registered dynamically at load time and used implicitely via + * <mxCodec> and the <mxCodecRegistry>. This codec only reads configuration + * data for existing key handlers, it does not encode or create key handlers. + */ + var codec = new mxObjectCodec(new mxDefaultKeyHandler()); + + /** + * Function: encode + * + * Returns null. + */ + codec.encode = function(enc, obj) + { + return null; + }; + + /** + * Function: decode + * + * Reads a sequence of the following child nodes + * and attributes: + * + * Child Nodes: + * + * add - Binds a keystroke to an actionname. + * + * Attributes: + * + * as - Keycode. + * action - Actionname to execute in editor. + * control - Optional boolean indicating if + * the control key must be pressed. + * + * Example: + * + * (code) + * <mxDefaultKeyHandler as="keyHandler"> + * <add as="88" control="true" action="cut"/> + * <add as="67" control="true" action="copy"/> + * <add as="86" control="true" action="paste"/> + * </mxDefaultKeyHandler> + * (end) + * + * The keycodes are for the x, c and v keys. + * + * See also: <mxDefaultKeyHandler.bindAction>, + * http://www.js-examples.com/page/tutorials__key_codes.html + */ + codec.decode = function(dec, node, into) + { + if (into != null) + { + var editor = into.editor; + node = node.firstChild; + + while (node != null) + { + if (!this.processInclude(dec, node, into) && + node.nodeName == 'add') + { + var as = node.getAttribute('as'); + var action = node.getAttribute('action'); + var control = node.getAttribute('control'); + + into.bindAction(as, action, control); + } + + node = node.nextSibling; + } + } + + return into; + }; + + // Returns the codec into the registry + return codec; + +}()); diff --git a/src/js/io/mxDefaultPopupMenuCodec.js b/src/js/io/mxDefaultPopupMenuCodec.js new file mode 100644 index 0000000..8d949ea --- /dev/null +++ b/src/js/io/mxDefaultPopupMenuCodec.js @@ -0,0 +1,54 @@ +/** + * $Id: mxDefaultPopupMenuCodec.js,v 1.6 2010-01-02 09:45:15 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +mxCodecRegistry.register(function() +{ + /** + * Class: mxDefaultPopupMenuCodec + * + * Custom codec for configuring <mxDefaultPopupMenu>s. This class is created + * and registered dynamically at load time and used implicitely via + * <mxCodec> and the <mxCodecRegistry>. This codec only reads configuration + * data for existing popup menus, it does not encode or create menus. Note + * that this codec only passes the configuration node to the popup menu, + * which uses the config to dynamically create menus. See + * <mxDefaultPopupMenu.createMenu>. + */ + var codec = new mxObjectCodec(new mxDefaultPopupMenu()); + + /** + * Function: encode + * + * Returns null. + */ + codec.encode = function(enc, obj) + { + return null; + }; + + /** + * Function: decode + * + * Uses the given node as the config for <mxDefaultPopupMenu>. + */ + codec.decode = function(dec, node, into) + { + var inc = node.getElementsByTagName('include')[0]; + + if (inc != null) + { + this.processInclude(dec, inc, into); + } + else if (into != null) + { + into.config = node; + } + + return into; + }; + + // Returns the codec into the registry + return codec; + +}()); diff --git a/src/js/io/mxDefaultToolbarCodec.js b/src/js/io/mxDefaultToolbarCodec.js new file mode 100644 index 0000000..6698b9b --- /dev/null +++ b/src/js/io/mxDefaultToolbarCodec.js @@ -0,0 +1,301 @@ +/** + * $Id: mxDefaultToolbarCodec.js,v 1.22 2012-04-11 07:00:52 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +mxCodecRegistry.register(function() +{ + /** + * Class: mxDefaultToolbarCodec + * + * Custom codec for configuring <mxDefaultToolbar>s. This class is created + * and registered dynamically at load time and used implicitely via + * <mxCodec> and the <mxCodecRegistry>. This codec only reads configuration + * data for existing toolbars handlers, it does not encode or create toolbars. + */ + var codec = new mxObjectCodec(new mxDefaultToolbar()); + + /** + * Function: encode + * + * Returns null. + */ + codec.encode = function(enc, obj) + { + return null; + }; + + /** + * Function: decode + * + * Reads a sequence of the following child nodes + * and attributes: + * + * Child Nodes: + * + * add - Adds a new item to the toolbar. See below for attributes. + * separator - Adds a vertical separator. No attributes. + * hr - Adds a horizontal separator. No attributes. + * br - Adds a linefeed. No attributes. + * + * Attributes: + * + * as - Resource key for the label. + * action - Name of the action to execute in enclosing editor. + * mode - Modename (see below). + * template - Template name for cell insertion. + * style - Optional style to override the template style. + * icon - Icon (relative/absolute URL). + * pressedIcon - Optional icon for pressed state (relative/absolute URL). + * id - Optional ID to be used for the created DOM element. + * toggle - Optional 0 or 1 to disable toggling of the element. Default is + * 1 (true). + * + * The action, mode and template attributes are mutually exclusive. The + * style can only be used with the template attribute. The add node may + * contain another sequence of add nodes with as and action attributes + * to create a combo box in the toolbar. If the icon is specified then + * a list of the child node is expected to have its template attribute + * set and the action is ignored instead. + * + * Nodes with a specified template may define a function to be used for + * inserting the cloned template into the graph. Here is an example of such + * a node: + * + * (code) + * <add as="Swimlane" template="swimlane" icon="images/swimlane.gif"><![CDATA[ + * function (editor, cell, evt, targetCell) + * { + * var pt = mxUtils.convertPoint( + * editor.graph.container, mxEvent.getClientX(evt), + * mxEvent.getClientY(evt)); + * return editor.addVertex(targetCell, cell, pt.x, pt.y); + * } + * ]]></add> + * (end) + * + * In the above function, editor is the enclosing <mxEditor> instance, cell + * is the clone of the template, evt is the mouse event that represents the + * drop and targetCell is the cell under the mousepointer where the drop + * occurred. The targetCell is retrieved using <mxGraph.getCellAt>. + * + * Futhermore, nodes with the mode attribute may define a function to + * be executed upon selection of the respective toolbar icon. In the + * example below, the default edge style is set when this specific + * connect-mode is activated: + * + * (code) + * <add as="connect" mode="connect"><![CDATA[ + * function (editor) + * { + * if (editor.defaultEdge != null) + * { + * editor.defaultEdge.style = 'straightEdge'; + * } + * } + * ]]></add> + * (end) + * + * Modes: + * + * select - Left mouse button used for rubberband- & cell-selection. + * connect - Allows connecting vertices by inserting new edges. + * pan - Disables selection and switches to panning on the left button. + * + * Example: + * + * To add items to the toolbar: + * + * (code) + * <mxDefaultToolbar as="toolbar"> + * <add as="save" action="save" icon="images/save.gif"/> + * <br/><hr/> + * <add as="select" mode="select" icon="images/select.gif"/> + * <add as="connect" mode="connect" icon="images/connect.gif"/> + * </mxDefaultToolbar> + * (end) + */ + codec.decode = function(dec, node, into) + { + if (into != null) + { + var editor = into.editor; + node = node.firstChild; + + while (node != null) + { + if (node.nodeType == mxConstants.NODETYPE_ELEMENT) + { + if (!this.processInclude(dec, node, into)) + { + if (node.nodeName == 'separator') + { + into.addSeparator(); + } + else if (node.nodeName == 'br') + { + into.toolbar.addBreak(); + } + else if (node.nodeName == 'hr') + { + into.toolbar.addLine(); + } + else if (node.nodeName == 'add') + { + var as = node.getAttribute('as'); + as = mxResources.get(as) || as; + var icon = node.getAttribute('icon'); + var pressedIcon = node.getAttribute('pressedIcon'); + var action = node.getAttribute('action'); + var mode = node.getAttribute('mode'); + var template = node.getAttribute('template'); + var toggle = node.getAttribute('toggle') != '0'; + var text = mxUtils.getTextContent(node); + var elt = null; + + if (action != null) + { + elt = into.addItem(as, icon, action, pressedIcon); + } + else if (mode != null) + { + var funct = mxUtils.eval(text); + elt = into.addMode(as, icon, mode, pressedIcon, funct); + } + else if (template != null || (text != null && text.length > 0)) + { + var cell = editor.templates[template]; + var style = node.getAttribute('style'); + + if (cell != null && style != null) + { + cell = cell.clone(); + cell.setStyle(style); + } + + var insertFunction = null; + + if (text != null && text.length > 0) + { + insertFunction = mxUtils.eval(text); + } + + elt = into.addPrototype(as, icon, cell, pressedIcon, insertFunction, toggle); + } + else + { + var children = mxUtils.getChildNodes(node); + + if (children.length > 0) + { + if (icon == null) + { + var combo = into.addActionCombo(as); + + for (var i=0; i<children.length; i++) + { + var child = children[i]; + + if (child.nodeName == 'separator') + { + into.addOption(combo, '---'); + } + else if (child.nodeName == 'add') + { + var lab = child.getAttribute('as'); + var act = child.getAttribute('action'); + into.addActionOption(combo, lab, act); + } + } + } + else + { + var select = null; + var create = function() + { + var template = editor.templates[select.value]; + + if (template != null) + { + var clone = template.clone(); + var style = select.options[select.selectedIndex].cellStyle; + + if (style != null) + { + clone.setStyle(style); + } + + return clone; + } + else + { + mxLog.warn('Template '+template+' not found'); + } + + return null; + }; + + var img = into.addPrototype(as, icon, create, null, null, toggle); + select = into.addCombo(); + + // Selects the toolbar icon if a selection change + // is made in the corresponding combobox. + mxEvent.addListener(select, 'change', function() + { + into.toolbar.selectMode(img, function(evt) + { + var pt = mxUtils.convertPoint(editor.graph.container, + mxEvent.getClientX(evt), mxEvent.getClientY(evt)); + + return editor.addVertex(null, funct(), pt.x, pt.y); + }); + + into.toolbar.noReset = false; + }); + + // Adds the entries to the combobox + for (var i=0; i<children.length; i++) + { + var child = children[i]; + + if (child.nodeName == 'separator') + { + into.addOption(select, '---'); + } + else if (child.nodeName == 'add') + { + var lab = child.getAttribute('as'); + var tmp = child.getAttribute('template'); + var option = into.addOption(select, lab, tmp || template); + option.cellStyle = child.getAttribute('style'); + } + } + + } + } + } + + // Assigns an ID to the created element to access it later. + if (elt != null) + { + var id = node.getAttribute('id'); + + if (id != null && id.length > 0) + { + elt.setAttribute('id', id); + } + } + } + } + } + + node = node.nextSibling; + } + } + + return into; + }; + + // Returns the codec into the registry + return codec; + +}()); diff --git a/src/js/io/mxEditorCodec.js b/src/js/io/mxEditorCodec.js new file mode 100644 index 0000000..f61bd95 --- /dev/null +++ b/src/js/io/mxEditorCodec.js @@ -0,0 +1,246 @@ +/** + * $Id: mxEditorCodec.js,v 1.11 2010-01-04 11:18:26 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +mxCodecRegistry.register(function() +{ + /** + * Class: mxEditorCodec + * + * Codec for <mxEditor>s. This class is created and registered + * dynamically at load time and used implicitely via <mxCodec> + * and the <mxCodecRegistry>. + * + * Transient Fields: + * + * - modified + * - lastSnapshot + * - ignoredChanges + * - undoManager + * - graphContainer + * - toolbarContainer + */ + var codec = new mxObjectCodec(new mxEditor(), + ['modified', 'lastSnapshot', 'ignoredChanges', + 'undoManager', 'graphContainer', 'toolbarContainer']); + + /** + * Function: beforeDecode + * + * Decodes the ui-part of the configuration node by reading + * a sequence of the following child nodes and attributes + * and passes the control to the default decoding mechanism: + * + * Child Nodes: + * + * stylesheet - Adds a CSS stylesheet to the document. + * resource - Adds the basename of a resource bundle. + * add - Creates or configures a known UI element. + * + * These elements may appear in any order given that the + * graph UI element is added before the toolbar element + * (see Known Keys). + * + * Attributes: + * + * as - Key for the UI element (see below). + * element - ID for the element in the document. + * style - CSS style to be used for the element or window. + * x - X coordinate for the new window. + * y - Y coordinate for the new window. + * width - Width for the new window. + * height - Optional height for the new window. + * name - Name of the stylesheet (absolute/relative URL). + * basename - Basename of the resource bundle (see <mxResources>). + * + * The x, y, width and height attributes are used to create a new + * <mxWindow> if the element attribute is not specified in an add + * node. The name and basename are only used in the stylesheet and + * resource nodes, respectively. + * + * Known Keys: + * + * graph - Main graph element (see <mxEditor.setGraphContainer>). + * title - Title element (see <mxEditor.setTitleContainer>). + * toolbar - Toolbar element (see <mxEditor.setToolbarContainer>). + * status - Status bar element (see <mxEditor.setStatusContainer>). + * + * Example: + * + * (code) + * <ui> + * <stylesheet name="css/process.css"/> + * <resource basename="resources/mxApplication"/> + * <add as="graph" element="graph" + * style="left:70px;right:20px;top:20px;bottom:40px"/> + * <add as="status" element="status"/> + * <add as="toolbar" x="10" y="20" width="54"/> + * </ui> + * (end) + */ + codec.afterDecode = function(dec, node, obj) + { + // Assigns the specified templates for edges + var defaultEdge = node.getAttribute('defaultEdge'); + + if (defaultEdge != null) + { + node.removeAttribute('defaultEdge'); + obj.defaultEdge = obj.templates[defaultEdge]; + } + + // Assigns the specified templates for groups + var defaultGroup = node.getAttribute('defaultGroup'); + + if (defaultGroup != null) + { + node.removeAttribute('defaultGroup'); + obj.defaultGroup = obj.templates[defaultGroup]; + } + + return obj; + }; + + /** + * Function: decodeChild + * + * Overrides decode child to handle special child nodes. + */ + codec.decodeChild = function(dec, child, obj) + { + if (child.nodeName == 'Array') + { + var role = child.getAttribute('as'); + + if (role == 'templates') + { + this.decodeTemplates(dec, child, obj); + return; + } + } + else if (child.nodeName == 'ui') + { + this.decodeUi(dec, child, obj); + return; + } + + mxObjectCodec.prototype.decodeChild.apply(this, arguments); + }; + + /** + * Function: decodeTemplates + * + * Decodes the cells from the given node as templates. + */ + codec.decodeUi = function(dec, node, editor) + { + var tmp = node.firstChild; + while (tmp != null) + { + if (tmp.nodeName == 'add') + { + var as = tmp.getAttribute('as'); + var elt = tmp.getAttribute('element'); + var style = tmp.getAttribute('style'); + var element = null; + + if (elt != null) + { + element = document.getElementById(elt); + + if (element != null && + style != null) + { + element.style.cssText += ';'+style; + } + } + else + { + var x = parseInt(tmp.getAttribute('x')); + var y = parseInt(tmp.getAttribute('y')); + var width = tmp.getAttribute('width'); + var height = tmp.getAttribute('height'); + + // Creates a new window around the element + element = document.createElement('div'); + element.style.cssText = style; + + var wnd = new mxWindow(mxResources.get(as) || as, + element, x, y, width, height, false, true); + wnd.setVisible(true); + } + + // TODO: Make more generic + if (as == 'graph') + { + editor.setGraphContainer(element); + } + else if (as == 'toolbar') + { + editor.setToolbarContainer(element); + } + else if (as == 'title') + { + editor.setTitleContainer(element); + } + else if (as == 'status') + { + editor.setStatusContainer(element); + } + else if (as == 'map') + { + editor.setMapContainer(element); + } + } + else if (tmp.nodeName == 'resource') + { + mxResources.add(tmp.getAttribute('basename')); + } + else if (tmp.nodeName == 'stylesheet') + { + mxClient.link('stylesheet', tmp.getAttribute('name')); + } + + tmp = tmp.nextSibling; + } + }; + + /** + * Function: decodeTemplates + * + * Decodes the cells from the given node as templates. + */ + codec.decodeTemplates = function(dec, node, editor) + { + if (editor.templates == null) + { + editor.templates = []; + } + + var children = mxUtils.getChildNodes(node); + for (var j=0; j<children.length; j++) + { + var name = children[j].getAttribute('as'); + var child = children[j].firstChild; + + while (child != null && child.nodeType != 1) + { + child = child.nextSibling; + } + + if (child != null) + { + // LATER: Only single cells means you need + // to group multiple cells within another + // cell. This should be changed to support + // arrays of cells, or the wrapper must + // be automatically handled in this class. + editor.templates[name] = dec.decodeCell(child); + } + } + }; + + // Returns the codec into the registry + return codec; + +}()); diff --git a/src/js/io/mxGenericChangeCodec.js b/src/js/io/mxGenericChangeCodec.js new file mode 100644 index 0000000..8da7789 --- /dev/null +++ b/src/js/io/mxGenericChangeCodec.js @@ -0,0 +1,64 @@ +/** + * $Id: mxGenericChangeCodec.js,v 1.11 2010-09-13 15:50:36 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxGenericChangeCodec + * + * Codec for <mxValueChange>s, <mxStyleChange>s, <mxGeometryChange>s, + * <mxCollapseChange>s and <mxVisibleChange>s. This class is created + * and registered dynamically at load time and used implicitely + * via <mxCodec> and the <mxCodecRegistry>. + * + * Transient Fields: + * + * - model + * - previous + * + * Reference Fields: + * + * - cell + * + * Constructor: mxGenericChangeCodec + * + * Factory function that creates a <mxObjectCodec> for + * the specified change and fieldname. + * + * Parameters: + * + * obj - An instance of the change object. + * variable - The fieldname for the change data. + */ +var mxGenericChangeCodec = function(obj, variable) +{ + var codec = new mxObjectCodec(obj, ['model', 'previous'], ['cell']); + + /** + * Function: afterDecode + * + * Restores the state by assigning the previous value. + */ + codec.afterDecode = function(dec, node, obj) + { + // Allows forward references in sessions. This is a workaround + // for the sequence of edits in mxGraph.moveCells and cellsAdded. + if (mxUtils.isNode(obj.cell)) + { + obj.cell = dec.decodeCell(obj.cell, false); + } + + obj.previous = obj[variable]; + + return obj; + }; + + return codec; +}; + +// Registers the codecs +mxCodecRegistry.register(mxGenericChangeCodec(new mxValueChange(), 'value')); +mxCodecRegistry.register(mxGenericChangeCodec(new mxStyleChange(), 'style')); +mxCodecRegistry.register(mxGenericChangeCodec(new mxGeometryChange(), 'geometry')); +mxCodecRegistry.register(mxGenericChangeCodec(new mxCollapseChange(), 'collapsed')); +mxCodecRegistry.register(mxGenericChangeCodec(new mxVisibleChange(), 'visible')); +mxCodecRegistry.register(mxGenericChangeCodec(new mxCellAttributeChange(), 'value')); diff --git a/src/js/io/mxGraphCodec.js b/src/js/io/mxGraphCodec.js new file mode 100644 index 0000000..f052e13 --- /dev/null +++ b/src/js/io/mxGraphCodec.js @@ -0,0 +1,28 @@ +/** + * $Id: mxGraphCodec.js,v 1.8 2010-06-10 06:54:18 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +mxCodecRegistry.register(function() +{ + /** + * Class: mxGraphCodec + * + * Codec for <mxGraph>s. This class is created and registered + * dynamically at load time and used implicitely via <mxCodec> + * and the <mxCodecRegistry>. + * + * Transient Fields: + * + * - graphListeners + * - eventListeners + * - view + * - container + * - cellRenderer + * - editor + * - selection + */ + return new mxObjectCodec(new mxGraph(), + ['graphListeners', 'eventListeners', 'view', 'container', + 'cellRenderer', 'editor', 'selection']); + +}()); diff --git a/src/js/io/mxGraphViewCodec.js b/src/js/io/mxGraphViewCodec.js new file mode 100644 index 0000000..110b212 --- /dev/null +++ b/src/js/io/mxGraphViewCodec.js @@ -0,0 +1,197 @@ +/** + * $Id: mxGraphViewCodec.js,v 1.18 2010-12-03 11:05:52 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +mxCodecRegistry.register(function() +{ + /** + * Class: mxGraphViewCodec + * + * Custom encoder for <mxGraphView>s. This class is created + * and registered dynamically at load time and used implicitely via + * <mxCodec> and the <mxCodecRegistry>. This codec only writes views + * into a XML format that can be used to create an image for + * the graph, that is, it contains absolute coordinates with + * computed perimeters, edge styles and cell styles. + */ + var codec = new mxObjectCodec(new mxGraphView()); + + /** + * Function: encode + * + * Encodes the given <mxGraphView> using <encodeCell> + * starting at the model's root. This returns the + * top-level graph node of the recursive encoding. + */ + codec.encode = function(enc, view) + { + return this.encodeCell(enc, view, + view.graph.getModel().getRoot()); + }; + + /** + * Function: encodeCell + * + * Recursively encodes the specifed cell. Uses layer + * as the default nodename. If the cell's parent is + * null, then graph is used for the nodename. If + * <mxGraphModel.isEdge> returns true for the cell, + * then edge is used for the nodename, else if + * <mxGraphModel.isVertex> returns true for the cell, + * then vertex is used for the nodename. + * + * <mxGraph.getLabel> is used to create the label + * attribute for the cell. For graph nodes and vertices + * the bounds are encoded into x, y, width and height. + * For edges the points are encoded into a points + * attribute as a space-separated list of comma-separated + * coordinate pairs (eg. x0,y0 x1,y1 ... xn,yn). All + * values from the cell style are added as attribute + * values to the node. + */ + codec.encodeCell = function(enc, view, cell) + { + var model = view.graph.getModel(); + var state = view.getState(cell); + var parent = model.getParent(cell); + + if (parent == null || state != null) + { + var childCount = model.getChildCount(cell); + var geo = view.graph.getCellGeometry(cell); + var name = null; + + if (parent == model.getRoot()) + { + name = 'layer'; + } + else if (parent == null) + { + name = 'graph'; + } + else if (model.isEdge(cell)) + { + name = 'edge'; + } + else if (childCount > 0 && geo != null) + { + name = 'group'; + } + else if (model.isVertex(cell)) + { + name = 'vertex'; + } + + if (name != null) + { + var node = enc.document.createElement(name); + var lab = view.graph.getLabel(cell); + + if (lab != null) + { + node.setAttribute('label', view.graph.getLabel(cell)); + + if (view.graph.isHtmlLabel(cell)) + { + node.setAttribute('html', true); + } + } + + if (parent == null) + { + var bounds = view.getGraphBounds(); + + if (bounds != null) + { + node.setAttribute('x', Math.round(bounds.x)); + node.setAttribute('y', Math.round(bounds.y)); + node.setAttribute('width', Math.round(bounds.width)); + node.setAttribute('height', Math.round(bounds.height)); + } + + node.setAttribute('scale', view.scale); + } + else if (state != null && geo != null) + { + // Writes each key, value in the style pair to an attribute + for (var i in state.style) + { + var value = state.style[i]; + + // Tries to turn objects and functions into strings + if (typeof(value) == 'function' && + typeof(value) == 'object') + { + value = mxStyleRegistry.getName(value); + } + + if (value != null && + typeof(value) != 'function' && + typeof(value) != 'object') + { + node.setAttribute(i, value); + } + } + + var abs = state.absolutePoints; + + // Writes the list of points into one attribute + if (abs != null && abs.length > 0) + { + var pts = Math.round(abs[0].x) + ',' + Math.round(abs[0].y); + + for (var i=1; i<abs.length; i++) + { + pts += ' ' + Math.round(abs[i].x) + ',' + + Math.round(abs[i].y); + } + + node.setAttribute('points', pts); + } + + // Writes the bounds into 4 attributes + else + { + node.setAttribute('x', Math.round(state.x)); + node.setAttribute('y', Math.round(state.y)); + node.setAttribute('width', Math.round(state.width)); + node.setAttribute('height', Math.round(state.height)); + } + + var offset = state.absoluteOffset; + + // Writes the offset into 2 attributes + if (offset != null) + { + if (offset.x != 0) + { + node.setAttribute('dx', Math.round(offset.x)); + } + + if (offset.y != 0) + { + node.setAttribute('dy', Math.round(offset.y)); + } + } + } + + for (var i=0; i<childCount; i++) + { + var childNode = this.encodeCell(enc, + view, model.getChildAt(cell, i)); + + if (childNode != null) + { + node.appendChild(childNode); + } + } + } + } + + return node; + }; + + // Returns the codec into the registry + return codec; + +}()); diff --git a/src/js/io/mxModelCodec.js b/src/js/io/mxModelCodec.js new file mode 100644 index 0000000..760a2b1 --- /dev/null +++ b/src/js/io/mxModelCodec.js @@ -0,0 +1,80 @@ +/** + * $Id: mxModelCodec.js,v 1.11 2010-11-23 08:46:41 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +mxCodecRegistry.register(function() +{ + /** + * Class: mxModelCodec + * + * Codec for <mxGraphModel>s. This class is created and registered + * dynamically at load time and used implicitely via <mxCodec> + * and the <mxCodecRegistry>. + */ + var codec = new mxObjectCodec(new mxGraphModel()); + + /** + * Function: encodeObject + * + * Encodes the given <mxGraphModel> by writing a (flat) XML sequence of + * cell nodes as produced by the <mxCellCodec>. The sequence is + * wrapped-up in a node with the name root. + */ + codec.encodeObject = function(enc, obj, node) + { + var rootNode = enc.document.createElement('root'); + enc.encodeCell(obj.getRoot(), rootNode); + node.appendChild(rootNode); + }; + + /** + * Function: decodeChild + * + * Overrides decode child to handle special child nodes. + */ + codec.decodeChild = function(dec, child, obj) + { + if (child.nodeName == 'root') + { + this.decodeRoot(dec, child, obj); + } + else + { + mxObjectCodec.prototype.decodeChild.apply(this, arguments); + } + }; + + /** + * Function: decodeRoot + * + * Reads the cells into the graph model. All cells + * are children of the root element in the node. + */ + codec.decodeRoot = function(dec, root, model) + { + var rootCell = null; + var tmp = root.firstChild; + + while (tmp != null) + { + var cell = dec.decodeCell(tmp); + + if (cell != null && cell.getParent() == null) + { + rootCell = cell; + } + + tmp = tmp.nextSibling; + } + + // Sets the root on the model if one has been decoded + if (rootCell != null) + { + model.setRoot(rootCell); + } + }; + + // Returns the codec into the registry + return codec; + +}()); diff --git a/src/js/io/mxObjectCodec.js b/src/js/io/mxObjectCodec.js new file mode 100644 index 0000000..9d2473a --- /dev/null +++ b/src/js/io/mxObjectCodec.js @@ -0,0 +1,983 @@ +/** + * $Id: mxObjectCodec.js,v 1.49 2010-12-01 09:19:58 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxObjectCodec + * + * Generic codec for JavaScript objects that implements a mapping between + * JavaScript objects and XML nodes that maps each field or element to an + * attribute or child node, and vice versa. + * + * Atomic Values: + * + * Consider the following example. + * + * (code) + * var obj = new Object(); + * obj.foo = "Foo"; + * obj.bar = "Bar"; + * (end) + * + * This object is encoded into an XML node using the following. + * + * (code) + * var enc = new mxCodec(); + * var node = enc.encode(obj); + * (end) + * + * The output of the encoding may be viewed using <mxLog> as follows. + * + * (code) + * mxLog.show(); + * mxLog.debug(mxUtils.getPrettyXml(node)); + * (end) + * + * Finally, the result of the encoding looks as follows. + * + * (code) + * <Object foo="Foo" bar="Bar"/> + * (end) + * + * In the above output, the foo and bar fields have been mapped to attributes + * with the same names, and the name of the constructor was used for the + * nodename. + * + * Booleans: + * + * Since booleans are numbers in JavaScript, all boolean values are encoded + * into 1 for true and 0 for false. The decoder also accepts the string true + * and false for boolean values. + * + * Objects: + * + * The above scheme is applied to all atomic fields, that is, to all non-object + * fields of an object. For object fields, a child node is created with a + * special attribute that contains the fieldname. This special attribute is + * called "as" and hence, as is a reserved word that should not be used for a + * fieldname. + * + * Consider the following example where foo is an object and bar is an atomic + * property of foo. + * + * (code) + * var obj = {foo: {bar: "Bar"}}; + * (end) + * + * This will be mapped to the following XML structure by mxObjectCodec. + * + * (code) + * <Object> + * <Object bar="Bar" as="foo"/> + * </Object> + * (end) + * + * In the above output, the inner Object node contains the as-attribute that + * specifies the fieldname in the enclosing object. That is, the field foo was + * mapped to a child node with an as-attribute that has the value foo. + * + * Arrays: + * + * Arrays are special objects that are either associative, in which case each + * key, value pair is treated like a field where the key is the fieldname, or + * they are a sequence of atomic values and objects, which is mapped to a + * sequence of child nodes. For object elements, the above scheme is applied + * without the use of the special as-attribute for creating each child. For + * atomic elements, a special add-node is created with the value stored in the + * value-attribute. + * + * For example, the following array contains one atomic value and one object + * with a field called bar. Furthermore it contains two associative entries + * called bar with an atomic value, and foo with an object value. + * + * (code) + * var obj = ["Bar", {bar: "Bar"}]; + * obj["bar"] = "Bar"; + * obj["foo"] = {bar: "Bar"}; + * (end) + * + * This array is represented by the following XML nodes. + * + * (code) + * <Array bar="Bar"> + * <add value="Bar"/> + * <Object bar="Bar"/> + * <Object bar="Bar" as="foo"/> + * </Array> + * (end) + * + * The Array node name is the name of the constructor. The additional + * as-attribute in the last child contains the key of the associative entry, + * whereas the second last child is part of the array sequence and does not + * have an as-attribute. + * + * References: + * + * Objects may be represented as child nodes or attributes with ID values, + * which are used to lookup the object in a table within <mxCodec>. The + * <isReference> function is in charge of deciding if a specific field should + * be encoded as a reference or not. Its default implementation returns true if + * the fieldname is in <idrefs>, an array of strings that is used to configure + * the <mxObjectCodec>. + * + * Using this approach, the mapping does not guarantee that the referenced + * object itself exists in the document. The fields that are encoded as + * references must be carefully chosen to make sure all referenced objects + * exist in the document, or may be resolved by some other means if necessary. + * + * For example, in the case of the graph model all cells are stored in a tree + * whose root is referenced by the model's root field. A tree is a structure + * that is well suited for an XML representation, however, the additional edges + * in the graph model have a reference to a source and target cell, which are + * also contained in the tree. To handle this case, the source and target cell + * of an edge are treated as references, whereas the children are treated as + * objects. Since all cells are contained in the tree and no edge references a + * source or target outside the tree, this setup makes sure all referenced + * objects are contained in the document. + * + * In the case of a tree structure we must further avoid infinite recursion by + * ignoring the parent reference of each child. This is done by returning true + * in <isExcluded>, whose default implementation uses the array of excluded + * fieldnames passed to the mxObjectCodec constructor. + * + * References are only used for cells in mxGraph. For defining other + * referencable object types, the codec must be able to work out the ID of an + * object. This is done by implementing <mxCodec.reference>. For decoding a + * reference, the XML node with the respective id-attribute is fetched from the + * document, decoded, and stored in a lookup table for later reference. For + * looking up external objects, <mxCodec.lookup> may be implemented. + * + * Expressions: + * + * For decoding JavaScript expressions, the add-node may be used with a text + * content that contains the JavaScript expression. For example, the following + * creates a field called foo in the enclosing object and assigns it the value + * of <mxConstants.ALIGN_LEFT>. + * + * (code) + * <Object> + * <add as="foo">mxConstants.ALIGN_LEFT</add> + * </Object> + * (end) + * + * The resulting object has a field called foo with the value "left". Its XML + * representation looks as follows. + * + * (code) + * <Object foo="left"/> + * (end) + * + * This means the expression is evaluated at decoding time and the result of + * the evaluation is stored in the respective field. Valid expressions are all + * JavaScript expressions, including function definitions, which are mapped to + * functions on the resulting object. + * + * Constructor: mxObjectCodec + * + * Constructs a new codec for the specified template object. + * The variables in the optional exclude array are ignored by + * the codec. Variables in the optional idrefs array are + * turned into references in the XML. The optional mapping + * may be used to map from variable names to XML attributes. + * The argument is created as follows: + * + * (code) + * var mapping = new Object(); + * mapping['variableName'] = 'attribute-name'; + * (end) + * + * Parameters: + * + * template - Prototypical instance of the object to be + * encoded/decoded. + * exclude - Optional array of fieldnames to be ignored. + * idrefs - Optional array of fieldnames to be converted to/from + * references. + * mapping - Optional mapping from field- to attributenames. + */ +function mxObjectCodec(template, exclude, idrefs, mapping) +{ + this.template = template; + + this.exclude = (exclude != null) ? exclude : []; + this.idrefs = (idrefs != null) ? idrefs : []; + this.mapping = (mapping != null) ? mapping : []; + + this.reverse = new Object(); + + for (var i in this.mapping) + { + this.reverse[this.mapping[i]] = i; + } +}; + +/** + * Variable: template + * + * Holds the template object associated with this codec. + */ +mxObjectCodec.prototype.template = null; + +/** + * Variable: exclude + * + * Array containing the variable names that should be + * ignored by the codec. + */ +mxObjectCodec.prototype.exclude = null; + +/** + * Variable: idrefs + * + * Array containing the variable names that should be + * turned into or converted from references. See + * <mxCodec.getId> and <mxCodec.getObject>. + */ +mxObjectCodec.prototype.idrefs = null; + +/** + * Variable: mapping + * + * Maps from from fieldnames to XML attribute names. + */ +mxObjectCodec.prototype.mapping = null; + +/** + * Variable: reverse + * + * Maps from from XML attribute names to fieldnames. + */ +mxObjectCodec.prototype.reverse = null; + +/** + * Function: getName + * + * Returns the name used for the nodenames and lookup of the codec when + * classes are encoded and nodes are decoded. For classes to work with + * this the codec registry automatically adds an alias for the classname + * if that is different than what this returns. The default implementation + * returns the classname of the template class. + */ +mxObjectCodec.prototype.getName = function() +{ + return mxUtils.getFunctionName(this.template.constructor); +}; + +/** + * Function: cloneTemplate + * + * Returns a new instance of the template for this codec. + */ +mxObjectCodec.prototype.cloneTemplate = function() +{ + return new this.template.constructor(); +}; + +/** + * Function: getFieldName + * + * Returns the fieldname for the given attributename. + * Looks up the value in the <reverse> mapping or returns + * the input if there is no reverse mapping for the + * given name. + */ +mxObjectCodec.prototype.getFieldName = function(attributename) +{ + if (attributename != null) + { + var mapped = this.reverse[attributename]; + + if (mapped != null) + { + attributename = mapped; + } + } + + return attributename; +}; + +/** + * Function: getAttributeName + * + * Returns the attributename for the given fieldname. + * Looks up the value in the <mapping> or returns + * the input if there is no mapping for the + * given name. + */ +mxObjectCodec.prototype.getAttributeName = function(fieldname) +{ + if (fieldname != null) + { + var mapped = this.mapping[fieldname]; + + if (mapped != null) + { + fieldname = mapped; + } + } + + return fieldname; +}; + +/** + * Function: isExcluded + * + * Returns true if the given attribute is to be ignored by the codec. This + * implementation returns true if the given fieldname is in <exclude> or + * if the fieldname equals <mxObjectIdentity.FIELD_NAME>. + * + * Parameters: + * + * obj - Object instance that contains the field. + * attr - Fieldname of the field. + * value - Value of the field. + * write - Boolean indicating if the field is being encoded or decoded. + * Write is true if the field is being encoded, else it is being decoded. + */ +mxObjectCodec.prototype.isExcluded = function(obj, attr, value, write) +{ + return attr == mxObjectIdentity.FIELD_NAME || + mxUtils.indexOf(this.exclude, attr) >= 0; +}; + +/** + * Function: isReference + * + * Returns true if the given fieldname is to be treated + * as a textual reference (ID). This implementation returns + * true if the given fieldname is in <idrefs>. + * + * Parameters: + * + * obj - Object instance that contains the field. + * attr - Fieldname of the field. + * value - Value of the field. + * write - Boolean indicating if the field is being encoded or decoded. + * Write is true if the field is being encoded, else it is being decoded. + */ +mxObjectCodec.prototype.isReference = function(obj, attr, value, write) +{ + return mxUtils.indexOf(this.idrefs, attr) >= 0; +}; + +/** + * Function: encode + * + * Encodes the specified object and returns a node + * representing then given object. Calls <beforeEncode> + * after creating the node and <afterEncode> with the + * resulting node after processing. + * + * Enc is a reference to the calling encoder. It is used + * to encode complex objects and create references. + * + * This implementation encodes all variables of an + * object according to the following rules: + * + * - If the variable name is in <exclude> then it is ignored. + * - If the variable name is in <idrefs> then <mxCodec.getId> + * is used to replace the object with its ID. + * - The variable name is mapped using <mapping>. + * - If obj is an array and the variable name is numeric + * (ie. an index) then it is not encoded. + * - If the value is an object, then the codec is used to + * create a child node with the variable name encoded into + * the "as" attribute. + * - Else, if <encodeDefaults> is true or the value differs + * from the template value, then ... + * - ... if obj is not an array, then the value is mapped to + * an attribute. + * - ... else if obj is an array, the value is mapped to an + * add child with a value attribute or a text child node, + * if the value is a function. + * + * If no ID exists for a variable in <idrefs> or if an object + * cannot be encoded, a warning is issued using <mxLog.warn>. + * + * Returns the resulting XML node that represents the given + * object. + * + * Parameters: + * + * enc - <mxCodec> that controls the encoding process. + * obj - Object to be encoded. + */ +mxObjectCodec.prototype.encode = function(enc, obj) +{ + var node = enc.document.createElement(this.getName()); + + obj = this.beforeEncode(enc, obj, node); + this.encodeObject(enc, obj, node); + + return this.afterEncode(enc, obj, node); +}; + +/** + * Function: encodeObject + * + * Encodes the value of each member in then given obj into the given node using + * <encodeValue>. + * + * Parameters: + * + * enc - <mxCodec> that controls the encoding process. + * obj - Object to be encoded. + * node - XML node that contains the encoded object. + */ +mxObjectCodec.prototype.encodeObject = function(enc, obj, node) +{ + enc.setAttribute(node, 'id', enc.getId(obj)); + + for (var i in obj) + { + var name = i; + var value = obj[name]; + + if (value != null && !this.isExcluded(obj, name, value, true)) + { + if (mxUtils.isNumeric(name)) + { + name = null; + } + + this.encodeValue(enc, obj, name, value, node); + } + } +}; + +/** + * Function: encodeValue + * + * Converts the given value according to the mappings + * and id-refs in this codec and uses <writeAttribute> + * to write the attribute into the given node. + * + * Parameters: + * + * enc - <mxCodec> that controls the encoding process. + * obj - Object whose property is going to be encoded. + * name - XML node that contains the encoded object. + * value - Value of the property to be encoded. + * node - XML node that contains the encoded object. + */ +mxObjectCodec.prototype.encodeValue = function(enc, obj, + name, value, node) +{ + if (value != null) + { + if (this.isReference(obj, name, value, true)) + { + var tmp = enc.getId(value); + + if (tmp == null) + { + mxLog.warn('mxObjectCodec.encode: No ID for ' + + this.getName() + '.' + name + '=' + value); + return; // exit + } + + value = tmp; + } + + var defaultValue = this.template[name]; + + // Checks if the value is a default value and + // the name is correct + if (name == null || enc.encodeDefaults || + defaultValue != value) + { + name = this.getAttributeName(name); + this.writeAttribute(enc, obj, name, value, node); + } + } +}; + +/** + * Function: writeAttribute + * + * Writes the given value into node using <writePrimitiveAttribute> + * or <writeComplexAttribute> depending on the type of the value. + */ +mxObjectCodec.prototype.writeAttribute = function(enc, obj, + attr, value, node) +{ + if (typeof(value) != 'object' /* primitive type */) + { + this.writePrimitiveAttribute(enc, obj, attr, value, node); + } + else /* complex type */ + { + this.writeComplexAttribute(enc, obj, attr, value, node); + } +}; + +/** + * Function: writePrimitiveAttribute + * + * Writes the given value as an attribute of the given node. + */ +mxObjectCodec.prototype.writePrimitiveAttribute = function(enc, obj, + attr, value, node) +{ + value = this.convertValueToXml(value); + + if (attr == null) + { + var child = enc.document.createElement('add'); + + if (typeof(value) == 'function') + { + child.appendChild( + enc.document.createTextNode(value)); + } + else + { + enc.setAttribute(child, 'value', value); + } + + node.appendChild(child); + } + else if (typeof(value) != 'function') + { + enc.setAttribute(node, attr, value); + } +}; + +/** + * Function: writeComplexAttribute + * + * Writes the given value as a child node of the given node. + */ +mxObjectCodec.prototype.writeComplexAttribute = function(enc, obj, + attr, value, node) +{ + var child = enc.encode(value); + + if (child != null) + { + if (attr != null) + { + child.setAttribute('as', attr); + } + + node.appendChild(child); + } + else + { + mxLog.warn('mxObjectCodec.encode: No node for ' + + this.getName() + '.' + attr + ': ' + value); + } +}; + +/** + * Function: convertValueToXml + * + * Converts true to "1" and false to "0". All other values are ignored. + */ +mxObjectCodec.prototype.convertValueToXml = function(value) +{ + // Makes sure to encode boolean values as numeric values + if (typeof(value.length) == 'undefined' && + (value == true || + value == false)) + { + // Checks if the value is true (do not use the + // value as is, because this would check if the + // value is not null, so 0 would be true! + value = (value == true) ? '1' : '0'; + } + return value; +}; + +/** + * Function: convertValueFromXml + * + * Converts booleans and numeric values to the respective types. + */ +mxObjectCodec.prototype.convertValueFromXml = function(value) +{ + if (mxUtils.isNumeric(value)) + { + value = parseFloat(value); + } + + return value; +}; + +/** + * Function: beforeEncode + * + * Hook for subclassers to pre-process the object before + * encoding. This returns the input object. The return + * value of this function is used in <encode> to perform + * the default encoding into the given node. + * + * Parameters: + * + * enc - <mxCodec> that controls the encoding process. + * obj - Object to be encoded. + * node - XML node to encode the object into. + */ +mxObjectCodec.prototype.beforeEncode = function(enc, obj, node) +{ + return obj; +}; + +/** + * Function: afterEncode + * + * Hook for subclassers to post-process the node + * for the given object after encoding and return the + * post-processed node. This implementation returns + * the input node. The return value of this method + * is returned to the encoder from <encode>. + * + * Parameters: + * + * enc - <mxCodec> that controls the encoding process. + * obj - Object to be encoded. + * node - XML node that represents the default encoding. + */ +mxObjectCodec.prototype.afterEncode = function(enc, obj, node) +{ + return node; +}; + +/** + * Function: decode + * + * Parses the given node into the object or returns a new object + * representing the given node. + * + * Dec is a reference to the calling decoder. It is used to decode + * complex objects and resolve references. + * + * If a node has an id attribute then the object cache is checked for the + * object. If the object is not yet in the cache then it is constructed + * using the constructor of <template> and cached in <mxCodec.objects>. + * + * This implementation decodes all attributes and childs of a node + * according to the following rules: + * + * - If the variable name is in <exclude> or if the attribute name is "id" + * or "as" then it is ignored. + * - If the variable name is in <idrefs> then <mxCodec.getObject> is used + * to replace the reference with an object. + * - The variable name is mapped using a reverse <mapping>. + * - If the value has a child node, then the codec is used to create a + * child object with the variable name taken from the "as" attribute. + * - If the object is an array and the variable name is empty then the + * value or child object is appended to the array. + * - If an add child has no value or the object is not an array then + * the child text content is evaluated using <mxUtils.eval>. + * + * For add nodes where the object is not an array and the variable name + * is defined, the default mechanism is used, allowing to override/add + * methods as follows: + * + * (code) + * <Object> + * <add as="hello"><![CDATA[ + * function(arg1) { + * mxUtils.alert('Hello '+arg1); + * } + * ]]></add> + * </Object> + * (end) + * + * If no object exists for an ID in <idrefs> a warning is issued + * using <mxLog.warn>. + * + * Returns the resulting object that represents the given XML node + * or the object given to the method as the into parameter. + * + * Parameters: + * + * dec - <mxCodec> that controls the decoding process. + * node - XML node to be decoded. + * into - Optional objec to encode the node into. + */ +mxObjectCodec.prototype.decode = function(dec, node, into) +{ + var id = node.getAttribute('id'); + var obj = dec.objects[id]; + + if (obj == null) + { + obj = into || this.cloneTemplate(); + + if (id != null) + { + dec.putObject(id, obj); + } + } + + node = this.beforeDecode(dec, node, obj); + this.decodeNode(dec, node, obj); + + return this.afterDecode(dec, node, obj); +}; + +/** + * Function: decodeNode + * + * Calls <decodeAttributes> and <decodeChildren> for the given node. + */ +mxObjectCodec.prototype.decodeNode = function(dec, node, obj) +{ + if (node != null) + { + this.decodeAttributes(dec, node, obj); + this.decodeChildren(dec, node, obj); + } +}; + +/** + * Function: decodeAttributes + * + * Decodes all attributes of the given node using <decodeAttribute>. + */ +mxObjectCodec.prototype.decodeAttributes = function(dec, node, obj) +{ + var attrs = node.attributes; + + if (attrs != null) + { + for (var i = 0; i < attrs.length; i++) + { + this.decodeAttribute(dec, attrs[i], obj); + } + } +}; + +/** + * Function: decodeAttribute + * + * Reads the given attribute into the specified object. + */ +mxObjectCodec.prototype.decodeAttribute = function(dec, attr, obj) +{ + var name = attr.nodeName; + + if (name != 'as' && name != 'id') + { + // Converts the string true and false to their boolean values. + // This may require an additional check on the obj to see if + // the existing field is a boolean value or uninitialized, in + // which case we may want to convert true and false to a string. + var value = this.convertValueFromXml(attr.nodeValue); + var fieldname = this.getFieldName(name); + + if (this.isReference(obj, fieldname, value, false)) + { + var tmp = dec.getObject(value); + + if (tmp == null) + { + mxLog.warn('mxObjectCodec.decode: No object for ' + + this.getName() + '.' + name + '=' + value); + return; // exit + } + + value = tmp; + } + + if (!this.isExcluded(obj, name, value, false)) + { + //mxLog.debug(mxUtils.getFunctionName(obj.constructor)+'.'+name+'='+value); + obj[name] = value; + } + } +}; + +/** + * Function: decodeChildren + * + * Decodec all children of the given node using <decodeChild>. + */ +mxObjectCodec.prototype.decodeChildren = function(dec, node, obj) +{ + var child = node.firstChild; + + while (child != null) + { + var tmp = child.nextSibling; + + if (child.nodeType == mxConstants.NODETYPE_ELEMENT && + !this.processInclude(dec, child, obj)) + { + this.decodeChild(dec, child, obj); + } + + child = tmp; + } +}; + +/** + * Function: decodeChild + * + * Reads the specified child into the given object. + */ +mxObjectCodec.prototype.decodeChild = function(dec, child, obj) +{ + var fieldname = this.getFieldName(child.getAttribute('as')); + + if (fieldname == null || + !this.isExcluded(obj, fieldname, child, false)) + { + var template = this.getFieldTemplate(obj, fieldname, child); + var value = null; + + if (child.nodeName == 'add') + { + value = child.getAttribute('value'); + + if (value == null) + { + value = mxUtils.eval(mxUtils.getTextContent(child)); + //mxLog.debug('Decoded '+fieldname+' '+mxUtils.getTextContent(child)); + } + } + else + { + value = dec.decode(child, template); + // mxLog.debug('Decoded '+node.nodeName+'.'+fieldname+'='+ + // ((tmp != null) ? tmp.constructor.name : 'null')); + } + + this.addObjectValue(obj, fieldname, value, template); + } +}; + +/** + * Function: getFieldTemplate + * + * Returns the template instance for the given field. This returns the + * value of the field, null if the value is an array or an empty collection + * if the value is a collection. The value is then used to populate the + * field for a new instance. For strongly typed languages it may be + * required to override this to return the correct collection instance + * based on the encoded child. + */ +mxObjectCodec.prototype.getFieldTemplate = function(obj, fieldname, child) +{ + var template = obj[fieldname]; + + // Non-empty arrays are replaced completely + if (template instanceof Array && template.length > 0) + { + template = null; + } + + return template; +}; + +/** + * Function: addObjectValue + * + * Sets the decoded child node as a value of the given object. If the + * object is a map, then the value is added with the given fieldname as a + * key. If the fieldname is not empty, then setFieldValue is called or + * else, if the object is a collection, the value is added to the + * collection. For strongly typed languages it may be required to + * override this with the correct code to add an entry to an object. + */ +mxObjectCodec.prototype.addObjectValue = function(obj, fieldname, value, template) +{ + if (value != null && value != template) + { + if (fieldname != null && fieldname.length > 0) + { + obj[fieldname] = value; + } + else + { + obj.push(value); + } + //mxLog.debug('Decoded '+mxUtils.getFunctionName(obj.constructor)+'.'+fieldname+': '+value); + } +}; + +/** + * Function: processInclude + * + * Returns true if the given node is an include directive and + * executes the include by decoding the XML document. Returns + * false if the given node is not an include directive. + * + * Parameters: + * + * dec - <mxCodec> that controls the encoding/decoding process. + * node - XML node to be checked. + * into - Optional object to pass-thru to the codec. + */ +mxObjectCodec.prototype.processInclude = function(dec, node, into) +{ + if (node.nodeName == 'include') + { + var name = node.getAttribute('name'); + + if (name != null) + { + try + { + var xml = mxUtils.load(name).getDocumentElement(); + + if (xml != null) + { + dec.decode(xml, into); + } + } + catch (e) + { + // ignore + } + } + + return true; + } + + return false; +}; + +/** + * Function: beforeDecode + * + * Hook for subclassers to pre-process the node for + * the specified object and return the node to be + * used for further processing by <decode>. + * The object is created based on the template in the + * calling method and is never null. This implementation + * returns the input node. The return value of this + * function is used in <decode> to perform + * the default decoding into the given object. + * + * Parameters: + * + * dec - <mxCodec> that controls the decoding process. + * node - XML node to be decoded. + * obj - Object to encode the node into. + */ +mxObjectCodec.prototype.beforeDecode = function(dec, node, obj) +{ + return node; +}; + +/** + * Function: afterDecode + * + * Hook for subclassers to post-process the object after + * decoding. This implementation returns the given object + * without any changes. The return value of this method + * is returned to the decoder from <decode>. + * + * Parameters: + * + * enc - <mxCodec> that controls the encoding process. + * node - XML node to be decoded. + * obj - Object that represents the default decoding. + */ +mxObjectCodec.prototype.afterDecode = function(dec, node, obj) +{ + return obj; +}; diff --git a/src/js/io/mxRootChangeCodec.js b/src/js/io/mxRootChangeCodec.js new file mode 100644 index 0000000..fda613a --- /dev/null +++ b/src/js/io/mxRootChangeCodec.js @@ -0,0 +1,83 @@ +/** + * $Id: mxRootChangeCodec.js,v 1.6 2010-09-15 14:38:51 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +mxCodecRegistry.register(function() +{ + /** + * Class: mxRootChangeCodec + * + * Codec for <mxRootChange>s. This class is created and registered + * dynamically at load time and used implicitely via <mxCodec> and + * the <mxCodecRegistry>. + * + * Transient Fields: + * + * - model + * - previous + * - root + */ + var codec = new mxObjectCodec(new mxRootChange(), + ['model', 'previous', 'root']); + + /** + * Function: onEncode + * + * Encodes the child recursively. + */ + codec.afterEncode = function(enc, obj, node) + { + enc.encodeCell(obj.root, node); + + return node; + }; + + /** + * Function: beforeDecode + * + * Decodes the optional children as cells + * using the respective decoder. + */ + codec.beforeDecode = function(dec, node, obj) + { + if (node.firstChild != null && + node.firstChild.nodeType == mxConstants.NODETYPE_ELEMENT) + { + // Makes sure the original node isn't modified + node = node.cloneNode(true); + + var tmp = node.firstChild; + obj.root = dec.decodeCell(tmp, false); + + var tmp2 = tmp.nextSibling; + tmp.parentNode.removeChild(tmp); + tmp = tmp2; + + while (tmp != null) + { + tmp2 = tmp.nextSibling; + dec.decodeCell(tmp); + tmp.parentNode.removeChild(tmp); + tmp = tmp2; + } + } + + return node; + }; + + /** + * Function: afterDecode + * + * Restores the state by assigning the previous value. + */ + codec.afterDecode = function(dec, node, obj) + { + obj.previous = obj.root; + + return obj; + }; + + // Returns the codec into the registry + return codec; + +}()); diff --git a/src/js/io/mxStylesheetCodec.js b/src/js/io/mxStylesheetCodec.js new file mode 100644 index 0000000..7636eb1 --- /dev/null +++ b/src/js/io/mxStylesheetCodec.js @@ -0,0 +1,210 @@ +/** + * $Id: mxStylesheetCodec.js,v 1.19 2011-06-13 08:18:42 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +mxCodecRegistry.register(function() +{ + /** + * Class: mxStylesheetCodec + * + * Codec for <mxStylesheet>s. This class is created and registered + * dynamically at load time and used implicitely via <mxCodec> + * and the <mxCodecRegistry>. + */ + var codec = new mxObjectCodec(new mxStylesheet()); + + /** + * Function: encode + * + * Encodes a stylesheet. See <decode> for a description of the + * format. + */ + codec.encode = function(enc, obj) + { + var node = enc.document.createElement(this.getName()); + + for (var i in obj.styles) + { + var style = obj.styles[i]; + var styleNode = enc.document.createElement('add'); + + if (i != null) + { + styleNode.setAttribute('as', i); + + for (var j in style) + { + var value = this.getStringValue(j, style[j]); + + if (value != null) + { + var entry = enc.document.createElement('add'); + entry.setAttribute('value', value); + entry.setAttribute('as', j); + styleNode.appendChild(entry); + } + } + + if (styleNode.childNodes.length > 0) + { + node.appendChild(styleNode); + } + } + } + + return node; + }; + + /** + * Function: getStringValue + * + * Returns the string for encoding the given value. + */ + codec.getStringValue = function(key, value) + { + var type = typeof(value); + + if (type == 'function') + { + value = mxStyleRegistry.getName(style[j]); + } + else if (type == 'object') + { + value = null; + } + + return value; + }; + + /** + * Function: decode + * + * Reads a sequence of the following child nodes + * and attributes: + * + * Child Nodes: + * + * add - Adds a new style. + * + * Attributes: + * + * as - Name of the style. + * extend - Name of the style to inherit from. + * + * Each node contains another sequence of add and remove nodes with the following + * attributes: + * + * as - Name of the style (see <mxConstants>). + * value - Value for the style. + * + * Instead of the value-attribute, one can put Javascript expressions into + * the node as follows: + * <add as="perimeter">mxPerimeter.RectanglePerimeter</add> + * + * A remove node will remove the entry with the name given in the as-attribute + * from the style. + * + * Example: + * + * (code) + * <mxStylesheet as="stylesheet"> + * <add as="text"> + * <add as="fontSize" value="12"/> + * </add> + * <add as="defaultVertex" extend="text"> + * <add as="shape" value="rectangle"/> + * </add> + * </mxStylesheet> + * (end) + */ + codec.decode = function(dec, node, into) + { + var obj = into || new this.template.constructor(); + var id = node.getAttribute('id'); + + if (id != null) + { + dec.objects[id] = obj; + } + + node = node.firstChild; + + while (node != null) + { + if (!this.processInclude(dec, node, obj) && + node.nodeName == 'add') + { + var as = node.getAttribute('as'); + + if (as != null) + { + var extend = node.getAttribute('extend'); + var style = (extend != null) ? mxUtils.clone(obj.styles[extend]) : null; + + if (style == null) + { + if (extend != null) + { + mxLog.warn('mxStylesheetCodec.decode: stylesheet ' + + extend + ' not found to extend'); + } + + style = new Object(); + } + + var entry = node.firstChild; + + while (entry != null) + { + if (entry.nodeType == mxConstants.NODETYPE_ELEMENT) + { + var key = entry.getAttribute('as'); + + if (entry.nodeName == 'add') + { + var text = mxUtils.getTextContent(entry); + var value = null; + + if (text != null && + text.length > 0) + { + value = mxUtils.eval(text); + } + else + { + value = entry.getAttribute('value'); + + if (mxUtils.isNumeric(value)) + { + value = parseFloat(value); + } + } + + if (value != null) + { + style[key] = value; + } + } + else if (entry.nodeName == 'remove') + { + delete style[key]; + } + } + + entry = entry.nextSibling; + } + + obj.putCellStyle(as, style); + } + } + + node = node.nextSibling; + } + + return obj; + }; + + // Returns the codec into the registry + return codec; + +}()); diff --git a/src/js/io/mxTerminalChangeCodec.js b/src/js/io/mxTerminalChangeCodec.js new file mode 100644 index 0000000..a51d871 --- /dev/null +++ b/src/js/io/mxTerminalChangeCodec.js @@ -0,0 +1,42 @@ +/** + * $Id: mxTerminalChangeCodec.js,v 1.7 2010-09-13 15:58:36 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +mxCodecRegistry.register(function() +{ + /** + * Class: mxTerminalChangeCodec + * + * Codec for <mxTerminalChange>s. This class is created and registered + * dynamically at load time and used implicitely via <mxCodec> and + * the <mxCodecRegistry>. + * + * Transient Fields: + * + * - model + * - previous + * + * Reference Fields: + * + * - cell + * - terminal + */ + var codec = new mxObjectCodec(new mxTerminalChange(), + ['model', 'previous'], ['cell', 'terminal']); + + /** + * Function: afterDecode + * + * Restores the state by assigning the previous value. + */ + codec.afterDecode = function(dec, node, obj) + { + obj.previous = obj.terminal; + + return obj; + }; + + // Returns the codec into the registry + return codec; + +}()); diff --git a/src/js/layout/hierarchical/model/mxGraphAbstractHierarchyCell.js b/src/js/layout/hierarchical/model/mxGraphAbstractHierarchyCell.js new file mode 100644 index 0000000..e2fe6a6 --- /dev/null +++ b/src/js/layout/hierarchical/model/mxGraphAbstractHierarchyCell.js @@ -0,0 +1,206 @@ +/** + * $Id: mxGraphAbstractHierarchyCell.js,v 1.12 2010-01-04 11:18:26 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxGraphAbstractHierarchyCell + * + * An abstraction of an internal hierarchy node or edge + * + * Constructor: mxGraphAbstractHierarchyCell + * + * Constructs a new hierarchical layout algorithm. + * + * Arguments: + * + * graph - Reference to the enclosing <mxGraph>. + * deterministic - Optional boolean that specifies if this layout should be + * deterministic. Default is true. + */ +function mxGraphAbstractHierarchyCell() +{ + this.x = []; + this.y = []; + this.temp = []; +}; + +/** + * Variable: maxRank + * + * The maximum rank this cell occupies. Default is -1. + */ +mxGraphAbstractHierarchyCell.prototype.maxRank = -1; + +/** + * Variable: minRank + * + * The minimum rank this cell occupies. Default is -1. + */ +mxGraphAbstractHierarchyCell.prototype.minRank = -1; + +/** + * Variable: x + * + * The x position of this cell for each layer it occupies + */ +mxGraphAbstractHierarchyCell.prototype.x = null; + +/** + * Variable: y + * + * The y position of this cell for each layer it occupies + */ +mxGraphAbstractHierarchyCell.prototype.y = null; + +/** + * Variable: width + * + * The width of this cell + */ +mxGraphAbstractHierarchyCell.prototype.width = 0; + +/** + * Variable: height + * + * The height of this cell + */ +mxGraphAbstractHierarchyCell.prototype.height = 0; + +/** + * Variable: nextLayerConnectedCells + * + * A cached version of the cells this cell connects to on the next layer up + */ +mxGraphAbstractHierarchyCell.prototype.nextLayerConnectedCells = null; + +/** + * Variable: previousLayerConnectedCells + * + * A cached version of the cells this cell connects to on the next layer down + */ +mxGraphAbstractHierarchyCell.prototype.previousLayerConnectedCells = null; + +/** + * Variable: temp + * + * Temporary variable for general use. Generally, try to avoid + * carrying information between stages. Currently, the longest + * path layering sets temp to the rank position in fixRanks() + * and the crossing reduction uses this. This meant temp couldn't + * be used for hashing the nodes in the model dfs and so hashCode + * was created + */ +mxGraphAbstractHierarchyCell.prototype.temp = null; + +/** + * Function: getNextLayerConnectedCells + * + * Returns the cells this cell connects to on the next layer up + */ +mxGraphAbstractHierarchyCell.prototype.getNextLayerConnectedCells = function(layer) +{ + return null; +}; + +/** + * Function: getPreviousLayerConnectedCells + * + * Returns the cells this cell connects to on the next layer down + */ +mxGraphAbstractHierarchyCell.prototype.getPreviousLayerConnectedCells = function(layer) +{ + return null; +}; + +/** + * Function: isEdge + * + * Returns whether or not this cell is an edge + */ +mxGraphAbstractHierarchyCell.prototype.isEdge = function() +{ + return false; +}; + +/** + * Function: isVertex + * + * Returns whether or not this cell is a node + */ +mxGraphAbstractHierarchyCell.prototype.isVertex = function() +{ + return false; +}; + +/** + * Function: getGeneralPurposeVariable + * + * Gets the value of temp for the specified layer + */ +mxGraphAbstractHierarchyCell.prototype.getGeneralPurposeVariable = function(layer) +{ + return null; +}; + +/** + * Function: setGeneralPurposeVariable + * + * Set the value of temp for the specified layer + */ +mxGraphAbstractHierarchyCell.prototype.setGeneralPurposeVariable = function(layer, value) +{ + return null; +}; + +/** + * Function: setX + * + * Set the value of x for the specified layer + */ +mxGraphAbstractHierarchyCell.prototype.setX = function(layer, value) +{ + if (this.isVertex()) + { + this.x[0] = value; + } + else if (this.isEdge()) + { + this.x[layer - this.minRank - 1] = value; + } +}; + +/** + * Function: getX + * + * Gets the value of x on the specified layer + */ +mxGraphAbstractHierarchyCell.prototype.getX = function(layer) +{ + if (this.isVertex()) + { + return this.x[0]; + } + else if (this.isEdge()) + { + return this.x[layer - this.minRank - 1]; + } + + return 0.0; +}; + +/** + * Function: setY + * + * Set the value of y for the specified layer + */ +mxGraphAbstractHierarchyCell.prototype.setY = function(layer, value) +{ + if (this.isVertex()) + { + this.y[0] = value; + } + else if (this.isEdge()) + { + this.y[layer -this. minRank - 1] = value; + } +}; diff --git a/src/js/layout/hierarchical/model/mxGraphHierarchyEdge.js b/src/js/layout/hierarchical/model/mxGraphHierarchyEdge.js new file mode 100644 index 0000000..8ba16dd --- /dev/null +++ b/src/js/layout/hierarchical/model/mxGraphHierarchyEdge.js @@ -0,0 +1,174 @@ +/** + * $Id: mxGraphHierarchyEdge.js,v 1.15 2012-06-12 20:23:14 david Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxGraphHierarchyEdge + * + * An abstraction of a hierarchical edge for the hierarchy layout + * + * Constructor: mxGraphHierarchyEdge + * + * Constructs a hierarchy edge + * + * Arguments: + * + * edges - a list of real graph edges this abstraction represents + */ +function mxGraphHierarchyEdge(edges) +{ + mxGraphAbstractHierarchyCell.apply(this, arguments); + this.edges = edges; +}; + +/** + * Extends mxGraphAbstractHierarchyCell. + */ +mxGraphHierarchyEdge.prototype = new mxGraphAbstractHierarchyCell(); +mxGraphHierarchyEdge.prototype.constructor = mxGraphHierarchyEdge; + +/** + * Variable: edges + * + * The graph edge(s) this object represents. Parallel edges are all grouped + * together within one hierarchy edge. + */ +mxGraphHierarchyEdge.prototype.edges = null; + +/** + * Variable: source + * + * The node this edge is sourced at + */ +mxGraphHierarchyEdge.prototype.source = null; + +/** + * Variable: target + * + * The node this edge targets + */ +mxGraphHierarchyEdge.prototype.target = null; + +/** + * Variable: isReversed + * + * Whether or not the direction of this edge has been reversed + * internally to create a DAG for the hierarchical layout + */ +mxGraphHierarchyEdge.prototype.isReversed = false; + +/** + * Function: invert + * + * Inverts the direction of this internal edge(s) + */ +mxGraphHierarchyEdge.prototype.invert = function(layer) +{ + var temp = this.source; + this.source = this.target; + this.target = temp; + this.isReversed = !this.isReversed; +}; + +/** + * Function: getNextLayerConnectedCells + * + * Returns the cells this cell connects to on the next layer up + */ +mxGraphHierarchyEdge.prototype.getNextLayerConnectedCells = function(layer) +{ + if (this.nextLayerConnectedCells == null) + { + this.nextLayerConnectedCells = []; + + for (var i = 0; i < this.temp.length; i++) + { + this.nextLayerConnectedCells[i] = []; + + if (i == this.temp.length - 1) + { + this.nextLayerConnectedCells[i].push(this.source); + } + else + { + this.nextLayerConnectedCells[i].push(this); + } + } + } + + return this.nextLayerConnectedCells[layer - this.minRank - 1]; +}; + +/** + * Function: getPreviousLayerConnectedCells + * + * Returns the cells this cell connects to on the next layer down + */ +mxGraphHierarchyEdge.prototype.getPreviousLayerConnectedCells = function(layer) +{ + if (this.previousLayerConnectedCells == null) + { + this.previousLayerConnectedCells = []; + + for (var i = 0; i < this.temp.length; i++) + { + this.previousLayerConnectedCells[i] = []; + + if (i == 0) + { + this.previousLayerConnectedCells[i].push(this.target); + } + else + { + this.previousLayerConnectedCells[i].push(this); + } + } + } + + return this.previousLayerConnectedCells[layer - this.minRank - 1]; +}; + +/** + * Function: isEdge + * + * Returns true. + */ +mxGraphHierarchyEdge.prototype.isEdge = function() +{ + return true; +}; + +/** + * Function: getGeneralPurposeVariable + * + * Gets the value of temp for the specified layer + */ +mxGraphHierarchyEdge.prototype.getGeneralPurposeVariable = function(layer) +{ + return this.temp[layer - this.minRank - 1]; +}; + +/** + * Function: setGeneralPurposeVariable + * + * Set the value of temp for the specified layer + */ +mxGraphHierarchyEdge.prototype.setGeneralPurposeVariable = function(layer, value) +{ + this.temp[layer - this.minRank - 1] = value; +}; + +/** + * Function: getCoreCell + * + * Gets the first core edge associated with this wrapper + */ +mxGraphHierarchyEdge.prototype.getCoreCell = function() +{ + if (this.edges != null && this.edges.length > 0) + { + return this.edges[0]; + } + + return null; +};
\ No newline at end of file diff --git a/src/js/layout/hierarchical/model/mxGraphHierarchyModel.js b/src/js/layout/hierarchical/model/mxGraphHierarchyModel.js new file mode 100644 index 0000000..ca2ba30 --- /dev/null +++ b/src/js/layout/hierarchical/model/mxGraphHierarchyModel.js @@ -0,0 +1,685 @@ +/** + * $Id: mxGraphHierarchyModel.js,v 1.33 2012-12-18 13:16:43 david Exp $ + * Copyright (c) 2006-2012, JGraph Ltd + */ +/** + * Class: mxGraphHierarchyModel + * + * Internal model of a hierarchical graph. This model stores nodes and edges + * equivalent to the real graph nodes and edges, but also stores the rank of the + * cells, the order within the ranks and the new candidate locations of cells. + * The internal model also reverses edge direction were appropriate , ignores + * self-loop and groups parallels together under one edge object. + * + * Constructor: mxGraphHierarchyModel + * + * Creates an internal ordered graph model using the vertices passed in. If + * there are any, leftward edge need to be inverted in the internal model + * + * Arguments: + * + * graph - the facade describing the graph to be operated on + * vertices - the vertices for this hierarchy + * ordered - whether or not the vertices are already ordered + * deterministic - whether or not this layout should be deterministic on each + * tightenToSource - whether or not to tighten vertices towards the sources + * scanRanksFromSinks - Whether rank assignment is from the sinks or sources. + * usage + */ +function mxGraphHierarchyModel(layout, vertices, roots, parent, tightenToSource) +{ + var graph = layout.getGraph(); + this.tightenToSource = tightenToSource; + this.roots = roots; + this.parent = parent; + + // map of cells to internal cell needed for second run through + // to setup the sink of edges correctly + this.vertexMapper = new Object(); + this.edgeMapper = new Object(); + this.maxRank = 0; + var internalVertices = []; + + if (vertices == null) + { + vertices = this.graph.getChildVertices(parent); + } + + this.maxRank = this.SOURCESCANSTARTRANK; + // map of cells to internal cell needed for second run through + // to setup the sink of edges correctly. Guess size by number + // of edges is roughly same as number of vertices. + this.createInternalCells(layout, vertices, internalVertices); + + // Go through edges set their sink values. Also check the + // ordering if and invert edges if necessary + for (var i = 0; i < vertices.length; i++) + { + var edges = internalVertices[i].connectsAsSource; + + for (var j = 0; j < edges.length; j++) + { + var internalEdge = edges[j]; + var realEdges = internalEdge.edges; + + // Only need to process the first real edge, since + // all the edges connect to the same other vertex + if (realEdges != null && realEdges.length > 0) + { + var realEdge = realEdges[0]; + var targetCell = graph.getView().getVisibleTerminal( + realEdge, false); + var targetCellId = mxCellPath.create(targetCell); + var internalTargetCell = this.vertexMapper[targetCellId]; + + if (internalVertices[i] == internalTargetCell) + { + // The real edge is reversed relative to the internal edge + targetCell = graph.getView().getVisibleTerminal( + realEdge, true); + targetCellId = mxCellPath.create(targetCell); + internalTargetCell = this.vertexMapper[targetCellId]; + } + + if (internalTargetCell != null + && internalVertices[i] != internalTargetCell) + { + internalEdge.target = internalTargetCell; + + if (internalTargetCell.connectsAsTarget.length == 0) + { + internalTargetCell.connectsAsTarget = []; + } + + if (mxUtils.indexOf(internalTargetCell.connectsAsTarget, internalEdge) < 0) + { + internalTargetCell.connectsAsTarget.push(internalEdge); + } + } + } + } + + // Use the temp variable in the internal nodes to mark this + // internal vertex as having been visited. + internalVertices[i].temp[0] = 1; + } +}; + +/** + * Variable: maxRank + * + * Stores the largest rank number allocated + */ +mxGraphHierarchyModel.prototype.maxRank = null; + +/** + * Variable: vertexMapper + * + * Map from graph vertices to internal model nodes. + */ +mxGraphHierarchyModel.prototype.vertexMapper = null; + +/** + * Variable: edgeMapper + * + * Map from graph edges to internal model edges + */ +mxGraphHierarchyModel.prototype.edgeMapper = null; + +/** + * Variable: ranks + * + * Mapping from rank number to actual rank + */ +mxGraphHierarchyModel.prototype.ranks = null; + +/** + * Variable: roots + * + * Store of roots of this hierarchy model, these are real graph cells, not + * internal cells + */ +mxGraphHierarchyModel.prototype.roots = null; + +/** + * Variable: parent + * + * The parent cell whose children are being laid out + */ +mxGraphHierarchyModel.prototype.parent = null; + +/** + * Variable: dfsCount + * + * Count of the number of times the ancestor dfs has been used. + */ +mxGraphHierarchyModel.prototype.dfsCount = 0; + +/** + * Variable: SOURCESCANSTARTRANK + * + * High value to start source layering scan rank value from. + */ +mxGraphHierarchyModel.prototype.SOURCESCANSTARTRANK = 100000000; + +/** + * Variable: tightenToSource + * + * Whether or not to tighten the assigned ranks of vertices up towards + * the source cells. + */ +mxGraphHierarchyModel.prototype.tightenToSource = false; + +/** + * Function: createInternalCells + * + * Creates all edges in the internal model + * + * Parameters: + * + * layout - Reference to the <mxHierarchicalLayout> algorithm. + * vertices - Array of <mxCells> that represent the vertices whom are to + * have an internal representation created. + * internalVertices - The array of <mxGraphHierarchyNodes> to have their + * information filled in using the real vertices. + */ +mxGraphHierarchyModel.prototype.createInternalCells = function(layout, vertices, internalVertices) +{ + var graph = layout.getGraph(); + + // Create internal edges + for (var i = 0; i < vertices.length; i++) + { + internalVertices[i] = new mxGraphHierarchyNode(vertices[i]); + var vertexId = mxCellPath.create(vertices[i]); + this.vertexMapper[vertexId] = internalVertices[i]; + + // If the layout is deterministic, order the cells + //List outgoingCells = graph.getNeighbours(vertices[i], deterministic); + var conns = layout.getEdges(vertices[i]); + var outgoingCells = graph.getOpposites(conns, vertices[i]); + internalVertices[i].connectsAsSource = []; + + // Create internal edges, but don't do any rank assignment yet + // First use the information from the greedy cycle remover to + // invert the leftward edges internally + for (var j = 0; j < outgoingCells.length; j++) + { + var cell = outgoingCells[j]; + + if (cell != vertices[i] && layout.graph.model.isVertex(cell) && + !layout.isVertexIgnored(cell)) + { + // We process all edge between this source and its targets + // If there are edges going both ways, we need to collect + // them all into one internal edges to avoid looping problems + // later. We assume this direction (source -> target) is the + // natural direction if at least half the edges are going in + // that direction. + + // The check below for edges[0] being in the vertex mapper is + // in case we've processed this the other way around + // (target -> source) and the number of edges in each direction + // are the same. All the graph edges will have been assigned to + // an internal edge going the other way, so we don't want to + // process them again + var undirectedEdges = graph.getEdgesBetween(vertices[i], + cell, false); + var directedEdges = graph.getEdgesBetween(vertices[i], + cell, true); + var edgeId = mxCellPath.create(undirectedEdges[0]); + + if (undirectedEdges != null && + undirectedEdges.length > 0 && + this.edgeMapper[edgeId] == null && + directedEdges.length * 2 >= undirectedEdges.length) + { + var internalEdge = new mxGraphHierarchyEdge(undirectedEdges); + + for (var k = 0; k < undirectedEdges.length; k++) + { + var edge = undirectedEdges[k]; + edgeId = mxCellPath.create(edge); + this.edgeMapper[edgeId] = internalEdge; + + // Resets all point on the edge and disables the edge style + // without deleting it from the cell style + graph.resetEdge(edge); + + if (layout.disableEdgeStyle) + { + layout.setEdgeStyleEnabled(edge, false); + layout.setOrthogonalEdge(edge,true); + } + } + + internalEdge.source = internalVertices[i]; + + if (mxUtils.indexOf(internalVertices[i].connectsAsSource, internalEdge) < 0) + { + internalVertices[i].connectsAsSource.push(internalEdge); + } + } + } + } + + // Ensure temp variable is cleared from any previous use + internalVertices[i].temp[0] = 0; + } +}; + +/** + * Function: initialRank + * + * Basic determination of minimum layer ranking by working from from sources + * or sinks and working through each node in the relevant edge direction. + * Starting at the sinks is basically a longest path layering algorithm. +*/ +mxGraphHierarchyModel.prototype.initialRank = function() +{ + var startNodes = []; + + if (this.roots != null) + { + for (var i = 0; i < this.roots.length; i++) + { + var vertexId = mxCellPath.create(this.roots[i]); + var internalNode = this.vertexMapper[vertexId]; + + if (internalNode != null) + { + startNodes.push(internalNode); + } + } + } + + for (var key in this.vertexMapper) + { + var internalNode = this.vertexMapper[key]; + + // Mark the node as not having had a layer assigned + internalNode.temp[0] = -1; + } + + var startNodesCopy = startNodes.slice(); + + while (startNodes.length > 0) + { + var internalNode = startNodes[0]; + var layerDeterminingEdges; + var edgesToBeMarked; + + layerDeterminingEdges = internalNode.connectsAsTarget; + edgesToBeMarked = internalNode.connectsAsSource; + + // flag to keep track of whether or not all layer determining + // edges have been scanned + var allEdgesScanned = true; + + // Work out the layer of this node from the layer determining + // edges. The minimum layer number of any node connected by one of + // the layer determining edges variable + var minimumLayer = this.SOURCESCANSTARTRANK; + + for (var i = 0; i < layerDeterminingEdges.length; i++) + { + var internalEdge = layerDeterminingEdges[i]; + + if (internalEdge.temp[0] == 5270620) + { + // This edge has been scanned, get the layer of the + // node on the other end + var otherNode = internalEdge.source; + minimumLayer = Math.min(minimumLayer, otherNode.temp[0] - 1); + } + else + { + allEdgesScanned = false; + + break; + } + } + + // If all edge have been scanned, assign the layer, mark all + // edges in the other direction and remove from the nodes list + if (allEdgesScanned) + { + internalNode.temp[0] = minimumLayer; + this.maxRank = Math.min(this.maxRank, minimumLayer); + + if (edgesToBeMarked != null) + { + for (var i = 0; i < edgesToBeMarked.length; i++) + { + var internalEdge = edgesToBeMarked[i]; + + // Assign unique stamp ( y/m/d/h ) + internalEdge.temp[0] = 5270620; + + // Add node on other end of edge to LinkedList of + // nodes to be analysed + var otherNode = internalEdge.target; + + // Only add node if it hasn't been assigned a layer + if (otherNode.temp[0] == -1) + { + startNodes.push(otherNode); + + // Mark this other node as neither being + // unassigned nor assigned so it isn't + // added to this list again, but it's + // layer isn't used in any calculation. + otherNode.temp[0] = -2; + } + } + } + + startNodes.shift(); + } + else + { + // Not all the edges have been scanned, get to the back of + // the class and put the dunces cap on + var removedCell = startNodes.shift(); + startNodes.push(internalNode); + + if (removedCell == internalNode && startNodes.length == 1) + { + // This is an error condition, we can't get out of + // this loop. It could happen for more than one node + // but that's a lot harder to detect. Log the error + // TODO make log comment + break; + } + } + } + + // Normalize the ranks down from their large starting value to place + // at least 1 sink on layer 0 + for (var key in this.vertexMapper) + { + var internalNode = this.vertexMapper[key]; + // Mark the node as not having had a layer assigned + internalNode.temp[0] -= this.maxRank; + } + + // Tighten the rank 0 nodes as far as possible + for ( var i = 0; i < startNodesCopy.length; i++) + { + var internalNode = startNodesCopy[i]; + var currentMaxLayer = 0; + var layerDeterminingEdges = internalNode.connectsAsSource; + + for ( var j = 0; j < layerDeterminingEdges.length; j++) + { + var internalEdge = layerDeterminingEdges[j]; + var otherNode = internalEdge.target; + internalNode.temp[0] = Math.max(currentMaxLayer, + otherNode.temp[0] + 1); + currentMaxLayer = internalNode.temp[0]; + } + } + + // Reset the maxRank to that which would be expected for a from-sink + // scan + this.maxRank = this.SOURCESCANSTARTRANK - this.maxRank; +}; + +/** + * Function: fixRanks + * + * Fixes the layer assignments to the values stored in the nodes. Also needs + * to create dummy nodes for edges that cross layers. + */ +mxGraphHierarchyModel.prototype.fixRanks = function() +{ + var rankList = []; + this.ranks = []; + + for (var i = 0; i < this.maxRank + 1; i++) + { + rankList[i] = []; + this.ranks[i] = rankList[i]; + } + + // Perform a DFS to obtain an initial ordering for each rank. + // Without doing this you would end up having to process + // crossings for a standard tree. + var rootsArray = null; + + if (this.roots != null) + { + var oldRootsArray = this.roots; + rootsArray = []; + + for (var i = 0; i < oldRootsArray.length; i++) + { + var cell = oldRootsArray[i]; + var cellId = mxCellPath.create(cell); + var internalNode = this.vertexMapper[cellId]; + rootsArray[i] = internalNode; + } + } + + this.visit(function(parent, node, edge, layer, seen) + { + if (seen == 0 && node.maxRank < 0 && node.minRank < 0) + { + rankList[node.temp[0]].push(node); + node.maxRank = node.temp[0]; + node.minRank = node.temp[0]; + + // Set temp[0] to the nodes position in the rank + node.temp[0] = rankList[node.maxRank].length - 1; + } + + if (parent != null && edge != null) + { + var parentToCellRankDifference = parent.maxRank - node.maxRank; + + if (parentToCellRankDifference > 1) + { + // There are ranks in between the parent and current cell + edge.maxRank = parent.maxRank; + edge.minRank = node.maxRank; + edge.temp = []; + edge.x = []; + edge.y = []; + + for (var i = edge.minRank + 1; i < edge.maxRank; i++) + { + // The connecting edge must be added to the + // appropriate ranks + rankList[i].push(edge); + edge.setGeneralPurposeVariable(i, rankList[i] + .length - 1); + } + } + } + }, rootsArray, false, null); +}; + +/** + * Function: visit + * + * A depth first search through the internal heirarchy model. + * + * Parameters: + * + * visitor - The visitor function pattern to be called for each node. + * trackAncestors - Whether or not the search is to keep track all nodes + * directly above this one in the search path. + */ +mxGraphHierarchyModel.prototype.visit = function(visitor, dfsRoots, trackAncestors, seenNodes) +{ + // Run dfs through on all roots + if (dfsRoots != null) + { + for (var i = 0; i < dfsRoots.length; i++) + { + var internalNode = dfsRoots[i]; + + if (internalNode != null) + { + if (seenNodes == null) + { + seenNodes = new Object(); + } + + if (trackAncestors) + { + // Set up hash code for root + internalNode.hashCode = []; + internalNode.hashCode[0] = this.dfsCount; + internalNode.hashCode[1] = i; + this.extendedDfs(null, internalNode, null, visitor, seenNodes, + internalNode.hashCode, i, 0); + } + else + { + this.dfs(null, internalNode, null, visitor, seenNodes, 0); + } + } + } + + this.dfsCount++; + } +}; + +/** + * Function: dfs + * + * Performs a depth first search on the internal hierarchy model + * + * Parameters: + * + * parent - the parent internal node of the current internal node + * root - the current internal node + * connectingEdge - the internal edge connecting the internal node and the parent + * internal node, if any + * visitor - the visitor pattern to be called for each node + * seen - a set of all nodes seen by this dfs a set of all of the + * ancestor node of the current node + * layer - the layer on the dfs tree ( not the same as the model ranks ) + */ +mxGraphHierarchyModel.prototype.dfs = function(parent, root, connectingEdge, visitor, seen, layer) +{ + if (root != null) + { + var rootId = mxCellPath.create(root.cell); + + if (seen[rootId] == null) + { + seen[rootId] = root; + visitor(parent, root, connectingEdge, layer, 0); + + // Copy the connects as source list so that visitors + // can change the original for edge direction inversions + var outgoingEdges = root.connectsAsSource.slice(); + + for (var i = 0; i< outgoingEdges.length; i++) + { + var internalEdge = outgoingEdges[i]; + var targetNode = internalEdge.target; + + // Root check is O(|roots|) + this.dfs(root, targetNode, internalEdge, visitor, seen, + layer + 1); + } + } + else + { + // Use the int field to indicate this node has been seen + visitor(parent, root, connectingEdge, layer, 1); + } + } +}; + +/** + * Function: extendedDfs + * + * Performs a depth first search on the internal hierarchy model. This dfs + * extends the default version by keeping track of cells ancestors, but it + * should be only used when necessary because of it can be computationally + * intensive for deep searches. + * + * Parameters: + * + * parent - the parent internal node of the current internal node + * root - the current internal node + * connectingEdge - the internal edge connecting the internal node and the parent + * internal node, if any + * visitor - the visitor pattern to be called for each node + * seen - a set of all nodes seen by this dfs + * ancestors - the parent hash code + * childHash - the new hash code for this node + * layer - the layer on the dfs tree ( not the same as the model ranks ) + */ +mxGraphHierarchyModel.prototype.extendedDfs = function(parent, root, connectingEdge, visitor, seen, ancestors, childHash, layer) +{ + // Explanation of custom hash set. Previously, the ancestors variable + // was passed through the dfs as a HashSet. The ancestors were copied + // into a new HashSet and when the new child was processed it was also + // added to the set. If the current node was in its ancestor list it + // meant there is a cycle in the graph and this information is passed + // to the visitor.visit() in the seen parameter. The HashSet clone was + // very expensive on CPU so a custom hash was developed using primitive + // types. temp[] couldn't be used so hashCode[] was added to each node. + // Each new child adds another int to the array, copying the prefix + // from its parent. Child of the same parent add different ints (the + // limit is therefore 2^32 children per parent...). If a node has a + // child with the hashCode already set then the child code is compared + // to the same portion of the current nodes array. If they match there + // is a loop. + // Note that the basic mechanism would only allow for 1 use of this + // functionality, so the root nodes have two ints. The second int is + // incremented through each node root and the first is incremented + // through each run of the dfs algorithm (therefore the dfs is not + // thread safe). The hash code of each node is set if not already set, + // or if the first int does not match that of the current run. + if (root != null) + { + if (parent != null) + { + // Form this nodes hash code if necessary, that is, if the + // hashCode variable has not been initialized or if the + // start of the parent hash code does not equal the start of + // this nodes hash code, indicating the code was set on a + // previous run of this dfs. + if (root.hashCode == null || + root.hashCode[0] != parent.hashCode[0]) + { + var hashCodeLength = parent.hashCode.length + 1; + root.hashCode = parent.hashCode.slice(); + root.hashCode[hashCodeLength - 1] = childHash; + } + } + + var rootId = mxCellPath.create(root.cell); + + if (seen[rootId] == null) + { + seen[rootId] = root; + visitor(parent, root, connectingEdge, layer, 0); + + // Copy the connects as source list so that visitors + // can change the original for edge direction inversions + var outgoingEdges = root.connectsAsSource.slice(); + + for (var i = 0; i < outgoingEdges.length; i++) + { + var internalEdge = outgoingEdges[i]; + var targetNode = internalEdge.target; + + // Root check is O(|roots|) + this.extendedDfs(root, targetNode, internalEdge, visitor, seen, + root.hashCode, i, layer + 1); + } + } + else + { + // Use the int field to indicate this node has been seen + visitor(parent, root, connectingEdge, layer, 1); + } + } +}; diff --git a/src/js/layout/hierarchical/model/mxGraphHierarchyNode.js b/src/js/layout/hierarchical/model/mxGraphHierarchyNode.js new file mode 100644 index 0000000..d901d57 --- /dev/null +++ b/src/js/layout/hierarchical/model/mxGraphHierarchyNode.js @@ -0,0 +1,210 @@ +/** + * $Id: mxGraphHierarchyNode.js,v 1.13 2012-06-12 20:24:58 david Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxGraphHierarchyNode + * + * An abstraction of a hierarchical edge for the hierarchy layout + * + * Constructor: mxGraphHierarchyNode + * + * Constructs an internal node to represent the specified real graph cell + * + * Arguments: + * + * cell - the real graph cell this node represents + */ +function mxGraphHierarchyNode(cell) +{ + mxGraphAbstractHierarchyCell.apply(this, arguments); + this.cell = cell; +}; + +/** + * Extends mxGraphAbstractHierarchyCell. + */ +mxGraphHierarchyNode.prototype = new mxGraphAbstractHierarchyCell(); +mxGraphHierarchyNode.prototype.constructor = mxGraphHierarchyNode; + +/** + * Variable: cell + * + * The graph cell this object represents. + */ +mxGraphHierarchyNode.prototype.cell = null; + +/** + * Variable: connectsAsTarget + * + * Collection of hierarchy edges that have this node as a target + */ +mxGraphHierarchyNode.prototype.connectsAsTarget = []; + +/** + * Variable: connectsAsSource + * + * Collection of hierarchy edges that have this node as a source + */ +mxGraphHierarchyNode.prototype.connectsAsSource = []; + +/** + * Variable: hashCode + * + * Assigns a unique hashcode for each node. Used by the model dfs instead + * of copying HashSets + */ +mxGraphHierarchyNode.prototype.hashCode = false; + +/** + * Function: getRankValue + * + * Returns the integer value of the layer that this node resides in + */ +mxGraphHierarchyNode.prototype.getRankValue = function(layer) +{ + return this.maxRank; +}; + +/** + * Function: getNextLayerConnectedCells + * + * Returns the cells this cell connects to on the next layer up + */ +mxGraphHierarchyNode.prototype.getNextLayerConnectedCells = function(layer) +{ + if (this.nextLayerConnectedCells == null) + { + this.nextLayerConnectedCells = []; + this.nextLayerConnectedCells[0] = []; + + for (var i = 0; i < this.connectsAsTarget.length; i++) + { + var edge = this.connectsAsTarget[i]; + + if (edge.maxRank == -1 || edge.maxRank == layer + 1) + { + // Either edge is not in any rank or + // no dummy nodes in edge, add node of other side of edge + this.nextLayerConnectedCells[0].push(edge.source); + } + else + { + // Edge spans at least two layers, add edge + this.nextLayerConnectedCells[0].push(edge); + } + } + } + + return this.nextLayerConnectedCells[0]; +}; + +/** + * Function: getPreviousLayerConnectedCells + * + * Returns the cells this cell connects to on the next layer down + */ +mxGraphHierarchyNode.prototype.getPreviousLayerConnectedCells = function(layer) +{ + if (this.previousLayerConnectedCells == null) + { + this.previousLayerConnectedCells = []; + this.previousLayerConnectedCells[0] = []; + + for (var i = 0; i < this.connectsAsSource.length; i++) + { + var edge = this.connectsAsSource[i]; + + if (edge.minRank == -1 || edge.minRank == layer - 1) + { + // No dummy nodes in edge, add node of other side of edge + this.previousLayerConnectedCells[0].push(edge.target); + } + else + { + // Edge spans at least two layers, add edge + this.previousLayerConnectedCells[0].push(edge); + } + } + } + + return this.previousLayerConnectedCells[0]; +}; + +/** + * Function: isVertex + * + * Returns true. + */ +mxGraphHierarchyNode.prototype.isVertex = function() +{ + return true; +}; + +/** + * Function: getGeneralPurposeVariable + * + * Gets the value of temp for the specified layer + */ +mxGraphHierarchyNode.prototype.getGeneralPurposeVariable = function(layer) +{ + return this.temp[0]; +}; + +/** + * Function: setGeneralPurposeVariable + * + * Set the value of temp for the specified layer + */ +mxGraphHierarchyNode.prototype.setGeneralPurposeVariable = function(layer, value) +{ + this.temp[0] = value; +}; + +/** + * Function: isAncestor + */ +mxGraphHierarchyNode.prototype.isAncestor = function(otherNode) +{ + // Firstly, the hash code of this node needs to be shorter than the + // other node + if (otherNode != null && this.hashCode != null && otherNode.hashCode != null + && this.hashCode.length < otherNode.hashCode.length) + { + if (this.hashCode == otherNode.hashCode) + { + return true; + } + + if (this.hashCode == null || this.hashCode == null) + { + return false; + } + + // Secondly, this hash code must match the start of the other + // node's hash code. Arrays.equals cannot be used here since + // the arrays are different length, and we do not want to + // perform another array copy. + for (var i = 0; i < this.hashCode.length; i++) + { + if (this.hashCode[i] != otherNode.hashCode[i]) + { + return false; + } + } + + return true; + } + + return false; +}; + +/** + * Function: getCoreCell + * + * Gets the core vertex associated with this wrapper + */ +mxGraphHierarchyNode.prototype.getCoreCell = function() +{ + return this.cell; +};
\ No newline at end of file diff --git a/src/js/layout/hierarchical/mxHierarchicalLayout.js b/src/js/layout/hierarchical/mxHierarchicalLayout.js new file mode 100644 index 0000000..6ce0e05 --- /dev/null +++ b/src/js/layout/hierarchical/mxHierarchicalLayout.js @@ -0,0 +1,623 @@ +/** + * $Id: mxHierarchicalLayout.js,v 1.30 2012-12-18 12:41:06 david Exp $ + * Copyright (c) 2005-2012, JGraph Ltd + */ +/** + * Class: mxHierarchicalLayout + * + * A hierarchical layout algorithm. + * + * Constructor: mxHierarchicalLayout + * + * Constructs a new hierarchical layout algorithm. + * + * Arguments: + * + * graph - Reference to the enclosing <mxGraph>. + * orientation - Optional constant that defines the orientation of this + * layout. + * deterministic - Optional boolean that specifies if this layout should be + * deterministic. Default is true. + */ +function mxHierarchicalLayout(graph, orientation, deterministic) +{ + mxGraphLayout.call(this, graph); + this.orientation = (orientation != null) ? orientation : mxConstants.DIRECTION_NORTH; + this.deterministic = (deterministic != null) ? deterministic : true; +}; + +/** + * Extends mxGraphLayout. + */ +mxHierarchicalLayout.prototype = new mxGraphLayout(); +mxHierarchicalLayout.prototype.constructor = mxHierarchicalLayout; + +/** + * Variable: roots + * + * Holds the array of <mxGraphLayouts> that this layout contains. + */ +mxHierarchicalLayout.prototype.roots = null; + +/** + * Variable: resizeParent + * + * Specifies if the parent should be resized after the layout so that it + * contains all the child cells. Default is false. See also <parentBorder>. + */ +mxHierarchicalLayout.prototype.resizeParent = false; + +/** + * Variable: moveParent + * + * Specifies if the parent should be moved if <resizeParent> is enabled. + * Default is false. + */ +mxHierarchicalLayout.prototype.moveParent = false; + +/** + * Variable: parentBorder + * + * The border to be added around the children if the parent is to be + * resized using <resizeParent>. Default is 0. + */ +mxHierarchicalLayout.prototype.parentBorder = 0; + +/** + * Variable: intraCellSpacing + * + * The spacing buffer added between cells on the same layer. Default is 30. + */ +mxHierarchicalLayout.prototype.intraCellSpacing = 30; + +/** + * Variable: interRankCellSpacing + * + * The spacing buffer added between cell on adjacent layers. Default is 50. + */ +mxHierarchicalLayout.prototype.interRankCellSpacing = 50; + +/** + * Variable: interHierarchySpacing + * + * The spacing buffer between unconnected hierarchies. Default is 60. + */ +mxHierarchicalLayout.prototype.interHierarchySpacing = 60; + +/** + * Variable: parallelEdgeSpacing + * + * The distance between each parallel edge on each ranks for long edges + */ +mxHierarchicalLayout.prototype.parallelEdgeSpacing = 10; + +/** + * Variable: orientation + * + * The position of the root node(s) relative to the laid out graph in. + * Default is <mxConstants.DIRECTION_NORTH>. + */ +mxHierarchicalLayout.prototype.orientation = mxConstants.DIRECTION_NORTH; + +/** + * Variable: fineTuning + * + * Whether or not to perform local optimisations and iterate multiple times + * through the algorithm. Default is true. + */ +mxHierarchicalLayout.prototype.fineTuning = true; + +/** + * + * Variable: tightenToSource + * + * Whether or not to tighten the assigned ranks of vertices up towards + * the source cells. + */ +mxHierarchicalLayout.prototype.tightenToSource = true; + +/** + * Variable: disableEdgeStyle + * + * Specifies if the STYLE_NOEDGESTYLE flag should be set on edges that are + * modified by the result. Default is true. + */ +mxHierarchicalLayout.prototype.disableEdgeStyle = true; + +/** + * Variable: promoteEdges + * + * Whether or not to promote edges that terminate on vertices with + * different but common ancestry to appear connected to the highest + * siblings in the ancestry chains + */ +mxHierarchicalLayout.prototype.promoteEdges = true; + +/** + * Variable: traverseAncestors + * + * Whether or not to navigate edges whose terminal vertices + * have different parents but are in the same ancestry chain + */ +mxHierarchicalLayout.prototype.traverseAncestors = true; + +/** + * Variable: model + * + * The internal <mxGraphHierarchyModel> formed of the layout. + */ +mxHierarchicalLayout.prototype.model = null; + +/** + * Function: getModel + * + * Returns the internal <mxGraphHierarchyModel> for this layout algorithm. + */ +mxHierarchicalLayout.prototype.getModel = function() +{ + return this.model; +}; + +/** + * Function: execute + * + * Executes the layout for the children of the specified parent. + * + * Parameters: + * + * parent - Parent <mxCell> that contains the children to be laid out. + * roots - Optional starting roots of the layout. + */ +mxHierarchicalLayout.prototype.execute = function(parent, roots) +{ + this.parent = parent; + var model = this.graph.model; + + // If the roots are set and the parent is set, only + // use the roots that are some dependent of the that + // parent. + // If just the root are set, use them as-is + // If just the parent is set use it's immediate + // children as the initial set + + if (roots == null && parent == null) + { + // TODO indicate the problem + return; + } + + if (roots != null && parent != null) + { + var rootsCopy = []; + + for (var i = 0; i < roots.length; i++) + { + + if (model.isAncestor(parent, roots[i])) + { + rootsCopy.push(roots[i]); + } + } + + this.roots = rootsCopy; + } + else + { + this.roots = roots; + } + + model.beginUpdate(); + try + { + this.run(parent); + + if (this.resizeParent && + !this.graph.isCellCollapsed(parent)) + { + this.graph.updateGroupBounds([parent], + this.parentBorder, this.moveParent); + } + } + finally + { + model.endUpdate(); + } +}; + +/** + * Function: findRoots + * + * Returns all visible children in the given parent which do not have + * incoming edges. If the result is empty then the children with the + * maximum difference between incoming and outgoing edges are returned. + * This takes into account edges that are being promoted to the given + * root due to invisible children or collapsed cells. + * + * Parameters: + * + * parent - <mxCell> whose children should be checked. + * vertices - array of vertices to limit search to + */ +mxHierarchicalLayout.prototype.findRoots = function(parent, vertices) +{ + var roots = []; + + if (parent != null && vertices != null) + { + var model = this.graph.model; + var best = null; + var maxDiff = -100000; + + for (var i in vertices) + { + var cell = vertices[i]; + + if (model.isVertex(cell) && this.graph.isCellVisible(cell)) + { + var conns = this.getEdges(cell); + var fanOut = 0; + var fanIn = 0; + + for (var k = 0; k < conns.length; k++) + { + var src = this.graph.view.getVisibleTerminal(conns[k], true); + + if (src == cell) + { + fanOut++; + } + else + { + fanIn++; + } + } + + if (fanIn == 0 && fanOut > 0) + { + roots.push(cell); + } + + var diff = fanOut - fanIn; + + if (diff > maxDiff) + { + maxDiff = diff; + best = cell; + } + } + } + + if (roots.length == 0 && best != null) + { + roots.push(best); + } + } + + return roots; +}; + +/** + * Function: getEdges + * + * Returns the connected edges for the given cell. + * + * Parameters: + * + * cell - <mxCell> whose edges should be returned. + */ +mxHierarchicalLayout.prototype.getEdges = function(cell) +{ + var model = this.graph.model; + var edges = []; + var isCollapsed = this.graph.isCellCollapsed(cell); + var childCount = model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + var child = model.getChildAt(cell, i); + + if (isCollapsed || !this.graph.isCellVisible(child)) + { + edges = edges.concat(model.getEdges(child, true, true)); + } + } + + edges = edges.concat(model.getEdges(cell, true, true)); + var result = []; + + for (var i = 0; i < edges.length; i++) + { + var state = this.graph.view.getState(edges[i]); + + var source = (state != null) ? state.getVisibleTerminal(true) : this.graph.view.getVisibleTerminal(edges[i], true); + var target = (state != null) ? state.getVisibleTerminal(false) : this.graph.view.getVisibleTerminal(edges[i], false); + + if ((source == target) || ((source != target) && ((target == cell && (this.parent == null || this.graph.isValidAncestor(source, this.parent, this.traverseAncestors))) || + (source == cell && (this.parent == null || + this.graph.isValidAncestor(target, this.parent, this.traverseAncestors)))))) + { + result.push(edges[i]); + } + } + + return result; +}; + +/** + * Function: run + * + * The API method used to exercise the layout upon the graph description + * and produce a separate description of the vertex position and edge + * routing changes made. It runs each stage of the layout that has been + * created. + */ +mxHierarchicalLayout.prototype.run = function(parent) +{ + // Separate out unconnected hierarchies + var hierarchyVertices = []; + var allVertexSet = []; + + if (this.roots == null && parent != null) + { + var filledVertexSet = this.filterDescendants(parent); + + this.roots = []; + var filledVertexSetEmpty = true; + + // Poor man's isSetEmpty + for (var key in filledVertexSet) + { + if (filledVertexSet[key] != null) + { + filledVertexSetEmpty = false; + break; + } + } + + while (!filledVertexSetEmpty) + { + var candidateRoots = this.findRoots(parent, filledVertexSet); + + for (var i = 0; i < candidateRoots.length; i++) + { + var vertexSet = []; + hierarchyVertices.push(vertexSet); + + this.traverse(candidateRoots[i], true, null, allVertexSet, vertexSet, + hierarchyVertices, filledVertexSet); + } + + for (var i = 0; i < candidateRoots.length; i++) + { + this.roots.push(candidateRoots[i]); + } + + filledVertexSetEmpty = true; + + // Poor man's isSetEmpty + for (var key in filledVertexSet) + { + if (filledVertexSet[key] != null) + { + filledVertexSetEmpty = false; + break; + } + } + } + } + else + { + // Find vertex set as directed traversal from roots + + for (var i = 0; i < roots.length; i++) + { + var vertexSet = []; + hierarchyVertices.push(vertexSet); + + traverse(roots.get(i), true, null, allVertexSet, vertexSet, + hierarchyVertices, null); + } + } + + // Iterate through the result removing parents who have children in this layout + + // Perform a layout for each seperate hierarchy + // Track initial coordinate x-positioning + var initialX = 0; + + for (var i = 0; i < hierarchyVertices.length; i++) + { + var vertexSet = hierarchyVertices[i]; + var tmp = []; + + for (var key in vertexSet) + { + tmp.push(vertexSet[key]); + } + + this.model = new mxGraphHierarchyModel(this, tmp, this.roots, + parent, this.tightenToSource); + + this.cycleStage(parent); + this.layeringStage(); + + this.crossingStage(parent); + initialX = this.placementStage(initialX, parent); + } +}; + +/** + * Function: filterDescendants + * + * Creates an array of descendant cells + */ +mxHierarchicalLayout.prototype.filterDescendants = function(cell) +{ + var model = this.graph.model; + var result = []; + + if (model.isVertex(cell) && cell != this.parent && this.graph.isCellVisible(cell)) + { + result.push(cell); + } + + if (this.traverseAncestors || cell == this.parent + && this.graph.isCellVisible(cell)) + { + var childCount = model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + var child = model.getChildAt(cell, i); + var children = this.filterDescendants(child); + + for (var j = 0; j < children.length; j++) + { + result[mxCellPath.create(children[j])] = children[j]; + } + } + } + + return result; +}; + +/** + * Traverses the (directed) graph invoking the given function for each + * visited vertex and edge. The function is invoked with the current vertex + * and the incoming edge as a parameter. This implementation makes sure + * each vertex is only visited once. The function may return false if the + * traversal should stop at the given vertex. + * + * Parameters: + * + * vertex - <mxCell> that represents the vertex where the traversal starts. + * directed - boolean indicating if edges should only be traversed + * from source to target. Default is true. + * edge - Optional <mxCell> that represents the incoming edge. This is + * null for the first step of the traversal. + * allVertices - Array of cell paths for the visited cells. + */ +mxHierarchicalLayout.prototype.traverse = function(vertex, directed, edge, allVertices, currentComp, + hierarchyVertices, filledVertexSet) +{ + var view = this.graph.view; + var model = this.graph.model; + + if (vertex != null && allVertices != null) + { + // Has this vertex been seen before in any traversal + // And if the filled vertex set is populated, only + // process vertices in that it contains + var vertexID = mxCellPath.create(vertex); + + if ((allVertices[vertexID] == null) + && (filledVertexSet == null ? true : filledVertexSet[vertexID] != null)) + { + if (currentComp[vertexID] == null) + { + currentComp[vertexID] = vertex; + } + if (allVertices[vertexID] == null) + { + allVertices[vertexID] = vertex; + } + + delete filledVertexSet[vertexID]; + + var edgeCount = model.getEdgeCount(vertex); + + if (edgeCount > 0) + { + for (var i = 0; i < edgeCount; i++) + { + var e = model.getEdgeAt(vertex, i); + var isSource = view.getVisibleTerminal(e, true) == vertex; + + if (!directed || isSource) + { + var next = view.getVisibleTerminal(e, !isSource); + currentComp = this.traverse(next, directed, e, allVertices, + currentComp, hierarchyVertices, + filledVertexSet); + } + } + } + } + else + { + if (currentComp[vertexID] == null) + { + // We've seen this vertex before, but not in the current component + // This component and the one it's in need to be merged + + for (var i = 0; i < hierarchyVertices.length; i++) + { + var comp = hierarchyVertices[i]; + + if (comp[vertexID] != null) + { + for (var key in currentComp) + { + comp[key] = currentComp[key]; + } + + // Remove the current component from the hierarchy set + hierarchyVertices.pop(); + return comp; + } + } + } + } + } + + return currentComp; +}; + +/** + * Function: cycleStage + * + * Executes the cycle stage using mxMinimumCycleRemover. + */ +mxHierarchicalLayout.prototype.cycleStage = function(parent) +{ + var cycleStage = new mxMinimumCycleRemover(this); + cycleStage.execute(parent); +}; + +/** + * Function: layeringStage + * + * Implements first stage of a Sugiyama layout. + */ +mxHierarchicalLayout.prototype.layeringStage = function() +{ + this.model.initialRank(); + this.model.fixRanks(); +}; + +/** + * Function: crossingStage + * + * Executes the crossing stage using mxMedianHybridCrossingReduction. + */ +mxHierarchicalLayout.prototype.crossingStage = function(parent) +{ + var crossingStage = new mxMedianHybridCrossingReduction(this); + crossingStage.execute(parent); +}; + +/** + * Function: placementStage + * + * Executes the placement stage using mxCoordinateAssignment. + */ +mxHierarchicalLayout.prototype.placementStage = function(initialX, parent) +{ + var placementStage = new mxCoordinateAssignment(this, this.intraCellSpacing, + this.interRankCellSpacing, this.orientation, initialX, + this.parallelEdgeSpacing); + placementStage.fineTuning = this.fineTuning; + placementStage.execute(parent); + + return placementStage.limitX + this.interHierarchySpacing; +}; diff --git a/src/js/layout/hierarchical/stage/mxCoordinateAssignment.js b/src/js/layout/hierarchical/stage/mxCoordinateAssignment.js new file mode 100644 index 0000000..8b73ccf --- /dev/null +++ b/src/js/layout/hierarchical/stage/mxCoordinateAssignment.js @@ -0,0 +1,1836 @@ +/** + * $Id: mxCoordinateAssignment.js,v 1.29 2012-06-21 14:28:09 david Exp $ + * Copyright (c) 2005-2012, JGraph Ltd + */ +/** + * Class: mxCoordinateAssignment + * + * Sets the horizontal locations of node and edge dummy nodes on each layer. + * Uses median down and up weighings as well as heuristics to straighten edges as + * far as possible. + * + * Constructor: mxCoordinateAssignment + * + * Creates a coordinate assignment. + * + * Arguments: + * + * intraCellSpacing - the minimum buffer between cells on the same rank + * interRankCellSpacing - the minimum distance between cells on adjacent ranks + * orientation - the position of the root node(s) relative to the graph + * initialX - the leftmost coordinate node placement starts at + */ +function mxCoordinateAssignment(layout, intraCellSpacing, interRankCellSpacing, + orientation, initialX, parallelEdgeSpacing) +{ + this.layout = layout; + this.intraCellSpacing = intraCellSpacing; + this.interRankCellSpacing = interRankCellSpacing; + this.orientation = orientation; + this.initialX = initialX; + this.parallelEdgeSpacing = parallelEdgeSpacing; +}; + +var mxHierarchicalEdgeStyle = +{ + ORTHOGONAL: 1, + POLYLINE: 2, + STRAIGHT: 3 +}; + +/** + * Extends mxHierarchicalLayoutStage. + */ +mxCoordinateAssignment.prototype = new mxHierarchicalLayoutStage(); +mxCoordinateAssignment.prototype.constructor = mxCoordinateAssignment; + +/** + * Variable: layout + * + * Reference to the enclosing <mxHierarchicalLayout>. + */ +mxCoordinateAssignment.prototype.layout = null; + +/** + * Variable: intraCellSpacing + * + * The minimum buffer between cells on the same rank. Default is 30. + */ +mxCoordinateAssignment.prototype.intraCellSpacing = 30; + +/** + * Variable: interRankCellSpacing + * + * The minimum distance between cells on adjacent ranks. Default is 10. + */ +mxCoordinateAssignment.prototype.interRankCellSpacing = 10; + +/** + * Variable: parallelEdgeSpacing + * + * The distance between each parallel edge on each ranks for long edges. + * Default is 10. + */ +mxCoordinateAssignment.prototype.parallelEdgeSpacing = 10; + +/** + * Variable: maxIterations + * + * The number of heuristic iterations to run. Default is 8. + */ +mxCoordinateAssignment.prototype.maxIterations = 8; + +/** + * Variable: prefHozEdgeSep + * + * The preferred horizontal distance between edges exiting a vertex + */ +mxCoordinateAssignment.prototype.prefHozEdgeSep = 5; + +/** + * Variable: prefVertEdgeOff + * + * The preferred vertical offset between edges exiting a vertex + */ +mxCoordinateAssignment.prototype.prefVertEdgeOff = 2; + +/** + * Variable: minEdgeJetty + * + * The minimum distance for an edge jetty from a vertex + */ +mxCoordinateAssignment.prototype.minEdgeJetty = 12; + +/** + * Variable: channelBuffer + * + * The size of the vertical buffer in the center of inter-rank channels + * where edge control points should not be placed + */ +mxCoordinateAssignment.prototype.channelBuffer = 4; + +/** + * Variable: jettyPositions + * + * Map of internal edges and (x,y) pair of positions of the start and end jetty + * for that edge where it connects to the source and target vertices. + * Note this should technically be a WeakHashMap, but since JS does not + * have an equivalent, housekeeping must be performed before using. + * i.e. check all edges are still in the model and clear the values. + * Note that the y co-ord is the offset of the jetty, not the + * absolute point + */ +mxCoordinateAssignment.prototype.jettyPositions = null; + +/** + * Variable: orientation + * + * The position of the root ( start ) node(s) relative to the rest of the + * laid out graph. Default is <mxConstants.DIRECTION_NORTH>. + */ +mxCoordinateAssignment.prototype.orientation = mxConstants.DIRECTION_NORTH; + +/** + * Variable: initialX + * + * The minimum x position node placement starts at + */ +mxCoordinateAssignment.prototype.initialX = null; + +/** + * Variable: limitX + * + * The maximum x value this positioning lays up to + */ +mxCoordinateAssignment.prototype.limitX = null; + +/** + * Variable: currentXDelta + * + * The sum of x-displacements for the current iteration + */ +mxCoordinateAssignment.prototype.currentXDelta = null; + +/** + * Variable: widestRank + * + * The rank that has the widest x position + */ +mxCoordinateAssignment.prototype.widestRank = null; + +/** + * Variable: rankTopY + * + * Internal cache of top-most values of Y for each rank + */ +mxCoordinateAssignment.prototype.rankTopY = null; + +/** + * Variable: rankBottomY + * + * Internal cache of bottom-most value of Y for each rank + */ +mxCoordinateAssignment.prototype.rankBottomY = null; + +/** + * Variable: widestRankValue + * + * The X-coordinate of the edge of the widest rank + */ +mxCoordinateAssignment.prototype.widestRankValue = null; + +/** + * Variable: rankWidths + * + * The width of all the ranks + */ +mxCoordinateAssignment.prototype.rankWidths = null; + +/** + * Variable: rankY + * + * The Y-coordinate of all the ranks + */ +mxCoordinateAssignment.prototype.rankY = null; + +/** + * Variable: fineTuning + * + * Whether or not to perform local optimisations and iterate multiple times + * through the algorithm. Default is true. + */ +mxCoordinateAssignment.prototype.fineTuning = true; + +/** + * Variable: edgeStyle + * + * The style to apply between cell layers to edge segments + */ +mxCoordinateAssignment.prototype.edgeStyle = mxHierarchicalEdgeStyle.POLYLINE; + +/** + * Variable: nextLayerConnectedCache + * + * A store of connections to the layer above for speed + */ +mxCoordinateAssignment.prototype.nextLayerConnectedCache = null; + +/** + * Variable: previousLayerConnectedCache + * + * A store of connections to the layer below for speed + */ +mxCoordinateAssignment.prototype.previousLayerConnectedCache = null; + +/** + * Variable: groupPadding + * + * Padding added to resized parents + */ +mxCoordinateAssignment.prototype.groupPadding = 10; + +/** + * Utility method to display current positions + */ +mxCoordinateAssignment.prototype.printStatus = function() +{ + var model = this.layout.getModel(); + mxLog.show(); + + mxLog.writeln('======Coord assignment debug======='); + + for (var j = 0; j < model.ranks.length; j++) + { + mxLog.write('Rank ', j, ' : ' ); + var rank = model.ranks[j]; + + for (var k = 0; k < rank.length; k++) + { + var cell = rank[k]; + + mxLog.write(cell.getGeneralPurposeVariable(j), ' '); + } + mxLog.writeln(); + } + + mxLog.writeln('===================================='); +}; + +/** + * Function: execute + * + * A basic horizontal coordinate assignment algorithm + */ +mxCoordinateAssignment.prototype.execute = function(parent) +{ + this.jettyPositions = []; + var model = this.layout.getModel(); + this.currentXDelta = 0.0; + + this.initialCoords(this.layout.getGraph(), model); + +// this.printStatus(); + + if (this.fineTuning) + { + this.minNode(model); + } + + var bestXDelta = 100000000.0; + + if (this.fineTuning) + { + for (var i = 0; i < this.maxIterations; i++) + { +// this.printStatus(); + + // Median Heuristic + if (i != 0) + { + this.medianPos(i, model); + this.minNode(model); + } + + // if the total offset is less for the current positioning, + // there are less heavily angled edges and so the current + // positioning is used + if (this.currentXDelta < bestXDelta) + { + for (var j = 0; j < model.ranks.length; j++) + { + var rank = model.ranks[j]; + + for (var k = 0; k < rank.length; k++) + { + var cell = rank[k]; + cell.setX(j, cell.getGeneralPurposeVariable(j)); + } + } + + bestXDelta = this.currentXDelta; + } + else + { + // Restore the best positions + for (var j = 0; j < model.ranks.length; j++) + { + var rank = model.ranks[j]; + + for (var k = 0; k < rank.length; k++) + { + var cell = rank[k]; + cell.setGeneralPurposeVariable(j, cell.getX(j)); + } + } + } + + this.minPath(this.layout.getGraph(), model); + + this.currentXDelta = 0; + } + } + + this.setCellLocations(this.layout.getGraph(), model); +}; + +/** + * Function: minNode + * + * Performs one median positioning sweep in both directions + */ +mxCoordinateAssignment.prototype.minNode = function(model) +{ + // Queue all nodes + var nodeList = []; + + // Need to be able to map from cell to cellWrapper + var map = []; + var rank = []; + + for (var i = 0; i <= model.maxRank; i++) + { + rank[i] = model.ranks[i]; + + for (var j = 0; j < rank[i].length; j++) + { + // Use the weight to store the rank and visited to store whether + // or not the cell is in the list + var node = rank[i][j]; + var nodeWrapper = new WeightedCellSorter(node, i); + nodeWrapper.rankIndex = j; + nodeWrapper.visited = true; + nodeList.push(nodeWrapper); + + var cellId = mxCellPath.create(node.getCoreCell()); + map[cellId] = nodeWrapper; + } + } + + // Set a limit of the maximum number of times we will access the queue + // in case a loop appears + var maxTries = nodeList.length * 10; + var count = 0; + + // Don't move cell within this value of their median + var tolerance = 1; + + while (nodeList.length > 0 && count <= maxTries) + { + var cellWrapper = nodeList.shift(); + var cell = cellWrapper.cell; + + var rankValue = cellWrapper.weightedValue; + var rankIndex = parseInt(cellWrapper.rankIndex); + + var nextLayerConnectedCells = cell.getNextLayerConnectedCells(rankValue); + var previousLayerConnectedCells = cell.getPreviousLayerConnectedCells(rankValue); + + var numNextLayerConnected = nextLayerConnectedCells.length; + var numPreviousLayerConnected = previousLayerConnectedCells.length; + + var medianNextLevel = this.medianXValue(nextLayerConnectedCells, + rankValue + 1); + var medianPreviousLevel = this.medianXValue(previousLayerConnectedCells, + rankValue - 1); + + var numConnectedNeighbours = numNextLayerConnected + + numPreviousLayerConnected; + var currentPosition = cell.getGeneralPurposeVariable(rankValue); + var cellMedian = currentPosition; + + if (numConnectedNeighbours > 0) + { + cellMedian = (medianNextLevel * numNextLayerConnected + medianPreviousLevel + * numPreviousLayerConnected) + / numConnectedNeighbours; + } + + // Flag storing whether or not position has changed + var positionChanged = false; + + if (cellMedian < currentPosition - tolerance) + { + if (rankIndex == 0) + { + cell.setGeneralPurposeVariable(rankValue, cellMedian); + positionChanged = true; + } + else + { + var leftCell = rank[rankValue][rankIndex - 1]; + var leftLimit = leftCell + .getGeneralPurposeVariable(rankValue); + leftLimit = leftLimit + leftCell.width / 2 + + this.intraCellSpacing + cell.width / 2; + + if (leftLimit < cellMedian) + { + cell.setGeneralPurposeVariable(rankValue, cellMedian); + positionChanged = true; + } + else if (leftLimit < cell + .getGeneralPurposeVariable(rankValue) + - tolerance) + { + cell.setGeneralPurposeVariable(rankValue, leftLimit); + positionChanged = true; + } + } + } + else if (cellMedian > currentPosition + tolerance) + { + var rankSize = rank[rankValue].length; + + if (rankIndex == rankSize - 1) + { + cell.setGeneralPurposeVariable(rankValue, cellMedian); + positionChanged = true; + } + else + { + var rightCell = rank[rankValue][rankIndex + 1]; + var rightLimit = rightCell + .getGeneralPurposeVariable(rankValue); + rightLimit = rightLimit - rightCell.width / 2 + - this.intraCellSpacing - cell.width / 2; + + if (rightLimit > cellMedian) + { + cell.setGeneralPurposeVariable(rankValue, cellMedian); + positionChanged = true; + } + else if (rightLimit > cell + .getGeneralPurposeVariable(rankValue) + + tolerance) + { + cell.setGeneralPurposeVariable(rankValue, rightLimit); + positionChanged = true; + } + } + } + + if (positionChanged) + { + // Add connected nodes to map and list + for (var i = 0; i < nextLayerConnectedCells.length; i++) + { + var connectedCell = nextLayerConnectedCells[i]; + var connectedCellId = mxCellPath.create(connectedCell.getCoreCell()); + var connectedCellWrapper = map[connectedCellId]; + + if (connectedCellWrapper != null) + { + if (connectedCellWrapper.visited == false) + { + connectedCellWrapper.visited = true; + nodeList.push(connectedCellWrapper); + } + } + } + + // Add connected nodes to map and list + for (var i = 0; i < previousLayerConnectedCells.length; i++) + { + var connectedCell = previousLayerConnectedCells[i]; + var connectedCellId = mxCellPath.create(connectedCell.getCoreCell()); + var connectedCellWrapper = map[connectedCellId]; + + if (connectedCellWrapper != null) + { + if (connectedCellWrapper.visited == false) + { + connectedCellWrapper.visited = true; + nodeList.push(connectedCellWrapper); + } + } + } + } + + cellWrapper.visited = false; + count++; + } +}; + +/** + * Function: medianPos + * + * Performs one median positioning sweep in one direction + * + * Parameters: + * + * i - the iteration of the whole process + * model - an internal model of the hierarchical layout + */ +mxCoordinateAssignment.prototype.medianPos = function(i, model) +{ + // Reverse sweep direction each time through this method + var downwardSweep = (i % 2 == 0); + + if (downwardSweep) + { + for (var j = model.maxRank; j > 0; j--) + { + this.rankMedianPosition(j - 1, model, j); + } + } + else + { + for (var j = 0; j < model.maxRank - 1; j++) + { + this.rankMedianPosition(j + 1, model, j); + } + } +}; + +/** + * Function: rankMedianPosition + * + * Performs median minimisation over one rank. + * + * Parameters: + * + * rankValue - the layer number of this rank + * model - an internal model of the hierarchical layout + * nextRankValue - the layer number whose connected cels are to be laid out + * relative to + */ +mxCoordinateAssignment.prototype.rankMedianPosition = function(rankValue, model, nextRankValue) +{ + var rank = model.ranks[rankValue]; + + // Form an array of the order in which the cell are to be processed + // , the order is given by the weighted sum of the in or out edges, + // depending on whether we're travelling up or down the hierarchy. + var weightedValues = []; + var cellMap = []; + + for (var i = 0; i < rank.length; i++) + { + var currentCell = rank[i]; + weightedValues[i] = new WeightedCellSorter(); + weightedValues[i].cell = currentCell; + weightedValues[i].rankIndex = i; + var currentCellId = mxCellPath.create(currentCell.getCoreCell()); + cellMap[currentCellId] = weightedValues[i]; + var nextLayerConnectedCells = null; + + if (nextRankValue < rankValue) + { + nextLayerConnectedCells = currentCell + .getPreviousLayerConnectedCells(rankValue); + } + else + { + nextLayerConnectedCells = currentCell + .getNextLayerConnectedCells(rankValue); + } + + // Calculate the weighing based on this node type and those this + // node is connected to on the next layer + weightedValues[i].weightedValue = this.calculatedWeightedValue( + currentCell, nextLayerConnectedCells); + } + + weightedValues.sort(WeightedCellSorter.prototype.compare); + + // Set the new position of each node within the rank using + // its temp variable + + for (var i = 0; i < weightedValues.length; i++) + { + var numConnectionsNextLevel = 0; + var cell = weightedValues[i].cell; + var nextLayerConnectedCells = null; + var medianNextLevel = 0; + + if (nextRankValue < rankValue) + { + nextLayerConnectedCells = cell.getPreviousLayerConnectedCells( + rankValue).slice(); + } + else + { + nextLayerConnectedCells = cell.getNextLayerConnectedCells( + rankValue).slice(); + } + + if (nextLayerConnectedCells != null) + { + numConnectionsNextLevel = nextLayerConnectedCells.length; + + if (numConnectionsNextLevel > 0) + { + medianNextLevel = this.medianXValue(nextLayerConnectedCells, + nextRankValue); + } + else + { + // For case of no connections on the next level set the + // median to be the current position and try to be + // positioned there + medianNextLevel = cell.getGeneralPurposeVariable(rankValue); + } + } + + var leftBuffer = 0.0; + var leftLimit = -100000000.0; + + for (var j = weightedValues[i].rankIndex - 1; j >= 0;) + { + var rankId = mxCellPath.create(rank[j].getCoreCell()); + var weightedValue = cellMap[rankId]; + + if (weightedValue != null) + { + var leftCell = weightedValue.cell; + + if (weightedValue.visited) + { + // The left limit is the right hand limit of that + // cell plus any allowance for unallocated cells + // in-between + leftLimit = leftCell + .getGeneralPurposeVariable(rankValue) + + leftCell.width + / 2.0 + + this.intraCellSpacing + + leftBuffer + cell.width / 2.0; + j = -1; + } + else + { + leftBuffer += leftCell.width + this.intraCellSpacing; + j--; + } + } + } + + var rightBuffer = 0.0; + var rightLimit = 100000000.0; + + for (var j = weightedValues[i].rankIndex + 1; j < weightedValues.length;) + { + var rankId = mxCellPath.create(rank[j].getCoreCell()); + var weightedValue = cellMap[rankId]; + + if (weightedValue != null) + { + var rightCell = weightedValue.cell; + + if (weightedValue.visited) + { + // The left limit is the right hand limit of that + // cell plus any allowance for unallocated cells + // in-between + rightLimit = rightCell + .getGeneralPurposeVariable(rankValue) + - rightCell.width + / 2.0 + - this.intraCellSpacing + - rightBuffer - cell.width / 2.0; + j = weightedValues.length; + } + else + { + rightBuffer += rightCell.width + this.intraCellSpacing; + j++; + } + } + } + + if (medianNextLevel >= leftLimit && medianNextLevel <= rightLimit) + { + cell.setGeneralPurposeVariable(rankValue, medianNextLevel); + } + else if (medianNextLevel < leftLimit) + { + // Couldn't place at median value, place as close to that + // value as possible + cell.setGeneralPurposeVariable(rankValue, leftLimit); + this.currentXDelta += leftLimit - medianNextLevel; + } + else if (medianNextLevel > rightLimit) + { + // Couldn't place at median value, place as close to that + // value as possible + cell.setGeneralPurposeVariable(rankValue, rightLimit); + this.currentXDelta += medianNextLevel - rightLimit; + } + + weightedValues[i].visited = true; + } +}; + +/** + * Function: calculatedWeightedValue + * + * Calculates the priority the specified cell has based on the type of its + * cell and the cells it is connected to on the next layer + * + * Parameters: + * + * currentCell - the cell whose weight is to be calculated + * collection - the cells the specified cell is connected to + */ +mxCoordinateAssignment.prototype.calculatedWeightedValue = function(currentCell, collection) +{ + var totalWeight = 0; + + for (var i = 0; i < collection.length; i++) + { + var cell = collection[i]; + + if (currentCell.isVertex() && cell.isVertex()) + { + totalWeight++; + } + else if (currentCell.isEdge() && cell.isEdge()) + { + totalWeight += 8; + } + else + { + totalWeight += 2; + } + } + + return totalWeight; +}; + +/** + * Function: medianXValue + * + * Calculates the median position of the connected cell on the specified + * rank + * + * Parameters: + * + * connectedCells - the cells the candidate connects to on this level + * rankValue - the layer number of this rank + */ +mxCoordinateAssignment.prototype.medianXValue = function(connectedCells, rankValue) +{ + if (connectedCells.length == 0) + { + return 0; + } + + var medianValues = []; + + for (var i = 0; i < connectedCells.length; i++) + { + medianValues[i] = connectedCells[i].getGeneralPurposeVariable(rankValue); + } + + medianValues.sort(function(a,b){return a - b;}); + + if (connectedCells.length % 2 == 1) + { + // For odd numbers of adjacent vertices return the median + return medianValues[Math.floor(connectedCells.length / 2)]; + } + else + { + var medianPoint = connectedCells.length / 2; + var leftMedian = medianValues[medianPoint - 1]; + var rightMedian = medianValues[medianPoint]; + + return ((leftMedian + rightMedian) / 2); + } +}; + +/** + * Function: initialCoords + * + * Sets up the layout in an initial positioning. The ranks are all centered + * as much as possible along the middle vertex in each rank. The other cells + * are then placed as close as possible on either side. + * + * Parameters: + * + * facade - the facade describing the input graph + * model - an internal model of the hierarchical layout + */ +mxCoordinateAssignment.prototype.initialCoords = function(facade, model) +{ + this.calculateWidestRank(facade, model); + + // Sweep up and down from the widest rank + for (var i = this.widestRank; i >= 0; i--) + { + if (i < model.maxRank) + { + this.rankCoordinates(i, facade, model); + } + } + + for (var i = this.widestRank+1; i <= model.maxRank; i++) + { + if (i > 0) + { + this.rankCoordinates(i, facade, model); + } + } +}; + +/** + * Function: rankCoordinates + * + * Sets up the layout in an initial positioning. All the first cells in each + * rank are moved to the left and the rest of the rank inserted as close + * together as their size and buffering permits. This method works on just + * the specified rank. + * + * Parameters: + * + * rankValue - the current rank being processed + * graph - the facade describing the input graph + * model - an internal model of the hierarchical layout + */ +mxCoordinateAssignment.prototype.rankCoordinates = function(rankValue, graph, model) +{ + var rank = model.ranks[rankValue]; + var maxY = 0.0; + var localX = this.initialX + (this.widestRankValue - this.rankWidths[rankValue]) + / 2; + + // Store whether or not any of the cells' bounds were unavailable so + // to only issue the warning once for all cells + var boundsWarning = false; + + for (var i = 0; i < rank.length; i++) + { + var node = rank[i]; + + if (node.isVertex()) + { + var bounds = this.layout.getVertexBounds(node.cell); + + if (bounds != null) + { + if (this.orientation == mxConstants.DIRECTION_NORTH || + this.orientation == mxConstants.DIRECTION_SOUTH) + { + node.width = bounds.width; + node.height = bounds.height; + } + else + { + node.width = bounds.height; + node.height = bounds.width; + } + } + else + { + boundsWarning = true; + } + + maxY = Math.max(maxY, node.height); + } + else if (node.isEdge()) + { + // The width is the number of additional parallel edges + // time the parallel edge spacing + var numEdges = 1; + + if (node.edges != null) + { + numEdges = node.edges.length; + } + else + { + mxLog.warn('edge.edges is null'); + } + + node.width = (numEdges - 1) * this.parallelEdgeSpacing; + } + + // Set the initial x-value as being the best result so far + localX += node.width / 2.0; + node.setX(rankValue, localX); + node.setGeneralPurposeVariable(rankValue, localX); + localX += node.width / 2.0; + localX += this.intraCellSpacing; + } + + if (boundsWarning == true) + { + mxLog.warn('At least one cell has no bounds'); + } +}; + +/** + * Function: calculateWidestRank + * + * Calculates the width rank in the hierarchy. Also set the y value of each + * rank whilst performing the calculation + * + * Parameters: + * + * graph - the facade describing the input graph + * model - an internal model of the hierarchical layout + */ +mxCoordinateAssignment.prototype.calculateWidestRank = function(graph, model) +{ + // Starting y co-ordinate + var y = -this.interRankCellSpacing; + + // Track the widest cell on the last rank since the y + // difference depends on it + var lastRankMaxCellHeight = 0.0; + this.rankWidths = []; + this.rankY = []; + + for (var rankValue = model.maxRank; rankValue >= 0; rankValue--) + { + // Keep track of the widest cell on this rank + var maxCellHeight = 0.0; + var rank = model.ranks[rankValue]; + var localX = this.initialX; + + // Store whether or not any of the cells' bounds were unavailable so + // to only issue the warning once for all cells + var boundsWarning = false; + + for (var i = 0; i < rank.length; i++) + { + var node = rank[i]; + + if (node.isVertex()) + { + var bounds = this.layout.getVertexBounds(node.cell); + + if (bounds != null) + { + if (this.orientation == mxConstants.DIRECTION_NORTH || + this.orientation == mxConstants.DIRECTION_SOUTH) + { + node.width = bounds.width; + node.height = bounds.height; + } + else + { + node.width = bounds.height; + node.height = bounds.width; + } + } + else + { + boundsWarning = true; + } + + maxCellHeight = Math.max(maxCellHeight, node.height); + } + else if (node.isEdge()) + { + // The width is the number of additional parallel edges + // time the parallel edge spacing + var numEdges = 1; + + if (node.edges != null) + { + numEdges = node.edges.length; + } + else + { + mxLog.warn('edge.edges is null'); + } + + node.width = (numEdges - 1) * this.parallelEdgeSpacing; + } + + // Set the initial x-value as being the best result so far + localX += node.width / 2.0; + node.setX(rankValue, localX); + node.setGeneralPurposeVariable(rankValue, localX); + localX += node.width / 2.0; + localX += this.intraCellSpacing; + + if (localX > this.widestRankValue) + { + this.widestRankValue = localX; + this.widestRank = rankValue; + } + + this.rankWidths[rankValue] = localX; + } + + if (boundsWarning == true) + { + mxLog.warn('At least one cell has no bounds'); + } + + this.rankY[rankValue] = y; + var distanceToNextRank = maxCellHeight / 2.0 + + lastRankMaxCellHeight / 2.0 + this.interRankCellSpacing; + lastRankMaxCellHeight = maxCellHeight; + + if (this.orientation == mxConstants.DIRECTION_NORTH || + this.orientation == mxConstants.DIRECTION_WEST) + { + y += distanceToNextRank; + } + else + { + y -= distanceToNextRank; + } + + for (var i = 0; i < rank.length; i++) + { + var cell = rank[i]; + cell.setY(rankValue, y); + } + } +}; + +/** + * Function: minPath + * + * Straightens out chains of virtual nodes where possibleacade to those stored after this layout + * processing step has completed. + * + * Parameters: + * + * graph - the facade describing the input graph + * model - an internal model of the hierarchical layout + */ +mxCoordinateAssignment.prototype.minPath = function(graph, model) +{ + // Work down and up each edge with at least 2 control points + // trying to straighten each one out. If the same number of + // straight segments are formed in both directions, the + // preferred direction used is the one where the final + // control points have the least offset from the connectable + // region of the terminating vertices + var edges = model.edgeMapper; + + for (var key in edges) + { + var cell = edges[key]; + + if (cell.maxRank - cell.minRank - 1 < 1) + { + continue; + } + + // At least two virtual nodes in the edge + // Check first whether the edge is already straight + var referenceX = cell + .getGeneralPurposeVariable(cell.minRank + 1); + var edgeStraight = true; + var refSegCount = 0; + + for (var i = cell.minRank + 2; i < cell.maxRank; i++) + { + var x = cell.getGeneralPurposeVariable(i); + + if (referenceX != x) + { + edgeStraight = false; + referenceX = x; + } + else + { + refSegCount++; + } + } + + if (!edgeStraight) + { + var upSegCount = 0; + var downSegCount = 0; + var upXPositions = []; + var downXPositions = []; + + var currentX = cell.getGeneralPurposeVariable(cell.minRank + 1); + + for (var i = cell.minRank + 1; i < cell.maxRank - 1; i++) + { + // Attempt to straight out the control point on the + // next segment up with the current control point. + var nextX = cell.getX(i + 1); + + if (currentX == nextX) + { + upXPositions[i - cell.minRank - 1] = currentX; + upSegCount++; + } + else if (this.repositionValid(model, cell, i + 1, currentX)) + { + upXPositions[i - cell.minRank - 1] = currentX; + upSegCount++; + // Leave currentX at same value + } + else + { + upXPositions[i - cell.minRank - 1] = nextX; + currentX = nextX; + } + } + + currentX = cell.getX(i); + + for (var i = cell.maxRank - 1; i > cell.minRank + 1; i--) + { + // Attempt to straight out the control point on the + // next segment down with the current control point. + var nextX = cell.getX(i - 1); + + if (currentX == nextX) + { + downXPositions[i - cell.minRank - 2] = currentX; + downSegCount++; + } + else if (this.repositionValid(model, cell, i - 1, currentX)) + { + downXPositions[i - cell.minRank - 2] = currentX; + downSegCount++; + // Leave currentX at same value + } + else + { + downXPositions[i - cell.minRank - 2] = cell.getX(i-1); + currentX = nextX; + } + } + + if (downSegCount > refSegCount || upSegCount > refSegCount) + { + if (downSegCount >= upSegCount) + { + // Apply down calculation values + for (var i = cell.maxRank - 2; i > cell.minRank; i--) + { + cell.setX(i, downXPositions[i - cell.minRank - 1]); + } + } + else if (upSegCount > downSegCount) + { + // Apply up calculation values + for (var i = cell.minRank + 2; i < cell.maxRank; i++) + { + cell.setX(i, upXPositions[i - cell.minRank - 2]); + } + } + else + { + // Neither direction provided a favourable result + // But both calculations are better than the + // existing solution, so apply the one with minimal + // offset to attached vertices at either end. + } + } + } + } +}; + +/** + * Function: repositionValid + * + * Determines whether or not a node may be moved to the specified x + * position on the specified rank + * + * Parameters: + * + * model - the layout model + * cell - the cell being analysed + * rank - the layer of the cell + * position - the x position being sought + */ +mxCoordinateAssignment.prototype.repositionValid = function(model, cell, rank, position) +{ + var rankArray = model.ranks[rank]; + var rankIndex = -1; + + for (var i = 0; i < rankArray.length; i++) + { + if (cell == rankArray[i]) + { + rankIndex = i; + break; + } + } + + if (rankIndex < 0) + { + return false; + } + + var currentX = cell.getGeneralPurposeVariable(rank); + + if (position < currentX) + { + // Trying to move node to the left. + if (rankIndex == 0) + { + // Left-most node, can move anywhere + return true; + } + + var leftCell = rankArray[rankIndex - 1]; + var leftLimit = leftCell.getGeneralPurposeVariable(rank); + leftLimit = leftLimit + leftCell.width / 2 + + this.intraCellSpacing + cell.width / 2; + + if (leftLimit <= position) + { + return true; + } + else + { + return false; + } + } + else if (position > currentX) + { + // Trying to move node to the right. + if (rankIndex == rankArray.length - 1) + { + // Right-most node, can move anywhere + return true; + } + + var rightCell = rankArray[rankIndex + 1]; + var rightLimit = rightCell.getGeneralPurposeVariable(rank); + rightLimit = rightLimit - rightCell.width / 2 + - this.intraCellSpacing - cell.width / 2; + + if (rightLimit >= position) + { + return true; + } + else + { + return false; + } + } + + return true; +}; + +/** + * Function: setCellLocations + * + * Sets the cell locations in the facade to those stored after this layout + * processing step has completed. + * + * Parameters: + * + * graph - the input graph + * model - the layout model + */ +mxCoordinateAssignment.prototype.setCellLocations = function(graph, model) +{ + this.rankTopY = []; + this.rankBottomY = []; + + for (var i = 0; i < model.ranks.length; i++) + { + this.rankTopY[i] = Number.MAX_VALUE; + this.rankBottomY[i] = 0.0; + } + + var parentsChanged = null; + + if (this.layout.resizeParent) + { + parentsChanged = new Object(); + } + + var edges = model.edgeMapper; + var vertices = model.vertexMapper; + + // Process vertices all first, since they define the lower and + // limits of each rank. Between these limits lie the channels + // where the edges can be routed across the graph + + for (var key in vertices) + { + var vertex = vertices[key]; + this.setVertexLocation(vertex); + + if (this.layout.resizeParent) + { + var parent = graph.model.getParent(vertex.cell); + var id = mxCellPath.create(parent); + + // Implements set semantic + if (parentsChanged[id] == null) + { + parentsChanged[id] = parent; + } + } + } + + if (this.layout.resizeParent && parentsChanged != null) + { + this.adjustParents(parentsChanged); + } + + // Post process edge styles. Needs the vertex locations set for initial + // values of the top and bottoms of each rank + if (this.edgeStyle == mxHierarchicalEdgeStyle.ORTHOGONAL + || this.edgeStyle == mxHierarchicalEdgeStyle.POLYLINE) + { + this.localEdgeProcessing(model); + } + + for (var key in edges) + { + this.setEdgePosition(edges[key]); + } +}; + +/** + * Function: adjustParents + * + * Adjust parent cells whose child geometries have changed. The default + * implementation adjusts the group to just fit around the children with + * a padding. + */ +mxCoordinateAssignment.prototype.adjustParents = function(parentsChanged) +{ + var tmp = []; + + for (var id in parentsChanged) + { + tmp.push(parentsChanged[id]); + } + + this.layout.arrangeGroups(mxUtils.sortCells(tmp, true), this.groupPadding); +}; + +/** + * Function: localEdgeProcessing + * + * Separates the x position of edges as they connect to vertices + * + * Parameters: + * + * model - the layout model + */ +mxCoordinateAssignment.prototype.localEdgeProcessing = function(model) +{ + var edgeMapping = model.edgeMapper; + + // Iterate through each vertex, look at the edges connected in + // both directions. + for (var rankIndex = 0; rankIndex < model.ranks.length; rankIndex++) + { + var rank = model.ranks[rankIndex]; + + for (var cellIndex = 0; cellIndex < rank.length; cellIndex++) + { + var cell = rank[cellIndex]; + + if (cell.isVertex()) + { + var currentCells = cell.getPreviousLayerConnectedCells(rankIndex); + + var currentRank = rankIndex - 1; + + // Two loops, last connected cells, and next + for (var k = 0; k < 2; k++) + { + if (currentRank > -1 + && currentRank < model.ranks.length + && currentCells != null + && currentCells.length > 0) + { + var sortedCells = []; + + for (var j = 0; j < currentCells.length; j++) + { + var sorter = new WeightedCellSorter( + currentCells[j], currentCells[j].getX(currentRank)); + sortedCells.push(sorter); + } + + sortedCells.sort(WeightedCellSorter.prototype.compare); + + var leftLimit = cell.x[0] - cell.width / 2; + var rightLimit = leftLimit + cell.width; + + // Connected edge count starts at 1 to allow for buffer + // with edge of vertex + var connectedEdgeCount = 0; + var connectedEdgeGroupCount = 0; + var connectedEdges = []; + // Calculate width requirements for all connected edges + for (var j = 0; j < sortedCells.length; j++) + { + var innerCell = sortedCells[j].cell; + var connections; + + if (innerCell.isVertex()) + { + // Get the connecting edge + if (k == 0) + { + connections = cell.connectsAsSource; + + } + else + { + connections = cell.connectsAsTarget; + } + + for (var connIndex = 0; connIndex < connections.length; connIndex++) + { + if (connections[connIndex].source == innerCell + || connections[connIndex].target == innerCell) + { + connectedEdgeCount += connections[connIndex].edges + .length; + connectedEdgeGroupCount++; + + connectedEdges.push(connections[connIndex]); + } + } + } + else + { + connectedEdgeCount += innerCell.edges.length; + connectedEdgeGroupCount++; + connectedEdges.push(innerCell); + } + } + + var requiredWidth = (connectedEdgeCount + 1) + * this.prefHozEdgeSep; + + // Add a buffer on the edges of the vertex if the edge count allows + if (cell.width > requiredWidth + + (2 * this.prefHozEdgeSep)) + { + leftLimit += this.prefHozEdgeSep; + rightLimit -= this.prefHozEdgeSep; + } + + var availableWidth = rightLimit - leftLimit; + var edgeSpacing = availableWidth / connectedEdgeCount; + + var currentX = leftLimit + edgeSpacing / 2.0; + var currentYOffset = this.minEdgeJetty - this.prefVertEdgeOff; + var maxYOffset = 0; + + for (var j = 0; j < connectedEdges.length; j++) + { + var numActualEdges = connectedEdges[j].edges + .length; + var edgeId = mxCellPath.create(connectedEdges[j].edges[0]); + var pos = this.jettyPositions[edgeId]; + + if (pos == null) + { + pos = []; + this.jettyPositions[edgeId] = pos; + } + + if (j < connectedEdgeCount / 2) + { + currentYOffset += this.prefVertEdgeOff; + } + else if (j > connectedEdgeCount / 2) + { + currentYOffset -= this.prefVertEdgeOff; + } + // Ignore the case if equals, this means the second of 2 + // jettys with the same y (even number of edges) + + for (var m = 0; m < numActualEdges; m++) + { + pos[m * 4 + k * 2] = currentX; + currentX += edgeSpacing; + pos[m * 4 + k * 2 + 1] = currentYOffset; + } + + maxYOffset = Math.max(maxYOffset, + currentYOffset); + } + } + + currentCells = cell.getNextLayerConnectedCells(rankIndex); + + currentRank = rankIndex + 1; + } + } + } + } +}; + +/** + * Function: setEdgePosition + * + * Fixes the control points + */ +mxCoordinateAssignment.prototype.setEdgePosition = function(cell) +{ + // For parallel edges we need to seperate out the points a + // little + var offsetX = 0; + // Only set the edge control points once + + if (cell.temp[0] != 101207) + { + var maxRank = cell.maxRank; + var minRank = cell.minRank; + + if (maxRank == minRank) + { + maxRank = cell.source.maxRank; + minRank = cell.target.minRank; + } + + var parallelEdgeCount = 0; + var edgeId = mxCellPath.create(cell.edges[0]); + var jettys = this.jettyPositions[edgeId]; + + var source = cell.isReversed ? cell.target.cell : cell.source.cell; + + for (var i = 0; i < cell.edges.length; i++) + { + var realEdge = cell.edges[i]; + var realSource = this.layout.graph.view.getVisibleTerminal(realEdge, true); + + //List oldPoints = graph.getPoints(realEdge); + var newPoints = []; + + // Single length reversed edges end up with the jettys in the wrong + // places. Since single length edges only have jettys, not segment + // control points, we just say the edge isn't reversed in this section + var reversed = cell.isReversed; + + if (realSource != source) + { + // The real edges include all core model edges and these can go + // in both directions. If the source of the hierarchical model edge + // isn't the source of the specific real edge in this iteration + // treat if as reversed + reversed = !reversed; + } + + // First jetty of edge + if (jettys != null) + { + var arrayOffset = reversed ? 2 : 0; + var y = reversed ? this.rankTopY[minRank] : this.rankBottomY[maxRank]; + var jetty = jettys[parallelEdgeCount * 4 + 1 + arrayOffset]; + + if (reversed) + { + jetty = -jetty; + } + + y += jetty; + var x = jettys[parallelEdgeCount * 4 + arrayOffset]; + + if (this.orientation == mxConstants.DIRECTION_NORTH + || this.orientation == mxConstants.DIRECTION_SOUTH) + { + newPoints.push(new mxPoint(x, y)); + } + else + { + newPoints.push(new mxPoint(y, x)); + } + } + + // Declare variables to define loop through edge points and + // change direction if edge is reversed + + var loopStart = cell.x.length - 1; + var loopLimit = -1; + var loopDelta = -1; + var currentRank = cell.maxRank - 1; + + if (reversed) + { + loopStart = 0; + loopLimit = cell.x.length; + loopDelta = 1; + currentRank = cell.minRank + 1; + } + // Reversed edges need the points inserted in + // reverse order + for (var j = loopStart; (cell.maxRank != cell.minRank) && j != loopLimit; j += loopDelta) + { + // The horizontal position in a vertical layout + var positionX = cell.x[j] + offsetX; + + // Work out the vertical positions in a vertical layout + // in the edge buffer channels above and below this rank + var topChannelY = (this.rankTopY[currentRank] + this.rankBottomY[currentRank + 1]) / 2.0; + var bottomChannelY = (this.rankTopY[currentRank - 1] + this.rankBottomY[currentRank]) / 2.0; + + if (reversed) + { + var tmp = topChannelY; + topChannelY = bottomChannelY; + bottomChannelY = tmp; + } + + if (this.orientation == mxConstants.DIRECTION_NORTH || + this.orientation == mxConstants.DIRECTION_SOUTH) + { + newPoints.push(new mxPoint(positionX, topChannelY)); + newPoints.push(new mxPoint(positionX, bottomChannelY)); + } + else + { + newPoints.push(new mxPoint(topChannelY, positionX)); + newPoints.push(new mxPoint(bottomChannelY, positionX)); + } + + this.limitX = Math.max(this.limitX, positionX); + currentRank += loopDelta; + } + + // Second jetty of edge + if (jettys != null) + { + var arrayOffset = reversed ? 2 : 0; + var rankY = reversed ? this.rankBottomY[maxRank] : this.rankTopY[minRank]; + var jetty = jettys[parallelEdgeCount * 4 + 3 - arrayOffset]; + + if (reversed) + { + jetty = -jetty; + } + var y = rankY - jetty; + var x = jettys[parallelEdgeCount * 4 + 2 - arrayOffset]; + + if (this.orientation == mxConstants.DIRECTION_NORTH || + this.orientation == mxConstants.DIRECTION_SOUTH) + { + newPoints.push(new mxPoint(x, y)); + } + else + { + newPoints.push(new mxPoint(y, x)); + } + } + + if (cell.isReversed) + { + this.processReversedEdge(cell, realEdge); + } + + this.layout.setEdgePoints(realEdge, newPoints); + + // Increase offset so next edge is drawn next to + // this one + if (offsetX == 0.0) + { + offsetX = this.parallelEdgeSpacing; + } + else if (offsetX > 0) + { + offsetX = -offsetX; + } + else + { + offsetX = -offsetX + this.parallelEdgeSpacing; + } + + parallelEdgeCount++; + } + + cell.temp[0] = 101207; + } +}; + + +/** + * Function: setVertexLocation + * + * Fixes the position of the specified vertex. + * + * Parameters: + * + * cell - the vertex to position + */ +mxCoordinateAssignment.prototype.setVertexLocation = function(cell) +{ + var realCell = cell.cell; + var positionX = cell.x[0] - cell.width / 2; + var positionY = cell.y[0] - cell.height / 2; + + this.rankTopY[cell.minRank] = Math.min(this.rankTopY[cell.minRank], positionY); + this.rankBottomY[cell.minRank] = Math.max(this.rankBottomY[cell.minRank], + positionY + cell.height); + + if (this.orientation == mxConstants.DIRECTION_NORTH || + this.orientation == mxConstants.DIRECTION_SOUTH) + { + this.layout.setVertexLocation(realCell, positionX, positionY); + } + else + { + this.layout.setVertexLocation(realCell, positionY, positionX); + } + + this.limitX = Math.max(this.limitX, positionX + cell.width); +}; + +/** + * Function: processReversedEdge + * + * Hook to add additional processing + * + * Parameters: + * + * edge - the hierarchical model edge + * realEdge - the real edge in the graph + */ +mxCoordinateAssignment.prototype.processReversedEdge = function(graph, model) +{ + // hook for subclassers +}; + +/** + * Class: WeightedCellSorter + * + * A utility class used to track cells whilst sorting occurs on the weighted + * sum of their connected edges. Does not violate (x.compareTo(y)==0) == + * (x.equals(y)) + * + * Constructor: WeightedCellSorter + * + * Constructs a new weighted cell sorted for the given cell and weight. + */ +function WeightedCellSorter(cell, weightedValue) +{ + this.cell = cell; + this.weightedValue = weightedValue; +}; + +/** + * Variable: weightedValue + * + * The weighted value of the cell stored. + */ +WeightedCellSorter.prototype.weightedValue = 0; + +/** + * Variable: nudge + * + * Whether or not to flip equal weight values. + */ +WeightedCellSorter.prototype.nudge = false; + +/** + * Variable: visited + * + * Whether or not this cell has been visited in the current assignment. + */ +WeightedCellSorter.prototype.visited = false; + +/** + * Variable: rankIndex + * + * The index this cell is in the model rank. + */ +WeightedCellSorter.prototype.rankIndex = null; + +/** + * Variable: cell + * + * The cell whose median value is being calculated. + */ +WeightedCellSorter.prototype.cell = null; + +/** + * Function: compare + * + * Compares two WeightedCellSorters. + */ +WeightedCellSorter.prototype.compare = function(a, b) +{ + if (a != null && b != null) + { + if (b.weightedValue > a.weightedValue) + { + return -1; + } + else if (b.weightedValue < a.weightedValue) + { + return 1; + } + else + { + if (b.nudge) + { + return -1; + } + else + { + return 1; + } + } + } + else + { + return 0; + } +}; diff --git a/src/js/layout/hierarchical/stage/mxHierarchicalLayoutStage.js b/src/js/layout/hierarchical/stage/mxHierarchicalLayoutStage.js new file mode 100644 index 0000000..2e635fc --- /dev/null +++ b/src/js/layout/hierarchical/stage/mxHierarchicalLayoutStage.js @@ -0,0 +1,25 @@ +/** + * $Id: mxHierarchicalLayoutStage.js,v 1.8 2010-01-02 09:45:15 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxHierarchicalLayoutStage + * + * The specific layout interface for hierarchical layouts. It adds a + * <code>run</code> method with a parameter for the hierarchical layout model + * that is shared between the layout stages. + * + * Constructor: mxHierarchicalLayoutStage + * + * Constructs a new hierarchical layout stage. + */ +function mxHierarchicalLayoutStage() { }; + +/** + * Function: execute + * + * Takes the graph detail and configuration information within the facade + * and creates the resulting laid out graph within that facade for further + * use. + */ +mxHierarchicalLayoutStage.prototype.execute = function(parent) { }; diff --git a/src/js/layout/hierarchical/stage/mxMedianHybridCrossingReduction.js b/src/js/layout/hierarchical/stage/mxMedianHybridCrossingReduction.js new file mode 100644 index 0000000..997890e --- /dev/null +++ b/src/js/layout/hierarchical/stage/mxMedianHybridCrossingReduction.js @@ -0,0 +1,674 @@ +/** + * $Id: mxMedianHybridCrossingReduction.js,v 1.25 2012-06-07 11:16:41 david Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxMedianHybridCrossingReduction + * + * Sets the horizontal locations of node and edge dummy nodes on each layer. + * Uses median down and up weighings as well heuristic to straighten edges as + * far as possible. + * + * Constructor: mxMedianHybridCrossingReduction + * + * Creates a coordinate assignment. + * + * Arguments: + * + * intraCellSpacing - the minimum buffer between cells on the same rank + * interRankCellSpacing - the minimum distance between cells on adjacent ranks + * orientation - the position of the root node(s) relative to the graph + * initialX - the leftmost coordinate node placement starts at + */ +function mxMedianHybridCrossingReduction(layout) +{ + this.layout = layout; +}; + +/** + * Extends mxMedianHybridCrossingReduction. + */ +mxMedianHybridCrossingReduction.prototype = new mxHierarchicalLayoutStage(); +mxMedianHybridCrossingReduction.prototype.constructor = mxMedianHybridCrossingReduction; + +/** + * Variable: layout + * + * Reference to the enclosing <mxHierarchicalLayout>. + */ +mxMedianHybridCrossingReduction.prototype.layout = null; + +/** + * Variable: maxIterations + * + * The maximum number of iterations to perform whilst reducing edge + * crossings. Default is 24. + */ +mxMedianHybridCrossingReduction.prototype.maxIterations = 24; + +/** + * Variable: nestedBestRanks + * + * Stores each rank as a collection of cells in the best order found for + * each layer so far + */ +mxMedianHybridCrossingReduction.prototype.nestedBestRanks = null; + +/** + * Variable: currentBestCrossings + * + * The total number of crossings found in the best configuration so far + */ +mxMedianHybridCrossingReduction.prototype.currentBestCrossings = 0; + +/** + * Variable: iterationsWithoutImprovement + * + * The total number of crossings found in the best configuration so far + */ +mxMedianHybridCrossingReduction.prototype.iterationsWithoutImprovement = 0; + +/** + * Variable: maxNoImprovementIterations + * + * The total number of crossings found in the best configuration so far + */ +mxMedianHybridCrossingReduction.prototype.maxNoImprovementIterations = 2; + +/** + * Function: execute + * + * Performs a vertex ordering within ranks as described by Gansner et al + * 1993 + */ +mxMedianHybridCrossingReduction.prototype.execute = function(parent) +{ + var model = this.layout.getModel(); + + // Stores initial ordering as being the best one found so far + this.nestedBestRanks = []; + + for (var i = 0; i < model.ranks.length; i++) + { + this.nestedBestRanks[i] = model.ranks[i].slice(); + } + + var iterationsWithoutImprovement = 0; + var currentBestCrossings = this.calculateCrossings(model); + + for (var i = 0; i < this.maxIterations && + iterationsWithoutImprovement < this.maxNoImprovementIterations; i++) + { + this.weightedMedian(i, model); + this.transpose(i, model); + var candidateCrossings = this.calculateCrossings(model); + + if (candidateCrossings < currentBestCrossings) + { + currentBestCrossings = candidateCrossings; + iterationsWithoutImprovement = 0; + + // Store the current rankings as the best ones + for (var j = 0; j < this.nestedBestRanks.length; j++) + { + var rank = model.ranks[j]; + + for (var k = 0; k < rank.length; k++) + { + var cell = rank[k]; + this.nestedBestRanks[j][cell.getGeneralPurposeVariable(j)] = cell; + } + } + } + else + { + // Increase count of iterations where we haven't improved the + // layout + iterationsWithoutImprovement++; + + // Restore the best values to the cells + for (var j = 0; j < this.nestedBestRanks.length; j++) + { + var rank = model.ranks[j]; + + for (var k = 0; k < rank.length; k++) + { + var cell = rank[k]; + cell.setGeneralPurposeVariable(j, k); + } + } + } + + if (currentBestCrossings == 0) + { + // Do nothing further + break; + } + } + + // Store the best rankings but in the model + var ranks = []; + var rankList = []; + + for (var i = 0; i < model.maxRank + 1; i++) + { + rankList[i] = []; + ranks[i] = rankList[i]; + } + + for (var i = 0; i < this.nestedBestRanks.length; i++) + { + for (var j = 0; j < this.nestedBestRanks[i].length; j++) + { + rankList[i].push(this.nestedBestRanks[i][j]); + } + } + + model.ranks = ranks; +}; + + +/** + * Function: calculateCrossings + * + * Calculates the total number of edge crossing in the current graph. + * Returns the current number of edge crossings in the hierarchy graph + * model in the current candidate layout + * + * Parameters: + * + * model - the internal model describing the hierarchy + */ +mxMedianHybridCrossingReduction.prototype.calculateCrossings = function(model) +{ + var numRanks = model.ranks.length; + var totalCrossings = 0; + + for (var i = 1; i < numRanks; i++) + { + totalCrossings += this.calculateRankCrossing(i, model); + } + + return totalCrossings; +}; + +/** + * Function: calculateRankCrossing + * + * Calculates the number of edges crossings between the specified rank and + * the rank below it. Returns the number of edges crossings with the rank + * beneath + * + * Parameters: + * + * i - the topmost rank of the pair ( higher rank value ) + * model - the internal model describing the hierarchy + */ +mxMedianHybridCrossingReduction.prototype.calculateRankCrossing = function(i, model) +{ + var totalCrossings = 0; + var rank = model.ranks[i]; + var previousRank = model.ranks[i - 1]; + + // Create an array of connections between these two levels + var currentRankSize = rank.length; + var previousRankSize = previousRank.length; + var connections = []; + + for (var j = 0; j < currentRankSize; j++) + { + connections[j] = []; + } + + // Iterate over the top rank and fill in the connection information + for (var j = 0; j < rank.length; j++) + { + var node = rank[j]; + var rankPosition = node.getGeneralPurposeVariable(i); + var connectedCells = node.getPreviousLayerConnectedCells(i); + + for (var k = 0; k < connectedCells.length; k++) + { + var connectedNode = connectedCells[k]; + var otherCellRankPosition = connectedNode.getGeneralPurposeVariable(i - 1); + connections[rankPosition][otherCellRankPosition] = 201207; + } + } + + // Iterate through the connection matrix, crossing edges are + // indicated by other connected edges with a greater rank position + // on one rank and lower position on the other + for (var j = 0; j < currentRankSize; j++) + { + for (var k = 0; k < previousRankSize; k++) + { + if (connections[j][k] == 201207) + { + // Draw a grid of connections, crossings are top right + // and lower left from this crossing pair + for (var j2 = j + 1; j2 < currentRankSize; j2++) + { + for (var k2 = 0; k2 < k; k2++) + { + if (connections[j2][k2] == 201207) + { + totalCrossings++; + } + } + } + + for (var j2 = 0; j2 < j; j2++) + { + for (var k2 = k + 1; k2 < previousRankSize; k2++) + { + if (connections[j2][k2] == 201207) + { + totalCrossings++; + } + } + } + + } + } + } + + return totalCrossings / 2; +}; + +/** + * Function: transpose + * + * Takes each possible adjacent cell pair on each rank and checks if + * swapping them around reduces the number of crossing + * + * Parameters: + * + * mainLoopIteration - the iteration number of the main loop + * model - the internal model describing the hierarchy + */ +mxMedianHybridCrossingReduction.prototype.transpose = function(mainLoopIteration, model) +{ + var improved = true; + + // Track the number of iterations in case of looping + var count = 0; + var maxCount = 10; + while (improved && count++ < maxCount) + { + // On certain iterations allow allow swapping of cell pairs with + // equal edge crossings switched or not switched. This help to + // nudge a stuck layout into a lower crossing total. + var nudge = mainLoopIteration % 2 == 1 && count % 2 == 1; + improved = false; + + for (var i = 0; i < model.ranks.length; i++) + { + var rank = model.ranks[i]; + var orderedCells = []; + + for (var j = 0; j < rank.length; j++) + { + var cell = rank[j]; + var tempRank = cell.getGeneralPurposeVariable(i); + + // FIXME: Workaround to avoid negative tempRanks + if (tempRank < 0) + { + tempRank = j; + } + orderedCells[tempRank] = cell; + } + + var leftCellAboveConnections = null; + var leftCellBelowConnections = null; + var rightCellAboveConnections = null; + var rightCellBelowConnections = null; + + var leftAbovePositions = null; + var leftBelowPositions = null; + var rightAbovePositions = null; + var rightBelowPositions = null; + + var leftCell = null; + var rightCell = null; + + for (var j = 0; j < (rank.length - 1); j++) + { + // For each intra-rank adjacent pair of cells + // see if swapping them around would reduce the + // number of edges crossing they cause in total + // On every cell pair except the first on each rank, we + // can save processing using the previous values for the + // right cell on the new left cell + if (j == 0) + { + leftCell = orderedCells[j]; + leftCellAboveConnections = leftCell + .getNextLayerConnectedCells(i); + leftCellBelowConnections = leftCell + .getPreviousLayerConnectedCells(i); + leftAbovePositions = []; + leftBelowPositions = []; + + for (var k = 0; k < leftCellAboveConnections.length; k++) + { + leftAbovePositions[k] = leftCellAboveConnections[k].getGeneralPurposeVariable(i + 1); + } + + for (var k = 0; k < leftCellBelowConnections.length; k++) + { + leftBelowPositions[k] = leftCellBelowConnections[k].getGeneralPurposeVariable(i - 1); + } + } + else + { + leftCellAboveConnections = rightCellAboveConnections; + leftCellBelowConnections = rightCellBelowConnections; + leftAbovePositions = rightAbovePositions; + leftBelowPositions = rightBelowPositions; + leftCell = rightCell; + } + + rightCell = orderedCells[j + 1]; + rightCellAboveConnections = rightCell + .getNextLayerConnectedCells(i); + rightCellBelowConnections = rightCell + .getPreviousLayerConnectedCells(i); + + rightAbovePositions = []; + rightBelowPositions = []; + + for (var k = 0; k < rightCellAboveConnections.length; k++) + { + rightAbovePositions[k] = rightCellAboveConnections[k].getGeneralPurposeVariable(i + 1); + } + + for (var k = 0; k < rightCellBelowConnections.length; k++) + { + rightBelowPositions[k] = rightCellBelowConnections[k].getGeneralPurposeVariable(i - 1); + } + + var totalCurrentCrossings = 0; + var totalSwitchedCrossings = 0; + + for (var k = 0; k < leftAbovePositions.length; k++) + { + for (var ik = 0; ik < rightAbovePositions.length; ik++) + { + if (leftAbovePositions[k] > rightAbovePositions[ik]) + { + totalCurrentCrossings++; + } + + if (leftAbovePositions[k] < rightAbovePositions[ik]) + { + totalSwitchedCrossings++; + } + } + } + + for (var k = 0; k < leftBelowPositions.length; k++) + { + for (var ik = 0; ik < rightBelowPositions.length; ik++) + { + if (leftBelowPositions[k] > rightBelowPositions[ik]) + { + totalCurrentCrossings++; + } + + if (leftBelowPositions[k] < rightBelowPositions[ik]) + { + totalSwitchedCrossings++; + } + } + } + + if ((totalSwitchedCrossings < totalCurrentCrossings) || + (totalSwitchedCrossings == totalCurrentCrossings && + nudge)) + { + var temp = leftCell.getGeneralPurposeVariable(i); + leftCell.setGeneralPurposeVariable(i, rightCell + .getGeneralPurposeVariable(i)); + rightCell.setGeneralPurposeVariable(i, temp); + + // With this pair exchanged we have to switch all of + // values for the left cell to the right cell so the + // next iteration for this rank uses it as the left + // cell again + rightCellAboveConnections = leftCellAboveConnections; + rightCellBelowConnections = leftCellBelowConnections; + rightAbovePositions = leftAbovePositions; + rightBelowPositions = leftBelowPositions; + rightCell = leftCell; + + if (!nudge) + { + // Don't count nudges as improvement or we'll end + // up stuck in two combinations and not finishing + // as early as we should + improved = true; + } + } + } + } + } +}; + +/** + * Function: weightedMedian + * + * Sweeps up or down the layout attempting to minimise the median placement + * of connected cells on adjacent ranks + * + * Parameters: + * + * iteration - the iteration number of the main loop + * model - the internal model describing the hierarchy + */ +mxMedianHybridCrossingReduction.prototype.weightedMedian = function(iteration, model) +{ + // Reverse sweep direction each time through this method + var downwardSweep = (iteration % 2 == 0); + if (downwardSweep) + { + for (var j = model.maxRank - 1; j >= 0; j--) + { + this.medianRank(j, downwardSweep); + } + } + else + { + for (var j = 1; j < model.maxRank; j++) + { + this.medianRank(j, downwardSweep); + } + } +}; + +/** + * Function: medianRank + * + * Attempts to minimise the median placement of connected cells on this rank + * and one of the adjacent ranks + * + * Parameters: + * + * rankValue - the layer number of this rank + * downwardSweep - whether or not this is a downward sweep through the graph + */ +mxMedianHybridCrossingReduction.prototype.medianRank = function(rankValue, downwardSweep) +{ + var numCellsForRank = this.nestedBestRanks[rankValue].length; + var medianValues = []; + var reservedPositions = []; + + for (var i = 0; i < numCellsForRank; i++) + { + var cell = this.nestedBestRanks[rankValue][i]; + var sorterEntry = new MedianCellSorter(); + sorterEntry.cell = cell; + + // Flip whether or not equal medians are flipped on up and down + // sweeps + // TODO re-implement some kind of nudge + // medianValues[i].nudge = !downwardSweep; + var nextLevelConnectedCells; + + if (downwardSweep) + { + nextLevelConnectedCells = cell + .getNextLayerConnectedCells(rankValue); + } + else + { + nextLevelConnectedCells = cell + .getPreviousLayerConnectedCells(rankValue); + } + + var nextRankValue; + + if (downwardSweep) + { + nextRankValue = rankValue + 1; + } + else + { + nextRankValue = rankValue - 1; + } + + if (nextLevelConnectedCells != null + && nextLevelConnectedCells.length != 0) + { + sorterEntry.medianValue = this.medianValue( + nextLevelConnectedCells, nextRankValue); + medianValues.push(sorterEntry); + } + else + { + // Nodes with no adjacent vertices are flagged in the reserved array + // to indicate they should be left in their current position. + reservedPositions[cell.getGeneralPurposeVariable(rankValue)] = true; + } + } + + medianValues.sort(MedianCellSorter.prototype.compare); + + // Set the new position of each node within the rank using + // its temp variable + for (var i = 0; i < numCellsForRank; i++) + { + if (reservedPositions[i] == null) + { + var cell = medianValues.shift().cell; + cell.setGeneralPurposeVariable(rankValue, i); + } + } +}; + +/** + * Function: medianValue + * + * Calculates the median rank order positioning for the specified cell using + * the connected cells on the specified rank. Returns the median rank + * ordering value of the connected cells + * + * Parameters: + * + * connectedCells - the cells on the specified rank connected to the + * specified cell + * rankValue - the rank that the connected cell lie upon + */ +mxMedianHybridCrossingReduction.prototype.medianValue = function(connectedCells, rankValue) +{ + var medianValues = []; + var arrayCount = 0; + + for (var i = 0; i < connectedCells.length; i++) + { + var cell = connectedCells[i]; + medianValues[arrayCount++] = cell.getGeneralPurposeVariable(rankValue); + } + + // Sort() sorts lexicographically by default (i.e. 11 before 9) so force + // numerical order sort + medianValues.sort(function(a,b){return a - b;}); + + if (arrayCount % 2 == 1) + { + // For odd numbers of adjacent vertices return the median + return medianValues[Math.floor(arrayCount / 2)]; + } + else if (arrayCount == 2) + { + return ((medianValues[0] + medianValues[1]) / 2.0); + } + else + { + var medianPoint = arrayCount / 2; + var leftMedian = medianValues[medianPoint - 1] - medianValues[0]; + var rightMedian = medianValues[arrayCount - 1] + - medianValues[medianPoint]; + + return (medianValues[medianPoint - 1] * rightMedian + medianValues[medianPoint] + * leftMedian) + / (leftMedian + rightMedian); + } +}; + +/** + * Class: MedianCellSorter + * + * A utility class used to track cells whilst sorting occurs on the median + * values. Does not violate (x.compareTo(y)==0) == (x.equals(y)) + * + * Constructor: MedianCellSorter + * + * Constructs a new median cell sorter. + */ +function MedianCellSorter() +{ + // empty +}; + +/** + * Variable: medianValue + * + * The weighted value of the cell stored. + */ +MedianCellSorter.prototype.medianValue = 0; + +/** + * Variable: cell + * + * The cell whose median value is being calculated + */ +MedianCellSorter.prototype.cell = false; + +/** + * Function: compare + * + * Compares two MedianCellSorters. + */ +MedianCellSorter.prototype.compare = function(a, b) +{ + if (a != null && b != null) + { + if (b.medianValue > a.medianValue) + { + return -1; + } + else if (b.medianValue < a.medianValue) + { + return 1; + } + else + { + return 0; + } + } + else + { + return 0; + } +}; diff --git a/src/js/layout/hierarchical/stage/mxMinimumCycleRemover.js b/src/js/layout/hierarchical/stage/mxMinimumCycleRemover.js new file mode 100644 index 0000000..4f18f62 --- /dev/null +++ b/src/js/layout/hierarchical/stage/mxMinimumCycleRemover.js @@ -0,0 +1,131 @@ +/** + * $Id: mxMinimumCycleRemover.js,v 1.14 2010-01-04 11:18:26 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxMinimumCycleRemover + * + * An implementation of the first stage of the Sugiyama layout. Straightforward + * longest path calculation of layer assignment + * + * Constructor: mxMinimumCycleRemover + * + * Creates a cycle remover for the given internal model. + */ +function mxMinimumCycleRemover(layout) +{ + this.layout = layout; +}; + +/** + * Extends mxHierarchicalLayoutStage. + */ +mxMinimumCycleRemover.prototype = new mxHierarchicalLayoutStage(); +mxMinimumCycleRemover.prototype.constructor = mxMinimumCycleRemover; + +/** + * Variable: layout + * + * Reference to the enclosing <mxHierarchicalLayout>. + */ +mxMinimumCycleRemover.prototype.layout = null; + +/** + * Function: execute + * + * Takes the graph detail and configuration information within the facade + * and creates the resulting laid out graph within that facade for further + * use. + */ +mxMinimumCycleRemover.prototype.execute = function(parent) +{ + var model = this.layout.getModel(); + var seenNodes = new Object(); + var unseenNodes = mxUtils.clone(model.vertexMapper, null, true); + + // Perform a dfs through the internal model. If a cycle is found, + // reverse it. + var rootsArray = null; + + if (model.roots != null) + { + var modelRoots = model.roots; + rootsArray = []; + + for (var i = 0; i < modelRoots.length; i++) + { + var nodeId = mxCellPath.create(modelRoots[i]); + rootsArray[i] = model.vertexMapper[nodeId]; + } + } + + model.visit(function(parent, node, connectingEdge, layer, seen) + { + // Check if the cell is in it's own ancestor list, if so + // invert the connecting edge and reverse the target/source + // relationship to that edge in the parent and the cell + if (node.isAncestor(parent)) + { + connectingEdge.invert(); + mxUtils.remove(connectingEdge, parent.connectsAsSource); + parent.connectsAsTarget.push(connectingEdge); + mxUtils.remove(connectingEdge, node.connectsAsTarget); + node.connectsAsSource.push(connectingEdge); + } + + var cellId = mxCellPath.create(node.cell); + seenNodes[cellId] = node; + delete unseenNodes[cellId]; + }, rootsArray, true, null); + + var possibleNewRoots = null; + + if (unseenNodes.lenth > 0) + { + possibleNewRoots = mxUtils.clone(unseenNodes, null, true); + } + + // If there are any nodes that should be nodes that the dfs can miss + // these need to be processed with the dfs and the roots assigned + // correctly to form a correct internal model + var seenNodesCopy = mxUtils.clone(seenNodes, null, true); + + // Pick a random cell and dfs from it + model.visit(function(parent, node, connectingEdge, layer, seen) + { + // Check if the cell is in it's own ancestor list, if so + // invert the connecting edge and reverse the target/source + // relationship to that edge in the parent and the cell + if (node.isAncestor(parent)) + { + connectingEdge.invert(); + mxUtils.remove(connectingEdge, parent.connectsAsSource); + node.connectsAsSource.push(connectingEdge); + parent.connectsAsTarget.push(connectingEdge); + mxUtils.remove(connectingEdge, node.connectsAsTarget); + } + + var cellId = mxCellPath.create(node.cell); + seenNodes[cellId] = node; + delete unseenNodes[cellId]; + }, unseenNodes, true, seenNodesCopy); + + var graph = this.layout.getGraph(); + + if (possibleNewRoots != null && possibleNewRoots.length > 0) + { + var roots = model.roots; + + for (var i = 0; i < possibleNewRoots.length; i++) + { + var node = possibleNewRoots[i]; + var realNode = node.cell; + var numIncomingEdges = graph.getIncomingEdges(realNode).length; + + if (numIncomingEdges == 0) + { + roots.push(realNode); + } + } + } +}; diff --git a/src/js/layout/mxCircleLayout.js b/src/js/layout/mxCircleLayout.js new file mode 100644 index 0000000..e3e6ec1 --- /dev/null +++ b/src/js/layout/mxCircleLayout.js @@ -0,0 +1,203 @@ +/** + * $Id: mxCircleLayout.js,v 1.25 2012-08-22 17:26:12 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxCircleLayout + * + * Extends <mxGraphLayout> to implement a circluar layout for a given radius. + * The vertices do not need to be connected for this layout to work and all + * connections between vertices are not taken into account. + * + * Example: + * + * (code) + * var layout = new mxCircleLayout(graph); + * layout.execute(graph.getDefaultParent()); + * (end) + * + * Constructor: mxCircleLayout + * + * Constructs a new circular layout for the specified radius. + * + * Arguments: + * + * graph - <mxGraph> that contains the cells. + * radius - Optional radius as an int. Default is 100. + */ +function mxCircleLayout(graph, radius) +{ + mxGraphLayout.call(this, graph); + this.radius = (radius != null) ? radius : 100; +}; + +/** + * Extends mxGraphLayout. + */ +mxCircleLayout.prototype = new mxGraphLayout(); +mxCircleLayout.prototype.constructor = mxCircleLayout; + +/** + * Variable: radius + * + * Integer specifying the size of the radius. Default is 100. + */ +mxCircleLayout.prototype.radius = null; + +/** + * Variable: moveCircle + * + * Boolean specifying if the circle should be moved to the top, + * left corner specified by <x0> and <y0>. Default is false. + */ +mxCircleLayout.prototype.moveCircle = false; + +/** + * Variable: x0 + * + * Integer specifying the left coordinate of the circle. + * Default is 0. + */ +mxCircleLayout.prototype.x0 = 0; + +/** + * Variable: y0 + * + * Integer specifying the top coordinate of the circle. + * Default is 0. + */ +mxCircleLayout.prototype.y0 = 0; + +/** + * Variable: resetEdges + * + * Specifies if all edge points of traversed edges should be removed. + * Default is true. + */ +mxCircleLayout.prototype.resetEdges = true; + +/** + * Variable: disableEdgeStyle + * + * Specifies if the STYLE_NOEDGESTYLE flag should be set on edges that are + * modified by the result. Default is true. + */ +mxCircleLayout.prototype.disableEdgeStyle = true; + +/** + * Function: execute + * + * Implements <mxGraphLayout.execute>. + */ +mxCircleLayout.prototype.execute = function(parent) +{ + var model = this.graph.getModel(); + + // Moves the vertices to build a circle. Makes sure the + // radius is large enough for the vertices to not + // overlap + model.beginUpdate(); + try + { + // Gets all vertices inside the parent and finds + // the maximum dimension of the largest vertex + var max = 0; + var top = null; + var left = null; + var vertices = []; + var childCount = model.getChildCount(parent); + + for (var i = 0; i < childCount; i++) + { + var cell = model.getChildAt(parent, i); + + if (!this.isVertexIgnored(cell)) + { + vertices.push(cell); + var bounds = this.getVertexBounds(cell); + + if (top == null) + { + top = bounds.y; + } + else + { + top = Math.min(top, bounds.y); + } + + if (left == null) + { + left = bounds.x; + } + else + { + left = Math.min(left, bounds.x); + } + + max = Math.max(max, Math.max(bounds.width, bounds.height)); + } + else if (!this.isEdgeIgnored(cell)) + { + // Resets the points on the traversed edge + if (this.resetEdges) + { + this.graph.resetEdge(cell); + } + + if (this.disableEdgeStyle) + { + this.setEdgeStyleEnabled(cell, false); + } + } + } + + var r = this.getRadius(vertices.length, max); + + // Moves the circle to the specified origin + if (this.moveCircle) + { + left = this.x0; + top = this.y0; + } + + this.circle(vertices, r, left, top); + } + finally + { + model.endUpdate(); + } +}; + +/** + * Function: getRadius + * + * Returns the radius to be used for the given vertex count. Max is the maximum + * width or height of all vertices in the layout. + */ +mxCircleLayout.prototype.getRadius = function(count, max) +{ + return Math.max(count * max / Math.PI, this.radius); +}; + +/** + * Function: circle + * + * Executes the circular layout for the specified array + * of vertices and the given radius. This is called from + * <execute>. + */ +mxCircleLayout.prototype.circle = function(vertices, r, left, top) +{ + var vertexCount = vertices.length; + var phi = 2 * Math.PI / vertexCount; + + for (var i = 0; i < vertexCount; i++) + { + if (this.isVertexMovable(vertices[i])) + { + this.setVertexLocation(vertices[i], + left + r + r * Math.sin(i*phi), + top + r + r * Math.cos(i*phi)); + } + } +}; diff --git a/src/js/layout/mxCompactTreeLayout.js b/src/js/layout/mxCompactTreeLayout.js new file mode 100644 index 0000000..db6324c --- /dev/null +++ b/src/js/layout/mxCompactTreeLayout.js @@ -0,0 +1,995 @@ +/** + * $Id: mxCompactTreeLayout.js,v 1.57 2012-05-24 13:09:34 david Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxCompactTreeLayout + * + * Extends <mxGraphLayout> to implement a compact tree (Moen) algorithm. This + * layout is suitable for graphs that have no cycles (trees). Vertices that are + * not connected to the tree will be ignored by this layout. + * + * Example: + * + * (code) + * var layout = new mxCompactTreeLayout(graph); + * layout.execute(graph.getDefaultParent()); + * (end) + * + * Constructor: mxCompactTreeLayout + * + * Constructs a new compact tree layout for the specified graph + * and orientation. + */ +function mxCompactTreeLayout(graph, horizontal, invert) +{ + mxGraphLayout.call(this, graph); + this.horizontal = (horizontal != null) ? horizontal : true; + this.invert = (invert != null) ? invert : false; +}; + +/** + * Extends mxGraphLayout. + */ +mxCompactTreeLayout.prototype = new mxGraphLayout(); +mxCompactTreeLayout.prototype.constructor = mxCompactTreeLayout; + +/** + * Variable: horizontal + * + * Specifies the orientation of the layout. Default is true. + */ +mxCompactTreeLayout.prototype.horizontal = null; + +/** + * Variable: invert + * + * Specifies if edge directions should be inverted. Default is false. + */ +mxCompactTreeLayout.prototype.invert = null; + +/** + * Variable: resizeParent + * + * If the parents should be resized to match the width/height of the + * children. Default is true. + */ +mxCompactTreeLayout.prototype.resizeParent = true; + +/** + * Variable: groupPadding + * + * Padding added to resized parents + */ +mxCompactTreeLayout.prototype.groupPadding = 10; + +/** + * Variable: parentsChanged + * + * A set of the parents that need updating based on children + * process as part of the layout + */ +mxCompactTreeLayout.prototype.parentsChanged = null; + +/** + * Variable: moveTree + * + * Specifies if the tree should be moved to the top, left corner + * if it is inside a top-level layer. Default is false. + */ +mxCompactTreeLayout.prototype.moveTree = false; + +/** + * Variable: levelDistance + * + * Holds the levelDistance. Default is 10. + */ +mxCompactTreeLayout.prototype.levelDistance = 10; + +/** + * Variable: nodeDistance + * + * Holds the nodeDistance. Default is 20. + */ +mxCompactTreeLayout.prototype.nodeDistance = 20; + +/** + * Variable: resetEdges + * + * Specifies if all edge points of traversed edges should be removed. + * Default is true. + */ +mxCompactTreeLayout.prototype.resetEdges = true; + +/** + * Variable: prefHozEdgeSep + * + * The preferred horizontal distance between edges exiting a vertex + */ +mxCompactTreeLayout.prototype.prefHozEdgeSep = 5; + +/** + * Variable: prefVertEdgeOff + * + * The preferred vertical offset between edges exiting a vertex + */ +mxCompactTreeLayout.prototype.prefVertEdgeOff = 4; + +/** + * Variable: minEdgeJetty + * + * The minimum distance for an edge jetty from a vertex + */ +mxCompactTreeLayout.prototype.minEdgeJetty = 8; + +/** + * Variable: channelBuffer + * + * The size of the vertical buffer in the center of inter-rank channels + * where edge control points should not be placed + */ +mxCompactTreeLayout.prototype.channelBuffer = 4; + +/** + * Variable: edgeRouting + * + * Whether or not to apply the internal tree edge routing + */ +mxCompactTreeLayout.prototype.edgeRouting = true; + +/** + * Function: isVertexIgnored + * + * Returns a boolean indicating if the given <mxCell> should be ignored as a + * vertex. This returns true if the cell has no connections. + * + * Parameters: + * + * vertex - <mxCell> whose ignored state should be returned. + */ +mxCompactTreeLayout.prototype.isVertexIgnored = function(vertex) +{ + return mxGraphLayout.prototype.isVertexIgnored.apply(this, arguments) || + this.graph.getConnections(vertex).length == 0; +}; + +/** + * Function: isHorizontal + * + * Returns <horizontal>. + */ +mxCompactTreeLayout.prototype.isHorizontal = function() +{ + return this.horizontal; +}; + +/** + * Function: execute + * + * Implements <mxGraphLayout.execute>. + * + * If the parent has any connected edges, then it is used as the root of + * the tree. Else, <mxGraph.findTreeRoots> will be used to find a suitable + * root node within the set of children of the given parent. + * + * Parameters: + * + * parent - <mxCell> whose children should be laid out. + * root - Optional <mxCell> that will be used as the root of the tree. + */ +mxCompactTreeLayout.prototype.execute = function(parent, root) +{ + this.parent = parent; + var model = this.graph.getModel(); + + if (root == null) + { + // Takes the parent as the root if it has outgoing edges + if (this.graph.getEdges(parent, model.getParent(parent), + this.invert, !this.invert, false).length > 0) + { + root = parent; + } + + // Tries to find a suitable root in the parent's + // children + else + { + var roots = this.graph.findTreeRoots(parent, true, this.invert); + + if (roots.length > 0) + { + for (var i = 0; i < roots.length; i++) + { + if (!this.isVertexIgnored(roots[i]) && + this.graph.getEdges(roots[i], null, + this.invert, !this.invert, false).length > 0) + { + root = roots[i]; + break; + } + } + } + } + } + + if (root != null) + { + if (this.resizeParent) + { + this.parentsChanged = new Object(); + } + else + { + this.parentsChanged = null; + } + + model.beginUpdate(); + + try + { + var node = this.dfs(root, parent); + + if (node != null) + { + this.layout(node); + var x0 = this.graph.gridSize; + var y0 = x0; + + if (!this.moveTree) + { + var g = this.getVertexBounds(root); + + if (g != null) + { + x0 = g.x; + y0 = g.y; + } + } + + var bounds = null; + + if (this.isHorizontal()) + { + bounds = this.horizontalLayout(node, x0, y0); + } + else + { + bounds = this.verticalLayout(node, null, x0, y0); + } + + if (bounds != null) + { + var dx = 0; + var dy = 0; + + if (bounds.x < 0) + { + dx = Math.abs(x0 - bounds.x); + } + + if (bounds.y < 0) + { + dy = Math.abs(y0 - bounds.y); + } + + if (dx != 0 || dy != 0) + { + this.moveNode(node, dx, dy); + } + + if (this.resizeParent) + { + this.adjustParents(); + } + + if (this.edgeRouting) + { + // Iterate through all edges setting their positions + this.localEdgeProcessing(node); + } + } + } + } + finally + { + model.endUpdate(); + } + } +}; + +/** + * Function: moveNode + * + * Moves the specified node and all of its children by the given amount. + */ +mxCompactTreeLayout.prototype.moveNode = function(node, dx, dy) +{ + node.x += dx; + node.y += dy; + this.apply(node); + + var child = node.child; + + while (child != null) + { + this.moveNode(child, dx, dy); + child = child.next; + } +}; + +/** + * Function: dfs + * + * Does a depth first search starting at the specified cell. + * Makes sure the specified parent is never left by the + * algorithm. + */ +mxCompactTreeLayout.prototype.dfs = function(cell, parent, visited) +{ + visited = (visited != null) ? visited : []; + + var id = mxCellPath.create(cell); + var node = null; + + if (cell != null && visited[id] == null && !this.isVertexIgnored(cell)) + { + visited[id] = cell; + node = this.createNode(cell); + + var model = this.graph.getModel(); + var prev = null; + var out = this.graph.getEdges(cell, parent, this.invert, !this.invert, false, true); + var view = this.graph.getView(); + + for (var i = 0; i < out.length; i++) + { + var edge = out[i]; + + if (!this.isEdgeIgnored(edge)) + { + // Resets the points on the traversed edge + if (this.resetEdges) + { + this.setEdgePoints(edge, null); + } + + if (this.edgeRouting) + { + this.setEdgeStyleEnabled(edge, false); + this.setEdgePoints(edge, null); + } + + // Checks if terminal in same swimlane + var state = view.getState(edge); + var target = (state != null) ? state.getVisibleTerminal(this.invert) : view.getVisibleTerminal(edge, this.invert); + var tmp = this.dfs(target, parent, visited); + + if (tmp != null && model.getGeometry(target) != null) + { + if (prev == null) + { + node.child = tmp; + } + else + { + prev.next = tmp; + } + + prev = tmp; + } + } + } + } + + return node; +}; + +/** + * Function: layout + * + * Starts the actual compact tree layout algorithm + * at the given node. + */ +mxCompactTreeLayout.prototype.layout = function(node) +{ + if (node != null) + { + var child = node.child; + + while (child != null) + { + this.layout(child); + child = child.next; + } + + if (node.child != null) + { + this.attachParent(node, this.join(node)); + } + else + { + this.layoutLeaf(node); + } + } +}; + +/** + * Function: horizontalLayout + */ +mxCompactTreeLayout.prototype.horizontalLayout = function(node, x0, y0, bounds) +{ + node.x += x0 + node.offsetX; + node.y += y0 + node.offsetY; + bounds = this.apply(node, bounds); + var child = node.child; + + if (child != null) + { + bounds = this.horizontalLayout(child, node.x, node.y, bounds); + var siblingOffset = node.y + child.offsetY; + var s = child.next; + + while (s != null) + { + bounds = this.horizontalLayout(s, node.x + child.offsetX, siblingOffset, bounds); + siblingOffset += s.offsetY; + s = s.next; + } + } + + return bounds; +}; + +/** + * Function: verticalLayout + */ +mxCompactTreeLayout.prototype.verticalLayout = function(node, parent, x0, y0, bounds) +{ + node.x += x0 + node.offsetY; + node.y += y0 + node.offsetX; + bounds = this.apply(node, bounds); + var child = node.child; + + if (child != null) + { + bounds = this.verticalLayout(child, node, node.x, node.y, bounds); + var siblingOffset = node.x + child.offsetY; + var s = child.next; + + while (s != null) + { + bounds = this.verticalLayout(s, node, siblingOffset, node.y + child.offsetX, bounds); + siblingOffset += s.offsetY; + s = s.next; + } + } + + return bounds; +}; + +/** + * Function: attachParent + */ +mxCompactTreeLayout.prototype.attachParent = function(node, height) +{ + var x = this.nodeDistance + this.levelDistance; + var y2 = (height - node.width) / 2 - this.nodeDistance; + var y1 = y2 + node.width + 2 * this.nodeDistance - height; + + node.child.offsetX = x + node.height; + node.child.offsetY = y1; + + node.contour.upperHead = this.createLine(node.height, 0, + this.createLine(x, y1, node.contour.upperHead)); + node.contour.lowerHead = this.createLine(node.height, 0, + this.createLine(x, y2, node.contour.lowerHead)); +}; + +/** + * Function: layoutLeaf + */ +mxCompactTreeLayout.prototype.layoutLeaf = function(node) +{ + var dist = 2 * this.nodeDistance; + + node.contour.upperTail = this.createLine( + node.height + dist, 0); + node.contour.upperHead = node.contour.upperTail; + node.contour.lowerTail = this.createLine( + 0, -node.width - dist); + node.contour.lowerHead = this.createLine( + node.height + dist, 0, node.contour.lowerTail); +}; + +/** + * Function: join + */ +mxCompactTreeLayout.prototype.join = function(node) +{ + var dist = 2 * this.nodeDistance; + + var child = node.child; + node.contour = child.contour; + var h = child.width + dist; + var sum = h; + child = child.next; + + while (child != null) + { + var d = this.merge(node.contour, child.contour); + child.offsetY = d + h; + child.offsetX = 0; + h = child.width + dist; + sum += d + h; + child = child.next; + } + + return sum; +}; + +/** + * Function: merge + */ +mxCompactTreeLayout.prototype.merge = function(p1, p2) +{ + var x = 0; + var y = 0; + var total = 0; + + var upper = p1.lowerHead; + var lower = p2.upperHead; + + while (lower != null && upper != null) + { + var d = this.offset(x, y, lower.dx, lower.dy, + upper.dx, upper.dy); + y += d; + total += d; + + if (x + lower.dx <= upper.dx) + { + x += lower.dx; + y += lower.dy; + lower = lower.next; + } + else + { + x -= upper.dx; + y -= upper.dy; + upper = upper.next; + } + } + + if (lower != null) + { + var b = this.bridge(p1.upperTail, 0, 0, lower, x, y); + p1.upperTail = (b.next != null) ? p2.upperTail : b; + p1.lowerTail = p2.lowerTail; + } + else + { + var b = this.bridge(p2.lowerTail, x, y, upper, 0, 0); + + if (b.next == null) + { + p1.lowerTail = b; + } + } + + p1.lowerHead = p2.lowerHead; + + return total; +}; + +/** + * Function: offset + */ +mxCompactTreeLayout.prototype.offset = function(p1, p2, a1, a2, b1, b2) +{ + var d = 0; + + if (b1 <= p1 || p1 + a1 <= 0) + { + return 0; + } + + var t = b1 * a2 - a1 * b2; + + if (t > 0) + { + if (p1 < 0) + { + var s = p1 * a2; + d = s / a1 - p2; + } + else if (p1 > 0) + { + var s = p1 * b2; + d = s / b1 - p2; + } + else + { + d = -p2; + } + } + else if (b1 < p1 + a1) + { + var s = (b1 - p1) * a2; + d = b2 - (p2 + s / a1); + } + else if (b1 > p1 + a1) + { + var s = (a1 + p1) * b2; + d = s / b1 - (p2 + a2); + } + else + { + d = b2 - (p2 + a2); + } + + if (d > 0) + { + return d; + } + else + { + return 0; + } +}; + +/** + * Function: bridge + */ +mxCompactTreeLayout.prototype.bridge = function(line1, x1, y1, line2, x2, y2) +{ + var dx = x2 + line2.dx - x1; + var dy = 0; + var s = 0; + + if (line2.dx == 0) + { + dy = line2.dy; + } + else + { + s = dx * line2.dy; + dy = s / line2.dx; + } + + var r = this.createLine(dx, dy, line2.next); + line1.next = this.createLine(0, y2 + line2.dy - dy - y1, r); + + return r; +}; + +/** + * Function: createNode + */ +mxCompactTreeLayout.prototype.createNode = function(cell) +{ + var node = new Object(); + node.cell = cell; + node.x = 0; + node.y = 0; + node.width = 0; + node.height = 0; + + var geo = this.getVertexBounds(cell); + + if (geo != null) + { + if (this.isHorizontal()) + { + node.width = geo.height; + node.height = geo.width; + } + else + { + node.width = geo.width; + node.height = geo.height; + } + } + + node.offsetX = 0; + node.offsetY = 0; + node.contour = new Object(); + + return node; +}; + +/** + * Function: apply + */ +mxCompactTreeLayout.prototype.apply = function(node, bounds) +{ + var model = this.graph.getModel(); + var cell = node.cell; + var g = model.getGeometry(cell); + + if (cell != null && g != null) + { + if (this.isVertexMovable(cell)) + { + g = this.setVertexLocation(cell, node.x, node.y); + + if (this.resizeParent) + { + var parent = model.getParent(cell); + var id = mxCellPath.create(parent); + + // Implements set semantic + if (this.parentsChanged[id] == null) + { + this.parentsChanged[id] = parent; + } + } + } + + if (bounds == null) + { + bounds = new mxRectangle(g.x, g.y, g.width, g.height); + } + else + { + bounds = new mxRectangle(Math.min(bounds.x, g.x), + Math.min(bounds.y, g.y), + Math.max(bounds.x + bounds.width, g.x + g.width), + Math.max(bounds.y + bounds.height, g.y + g.height)); + } + } + + return bounds; +}; + +/** + * Function: createLine + */ +mxCompactTreeLayout.prototype.createLine = function(dx, dy, next) +{ + var line = new Object(); + line.dx = dx; + line.dy = dy; + line.next = next; + + return line; +}; + +/** + * Function: adjustParents + * + * Adjust parent cells whose child geometries have changed. The default + * implementation adjusts the group to just fit around the children with + * a padding. + */ +mxCompactTreeLayout.prototype.adjustParents = function() +{ + var tmp = []; + + for (var id in this.parentsChanged) + { + tmp.push(this.parentsChanged[id]); + } + + this.arrangeGroups(mxUtils.sortCells(tmp, true), this.groupPadding); +}; + +/** + * Function: localEdgeProcessing + * + * Moves the specified node and all of its children by the given amount. + */ +mxCompactTreeLayout.prototype.localEdgeProcessing = function(node) +{ + this.processNodeOutgoing(node); + var child = node.child; + + while (child != null) + { + this.localEdgeProcessing(child); + child = child.next; + } +}; + +/** + * Function: localEdgeProcessing + * + * Separates the x position of edges as they connect to vertices + */ +mxCompactTreeLayout.prototype.processNodeOutgoing = function(node) +{ + var child = node.child; + var parentCell = node.cell; + + var childCount = 0; + var sortedCells = []; + + while (child != null) + { + childCount++; + + var sortingCriterion = child.x; + + if (this.horizontal) + { + sortingCriterion = child.y; + } + + sortedCells.push(new WeightedCellSorter(child, sortingCriterion)); + child = child.next; + } + + sortedCells.sort(WeightedCellSorter.prototype.compare); + + var availableWidth = node.width; + + var requiredWidth = (childCount + 1) * this.prefHozEdgeSep; + + // Add a buffer on the edges of the vertex if the edge count allows + if (availableWidth > requiredWidth + (2 * this.prefHozEdgeSep)) + { + availableWidth -= 2 * this.prefHozEdgeSep; + } + + var edgeSpacing = availableWidth / childCount; + + var currentXOffset = edgeSpacing / 2.0; + + if (availableWidth > requiredWidth + (2 * this.prefHozEdgeSep)) + { + currentXOffset += this.prefHozEdgeSep; + } + + var currentYOffset = this.minEdgeJetty - this.prefVertEdgeOff; + var maxYOffset = 0; + + var parentBounds = this.getVertexBounds(parentCell); + child = node.child; + + for (var j = 0; j < sortedCells.length; j++) + { + var childCell = sortedCells[j].cell.cell; + var childBounds = this.getVertexBounds(childCell); + + var edges = this.graph.getEdgesBetween(parentCell, + childCell, false); + + var newPoints = []; + var x = 0; + var y = 0; + + for (var i = 0; i < edges.length; i++) + { + if (this.horizontal) + { + // Use opposite co-ords, calculation was done for + // + x = parentBounds.x + parentBounds.width; + y = parentBounds.y + currentXOffset; + newPoints.push(new mxPoint(x, y)); + x = parentBounds.x + parentBounds.width + + currentYOffset; + newPoints.push(new mxPoint(x, y)); + y = childBounds.y + childBounds.height / 2.0; + newPoints.push(new mxPoint(x, y)); + this.setEdgePoints(edges[i], newPoints); + } + else + { + x = parentBounds.x + currentXOffset; + y = parentBounds.y + parentBounds.height; + newPoints.push(new mxPoint(x, y)); + y = parentBounds.y + parentBounds.height + + currentYOffset; + newPoints.push(new mxPoint(x, y)); + x = childBounds.x + childBounds.width / 2.0; + newPoints.push(new mxPoint(x, y)); + this.setEdgePoints(edges[i], newPoints); + } + } + + if (j < childCount / 2) + { + currentYOffset += this.prefVertEdgeOff; + } + else if (j > childCount / 2) + { + currentYOffset -= this.prefVertEdgeOff; + } + // Ignore the case if equals, this means the second of 2 + // jettys with the same y (even number of edges) + + // pos[k * 2] = currentX; + currentXOffset += edgeSpacing; + // pos[k * 2 + 1] = currentYOffset; + + maxYOffset = Math.max(maxYOffset, currentYOffset); + } +}; + +/** + * Class: WeightedCellSorter + * + * A utility class used to track cells whilst sorting occurs on the weighted + * sum of their connected edges. Does not violate (x.compareTo(y)==0) == + * (x.equals(y)) + * + * Constructor: WeightedCellSorter + * + * Constructs a new weighted cell sorted for the given cell and weight. + */ +function WeightedCellSorter(cell, weightedValue) +{ + this.cell = cell; + this.weightedValue = weightedValue; +}; + +/** + * Variable: weightedValue + * + * The weighted value of the cell stored. + */ +WeightedCellSorter.prototype.weightedValue = 0; + +/** + * Variable: nudge + * + * Whether or not to flip equal weight values. + */ +WeightedCellSorter.prototype.nudge = false; + +/** + * Variable: visited + * + * Whether or not this cell has been visited in the current assignment. + */ +WeightedCellSorter.prototype.visited = false; + +/** + * Variable: rankIndex + * + * The index this cell is in the model rank. + */ +WeightedCellSorter.prototype.rankIndex = null; + +/** + * Variable: cell + * + * The cell whose median value is being calculated. + */ +WeightedCellSorter.prototype.cell = null; + +/** + * Function: compare + * + * Compares two WeightedCellSorters. + */ +WeightedCellSorter.prototype.compare = function(a, b) +{ + if (a != null && b != null) + { + if (b.weightedValue > a.weightedValue) + { + return 1; + } + else if (b.weightedValue < a.weightedValue) + { + return -1; + } + else + { + if (b.nudge) + { + return 1; + } + else + { + return -1; + } + } + } + else + { + return 0; + } +};
\ No newline at end of file diff --git a/src/js/layout/mxCompositeLayout.js b/src/js/layout/mxCompositeLayout.js new file mode 100644 index 0000000..2ceb5f5 --- /dev/null +++ b/src/js/layout/mxCompositeLayout.js @@ -0,0 +1,101 @@ +/** + * $Id: mxCompositeLayout.js,v 1.11 2010-01-02 09:45:15 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxCompositeLayout + * + * Allows to compose multiple layouts into a single layout. The master layout + * is the layout that handles move operations if another layout than the first + * element in <layouts> should be used. The <master> layout is not executed as + * the code assumes that it is part of <layouts>. + * + * Example: + * (code) + * var first = new mxFastOrganicLayout(graph); + * var second = new mxParallelEdgeLayout(graph); + * var layout = new mxCompositeLayout(graph, [first, second], first); + * layout.execute(graph.getDefaultParent()); + * (end) + * + * Constructor: mxCompositeLayout + * + * Constructs a new layout using the given layouts. The graph instance is + * required for creating the transaction that contains all layouts. + * + * Arguments: + * + * graph - Reference to the enclosing <mxGraph>. + * layouts - Array of <mxGraphLayouts>. + * master - Optional layout that handles moves. If no layout is given then + * the first layout of the above array is used to handle moves. + */ +function mxCompositeLayout(graph, layouts, master) +{ + mxGraphLayout.call(this, graph); + this.layouts = layouts; + this.master = master; +}; + +/** + * Extends mxGraphLayout. + */ +mxCompositeLayout.prototype = new mxGraphLayout(); +mxCompositeLayout.prototype.constructor = mxCompositeLayout; + +/** + * Variable: layouts + * + * Holds the array of <mxGraphLayouts> that this layout contains. + */ +mxCompositeLayout.prototype.layouts = null; + +/** + * Variable: layouts + * + * Reference to the <mxGraphLayouts> that handles moves. If this is null + * then the first layout in <layouts> is used. + */ +mxCompositeLayout.prototype.master = null; + +/** + * Function: moveCell + * + * Implements <mxGraphLayout.moveCell> by calling move on <master> or the first + * layout in <layouts>. + */ +mxCompositeLayout.prototype.moveCell = function(cell, x, y) +{ + if (this.master != null) + { + this.master.move.apply(this.master, arguments); + } + else + { + this.layouts[0].move.apply(this.layouts[0], arguments); + } +}; + +/** + * Function: execute + * + * Implements <mxGraphLayout.execute> by executing all <layouts> in a + * single transaction. + */ +mxCompositeLayout.prototype.execute = function(parent) +{ + var model = this.graph.getModel(); + + model.beginUpdate(); + try + { + for (var i = 0; i < this.layouts.length; i++) + { + this.layouts[i].execute.apply(this.layouts[i], arguments); + } + } + finally + { + model.endUpdate(); + } +}; diff --git a/src/js/layout/mxEdgeLabelLayout.js b/src/js/layout/mxEdgeLabelLayout.js new file mode 100644 index 0000000..2bfb3c2 --- /dev/null +++ b/src/js/layout/mxEdgeLabelLayout.js @@ -0,0 +1,165 @@ +/** + * $Id: mxEdgeLabelLayout.js,v 1.8 2010-01-04 11:18:25 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxEdgeLabelLayout + * + * Extends <mxGraphLayout> to implement an edge label layout. This layout + * makes use of cell states, which means the graph must be validated in + * a graph view (so that the label bounds are available) before this layout + * can be executed. + * + * Example: + * + * (code) + * var layout = new mxEdgeLabelLayout(graph); + * layout.execute(graph.getDefaultParent()); + * (end) + * + * Constructor: mxEdgeLabelLayout + * + * Constructs a new edge label layout. + * + * Arguments: + * + * graph - <mxGraph> that contains the cells. + */ +function mxEdgeLabelLayout(graph, radius) +{ + mxGraphLayout.call(this, graph); +}; + +/** + * Extends mxGraphLayout. + */ +mxEdgeLabelLayout.prototype = new mxGraphLayout(); +mxEdgeLabelLayout.prototype.constructor = mxEdgeLabelLayout; + +/** + * Function: execute + * + * Implements <mxGraphLayout.execute>. + */ +mxEdgeLabelLayout.prototype.execute = function(parent) +{ + var view = this.graph.view; + var model = this.graph.getModel(); + + // Gets all vertices and edges inside the parent + var edges = []; + var vertices = []; + var childCount = model.getChildCount(parent); + + for (var i = 0; i < childCount; i++) + { + var cell = model.getChildAt(parent, i); + var state = view.getState(cell); + + if (state != null) + { + if (!this.isVertexIgnored(cell)) + { + vertices.push(state); + } + else if (!this.isEdgeIgnored(cell)) + { + edges.push(state); + } + } + } + + this.placeLabels(vertices, edges); +}; + +/** + * Function: placeLabels + * + * Places the labels of the given edges. + */ +mxEdgeLabelLayout.prototype.placeLabels = function(v, e) +{ + var model = this.graph.getModel(); + + // Moves the vertices to build a circle. Makes sure the + // radius is large enough for the vertices to not + // overlap + model.beginUpdate(); + try + { + for (var i = 0; i < e.length; i++) + { + var edge = e[i]; + + if (edge != null && edge.text != null && + edge.text.boundingBox != null) + { + for (var j = 0; j < v.length; j++) + { + var vertex = v[j]; + + if (vertex != null) + { + this.avoid(edge, vertex); + } + } + } + } + } + finally + { + model.endUpdate(); + } +}; + +/** + * Function: avoid + * + * Places the labels of the given edges. + */ +mxEdgeLabelLayout.prototype.avoid = function(edge, vertex) +{ + var model = this.graph.getModel(); + var labRect = edge.text.boundingBox; + + if (mxUtils.intersects(labRect, vertex)) + { + var dy1 = -labRect.y - labRect.height + vertex.y; + var dy2 = -labRect.y + vertex.y + vertex.height; + + var dy = (Math.abs(dy1) < Math.abs(dy2)) ? dy1 : dy2; + + var dx1 = -labRect.x - labRect.width + vertex.x; + var dx2 = -labRect.x + vertex.x + vertex.width; + + var dx = (Math.abs(dx1) < Math.abs(dx2)) ? dx1 : dx2; + + if (Math.abs(dx) < Math.abs(dy)) + { + dy = 0; + } + else + { + dx = 0; + } + + var g = model.getGeometry(edge.cell); + + if (g != null) + { + g = g.clone(); + + if (g.offset != null) + { + g.offset.x += dx; + g.offset.y += dy; + } + else + { + g.offset = new mxPoint(dx, dy); + } + + model.setGeometry(edge.cell, g); + } + } +}; diff --git a/src/js/layout/mxFastOrganicLayout.js b/src/js/layout/mxFastOrganicLayout.js new file mode 100644 index 0000000..d7d6b5d --- /dev/null +++ b/src/js/layout/mxFastOrganicLayout.js @@ -0,0 +1,591 @@ +/** + * $Id: mxFastOrganicLayout.js,v 1.37 2011-04-28 13:14:55 david Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxFastOrganicLayout + * + * Extends <mxGraphLayout> to implement a fast organic layout algorithm. + * The vertices need to be connected for this layout to work, vertices + * with no connections are ignored. + * + * Example: + * + * (code) + * var layout = new mxFastOrganicLayout(graph); + * layout.execute(graph.getDefaultParent()); + * (end) + * + * Constructor: mxCompactTreeLayout + * + * Constructs a new fast organic layout for the specified graph. + */ +function mxFastOrganicLayout(graph) +{ + mxGraphLayout.call(this, graph); +}; + +/** + * Extends mxGraphLayout. + */ +mxFastOrganicLayout.prototype = new mxGraphLayout(); +mxFastOrganicLayout.prototype.constructor = mxFastOrganicLayout; + +/** + * Variable: useInputOrigin + * + * Specifies if the top left corner of the input cells should be the origin + * of the layout result. Default is true. + */ +mxFastOrganicLayout.prototype.useInputOrigin = true; + +/** + * Variable: resetEdges + * + * Specifies if all edge points of traversed edges should be removed. + * Default is true. + */ +mxFastOrganicLayout.prototype.resetEdges = true; + +/** + * Variable: disableEdgeStyle + * + * Specifies if the STYLE_NOEDGESTYLE flag should be set on edges that are + * modified by the result. Default is true. + */ +mxFastOrganicLayout.prototype.disableEdgeStyle = true; + +/** + * Variable: forceConstant + * + * The force constant by which the attractive forces are divided and the + * replusive forces are multiple by the square of. The value equates to the + * average radius there is of free space around each node. Default is 50. + */ +mxFastOrganicLayout.prototype.forceConstant = 50; + +/** + * Variable: forceConstantSquared + * + * Cache of <forceConstant>^2 for performance. + */ +mxFastOrganicLayout.prototype.forceConstantSquared = 0; + +/** + * Variable: minDistanceLimit + * + * Minimal distance limit. Default is 2. Prevents of + * dividing by zero. + */ +mxFastOrganicLayout.prototype.minDistanceLimit = 2; + +/** + * Variable: minDistanceLimit + * + * Minimal distance limit. Default is 2. Prevents of + * dividing by zero. + */ +mxFastOrganicLayout.prototype.maxDistanceLimit = 500; + +/** + * Variable: minDistanceLimitSquared + * + * Cached version of <minDistanceLimit> squared. + */ +mxFastOrganicLayout.prototype.minDistanceLimitSquared = 4; + +/** + * Variable: initialTemp + * + * Start value of temperature. Default is 200. + */ +mxFastOrganicLayout.prototype.initialTemp = 200; + +/** + * Variable: temperature + * + * Temperature to limit displacement at later stages of layout. + */ +mxFastOrganicLayout.prototype.temperature = 0; + +/** + * Variable: maxIterations + * + * Total number of iterations to run the layout though. + */ +mxFastOrganicLayout.prototype.maxIterations = 0; + +/** + * Variable: iteration + * + * Current iteration count. + */ +mxFastOrganicLayout.prototype.iteration = 0; + +/** + * Variable: vertexArray + * + * An array of all vertices to be laid out. + */ +mxFastOrganicLayout.prototype.vertexArray; + +/** + * Variable: dispX + * + * An array of locally stored X co-ordinate displacements for the vertices. + */ +mxFastOrganicLayout.prototype.dispX; + +/** + * Variable: dispY + * + * An array of locally stored Y co-ordinate displacements for the vertices. + */ +mxFastOrganicLayout.prototype.dispY; + +/** + * Variable: cellLocation + * + * An array of locally stored co-ordinate positions for the vertices. + */ +mxFastOrganicLayout.prototype.cellLocation; + +/** + * Variable: radius + * + * The approximate radius of each cell, nodes only. + */ +mxFastOrganicLayout.prototype.radius; + +/** + * Variable: radiusSquared + * + * The approximate radius squared of each cell, nodes only. + */ +mxFastOrganicLayout.prototype.radiusSquared; + +/** + * Variable: isMoveable + * + * Array of booleans representing the movable states of the vertices. + */ +mxFastOrganicLayout.prototype.isMoveable; + +/** + * Variable: neighbours + * + * Local copy of cell neighbours. + */ +mxFastOrganicLayout.prototype.neighbours; + +/** + * Variable: indices + * + * Hashtable from cells to local indices. + */ +mxFastOrganicLayout.prototype.indices; + +/** + * Variable: allowedToRun + * + * Boolean flag that specifies if the layout is allowed to run. If this is + * set to false, then the layout exits in the following iteration. + */ +mxFastOrganicLayout.prototype.allowedToRun = true; + +/** + * Function: isVertexIgnored + * + * Returns a boolean indicating if the given <mxCell> should be ignored as a + * vertex. This returns true if the cell has no connections. + * + * Parameters: + * + * vertex - <mxCell> whose ignored state should be returned. + */ +mxFastOrganicLayout.prototype.isVertexIgnored = function(vertex) +{ + return mxGraphLayout.prototype.isVertexIgnored.apply(this, arguments) || + this.graph.getConnections(vertex).length == 0; +}; + +/** + * Function: execute + * + * Implements <mxGraphLayout.execute>. This operates on all children of the + * given parent where <isVertexIgnored> returns false. + */ +mxFastOrganicLayout.prototype.execute = function(parent) +{ + var model = this.graph.getModel(); + this.vertexArray = []; + var cells = this.graph.getChildVertices(parent); + + for (var i = 0; i < cells.length; i++) + { + if (!this.isVertexIgnored(cells[i])) + { + this.vertexArray.push(cells[i]); + } + } + + var initialBounds = (this.useInputOrigin) ? + this.graph.view.getBounds(this.vertexArray) : + null; + var n = this.vertexArray.length; + + this.indices = []; + this.dispX = []; + this.dispY = []; + this.cellLocation = []; + this.isMoveable = []; + this.neighbours = []; + this.radius = []; + this.radiusSquared = []; + + if (this.forceConstant < 0.001) + { + this.forceConstant = 0.001; + } + + this.forceConstantSquared = this.forceConstant * this.forceConstant; + + // Create a map of vertices first. This is required for the array of + // arrays called neighbours which holds, for each vertex, a list of + // ints which represents the neighbours cells to that vertex as + // the indices into vertexArray + for (var i = 0; i < this.vertexArray.length; i++) + { + var vertex = this.vertexArray[i]; + this.cellLocation[i] = []; + + // Set up the mapping from array indices to cells + var id = mxCellPath.create(vertex); + this.indices[id] = i; + var bounds = this.getVertexBounds(vertex); + + // Set the X,Y value of the internal version of the cell to + // the center point of the vertex for better positioning + var width = bounds.width; + var height = bounds.height; + + // Randomize (0, 0) locations + var x = bounds.x; + var y = bounds.y; + + this.cellLocation[i][0] = x + width / 2.0; + this.cellLocation[i][1] = y + height / 2.0; + this.radius[i] = Math.min(width, height); + this.radiusSquared[i] = this.radius[i] * this.radius[i]; + } + + // Moves cell location back to top-left from center locations used in + // algorithm, resetting the edge points is part of the transaction + model.beginUpdate(); + try + { + for (var i = 0; i < n; i++) + { + this.dispX[i] = 0; + this.dispY[i] = 0; + this.isMoveable[i] = this.isVertexMovable(this.vertexArray[i]); + + // Get lists of neighbours to all vertices, translate the cells + // obtained in indices into vertexArray and store as an array + // against the orginial cell index + var edges = this.graph.getConnections(this.vertexArray[i], parent); + var cells = this.graph.getOpposites(edges, this.vertexArray[i]); + this.neighbours[i] = []; + + for (var j = 0; j < cells.length; j++) + { + // Resets the points on the traversed edge + if (this.resetEdges) + { + this.graph.resetEdge(edges[j]); + } + + if (this.disableEdgeStyle) + { + this.setEdgeStyleEnabled(edges[j], false); + } + + // Looks the cell up in the indices dictionary + var id = mxCellPath.create(cells[j]); + var index = this.indices[id]; + + // Check the connected cell in part of the vertex list to be + // acted on by this layout + if (index != null) + { + this.neighbours[i][j] = index; + } + + // Else if index of the other cell doesn't correspond to + // any cell listed to be acted upon in this layout. Set + // the index to the value of this vertex (a dummy self-loop) + // so the attraction force of the edge is not calculated + else + { + this.neighbours[i][j] = i; + } + } + } + this.temperature = this.initialTemp; + + // If max number of iterations has not been set, guess it + if (this.maxIterations == 0) + { + this.maxIterations = 20 * Math.sqrt(n); + } + + // Main iteration loop + for (this.iteration = 0; this.iteration < this.maxIterations; this.iteration++) + { + if (!this.allowedToRun) + { + return; + } + + // Calculate repulsive forces on all vertices + this.calcRepulsion(); + + // Calculate attractive forces through edges + this.calcAttraction(); + + this.calcPositions(); + this.reduceTemperature(); + } + + var minx = null; + var miny = null; + + for (var i = 0; i < this.vertexArray.length; i++) + { + var vertex = this.vertexArray[i]; + + if (this.isVertexMovable(vertex)) + { + var bounds = this.getVertexBounds(vertex); + + if (bounds != null) + { + this.cellLocation[i][0] -= bounds.width / 2.0; + this.cellLocation[i][1] -= bounds.height / 2.0; + + var x = this.graph.snap(this.cellLocation[i][0]); + var y = this.graph.snap(this.cellLocation[i][1]); + + this.setVertexLocation(vertex, x, y); + + if (minx == null) + { + minx = x; + } + else + { + minx = Math.min(minx, x); + } + + if (miny == null) + { + miny = y; + } + else + { + miny = Math.min(miny, y); + } + } + } + } + + // Modifies the cloned geometries in-place. Not needed + // to clone the geometries again as we're in the same + // undoable change. + var dx = -(minx || 0) + 1; + var dy = -(miny || 0) + 1; + + if (initialBounds != null) + { + dx += initialBounds.x; + dy += initialBounds.y; + } + + this.graph.moveCells(this.vertexArray, dx, dy); + } + finally + { + model.endUpdate(); + } +}; + +/** + * Function: calcPositions + * + * Takes the displacements calculated for each cell and applies them to the + * local cache of cell positions. Limits the displacement to the current + * temperature. + */ +mxFastOrganicLayout.prototype.calcPositions = function() +{ + for (var index = 0; index < this.vertexArray.length; index++) + { + if (this.isMoveable[index]) + { + // Get the distance of displacement for this node for this + // iteration + var deltaLength = Math.sqrt(this.dispX[index] * this.dispX[index] + + this.dispY[index] * this.dispY[index]); + + if (deltaLength < 0.001) + { + deltaLength = 0.001; + } + + // Scale down by the current temperature if less than the + // displacement distance + var newXDisp = this.dispX[index] / deltaLength + * Math.min(deltaLength, this.temperature); + + var newYDisp = this.dispY[index] / deltaLength + * Math.min(deltaLength, this.temperature); + + // reset displacements + this.dispX[index] = 0; + this.dispY[index] = 0; + + // Update the cached cell locations + this.cellLocation[index][0] += newXDisp; + this.cellLocation[index][1] += newYDisp; + } + } +}; + +/** + * Function: calcAttraction + * + * Calculates the attractive forces between all laid out nodes linked by + * edges + */ +mxFastOrganicLayout.prototype.calcAttraction = function() +{ + // Check the neighbours of each vertex and calculate the attractive + // force of the edge connecting them + for (var i = 0; i < this.vertexArray.length; i++) + { + for (var k = 0; k < this.neighbours[i].length; k++) + { + // Get the index of the othe cell in the vertex array + var j = this.neighbours[i][k]; + + // Do not proceed self-loops + if (i != j && + this.isMoveable[i] && + this.isMoveable[j]) + { + var xDelta = this.cellLocation[i][0] - this.cellLocation[j][0]; + var yDelta = this.cellLocation[i][1] - this.cellLocation[j][1]; + + // The distance between the nodes + var deltaLengthSquared = xDelta * xDelta + yDelta + * yDelta - this.radiusSquared[i] - this.radiusSquared[j]; + + if (deltaLengthSquared < this.minDistanceLimitSquared) + { + deltaLengthSquared = this.minDistanceLimitSquared; + } + + var deltaLength = Math.sqrt(deltaLengthSquared); + var force = (deltaLengthSquared) / this.forceConstant; + + var displacementX = (xDelta / deltaLength) * force; + var displacementY = (yDelta / deltaLength) * force; + + this.dispX[i] -= displacementX; + this.dispY[i] -= displacementY; + + this.dispX[j] += displacementX; + this.dispY[j] += displacementY; + } + } + } +}; + +/** + * Function: calcRepulsion + * + * Calculates the repulsive forces between all laid out nodes + */ +mxFastOrganicLayout.prototype.calcRepulsion = function() +{ + var vertexCount = this.vertexArray.length; + + for (var i = 0; i < vertexCount; i++) + { + for (var j = i; j < vertexCount; j++) + { + // Exits if the layout is no longer allowed to run + if (!this.allowedToRun) + { + return; + } + + if (j != i && + this.isMoveable[i] && + this.isMoveable[j]) + { + var xDelta = this.cellLocation[i][0] - this.cellLocation[j][0]; + var yDelta = this.cellLocation[i][1] - this.cellLocation[j][1]; + + if (xDelta == 0) + { + xDelta = 0.01 + Math.random(); + } + + if (yDelta == 0) + { + yDelta = 0.01 + Math.random(); + } + + // Distance between nodes + var deltaLength = Math.sqrt((xDelta * xDelta) + + (yDelta * yDelta)); + var deltaLengthWithRadius = deltaLength - this.radius[i] + - this.radius[j]; + + if (deltaLengthWithRadius > this.maxDistanceLimit) + { + // Ignore vertices too far apart + continue; + } + + if (deltaLengthWithRadius < this.minDistanceLimit) + { + deltaLengthWithRadius = this.minDistanceLimit; + } + + var force = this.forceConstantSquared / deltaLengthWithRadius; + + var displacementX = (xDelta / deltaLength) * force; + var displacementY = (yDelta / deltaLength) * force; + + this.dispX[i] += displacementX; + this.dispY[i] += displacementY; + + this.dispX[j] -= displacementX; + this.dispY[j] -= displacementY; + } + } + } +}; + +/** + * Function: reduceTemperature + * + * Reduces the temperature of the layout from an initial setting in a linear + * fashion to zero. + */ +mxFastOrganicLayout.prototype.reduceTemperature = function() +{ + this.temperature = this.initialTemp * (1.0 - this.iteration / this.maxIterations); +}; diff --git a/src/js/layout/mxGraphLayout.js b/src/js/layout/mxGraphLayout.js new file mode 100644 index 0000000..c9f5f32 --- /dev/null +++ b/src/js/layout/mxGraphLayout.js @@ -0,0 +1,503 @@ +/** + * $Id: mxGraphLayout.js,v 1.48 2012-08-21 17:22:21 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxGraphLayout + * + * Base class for all layout algorithms in mxGraph. Main public functions are + * <move> for handling a moved cell within a layouted parent, and <execute> for + * running the layout on a given parent cell. + * + * Known Subclasses: + * + * <mxCircleLayout>, <mxCompactTreeLayout>, <mxCompositeLayout>, + * <mxFastOrganicLayout>, <mxParallelEdgeLayout>, <mxPartitionLayout>, + * <mxStackLayout> + * + * Constructor: mxGraphLayout + * + * Constructs a new layout using the given layouts. + * + * Arguments: + * + * graph - Enclosing + */ +function mxGraphLayout(graph) +{ + this.graph = graph; +}; + +/** + * Variable: graph + * + * Reference to the enclosing <mxGraph>. + */ +mxGraphLayout.prototype.graph = null; + +/** + * Variable: useBoundingBox + * + * Boolean indicating if the bounding box of the label should be used if + * its available. Default is true. + */ +mxGraphLayout.prototype.useBoundingBox = true; + +/** + * Variable: parent + * + * The parent cell of the layout, if any + */ +mxGraphLayout.prototype.parent = null; + +/** + * Function: moveCell + * + * Notified when a cell is being moved in a parent that has automatic + * layout to update the cell state (eg. index) so that the outcome of the + * layout will position the vertex as close to the point (x, y) as + * possible. + * + * Empty implementation. + * + * Parameters: + * + * cell - <mxCell> which has been moved. + * x - X-coordinate of the new cell location. + * y - Y-coordinate of the new cell location. + */ +mxGraphLayout.prototype.moveCell = function(cell, x, y) { }; + +/** + * Function: execute + * + * Executes the layout algorithm for the children of the given parent. + * + * Parameters: + * + * parent - <mxCell> whose children should be layed out. + */ +mxGraphLayout.prototype.execute = function(parent) { }; + +/** + * Function: getGraph + * + * Returns the graph that this layout operates on. + */ +mxGraphLayout.prototype.getGraph = function() +{ + return this.graph; +}; + +/** + * Function: getConstraint + * + * Returns the constraint for the given key and cell. The optional edge and + * source arguments are used to return inbound and outgoing routing- + * constraints for the given edge and vertex. This implementation always + * returns the value for the given key in the style of the given cell. + * + * Parameters: + * + * key - Key of the constraint to be returned. + * cell - <mxCell> whose constraint should be returned. + * edge - Optional <mxCell> that represents the connection whose constraint + * should be returned. Default is null. + * source - Optional boolean that specifies if the connection is incoming + * or outgoing. Default is null. + */ +mxGraphLayout.prototype.getConstraint = function(key, cell, edge, source) +{ + var state = this.graph.view.getState(cell); + var style = (state != null) ? state.style : this.graph.getCellStyle(cell); + + return (style != null) ? style[key] : null; +}; + +/** + * Function: traverse + * + * Traverses the (directed) graph invoking the given function for each + * visited vertex and edge. The function is invoked with the current vertex + * and the incoming edge as a parameter. This implementation makes sure + * each vertex is only visited once. The function may return false if the + * traversal should stop at the given vertex. + * + * Example: + * + * (code) + * mxLog.show(); + * var cell = graph.getSelectionCell(); + * graph.traverse(cell, false, function(vertex, edge) + * { + * mxLog.debug(graph.getLabel(vertex)); + * }); + * (end) + * + * Parameters: + * + * vertex - <mxCell> that represents the vertex where the traversal starts. + * directed - Optional boolean indicating if edges should only be traversed + * from source to target. Default is true. + * func - Visitor function that takes the current vertex and the incoming + * edge as arguments. The traversal stops if the function returns false. + * edge - Optional <mxCell> that represents the incoming edge. This is + * null for the first step of the traversal. + * visited - Optional array of cell paths for the visited cells. + */ +mxGraphLayout.traverse = function(vertex, directed, func, edge, visited) +{ + if (func != null && vertex != null) + { + directed = (directed != null) ? directed : true; + visited = visited || []; + var id = mxCellPath.create(vertex); + + if (visited[id] == null) + { + visited[id] = vertex; + var result = func(vertex, edge); + + if (result == null || result) + { + var edgeCount = this.graph.model.getEdgeCount(vertex); + + if (edgeCount > 0) + { + for (var i = 0; i < edgeCount; i++) + { + var e = this.graph.model.getEdgeAt(vertex, i); + var isSource = this.graph.model.getTerminal(e, true) == vertex; + + if (!directed || isSource) + { + var next = this.graph.view.getVisibleTerminal(e, !isSource); + this.traverse(next, directed, func, e, visited); + } + } + } + } + } + } +}; + +/** + * Function: isVertexMovable + * + * Returns a boolean indicating if the given <mxCell> is movable or + * bendable by the algorithm. This implementation returns true if the given + * cell is movable in the graph. + * + * Parameters: + * + * cell - <mxCell> whose movable state should be returned. + */ +mxGraphLayout.prototype.isVertexMovable = function(cell) +{ + return this.graph.isCellMovable(cell); +}; + +/** + * Function: isVertexIgnored + * + * Returns a boolean indicating if the given <mxCell> should be ignored by + * the algorithm. This implementation returns false for all vertices. + * + * Parameters: + * + * vertex - <mxCell> whose ignored state should be returned. + */ +mxGraphLayout.prototype.isVertexIgnored = function(vertex) +{ + return !this.graph.getModel().isVertex(vertex) || + !this.graph.isCellVisible(vertex); +}; + +/** + * Function: isEdgeIgnored + * + * Returns a boolean indicating if the given <mxCell> should be ignored by + * the algorithm. This implementation returns false for all vertices. + * + * Parameters: + * + * cell - <mxCell> whose ignored state should be returned. + */ +mxGraphLayout.prototype.isEdgeIgnored = function(edge) +{ + var model = this.graph.getModel(); + + return !model.isEdge(edge) || + !this.graph.isCellVisible(edge) || + model.getTerminal(edge, true) == null || + model.getTerminal(edge, false) == null; +}; + +/** + * Function: setEdgeStyleEnabled + * + * Disables or enables the edge style of the given edge. + */ +mxGraphLayout.prototype.setEdgeStyleEnabled = function(edge, value) +{ + this.graph.setCellStyles(mxConstants.STYLE_NOEDGESTYLE, + (value) ? '0' : '1', [edge]); +}; + +/** + * Function: setOrthogonalEdge + * + * Disables or enables orthogonal end segments of the given edge. + */ +mxGraphLayout.prototype.setOrthogonalEdge = function(edge, value) +{ + this.graph.setCellStyles(mxConstants.STYLE_ORTHOGONAL, + (value) ? '1' : '0', [edge]); +}; + +/** + * Function: getParentOffset + * + * Determines the offset of the given parent to the parent + * of the layout + */ +mxGraphLayout.prototype.getParentOffset = function(parent) +{ + var result = new mxPoint(); + + if (parent != null && parent != this.parent) + { + var model = this.graph.getModel(); + + if (model.isAncestor(this.parent, parent)) + { + var parentGeo = model.getGeometry(parent); + + while (parent != this.parent) + { + result.x = result.x + parentGeo.x; + result.y = result.y + parentGeo.y; + + parent = model.getParent(parent);; + parentGeo = model.getGeometry(parent); + } + } + } + + return result; +}; + +/** + * Function: setEdgePoints + * + * Replaces the array of mxPoints in the geometry of the given edge + * with the given array of mxPoints. + */ +mxGraphLayout.prototype.setEdgePoints = function(edge, points) +{ + if (edge != null) + { + var model = this.graph.model; + var geometry = model.getGeometry(edge); + + if (geometry == null) + { + geometry = new mxGeometry(); + geometry.setRelative(true); + } + else + { + geometry = geometry.clone(); + } + + if (this.parent != null && points != null) + { + var parent = model.getParent(edge); + + var parentOffset = this.getParentOffset(parent); + + for (var i = 0; i < points.length; i++) + { + points[i].x = points[i].x - parentOffset.x; + points[i].y = points[i].y - parentOffset.y; + } + } + + geometry.points = points; + model.setGeometry(edge, geometry); + } +}; + +/** + * Function: setVertexLocation + * + * Sets the new position of the given cell taking into account the size of + * the bounding box if <useBoundingBox> is true. The change is only carried + * out if the new location is not equal to the existing location, otherwise + * the geometry is not replaced with an updated instance. The new or old + * bounds are returned (including overlapping labels). + * + * Parameters: + * + * cell - <mxCell> whose geometry is to be set. + * x - Integer that defines the x-coordinate of the new location. + * y - Integer that defines the y-coordinate of the new location. + */ +mxGraphLayout.prototype.setVertexLocation = function(cell, x, y) +{ + var model = this.graph.getModel(); + var geometry = model.getGeometry(cell); + var result = null; + + if (geometry != null) + { + result = new mxRectangle(x, y, geometry.width, geometry.height); + + // Checks for oversize labels and shifts the result + // TODO: Use mxUtils.getStringSize for label bounds + if (this.useBoundingBox) + { + var state = this.graph.getView().getState(cell); + + if (state != null && state.text != null && state.text.boundingBox != null) + { + var scale = this.graph.getView().scale; + var box = state.text.boundingBox; + + if (state.text.boundingBox.x < state.x) + { + x += (state.x - box.x) / scale; + result.width = box.width; + } + + if (state.text.boundingBox.y < state.y) + { + y += (state.y - box.y) / scale; + result.height = box.height; + } + } + } + + if (this.parent != null) + { + var parent = model.getParent(cell); + + if (parent != null && parent != this.parent) + { + var parentOffset = this.getParentOffset(parent); + + x = x - parentOffset.x; + y = y - parentOffset.y; + } + } + + if (geometry.x != x || geometry.y != y) + { + geometry = geometry.clone(); + geometry.x = x; + geometry.y = y; + + model.setGeometry(cell, geometry); + } + } + + return result; +}; + +/** + * Function: getVertexBounds + * + * Returns an <mxRectangle> that defines the bounds of the given cell or + * the bounding box if <useBoundingBox> is true. + */ +mxGraphLayout.prototype.getVertexBounds = function(cell) +{ + var geo = this.graph.getModel().getGeometry(cell); + + // Checks for oversize label bounding box and corrects + // the return value accordingly + // TODO: Use mxUtils.getStringSize for label bounds + if (this.useBoundingBox) + { + var state = this.graph.getView().getState(cell); + + if (state != null && state.text != null && state.text.boundingBox != null) + { + var scale = this.graph.getView().scale; + var tmp = state.text.boundingBox; + + var dx0 = Math.max(state.x - tmp.x, 0) / scale; + var dy0 = Math.max(state.y - tmp.y, 0) / scale; + var dx1 = Math.max((tmp.x + tmp.width) - (state.x + state.width), 0) / scale; + var dy1 = Math.max((tmp.y + tmp.height) - (state.y + state.height), 0) / scale; + + geo = new mxRectangle(geo.x - dx0, geo.y - dy0, + geo.width + dx0 + dx1, geo.height + dy0 + dy1); + } + } + + if (this.parent != null) + { + var parent = this.graph.getModel().getParent(cell); + geo = geo.clone(); + + if (parent != null && parent != this.parent) + { + var parentOffset = this.getParentOffset(parent); + geo.x = geo.x + parentOffset.x; + geo.y = geo.y + parentOffset.y; + } + } + + return new mxRectangle(geo.x, geo.y, geo.width, geo.height); +}; + +/** + * Function: arrangeGroups + * + * Updates the bounds of the given groups to include all children. Call + * this with the groups in parent to child order, top-most group first, eg. + * + * arrangeGroups(graph, mxUtils.sortCells(Arrays.asList( + * new Object[] { v1, v3 }), true).toArray(), 10); + */ +mxGraphLayout.prototype.arrangeGroups = function(groups, border) +{ + this.graph.getModel().beginUpdate(); + try + { + for (var i = groups.length - 1; i >= 0; i--) + { + var group = groups[i]; + var children = this.graph.getChildVertices(group); + var bounds = this.graph.getBoundingBoxFromGeometry(children); + var geometry = this.graph.getCellGeometry(group); + var left = 0; + var top = 0; + + // Adds the size of the title area for swimlanes + if (this.graph.isSwimlane(group)) + { + var size = this.graph.getStartSize(group); + left = size.width; + top = size.height; + } + + if (bounds != null && geometry != null) + { + geometry = geometry.clone(); + geometry.x = geometry.x + bounds.x - border - left; + geometry.y = geometry.y + bounds.y - border - top; + geometry.width = bounds.width + 2 * border + left; + geometry.height = bounds.height + 2 * border + top; + this.graph.getModel().setGeometry(group, geometry); + this.graph.moveCells(children, border + left - bounds.x, + border + top - bounds.y); + } + } + } + finally + { + this.graph.getModel().endUpdate(); + } +}; diff --git a/src/js/layout/mxParallelEdgeLayout.js b/src/js/layout/mxParallelEdgeLayout.js new file mode 100644 index 0000000..e1ad57c --- /dev/null +++ b/src/js/layout/mxParallelEdgeLayout.js @@ -0,0 +1,198 @@ +/** + * $Id: mxParallelEdgeLayout.js,v 1.24 2012-03-27 15:03:34 david Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxParallelEdgeLayout + * + * Extends <mxGraphLayout> for arranging parallel edges. This layout works + * on edges for all pairs of vertices where there is more than one edge + * connecting the latter. + * + * Example: + * + * (code) + * var layout = new mxParallelEdgeLayout(graph); + * layout.execute(graph.getDefaultParent()); + * (end) + * + * Constructor: mxCompactTreeLayout + * + * Constructs a new fast organic layout for the specified graph. + */ +function mxParallelEdgeLayout(graph) +{ + mxGraphLayout.call(this, graph); +}; + +/** + * Extends mxGraphLayout. + */ +mxParallelEdgeLayout.prototype = new mxGraphLayout(); +mxParallelEdgeLayout.prototype.constructor = mxParallelEdgeLayout; + +/** + * Variable: spacing + * + * Defines the spacing between the parallels. Default is 20. + */ +mxParallelEdgeLayout.prototype.spacing = 20; + +/** + * Function: execute + * + * Implements <mxGraphLayout.execute>. + */ +mxParallelEdgeLayout.prototype.execute = function(parent) +{ + var lookup = this.findParallels(parent); + + this.graph.model.beginUpdate(); + try + { + for (var i in lookup) + { + var parallels = lookup[i]; + + if (parallels.length > 1) + { + this.layout(parallels); + } + } + } + finally + { + this.graph.model.endUpdate(); + } +}; + +/** + * Function: findParallels + * + * Finds the parallel edges in the given parent. + */ +mxParallelEdgeLayout.prototype.findParallels = function(parent) +{ + var model = this.graph.getModel(); + var lookup = []; + var childCount = model.getChildCount(parent); + + for (var i = 0; i < childCount; i++) + { + var child = model.getChildAt(parent, i); + + if (!this.isEdgeIgnored(child)) + { + var id = this.getEdgeId(child); + + if (id != null) + { + if (lookup[id] == null) + { + lookup[id] = []; + } + + lookup[id].push(child); + } + } + } + + return lookup; +}; + +/** + * Function: getEdgeId + * + * Returns a unique ID for the given edge. The id is independent of the + * edge direction and is built using the visible terminal of the given + * edge. + */ +mxParallelEdgeLayout.prototype.getEdgeId = function(edge) +{ + var view = this.graph.getView(); + + var state = view.getState(edge); + + var src = (state != null) ? state.getVisibleTerminal(true) : view.getVisibleTerminal(edge, true); + var trg = (state != null) ? state.getVisibleTerminal(false) : view.getVisibleTerminal(edge, false); + + if (src != null && trg != null) + { + src = mxCellPath.create(src); + trg = mxCellPath.create(trg); + + return (src > trg) ? trg+'-'+src : src+'-'+trg; + } + + return null; +}; + +/** + * Function: layout + * + * Lays out the parallel edges in the given array. + */ +mxParallelEdgeLayout.prototype.layout = function(parallels) +{ + var edge = parallels[0]; + var model = this.graph.getModel(); + + var src = model.getGeometry(model.getTerminal(edge, true)); + var trg = model.getGeometry(model.getTerminal(edge, false)); + + // Routes multiple loops + if (src == trg) + { + var x0 = src.x + src.width + this.spacing; + var y0 = src.y + src.height / 2; + + for (var i = 0; i < parallels.length; i++) + { + this.route(parallels[i], x0, y0); + x0 += this.spacing; + } + } + else if (src != null && trg != null) + { + // Routes parallel edges + var scx = src.x + src.width / 2; + var scy = src.y + src.height / 2; + + var tcx = trg.x + trg.width / 2; + var tcy = trg.y + trg.height / 2; + + var dx = tcx - scx; + var dy = tcy - scy; + + var len = Math.sqrt(dx*dx+dy*dy); + + var x0 = scx + dx / 2; + var y0 = scy + dy / 2; + + var nx = dy * this.spacing / len; + var ny = dx * this.spacing / len; + + x0 += nx * (parallels.length - 1) / 2; + y0 -= ny * (parallels.length - 1) / 2; + + for (var i = 0; i < parallels.length; i++) + { + this.route(parallels[i], x0, y0); + x0 -= nx; + y0 += ny; + } + } +}; + +/** + * Function: route + * + * Routes the given edge via the given point. + */ +mxParallelEdgeLayout.prototype.route = function(edge, x, y) +{ + if (this.graph.isCellMovable(edge)) + { + this.setEdgePoints(edge, [new mxPoint(x, y)]); + } +}; diff --git a/src/js/layout/mxPartitionLayout.js b/src/js/layout/mxPartitionLayout.js new file mode 100644 index 0000000..d3592f8 --- /dev/null +++ b/src/js/layout/mxPartitionLayout.js @@ -0,0 +1,240 @@ +/** + * $Id: mxPartitionLayout.js,v 1.25 2010-01-04 11:18:25 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxPartitionLayout + * + * Extends <mxGraphLayout> for partitioning the parent cell vertically or + * horizontally by filling the complete area with the child cells. A horizontal + * layout partitions the height of the given parent whereas a a non-horizontal + * layout partitions the width. If the parent is a layer (that is, a child of + * the root node), then the current graph size is partitioned. The children do + * not need to be connected for this layout to work. + * + * Example: + * + * (code) + * var layout = new mxPartitionLayout(graph, true, 10, 20); + * layout.execute(graph.getDefaultParent()); + * (end) + * + * Constructor: mxPartitionLayout + * + * Constructs a new stack layout layout for the specified graph, + * spacing, orientation and offset. + */ +function mxPartitionLayout(graph, horizontal, spacing, border) +{ + mxGraphLayout.call(this, graph); + this.horizontal = (horizontal != null) ? horizontal : true; + this.spacing = spacing || 0; + this.border = border || 0; +}; + +/** + * Extends mxGraphLayout. + */ +mxPartitionLayout.prototype = new mxGraphLayout(); +mxPartitionLayout.prototype.constructor = mxPartitionLayout; + +/** + * Variable: horizontal + * + * Boolean indicating the direction in which the space is partitioned. + * Default is true. + */ +mxPartitionLayout.prototype.horizontal = null; + +/** + * Variable: spacing + * + * Integer that specifies the absolute spacing in pixels between the + * children. Default is 0. + */ +mxPartitionLayout.prototype.spacing = null; + +/** + * Variable: border + * + * Integer that specifies the absolute inset in pixels for the parent that + * contains the children. Default is 0. + */ +mxPartitionLayout.prototype.border = null; + +/** + * Variable: resizeVertices + * + * Boolean that specifies if vertices should be resized. Default is true. + */ +mxPartitionLayout.prototype.resizeVertices = true; + +/** + * Function: isHorizontal + * + * Returns <horizontal>. + */ +mxPartitionLayout.prototype.isHorizontal = function() +{ + return this.horizontal; +}; + +/** + * Function: moveCell + * + * Implements <mxGraphLayout.moveCell>. + */ +mxPartitionLayout.prototype.moveCell = function(cell, x, y) +{ + var model = this.graph.getModel(); + var parent = model.getParent(cell); + + if (cell != null && + parent != null) + { + var i = 0; + var last = 0; + var childCount = model.getChildCount(parent); + + // Finds index of the closest swimlane + // TODO: Take into account the orientation + for (i = 0; i < childCount; i++) + { + var child = model.getChildAt(parent, i); + var bounds = this.getVertexBounds(child); + + if (bounds != null) + { + var tmp = bounds.x + bounds.width / 2; + + if (last < x && tmp > x) + { + break; + } + + last = tmp; + } + } + + // Changes child order in parent + var idx = parent.getIndex(cell); + idx = Math.max(0, i - ((i > idx) ? 1 : 0)); + + model.add(parent, cell, idx); + } +}; + +/** + * Function: execute + * + * Implements <mxGraphLayout.execute>. All children where <isVertexIgnored> + * returns false and <isVertexMovable> returns true are modified. + */ +mxPartitionLayout.prototype.execute = function(parent) +{ + var horizontal = this.isHorizontal(); + var model = this.graph.getModel(); + var pgeo = model.getGeometry(parent); + + // Handles special case where the parent is either a layer with no + // geometry or the current root of the view in which case the size + // of the graph's container will be used. + if (this.graph.container != null && + ((pgeo == null && + model.isLayer(parent)) || + parent == this.graph.getView().currentRoot)) + { + var width = this.graph.container.offsetWidth - 1; + var height = this.graph.container.offsetHeight - 1; + pgeo = new mxRectangle(0, 0, width, height); + } + + if (pgeo != null) + { + var children = []; + var childCount = model.getChildCount(parent); + + for (var i = 0; i < childCount; i++) + { + var child = model.getChildAt(parent, i); + + if (!this.isVertexIgnored(child) && + this.isVertexMovable(child)) + { + children.push(child); + } + } + + var n = children.length; + + if (n > 0) + { + var x0 = this.border; + var y0 = this.border; + var other = (horizontal) ? pgeo.height : pgeo.width; + other -= 2 * this.border; + + var size = (this.graph.isSwimlane(parent)) ? + this.graph.getStartSize(parent) : + new mxRectangle(); + + other -= (horizontal) ? size.height : size.width; + x0 = x0 + size.width; + y0 = y0 + size.height; + + var tmp = this.border + (n - 1) * this.spacing; + var value = (horizontal) ? + ((pgeo.width - x0 - tmp) / n) : + ((pgeo.height - y0 - tmp) / n); + + // Avoids negative values, that is values where the sum of the + // spacing plus the border is larger then the available space + if (value > 0) + { + model.beginUpdate(); + try + { + for (var i = 0; i < n; i++) + { + var child = children[i]; + var geo = model.getGeometry(child); + + if (geo != null) + { + geo = geo.clone(); + geo.x = x0; + geo.y = y0; + + if (horizontal) + { + if (this.resizeVertices) + { + geo.width = value; + geo.height = other; + } + + x0 += value + this.spacing; + } + else + { + if (this.resizeVertices) + { + geo.height = value; + geo.width = other; + } + + y0 += value + this.spacing; + } + + model.setGeometry(child, geo); + } + } + } + finally + { + model.endUpdate(); + } + } + } + } +}; diff --git a/src/js/layout/mxStackLayout.js b/src/js/layout/mxStackLayout.js new file mode 100644 index 0000000..7f5cd47 --- /dev/null +++ b/src/js/layout/mxStackLayout.js @@ -0,0 +1,381 @@ +/** + * $Id: mxStackLayout.js,v 1.47 2012-12-14 08:54:34 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxStackLayout + * + * Extends <mxGraphLayout> to create a horizontal or vertical stack of the + * child vertices. The children do not need to be connected for this layout + * to work. + * + * Example: + * + * (code) + * var layout = new mxStackLayout(graph, true); + * layout.execute(graph.getDefaultParent()); + * (end) + * + * Constructor: mxStackLayout + * + * Constructs a new stack layout layout for the specified graph, + * spacing, orientation and offset. + */ +function mxStackLayout(graph, horizontal, spacing, x0, y0, border) +{ + mxGraphLayout.call(this, graph); + this.horizontal = (horizontal != null) ? horizontal : true; + this.spacing = (spacing != null) ? spacing : 0; + this.x0 = (x0 != null) ? x0 : 0; + this.y0 = (y0 != null) ? y0 : 0; + this.border = (border != null) ? border : 0; +}; + +/** + * Extends mxGraphLayout. + */ +mxStackLayout.prototype = new mxGraphLayout(); +mxStackLayout.prototype.constructor = mxStackLayout; + +/** + * Variable: horizontal + * + * Specifies the orientation of the layout. Default is true. + */ +mxStackLayout.prototype.horizontal = null; + +/** + * Variable: spacing + * + * Specifies the spacing between the cells. Default is 0. + */ +mxStackLayout.prototype.spacing = null; + +/** + * Variable: x0 + * + * Specifies the horizontal origin of the layout. Default is 0. + */ +mxStackLayout.prototype.x0 = null; + +/** + * Variable: y0 + * + * Specifies the vertical origin of the layout. Default is 0. + */ +mxStackLayout.prototype.y0 = null; + +/** + * Variable: border + * + * Border to be added if fill is true. Default is 0. + */ +mxStackLayout.prototype.border = 0; + +/** + * Variable: keepFirstLocation + * + * Boolean indicating if the location of the first cell should be + * kept, that is, it will not be moved to x0 or y0. + */ +mxStackLayout.prototype.keepFirstLocation = false; + +/** + * Variable: fill + * + * Boolean indicating if dimension should be changed to fill out the parent + * cell. Default is false. + */ +mxStackLayout.prototype.fill = false; + +/** + * Variable: resizeParent + * + * If the parent should be resized to match the width/height of the + * stack. Default is false. + */ +mxStackLayout.prototype.resizeParent = false; + +/** + * Variable: resizeLast + * + * If the last element should be resized to fill out the parent. Default is + * false. If <resizeParent> is true then this is ignored. + */ +mxStackLayout.prototype.resizeLast = false; + +/** + * Variable: wrap + * + * Value at which a new column or row should be created. Default is null. + */ +mxStackLayout.prototype.wrap = null; + +/** + * Function: isHorizontal + * + * Returns <horizontal>. + */ +mxStackLayout.prototype.isHorizontal = function() +{ + return this.horizontal; +}; + +/** + * Function: moveCell + * + * Implements <mxGraphLayout.moveCell>. + */ +mxStackLayout.prototype.moveCell = function(cell, x, y) +{ + var model = this.graph.getModel(); + var parent = model.getParent(cell); + var horizontal = this.isHorizontal(); + + if (cell != null && parent != null) + { + var i = 0; + var last = 0; + var childCount = model.getChildCount(parent); + var value = (horizontal) ? x : y; + var pstate = this.graph.getView().getState(parent); + + if (pstate != null) + { + value -= (horizontal) ? pstate.x : pstate.y; + } + + for (i = 0; i < childCount; i++) + { + var child = model.getChildAt(parent, i); + + if (child != cell) + { + var bounds = model.getGeometry(child); + + if (bounds != null) + { + var tmp = (horizontal) ? + bounds.x + bounds.width / 2 : + bounds.y + bounds.height / 2; + + if (last < value && tmp > value) + { + break; + } + + last = tmp; + } + } + } + + // Changes child order in parent + var idx = parent.getIndex(cell); + idx = Math.max(0, i - ((i > idx) ? 1 : 0)); + + model.add(parent, cell, idx); + } +}; + +/** + * Function: getParentSize + * + * Returns the size for the parent container or the size of the graph + * container if the parent is a layer or the root of the model. + */ +mxStackLayout.prototype.getParentSize = function(parent) +{ + var model = this.graph.getModel(); + var pgeo = model.getGeometry(parent); + + // Handles special case where the parent is either a layer with no + // geometry or the current root of the view in which case the size + // of the graph's container will be used. + if (this.graph.container != null && ((pgeo == null && + model.isLayer(parent)) || parent == this.graph.getView().currentRoot)) + { + var width = this.graph.container.offsetWidth - 1; + var height = this.graph.container.offsetHeight - 1; + pgeo = new mxRectangle(0, 0, width, height); + } + + return pgeo; +}; + +/** + * Function: execute + * + * Implements <mxGraphLayout.execute>. + * + * Only children where <isVertexIgnored> returns false are taken into + * account. + */ +mxStackLayout.prototype.execute = function(parent) +{ + if (parent != null) + { + var horizontal = this.isHorizontal(); + var model = this.graph.getModel(); + var pgeo = this.getParentSize(parent); + + var fillValue = 0; + + if (pgeo != null) + { + fillValue = (horizontal) ? pgeo.height : pgeo.width; + } + + fillValue -= 2 * this.spacing + 2 * this.border; + var x0 = this.x0 + this.border; + var y0 = this.y0 + this.border; + + // Handles swimlane start size + if (this.graph.isSwimlane(parent)) + { + // Uses computed style to get latest + var style = this.graph.getCellStyle(parent); + var start = mxUtils.getValue(style, mxConstants.STYLE_STARTSIZE, mxConstants.DEFAULT_STARTSIZE); + var horz = mxUtils.getValue(style, mxConstants.STYLE_HORIZONTAL, true); + + if (horizontal == horz) + { + fillValue -= start; + } + + if (horizontal) + { + y0 += start; + } + else + { + x0 += start; + } + } + + model.beginUpdate(); + try + { + var tmp = 0; + var last = null; + var childCount = model.getChildCount(parent); + + for (var i = 0; i < childCount; i++) + { + var child = model.getChildAt(parent, i); + + if (!this.isVertexIgnored(child) && this.isVertexMovable(child)) + { + var geo = model.getGeometry(child); + + if (geo != null) + { + geo = geo.clone(); + + if (this.wrap != null && last != null) + { + if ((horizontal && last.x + last.width + + geo.width + 2 * this.spacing > this.wrap) || + (!horizontal && last.y + last.height + + geo.height + 2 * this.spacing > this.wrap)) + { + last = null; + + if (horizontal) + { + y0 += tmp + this.spacing; + } + else + { + x0 += tmp + this.spacing; + } + + tmp = 0; + } + } + + tmp = Math.max(tmp, (horizontal) ? geo.height : geo.width); + + if (last != null) + { + if (horizontal) + { + geo.x = last.x + last.width + this.spacing; + } + else + { + geo.y = last.y + last.height + this.spacing; + } + } + else if (!this.keepFirstLocation) + { + if (horizontal) + { + geo.x = x0; + } + else + { + geo.y = y0; + } + } + + if (horizontal) + { + geo.y = y0; + } + else + { + geo.x = x0; + } + + if (this.fill && fillValue > 0) + { + if (horizontal) + { + geo.height = fillValue; + } + else + { + geo.width = fillValue; + } + } + + model.setGeometry(child, geo); + last = geo; + } + } + } + + if (this.resizeParent && pgeo != null && last != null && + !this.graph.isCellCollapsed(parent)) + { + pgeo = pgeo.clone(); + + if (horizontal) + { + pgeo.width = last.x + last.width + this.spacing; + } + else + { + pgeo.height = last.y + last.height + this.spacing; + } + + model.setGeometry(parent, pgeo); + } + else if (this.resizeLast && pgeo != null && last != null) + { + if (horizontal) + { + last.width = pgeo.width - last.x - this.spacing; + } + else + { + last.height = pgeo.height - last.y - this.spacing; + } + } + } + finally + { + model.endUpdate(); + } + } +}; diff --git a/src/js/model/mxCell.js b/src/js/model/mxCell.js new file mode 100644 index 0000000..cb5eb9f --- /dev/null +++ b/src/js/model/mxCell.js @@ -0,0 +1,806 @@ +/** + * $Id: mxCell.js,v 1.36 2011-06-17 13:45:08 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxCell + * + * Cells are the elements of the graph model. They represent the state + * of the groups, vertices and edges in a graph. + * + * Custom attributes: + * + * For custom attributes we recommend using an XML node as the value of a cell. + * The following code can be used to create a cell with an XML node as the + * value: + * + * (code) + * var doc = mxUtils.createXmlDocument(); + * var node = doc.createElement('MyNode') + * node.setAttribute('label', 'MyLabel'); + * node.setAttribute('attribute1', 'value1'); + * graph.insertVertex(graph.getDefaultParent(), null, node, 40, 40, 80, 30); + * (end) + * + * For the label to work, <mxGraph.convertValueToString> and + * <mxGraph.cellLabelChanged> should be overridden as follows: + * + * (code) + * graph.convertValueToString = function(cell) + * { + * if (mxUtils.isNode(cell.value)) + * { + * return cell.getAttribute('label', '') + * } + * }; + * + * var cellLabelChanged = graph.cellLabelChanged; + * graph.cellLabelChanged = function(cell, newValue, autoSize) + * { + * if (mxUtils.isNode(cell.value)) + * { + * // Clones the value for correct undo/redo + * var elt = cell.value.cloneNode(true); + * elt.setAttribute('label', newValue); + * newValue = elt; + * } + * + * cellLabelChanged.apply(this, arguments); + * }; + * (end) + * + * Callback: onInit + * + * Called from within the constructor. + * + * Constructor: mxCell + * + * Constructs a new cell to be used in a graph model. + * This method invokes <onInit> upon completion. + * + * Parameters: + * + * value - Optional object that represents the cell value. + * geometry - Optional <mxGeometry> that specifies the geometry. + * style - Optional formatted string that defines the style. + */ +function mxCell(value, geometry, style) +{ + this.value = value; + this.setGeometry(geometry); + this.setStyle(style); + + if (this.onInit != null) + { + this.onInit(); + } +}; + +/** + * Variable: id + * + * Holds the Id. Default is null. + */ +mxCell.prototype.id = null; + +/** + * Variable: value + * + * Holds the user object. Default is null. + */ +mxCell.prototype.value = null; + +/** + * Variable: geometry + * + * Holds the <mxGeometry>. Default is null. + */ +mxCell.prototype.geometry = null; + +/** + * Variable: style + * + * Holds the style as a string of the form [(stylename|key=value);]. Default is + * null. + */ +mxCell.prototype.style = null; + +/** + * Variable: vertex + * + * Specifies whether the cell is a vertex. Default is false. + */ +mxCell.prototype.vertex = false; + +/** + * Variable: edge + * + * Specifies whether the cell is an edge. Default is false. + */ +mxCell.prototype.edge = false; + +/** + * Variable: connectable + * + * Specifies whether the cell is connectable. Default is true. + */ +mxCell.prototype.connectable = true; + +/** + * Variable: visible + * + * Specifies whether the cell is visible. Default is true. + */ +mxCell.prototype.visible = true; + +/** + * Variable: collapsed + * + * Specifies whether the cell is collapsed. Default is false. + */ +mxCell.prototype.collapsed = false; + +/** + * Variable: parent + * + * Reference to the parent cell. + */ +mxCell.prototype.parent = null; + +/** + * Variable: source + * + * Reference to the source terminal. + */ +mxCell.prototype.source = null; + +/** + * Variable: target + * + * Reference to the target terminal. + */ +mxCell.prototype.target = null; + +/** + * Variable: children + * + * Holds the child cells. + */ +mxCell.prototype.children = null; + +/** + * Variable: edges + * + * Holds the edges. + */ +mxCell.prototype.edges = null; + +/** + * Variable: mxTransient + * + * List of members that should not be cloned inside <clone>. This field is + * passed to <mxUtils.clone> and is not made persistent in <mxCellCodec>. + * This is not a convention for all classes, it is only used in this class + * to mark transient fields since transient modifiers are not supported by + * the language. + */ +mxCell.prototype.mxTransient = ['id', 'value', 'parent', 'source', + 'target', 'children', 'edges']; + +/** + * Function: getId + * + * Returns the Id of the cell as a string. + */ +mxCell.prototype.getId = function() +{ + return this.id; +}; + +/** + * Function: setId + * + * Sets the Id of the cell to the given string. + */ +mxCell.prototype.setId = function(id) +{ + this.id = id; +}; + +/** + * Function: getValue + * + * Returns the user object of the cell. The user + * object is stored in <value>. + */ +mxCell.prototype.getValue = function() +{ + return this.value; +}; + +/** + * Function: setValue + * + * Sets the user object of the cell. The user object + * is stored in <value>. + */ +mxCell.prototype.setValue = function(value) +{ + this.value = value; +}; + +/** + * Function: valueChanged + * + * Changes the user object after an in-place edit + * and returns the previous value. This implementation + * replaces the user object with the given value and + * returns the old user object. + */ +mxCell.prototype.valueChanged = function(newValue) +{ + var previous = this.getValue(); + this.setValue(newValue); + + return previous; +}; + +/** + * Function: getGeometry + * + * Returns the <mxGeometry> that describes the <geometry>. + */ +mxCell.prototype.getGeometry = function() +{ + return this.geometry; +}; + +/** + * Function: setGeometry + * + * Sets the <mxGeometry> to be used as the <geometry>. + */ +mxCell.prototype.setGeometry = function(geometry) +{ + this.geometry = geometry; +}; + +/** + * Function: getStyle + * + * Returns a string that describes the <style>. + */ +mxCell.prototype.getStyle = function() +{ + return this.style; +}; + +/** + * Function: setStyle + * + * Sets the string to be used as the <style>. + */ +mxCell.prototype.setStyle = function(style) +{ + this.style = style; +}; + +/** + * Function: isVertex + * + * Returns true if the cell is a vertex. + */ +mxCell.prototype.isVertex = function() +{ + return this.vertex; +}; + +/** + * Function: setVertex + * + * Specifies if the cell is a vertex. This should only be assigned at + * construction of the cell and not be changed during its lifecycle. + * + * Parameters: + * + * vertex - Boolean that specifies if the cell is a vertex. + */ +mxCell.prototype.setVertex = function(vertex) +{ + this.vertex = vertex; +}; + +/** + * Function: isEdge + * + * Returns true if the cell is an edge. + */ +mxCell.prototype.isEdge = function() +{ + return this.edge; +}; + +/** + * Function: setEdge + * + * Specifies if the cell is an edge. This should only be assigned at + * construction of the cell and not be changed during its lifecycle. + * + * Parameters: + * + * edge - Boolean that specifies if the cell is an edge. + */ +mxCell.prototype.setEdge = function(edge) +{ + this.edge = edge; +}; + +/** + * Function: isConnectable + * + * Returns true if the cell is connectable. + */ +mxCell.prototype.isConnectable = function() +{ + return this.connectable; +}; + +/** + * Function: setConnectable + * + * Sets the connectable state. + * + * Parameters: + * + * connectable - Boolean that specifies the new connectable state. + */ +mxCell.prototype.setConnectable = function(connectable) +{ + this.connectable = connectable; +}; + +/** + * Function: isVisible + * + * Returns true if the cell is visibile. + */ +mxCell.prototype.isVisible = function() +{ + return this.visible; +}; + +/** + * Function: setVisible + * + * Specifies if the cell is visible. + * + * Parameters: + * + * visible - Boolean that specifies the new visible state. + */ +mxCell.prototype.setVisible = function(visible) +{ + this.visible = visible; +}; + +/** + * Function: isCollapsed + * + * Returns true if the cell is collapsed. + */ +mxCell.prototype.isCollapsed = function() +{ + return this.collapsed; +}; + +/** + * Function: setCollapsed + * + * Sets the collapsed state. + * + * Parameters: + * + * collapsed - Boolean that specifies the new collapsed state. + */ +mxCell.prototype.setCollapsed = function(collapsed) +{ + this.collapsed = collapsed; +}; + +/** + * Function: getParent + * + * Returns the cell's parent. + */ +mxCell.prototype.getParent = function() +{ + return this.parent; +}; + +/** + * Function: setParent + * + * Sets the parent cell. + * + * Parameters: + * + * parent - <mxCell> that represents the new parent. + */ +mxCell.prototype.setParent = function(parent) +{ + this.parent = parent; +}; + +/** + * Function: getTerminal + * + * Returns the source or target terminal. + * + * Parameters: + * + * source - Boolean that specifies if the source terminal should be + * returned. + */ +mxCell.prototype.getTerminal = function(source) +{ + return (source) ? this.source : this.target; +}; + +/** + * Function: setTerminal + * + * Sets the source or target terminal and returns the new terminal. + * + * Parameters: + * + * terminal - <mxCell> that represents the new source or target terminal. + * isSource - Boolean that specifies if the source or target terminal + * should be set. + */ +mxCell.prototype.setTerminal = function(terminal, isSource) +{ + if (isSource) + { + this.source = terminal; + } + else + { + this.target = terminal; + } + + return terminal; +}; + +/** + * Function: getChildCount + * + * Returns the number of child cells. + */ +mxCell.prototype.getChildCount = function() +{ + return (this.children == null) ? 0 : this.children.length; +}; + +/** + * Function: getIndex + * + * Returns the index of the specified child in the child array. + * + * Parameters: + * + * child - Child whose index should be returned. + */ +mxCell.prototype.getIndex = function(child) +{ + return mxUtils.indexOf(this.children, child); +}; + +/** + * Function: getChildAt + * + * Returns the child at the specified index. + * + * Parameters: + * + * index - Integer that specifies the child to be returned. + */ +mxCell.prototype.getChildAt = function(index) +{ + return (this.children == null) ? null : this.children[index]; +}; + +/** + * Function: insert + * + * Inserts the specified child into the child array at the specified index + * and updates the parent reference of the child. If not childIndex is + * specified then the child is appended to the child array. Returns the + * inserted child. + * + * Parameters: + * + * child - <mxCell> to be inserted or appended to the child array. + * index - Optional integer that specifies the index at which the child + * should be inserted into the child array. + */ +mxCell.prototype.insert = function(child, index) +{ + if (child != null) + { + if (index == null) + { + index = this.getChildCount(); + + if (child.getParent() == this) + { + index--; + } + } + + child.removeFromParent(); + child.setParent(this); + + if (this.children == null) + { + this.children = []; + this.children.push(child); + } + else + { + this.children.splice(index, 0, child); + } + } + + return child; +}; + +/** + * Function: remove + * + * Removes the child at the specified index from the child array and + * returns the child that was removed. Will remove the parent reference of + * the child. + * + * Parameters: + * + * index - Integer that specifies the index of the child to be + * removed. + */ +mxCell.prototype.remove = function(index) +{ + var child = null; + + if (this.children != null && index >= 0) + { + child = this.getChildAt(index); + + if (child != null) + { + this.children.splice(index, 1); + child.setParent(null); + } + } + + return child; +}; + +/** + * Function: removeFromParent + * + * Removes the cell from its parent. + */ +mxCell.prototype.removeFromParent = function() +{ + if (this.parent != null) + { + var index = this.parent.getIndex(this); + this.parent.remove(index); + } +}; + +/** + * Function: getEdgeCount + * + * Returns the number of edges in the edge array. + */ +mxCell.prototype.getEdgeCount = function() +{ + return (this.edges == null) ? 0 : this.edges.length; +}; + +/** + * Function: getEdgeIndex + * + * Returns the index of the specified edge in <edges>. + * + * Parameters: + * + * edge - <mxCell> whose index in <edges> should be returned. + */ +mxCell.prototype.getEdgeIndex = function(edge) +{ + return mxUtils.indexOf(this.edges, edge); +}; + +/** + * Function: getEdgeAt + * + * Returns the edge at the specified index in <edges>. + * + * Parameters: + * + * index - Integer that specifies the index of the edge to be returned. + */ +mxCell.prototype.getEdgeAt = function(index) +{ + return (this.edges == null) ? null : this.edges[index]; +}; + +/** + * Function: insertEdge + * + * Inserts the specified edge into the edge array and returns the edge. + * Will update the respective terminal reference of the edge. + * + * Parameters: + * + * edge - <mxCell> to be inserted into the edge array. + * isOutgoing - Boolean that specifies if the edge is outgoing. + */ +mxCell.prototype.insertEdge = function(edge, isOutgoing) +{ + if (edge != null) + { + edge.removeFromTerminal(isOutgoing); + edge.setTerminal(this, isOutgoing); + + if (this.edges == null || + edge.getTerminal(!isOutgoing) != this || + mxUtils.indexOf(this.edges, edge) < 0) + { + if (this.edges == null) + { + this.edges = []; + } + + this.edges.push(edge); + } + } + + return edge; +}; + +/** + * Function: removeEdge + * + * Removes the specified edge from the edge array and returns the edge. + * Will remove the respective terminal reference from the edge. + * + * Parameters: + * + * edge - <mxCell> to be removed from the edge array. + * isOutgoing - Boolean that specifies if the edge is outgoing. + */ +mxCell.prototype.removeEdge = function(edge, isOutgoing) +{ + if (edge != null) + { + if (edge.getTerminal(!isOutgoing) != this && + this.edges != null) + { + var index = this.getEdgeIndex(edge); + + if (index >= 0) + { + this.edges.splice(index, 1); + } + } + + edge.setTerminal(null, isOutgoing); + } + + return edge; +}; + +/** + * Function: removeFromTerminal + * + * Removes the edge from its source or target terminal. + * + * Parameters: + * + * isSource - Boolean that specifies if the edge should be removed from its + * source or target terminal. + */ +mxCell.prototype.removeFromTerminal = function(isSource) +{ + var terminal = this.getTerminal(isSource); + + if (terminal != null) + { + terminal.removeEdge(this, isSource); + } +}; + +/** + * Function: getAttribute + * + * Returns the specified attribute from the user object if it is an XML + * node. + * + * Parameters: + * + * name - Name of the attribute whose value should be returned. + * defaultValue - Optional default value to use if the attribute has no + * value. + */ +mxCell.prototype.getAttribute = function(name, defaultValue) +{ + var userObject = this.getValue(); + + var val = (userObject != null && + userObject.nodeType == mxConstants.NODETYPE_ELEMENT) ? + userObject.getAttribute(name) : null; + + return val || defaultValue; +}; + +/** + * Function: setAttribute + * + * Sets the specified attribute on the user object if it is an XML node. + * + * Parameters: + * + * name - Name of the attribute whose value should be set. + * value - New value of the attribute. + */ +mxCell.prototype.setAttribute = function(name, value) +{ + var userObject = this.getValue(); + + if (userObject != null && + userObject.nodeType == mxConstants.NODETYPE_ELEMENT) + { + userObject.setAttribute(name, value); + } +}; + +/** + * Function: clone + * + * Returns a clone of the cell. Uses <cloneValue> to clone + * the user object. All fields in <mxTransient> are ignored + * during the cloning. + */ +mxCell.prototype.clone = function() +{ + var clone = mxUtils.clone(this, this.mxTransient); + clone.setValue(this.cloneValue()); + + return clone; +}; + +/** + * Function: cloneValue + * + * Returns a clone of the cell's user object. + */ +mxCell.prototype.cloneValue = function() +{ + var value = this.getValue(); + + if (value != null) + { + if (typeof(value.clone) == 'function') + { + value = value.clone(); + } + else if (!isNaN(value.nodeType)) + { + value = value.cloneNode(true); + } + } + + return value; +}; diff --git a/src/js/model/mxCellPath.js b/src/js/model/mxCellPath.js new file mode 100644 index 0000000..71a379e --- /dev/null +++ b/src/js/model/mxCellPath.js @@ -0,0 +1,163 @@ +/** + * $Id: mxCellPath.js,v 1.12 2010-01-02 09:45:15 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +var mxCellPath = +{ + + /** + * Class: mxCellPath + * + * Implements a mechanism for temporary cell Ids. + * + * Variable: PATH_SEPARATOR + * + * Defines the separator between the path components. Default is ".". + */ + PATH_SEPARATOR: '.', + + /** + * Function: create + * + * Creates the cell path for the given cell. The cell path is a + * concatenation of the indices of all ancestors on the (finite) path to + * the root, eg. "0.0.0.1". + * + * Parameters: + * + * cell - Cell whose path should be returned. + */ + create: function(cell) + { + var result = ''; + + if (cell != null) + { + var parent = cell.getParent(); + + while (parent != null) + { + var index = parent.getIndex(cell); + result = index + mxCellPath.PATH_SEPARATOR + result; + + cell = parent; + parent = cell.getParent(); + } + } + + // Removes trailing separator + var n = result.length; + + if (n > 1) + { + result = result.substring(0, n - 1); + } + + return result; + }, + + /** + * Function: getParentPath + * + * Returns the path for the parent of the cell represented by the given + * path. Returns null if the given path has no parent. + * + * Parameters: + * + * path - Path whose parent path should be returned. + */ + getParentPath: function(path) + { + if (path != null) + { + var index = path.lastIndexOf(mxCellPath.PATH_SEPARATOR); + + if (index >= 0) + { + return path.substring(0, index); + } + else if (path.length > 0) + { + return ''; + } + } + + return null; + }, + + /** + * Function: resolve + * + * Returns the cell for the specified cell path using the given root as the + * root of the path. + * + * Parameters: + * + * root - Root cell of the path to be resolved. + * path - String that defines the path. + */ + resolve: function(root, path) + { + var parent = root; + + if (path != null) + { + var tokens = path.split(mxCellPath.PATH_SEPARATOR); + + for (var i=0; i<tokens.length; i++) + { + parent = parent.getChildAt(parseInt(tokens[i])); + } + } + + return parent; + }, + + /** + * Function: compare + * + * Compares the given cell paths and returns -1 if p1 is smaller, 0 if + * p1 is equal and 1 if p1 is greater than p2. + */ + compare: function(p1, p2) + { + var min = Math.min(p1.length, p2.length); + var comp = 0; + + for (var i = 0; i < min; i++) + { + if (p1[i] != p2[i]) + { + if (p1[i].length == 0 || + p2[i].length == 0) + { + comp = (p1[i] == p2[i]) ? 0 : ((p1[i] > p2[i]) ? 1 : -1); + } + else + { + var t1 = parseInt(p1[i]); + var t2 = parseInt(p2[i]); + + comp = (t1 == t2) ? 0 : ((t1 > t2) ? 1 : -1); + } + + break; + } + } + + // Compares path length if both paths are equal to this point + if (comp == 0) + { + var t1 = p1.length; + var t2 = p2.length; + + if (t1 != t2) + { + comp = (t1 > t2) ? 1 : -1; + } + } + + return comp; + } + +}; diff --git a/src/js/model/mxGeometry.js b/src/js/model/mxGeometry.js new file mode 100644 index 0000000..51a7d3b --- /dev/null +++ b/src/js/model/mxGeometry.js @@ -0,0 +1,277 @@ +/** + * $Id: mxGeometry.js,v 1.26 2010-01-02 09:45:15 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxGeometry + * + * Extends <mxRectangle> to represent the geometry of a cell. + * + * For vertices, the geometry consists of the x- and y-location, and the width + * and height. For edges, the geometry consists of the optional terminal- and + * control points. The terminal points are only required if an edge is + * unconnected, and are stored in the sourcePoint> and <targetPoint> + * variables, respectively. + * + * Example: + * + * If an edge is unconnected, that is, it has no source or target terminal, + * then a geometry with terminal points for a new edge can be defined as + * follows. + * + * (code) + * geometry.setTerminalPoint(new mxPoint(x1, y1), true); + * geometry.points = [new mxPoint(x2, y2)]; + * geometry.setTerminalPoint(new mxPoint(x3, y3), false); + * (end) + * + * Control points are used regardless of the connected state of an edge and may + * be ignored or interpreted differently depending on the edge's <mxEdgeStyle>. + * + * To disable automatic reset of control points after a cell has been moved or + * resized, the the <mxGraph.resizeEdgesOnMove> and + * <mxGraph.resetEdgesOnResize> may be used. + * + * Edge Labels: + * + * Using the x- and y-coordinates of a cell's geometry, it is possible to + * position the label on edges on a specific location on the actual edge shape + * as it appears on the screen. The x-coordinate of an edge's geometry is used + * to describe the distance from the center of the edge from -1 to 1 with 0 + * being the center of the edge and the default value. The y-coordinate of an + * edge's geometry is used to describe the absolute, orthogonal distance in + * pixels from that point. In addition, the <mxGeometry.offset> is used as an + * absolute offset vector from the resulting point. + * + * This coordinate system is applied if <relative> is true, otherwise the + * offset defines the absolute vector from the edge's center point to the + * label. + * + * Ports: + * + * The term "port" refers to a relatively positioned, connectable child cell, + * which is used to specify the connection between the parent and another cell + * in the graph. Ports are typically modeled as vertices with relative + * geometries. + * + * Offsets: + * + * The <offset> field is interpreted in 3 different ways, depending on the cell + * and the geometry. For edges, the offset defines the absolute offset for the + * edge label. For relative geometries, the offset defines the absolute offset + * for the origin (top, left corner) of the vertex, otherwise the offset + * defines the absolute offset for the label inside the vertex or group. + * + * Constructor: mxGeometry + * + * Constructs a new object to describe the size and location of a vertex or + * the control points of an edge. + */ +function mxGeometry(x, y, width, height) +{ + mxRectangle.call(this, x, y, width, height); +}; + +/** + * Extends mxRectangle. + */ +mxGeometry.prototype = new mxRectangle(); +mxGeometry.prototype.constructor = mxGeometry; + +/** + * Variable: TRANSLATE_CONTROL_POINTS + * + * Global switch to translate the points in translate. Default is true. + */ +mxGeometry.prototype.TRANSLATE_CONTROL_POINTS = true; + +/** + * Variable: alternateBounds + * + * Stores alternate values for x, y, width and height in a rectangle. See + * <swap> to exchange the values. Default is null. + */ +mxGeometry.prototype.alternateBounds = null; + +/** + * Variable: sourcePoint + * + * Defines the source <mxPoint> of the edge. This is used if the + * corresponding edge does not have a source vertex. Otherwise it is + * ignored. Default is null. + */ +mxGeometry.prototype.sourcePoint = null; + +/** + * Variable: targetPoint + * + * Defines the target <mxPoint> of the edge. This is used if the + * corresponding edge does not have a target vertex. Otherwise it is + * ignored. Default is null. + */ +mxGeometry.prototype.targetPoint = null; + +/** + * Variable: points + * + * Array of <mxPoints> which specifies the control points along the edge. + * These points are the intermediate points on the edge, for the endpoints + * use <targetPoint> and <sourcePoint> or set the terminals of the edge to + * a non-null value. Default is null. + */ +mxGeometry.prototype.points = null; + +/** + * Variable: offset + * + * For edges, this holds the offset (in pixels) from the position defined + * by <x> and <y> on the edge. For relative geometries (for vertices), this + * defines the absolute offset from the point defined by the relative + * coordinates. For absolute geometries (for vertices), this defines the + * offset for the label. Default is null. + */ +mxGeometry.prototype.offset = null; + +/** + * Variable: relative + * + * Specifies if the coordinates in the geometry are to be interpreted as + * relative coordinates. For edges, this is used to define the location of + * the edge label relative to the edge as rendered on the display. For + * vertices, this specifies the relative location inside the bounds of the + * parent cell. + * + * If this is false, then the coordinates are relative to the origin of the + * parent cell or, for edges, the edge label position is relative to the + * center of the edge as rendered on screen. + * + * Default is false. + */ +mxGeometry.prototype.relative = false; + +/** + * Function: swap + * + * Swaps the x, y, width and height with the values stored in + * <alternateBounds> and puts the previous values into <alternateBounds> as + * a rectangle. This operation is carried-out in-place, that is, using the + * existing geometry instance. If this operation is called during a graph + * model transactional change, then the geometry should be cloned before + * calling this method and setting the geometry of the cell using + * <mxGraphModel.setGeometry>. + */ +mxGeometry.prototype.swap = function() +{ + if (this.alternateBounds != null) + { + var old = new mxRectangle( + this.x, this.y, this.width, this.height); + + this.x = this.alternateBounds.x; + this.y = this.alternateBounds.y; + this.width = this.alternateBounds.width; + this.height = this.alternateBounds.height; + + this.alternateBounds = old; + } +}; + +/** + * Function: getTerminalPoint + * + * Returns the <mxPoint> representing the source or target point of this + * edge. This is only used if the edge has no source or target vertex. + * + * Parameters: + * + * isSource - Boolean that specifies if the source or target point + * should be returned. + */ +mxGeometry.prototype.getTerminalPoint = function(isSource) +{ + return (isSource) ? this.sourcePoint : this.targetPoint; +}; + +/** + * Function: setTerminalPoint + * + * Sets the <sourcePoint> or <targetPoint> to the given <mxPoint> and + * returns the new point. + * + * Parameters: + * + * point - Point to be used as the new source or target point. + * isSource - Boolean that specifies if the source or target point + * should be set. + */ +mxGeometry.prototype.setTerminalPoint = function(point, isSource) +{ + if (isSource) + { + this.sourcePoint = point; + } + else + { + this.targetPoint = point; + } + + return point; +}; + +/** + * Function: translate + * + * Translates the geometry by the specified amount. That is, <x> and <y> + * of the geometry, the <sourcePoint>, <targetPoint> and all elements of + * <points> are translated by the given amount. <x> and <y> are only + * translated if <relative> is false. If <TRANSLATE_CONTROL_POINTS> is + * false, then <points> are not modified by this function. + * + * Parameters: + * + * dx - Integer that specifies the x-coordinate of the translation. + * dy - Integer that specifies the y-coordinate of the translation. + */ +mxGeometry.prototype.translate = function(dx, dy) +{ + var clone = this.clone(); + + // Translates the geometry + if (!this.relative) + { + this.x += dx; + this.y += dy; + } + + // Translates the source point + if (this.sourcePoint != null) + { + this.sourcePoint.x += dx; + this.sourcePoint.y += dy; + } + + // Translates the target point + if (this.targetPoint != null) + { + this.targetPoint.x += dx; + this.targetPoint.y += dy; + } + + // Translate the control points + if (this.TRANSLATE_CONTROL_POINTS && + this.points != null) + { + var count = this.points.length; + + for (var i = 0; i < count; i++) + { + var pt = this.points[i]; + + if (pt != null) + { + pt.x += dx; + pt.y += dy; + } + } + } +}; diff --git a/src/js/model/mxGraphModel.js b/src/js/model/mxGraphModel.js new file mode 100644 index 0000000..c65c0e1 --- /dev/null +++ b/src/js/model/mxGraphModel.js @@ -0,0 +1,2622 @@ +/** + * $Id: mxGraphModel.js,v 1.125 2012-04-16 10:48:43 david Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxGraphModel + * + * Extends <mxEventSource> to implement a graph model. The graph model acts as + * a wrapper around the cells which are in charge of storing the actual graph + * datastructure. The model acts as a transactional wrapper with event + * notification for all changes, whereas the cells contain the atomic + * operations for updating the actual datastructure. + * + * Layers: + * + * The cell hierarchy in the model must have a top-level root cell which + * contains the layers (typically one default layer), which in turn contain the + * top-level cells of the layers. This means each cell is contained in a layer. + * If no layers are required, then all new cells should be added to the default + * layer. + * + * Layers are useful for hiding and showing groups of cells, or for placing + * groups of cells on top of other cells in the display. To identify a layer, + * the <isLayer> function is used. It returns true if the parent of the given + * cell is the root of the model. + * + * Encoding the model: + * + * To encode a graph model, use the following code: + * + * (code) + * var enc = new mxCodec(); + * var node = enc.encode(graph.getModel()); + * (end) + * + * This will create an XML node that contains all the model information. + * + * Encoding and decoding changes: + * + * For the encoding of changes, a graph model listener is required that encodes + * each change from the given array of changes. + * + * (code) + * model.addListener(mxEvent.CHANGE, function(sender, evt) + * { + * var changes = evt.getProperty('edit').changes; + * var nodes = []; + * var codec = new mxCodec(); + * + * for (var i = 0; i < changes.length; i++) + * { + * nodes.push(codec.encode(changes[i])); + * } + * // do something with the nodes + * }); + * (end) + * + * For the decoding and execution of changes, the codec needs a lookup function + * that allows it to resolve cell IDs as follows: + * + * (code) + * var codec = new mxCodec(); + * codec.lookup = function(id) + * { + * return model.getCell(id); + * } + * (end) + * + * For each encoded change (represented by a node), the following code can be + * used to carry out the decoding and create a change object. + * + * (code) + * var changes = []; + * var change = codec.decode(node); + * change.model = model; + * change.execute(); + * changes.push(change); + * (end) + * + * The changes can then be dispatched using the model as follows. + * + * (code) + * var edit = new mxUndoableEdit(model, false); + * edit.changes = changes; + * + * edit.notify = function() + * { + * edit.source.fireEvent(new mxEventObject(mxEvent.CHANGE, + * 'edit', edit, 'changes', edit.changes)); + * edit.source.fireEvent(new mxEventObject(mxEvent.NOTIFY, + * 'edit', edit, 'changes', edit.changes)); + * } + * + * model.fireEvent(new mxEventObject(mxEvent.UNDO, 'edit', edit)); + * model.fireEvent(new mxEventObject(mxEvent.CHANGE, + * 'edit', edit, 'changes', changes)); + * (end) + * + * Event: mxEvent.CHANGE + * + * Fires when an undoable edit is dispatched. The <code>edit</code> property + * contains the <mxUndoableEdit>. The <code>changes</code> property contains + * the array of atomic changes inside the undoable edit. The changes property + * is <strong>deprecated</strong>, please use edit.changes instead. + * + * Example: + * + * For finding newly inserted cells, the following code can be used: + * + * (code) + * graph.model.addListener(mxEvent.CHANGE, function(sender, evt) + * { + * var changes = evt.getProperty('edit').changes; + * + * for (var i = 0; i < changes.length; i++) + * { + * var change = changes[i]; + * + * if (change instanceof mxChildChange && + * change.change.previous == null) + * { + * graph.startEditingAtCell(change.child); + * break; + * } + * } + * }); + * (end) + * + * + * Event: mxEvent.NOTIFY + * + * Same as <mxEvent.CHANGE>, this event can be used for classes that need to + * implement a sync mechanism between this model and, say, a remote model. In + * such a setup, only local changes should trigger a notify event and all + * changes should trigger a change event. + * + * Event: mxEvent.EXECUTE + * + * Fires between begin- and endUpdate and after an atomic change was executed + * in the model. The <code>change</code> property contains the atomic change + * that was executed. + * + * Event: mxEvent.BEGIN_UPDATE + * + * Fires after the <updateLevel> was incremented in <beginUpdate>. This event + * contains no properties. + * + * Event: mxEvent.END_UPDATE + * + * Fires after the <updateLevel> was decreased in <endUpdate> but before any + * notification or change dispatching. The <code>edit</code> property contains + * the <currentEdit>. + * + * Event: mxEvent.BEFORE_UNDO + * + * Fires before the change is dispatched after the update level has reached 0 + * in <endUpdate>. The <code>edit</code> property contains the <curreneEdit>. + * + * Event: mxEvent.UNDO + * + * Fires after the change was dispatched in <endUpdate>. The <code>edit</code> + * property contains the <currentEdit>. + * + * Constructor: mxGraphModel + * + * Constructs a new graph model. If no root is specified then a new root + * <mxCell> with a default layer is created. + * + * Parameters: + * + * root - <mxCell> that represents the root cell. + */ +function mxGraphModel(root) +{ + this.currentEdit = this.createUndoableEdit(); + + if (root != null) + { + this.setRoot(root); + } + else + { + this.clear(); + } +}; + +/** + * Extends mxEventSource. + */ +mxGraphModel.prototype = new mxEventSource(); +mxGraphModel.prototype.constructor = mxGraphModel; + +/** + * Variable: root + * + * Holds the root cell, which in turn contains the cells that represent the + * layers of the diagram as child cells. That is, the actual elements of the + * diagram are supposed to live in the third generation of cells and below. + */ +mxGraphModel.prototype.root = null; + +/** + * Variable: cells + * + * Maps from Ids to cells. + */ +mxGraphModel.prototype.cells = null; + +/** + * Variable: maintainEdgeParent + * + * Specifies if edges should automatically be moved into the nearest common + * ancestor of their terminals. Default is true. + */ +mxGraphModel.prototype.maintainEdgeParent = true; + +/** + * Variable: createIds + * + * Specifies if the model should automatically create Ids for new cells. + * Default is true. + */ +mxGraphModel.prototype.createIds = true; + +/** + * Variable: prefix + * + * Defines the prefix of new Ids. Default is an empty string. + */ +mxGraphModel.prototype.prefix = ''; + +/** + * Variable: postfix + * + * Defines the postfix of new Ids. Default is an empty string. + */ +mxGraphModel.prototype.postfix = ''; + +/** + * Variable: nextId + * + * Specifies the next Id to be created. Initial value is 0. + */ +mxGraphModel.prototype.nextId = 0; + +/** + * Variable: currentEdit + * + * Holds the changes for the current transaction. If the transaction is + * closed then a new object is created for this variable using + * <createUndoableEdit>. + */ +mxGraphModel.prototype.currentEdit = null; + +/** + * Variable: updateLevel + * + * Counter for the depth of nested transactions. Each call to <beginUpdate> + * will increment this number and each call to <endUpdate> will decrement + * it. When the counter reaches 0, the transaction is closed and the + * respective events are fired. Initial value is 0. + */ +mxGraphModel.prototype.updateLevel = 0; + +/** + * Variable: endingUpdate + * + * True if the program flow is currently inside endUpdate. + */ +mxGraphModel.prototype.endingUpdate = false; + +/** + * Function: clear + * + * Sets a new root using <createRoot>. + */ +mxGraphModel.prototype.clear = function() +{ + this.setRoot(this.createRoot()); +}; + +/** + * Function: isCreateIds + * + * Returns <createIds>. + */ +mxGraphModel.prototype.isCreateIds = function() +{ + return this.createIds; +}; + +/** + * Function: setCreateIds + * + * Sets <createIds>. + */ +mxGraphModel.prototype.setCreateIds = function(value) +{ + this.createIds = value; +}; + +/** + * Function: createRoot + * + * Creates a new root cell with a default layer (child 0). + */ +mxGraphModel.prototype.createRoot = function() +{ + var cell = new mxCell(); + cell.insert(new mxCell()); + + return cell; +}; + +/** + * Function: getCell + * + * Returns the <mxCell> for the specified Id or null if no cell can be + * found for the given Id. + * + * Parameters: + * + * id - A string representing the Id of the cell. + */ +mxGraphModel.prototype.getCell = function(id) +{ + return (this.cells != null) ? this.cells[id] : null; +}; + +/** + * Function: filterCells + * + * Returns the cells from the given array where the fiven filter function + * returns true. + */ +mxGraphModel.prototype.filterCells = function(cells, filter) +{ + var result = null; + + if (cells != null) + { + result = []; + + for (var i = 0; i < cells.length; i++) + { + if (filter(cells[i])) + { + result.push(cells[i]); + } + } + } + + return result; +}; + +/** + * Function: getDescendants + * + * Returns all descendants of the given cell and the cell itself in an array. + * + * Parameters: + * + * parent - <mxCell> whose descendants should be returned. + */ +mxGraphModel.prototype.getDescendants = function(parent) +{ + return this.filterDescendants(null, parent); +}; + +/** + * Function: filterDescendants + * + * Visits all cells recursively and applies the specified filter function + * to each cell. If the function returns true then the cell is added + * to the resulting array. The parent and result paramters are optional. + * If parent is not specified then the recursion starts at <root>. + * + * Example: + * The following example extracts all vertices from a given model: + * (code) + * var filter = function(cell) + * { + * return model.isVertex(cell); + * } + * var vertices = model.filterDescendants(filter); + * (code) + * + * Parameters: + * + * filter - JavaScript function that takes an <mxCell> as an argument + * and returns a boolean. + * parent - Optional <mxCell> that is used as the root of the recursion. + */ +mxGraphModel.prototype.filterDescendants = function(filter, parent) +{ + // Creates a new array for storing the result + var result = []; + + // Recursion starts at the root of the model + parent = parent || this.getRoot(); + + // Checks if the filter returns true for the cell + // and adds it to the result array + if (filter == null || filter(parent)) + { + result.push(parent); + } + + // Visits the children of the cell + var childCount = this.getChildCount(parent); + + for (var i = 0; i < childCount; i++) + { + var child = this.getChildAt(parent, i); + result = result.concat(this.filterDescendants(filter, child)); + } + + return result; +}; + +/** + * Function: getRoot + * + * Returns the root of the model or the topmost parent of the given cell. + * + * Parameters: + * + * cell - Optional <mxCell> that specifies the child. + */ +mxGraphModel.prototype.getRoot = function(cell) +{ + var root = cell || this.root; + + if (cell != null) + { + while (cell != null) + { + root = cell; + cell = this.getParent(cell); + } + } + + return root; +}; + +/** + * Function: setRoot + * + * Sets the <root> of the model using <mxRootChange> and adds the change to + * the current transaction. This resets all datastructures in the model and + * is the preferred way of clearing an existing model. Returns the new + * root. + * + * Example: + * + * (code) + * var root = new mxCell(); + * root.insert(new mxCell()); + * model.setRoot(root); + * (end) + * + * Parameters: + * + * root - <mxCell> that specifies the new root. + */ +mxGraphModel.prototype.setRoot = function(root) +{ + this.execute(new mxRootChange(this, root)); + + return root; +}; + +/** + * Function: rootChanged + * + * Inner callback to change the root of the model and update the internal + * datastructures, such as <cells> and <nextId>. Returns the previous root. + * + * Parameters: + * + * root - <mxCell> that specifies the new root. + */ +mxGraphModel.prototype.rootChanged = function(root) +{ + var oldRoot = this.root; + this.root = root; + + // Resets counters and datastructures + this.nextId = 0; + this.cells = null; + this.cellAdded(root); + + return oldRoot; +}; + +/** + * Function: isRoot + * + * Returns true if the given cell is the root of the model and a non-null + * value. + * + * Parameters: + * + * cell - <mxCell> that represents the possible root. + */ +mxGraphModel.prototype.isRoot = function(cell) +{ + return cell != null && this.root == cell; +}; + +/** + * Function: isLayer + * + * Returns true if <isRoot> returns true for the parent of the given cell. + * + * Parameters: + * + * cell - <mxCell> that represents the possible layer. + */ +mxGraphModel.prototype.isLayer = function(cell) +{ + return this.isRoot(this.getParent(cell)); +}; + +/** + * Function: isAncestor + * + * Returns true if the given parent is an ancestor of the given child. + * + * Parameters: + * + * parent - <mxCell> that specifies the parent. + * child - <mxCell> that specifies the child. + */ +mxGraphModel.prototype.isAncestor = function(parent, child) +{ + while (child != null && child != parent) + { + child = this.getParent(child); + } + + return child == parent; +}; + +/** + * Function: contains + * + * Returns true if the model contains the given <mxCell>. + * + * Parameters: + * + * cell - <mxCell> that specifies the cell. + */ +mxGraphModel.prototype.contains = function(cell) +{ + return this.isAncestor(this.root, cell); +}; + +/** + * Function: getParent + * + * Returns the parent of the given cell. + * + * Parameters: + * + * cell - <mxCell> whose parent should be returned. + */ +mxGraphModel.prototype.getParent = function(cell) +{ + return (cell != null) ? cell.getParent() : null; +}; + +/** + * Function: add + * + * Adds the specified child to the parent at the given index using + * <mxChildChange> and adds the change to the current transaction. If no + * index is specified then the child is appended to the parent's array of + * children. Returns the inserted child. + * + * Parameters: + * + * parent - <mxCell> that specifies the parent to contain the child. + * child - <mxCell> that specifies the child to be inserted. + * index - Optional integer that specifies the index of the child. + */ +mxGraphModel.prototype.add = function(parent, child, index) +{ + if (child != parent && parent != null && child != null) + { + // Appends the child if no index was specified + if (index == null) + { + index = this.getChildCount(parent); + } + + var parentChanged = parent != this.getParent(child); + this.execute(new mxChildChange(this, parent, child, index)); + + // Maintains the edges parents by moving the edges + // into the nearest common ancestor of its + // terminals + if (this.maintainEdgeParent && parentChanged) + { + this.updateEdgeParents(child); + } + } + + return child; +}; + +/** + * Function: cellAdded + * + * Inner callback to update <cells> when a cell has been added. This + * implementation resolves collisions by creating new Ids. To change the + * ID of a cell after it was inserted into the model, use the following + * code: + * + * (code + * delete model.cells[cell.getId()]; + * cell.setId(newId); + * model.cells[cell.getId()] = cell; + * (end) + * + * If the change of the ID should be part of the command history, then the + * cell should be removed from the model and a clone with the new ID should + * be reinserted into the model instead. + * + * Parameters: + * + * cell - <mxCell> that specifies the cell that has been added. + */ +mxGraphModel.prototype.cellAdded = function(cell) +{ + if (cell != null) + { + // Creates an Id for the cell if not Id exists + if (cell.getId() == null && this.createIds) + { + cell.setId(this.createId(cell)); + } + + if (cell.getId() != null) + { + var collision = this.getCell(cell.getId()); + + if (collision != cell) + { + // Creates new Id for the cell + // as long as there is a collision + while (collision != null) + { + cell.setId(this.createId(cell)); + collision = this.getCell(cell.getId()); + } + + // Lazily creates the cells dictionary + if (this.cells == null) + { + this.cells = new Object(); + } + + this.cells[cell.getId()] = cell; + } + } + + // Makes sure IDs of deleted cells are not reused + if (mxUtils.isNumeric(cell.getId())) + { + this.nextId = Math.max(this.nextId, cell.getId()); + } + + // Recursively processes child cells + var childCount = this.getChildCount(cell); + + for (var i=0; i<childCount; i++) + { + this.cellAdded(this.getChildAt(cell, i)); + } + } +}; + +/** + * Function: createId + * + * Hook method to create an Id for the specified cell. This implementation + * concatenates <prefix>, id and <postfix> to create the Id and increments + * <nextId>. The cell is ignored by this implementation, but can be used in + * overridden methods to prefix the Ids with eg. the cell type. + * + * Parameters: + * + * cell - <mxCell> to create the Id for. + */ +mxGraphModel.prototype.createId = function(cell) +{ + var id = this.nextId; + this.nextId++; + + return this.prefix + id + this.postfix; +}; + +/** + * Function: updateEdgeParents + * + * Updates the parent for all edges that are connected to cell or one of + * its descendants using <updateEdgeParent>. + */ +mxGraphModel.prototype.updateEdgeParents = function(cell, root) +{ + // Gets the topmost node of the hierarchy + root = root || this.getRoot(cell); + + // Updates edges on children first + var childCount = this.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + var child = this.getChildAt(cell, i); + this.updateEdgeParents(child, root); + } + + // Updates the parents of all connected edges + var edgeCount = this.getEdgeCount(cell); + var edges = []; + + for (var i = 0; i < edgeCount; i++) + { + edges.push(this.getEdgeAt(cell, i)); + } + + for (var i = 0; i < edges.length; i++) + { + var edge = edges[i]; + + // Updates edge parent if edge and child have + // a common root node (does not need to be the + // model root node) + if (this.isAncestor(root, edge)) + { + this.updateEdgeParent(edge, root); + } + } +}; + +/** + * Function: updateEdgeParent + * + * Inner callback to update the parent of the specified <mxCell> to the + * nearest-common-ancestor of its two terminals. + * + * Parameters: + * + * edge - <mxCell> that specifies the edge. + * root - <mxCell> that represents the current root of the model. + */ +mxGraphModel.prototype.updateEdgeParent = function(edge, root) +{ + var source = this.getTerminal(edge, true); + var target = this.getTerminal(edge, false); + var cell = null; + + // Uses the first non-relative descendants of the source terminal + while (source != null && !this.isEdge(source) && + source.geometry != null && source.geometry.relative) + { + source = this.getParent(source); + } + + // Uses the first non-relative descendants of the target terminal + while (target != null && !this.isEdge(target) && + target.geometry != null && target.geometry.relative) + { + target = this.getParent(target); + } + + if (this.isAncestor(root, source) && this.isAncestor(root, target)) + { + if (source == target) + { + cell = this.getParent(source); + } + else + { + cell = this.getNearestCommonAncestor(source, target); + } + + if (cell != null && (this.getParent(cell) != this.root || + this.isAncestor(cell, edge)) && this.getParent(edge) != cell) + { + var geo = this.getGeometry(edge); + + if (geo != null) + { + var origin1 = this.getOrigin(this.getParent(edge)); + var origin2 = this.getOrigin(cell); + + var dx = origin2.x - origin1.x; + var dy = origin2.y - origin1.y; + + geo = geo.clone(); + geo.translate(-dx, -dy); + this.setGeometry(edge, geo); + } + + this.add(cell, edge, this.getChildCount(cell)); + } + } +}; + +/** + * Function: getOrigin + * + * Returns the absolute, accumulated origin for the children inside the + * given parent as an <mxPoint>. + */ +mxGraphModel.prototype.getOrigin = function(cell) +{ + var result = null; + + if (cell != null) + { + result = this.getOrigin(this.getParent(cell)); + + if (!this.isEdge(cell)) + { + var geo = this.getGeometry(cell); + + if (geo != null) + { + result.x += geo.x; + result.y += geo.y; + } + } + } + else + { + result = new mxPoint(); + } + + return result; +}; + +/** + * Function: getNearestCommonAncestor + * + * Returns the nearest common ancestor for the specified cells. + * + * Parameters: + * + * cell1 - <mxCell> that specifies the first cell in the tree. + * cell2 - <mxCell> that specifies the second cell in the tree. + */ +mxGraphModel.prototype.getNearestCommonAncestor = function(cell1, cell2) +{ + if (cell1 != null && cell2 != null) + { + // Creates the cell path for the second cell + var path = mxCellPath.create(cell2); + + if (path != null && path.length > 0) + { + // Bubbles through the ancestors of the first + // cell to find the nearest common ancestor. + var cell = cell1; + var current = mxCellPath.create(cell); + + // Inverts arguments + if (path.length < current.length) + { + cell = cell2; + var tmp = current; + current = path; + path = tmp; + } + + while (cell != null) + { + var parent = this.getParent(cell); + + // Checks if the cell path is equal to the beginning of the given cell path + if (path.indexOf(current + mxCellPath.PATH_SEPARATOR) == 0 && parent != null) + { + return cell; + } + + current = mxCellPath.getParentPath(current); + cell = parent; + } + } + } + + return null; +}; + +/** + * Function: remove + * + * Removes the specified cell from the model using <mxChildChange> and adds + * the change to the current transaction. This operation will remove the + * cell and all of its children from the model. Returns the removed cell. + * + * Parameters: + * + * cell - <mxCell> that should be removed. + */ +mxGraphModel.prototype.remove = function(cell) +{ + if (cell == this.root) + { + this.setRoot(null); + } + else if (this.getParent(cell) != null) + { + this.execute(new mxChildChange(this, null, cell)); + } + + return cell; +}; + +/** + * Function: cellRemoved + * + * Inner callback to update <cells> when a cell has been removed. + * + * Parameters: + * + * cell - <mxCell> that specifies the cell that has been removed. + */ +mxGraphModel.prototype.cellRemoved = function(cell) +{ + if (cell != null && this.cells != null) + { + // Recursively processes child cells + var childCount = this.getChildCount(cell); + + for (var i = childCount - 1; i >= 0; i--) + { + this.cellRemoved(this.getChildAt(cell, i)); + } + + // Removes the dictionary entry for the cell + if (this.cells != null && cell.getId() != null) + { + delete this.cells[cell.getId()]; + } + } +}; + +/** + * Function: parentForCellChanged + * + * Inner callback to update the parent of a cell using <mxCell.insert> + * on the parent and return the previous parent. + * + * Parameters: + * + * cell - <mxCell> to update the parent for. + * parent - <mxCell> that specifies the new parent of the cell. + * index - Optional integer that defines the index of the child + * in the parent's child array. + */ +mxGraphModel.prototype.parentForCellChanged = function(cell, parent, index) +{ + var previous = this.getParent(cell); + + if (parent != null) + { + if (parent != previous || previous.getIndex(cell) != index) + { + parent.insert(cell, index); + } + } + else if (previous != null) + { + var oldIndex = previous.getIndex(cell); + previous.remove(oldIndex); + } + + // Checks if the previous parent was already in the + // model and avoids calling cellAdded if it was. + if (!this.contains(previous) && parent != null) + { + this.cellAdded(cell); + } + else if (parent == null) + { + this.cellRemoved(cell); + } + + return previous; +}; + +/** + * Function: getChildCount + * + * Returns the number of children in the given cell. + * + * Parameters: + * + * cell - <mxCell> whose number of children should be returned. + */ +mxGraphModel.prototype.getChildCount = function(cell) +{ + return (cell != null) ? cell.getChildCount() : 0; +}; + +/** + * Function: getChildAt + * + * Returns the child of the given <mxCell> at the given index. + * + * Parameters: + * + * cell - <mxCell> that represents the parent. + * index - Integer that specifies the index of the child to be returned. + */ +mxGraphModel.prototype.getChildAt = function(cell, index) +{ + return (cell != null) ? cell.getChildAt(index) : null; +}; + +/** + * Function: getChildren + * + * Returns all children of the given <mxCell> as an array of <mxCells>. The + * return value should be only be read. + * + * Parameters: + * + * cell - <mxCell> the represents the parent. + */ +mxGraphModel.prototype.getChildren = function(cell) +{ + return (cell != null) ? cell.children : null; +}; + +/** + * Function: getChildVertices + * + * Returns the child vertices of the given parent. + * + * Parameters: + * + * cell - <mxCell> whose child vertices should be returned. + */ +mxGraphModel.prototype.getChildVertices = function(parent) +{ + return this.getChildCells(parent, true, false); +}; + +/** + * Function: getChildEdges + * + * Returns the child edges of the given parent. + * + * Parameters: + * + * cell - <mxCell> whose child edges should be returned. + */ +mxGraphModel.prototype.getChildEdges = function(parent) +{ + return this.getChildCells(parent, false, true); +}; + +/** + * Function: getChildCells + * + * Returns the children of the given cell that are vertices and/or edges + * depending on the arguments. + * + * Parameters: + * + * cell - <mxCell> the represents the parent. + * vertices - Boolean indicating if child vertices should be returned. + * Default is false. + * edges - Boolean indicating if child edges should be returned. + * Default is false. + */ +mxGraphModel.prototype.getChildCells = function(parent, vertices, edges) +{ + vertices = (vertices != null) ? vertices : false; + edges = (edges != null) ? edges : false; + + var childCount = this.getChildCount(parent); + var result = []; + + for (var i = 0; i < childCount; i++) + { + var child = this.getChildAt(parent, i); + + if ((!edges && !vertices) || (edges && this.isEdge(child)) || + (vertices && this.isVertex(child))) + { + result.push(child); + } + } + + return result; +}; + +/** + * Function: getTerminal + * + * Returns the source or target <mxCell> of the given edge depending on the + * value of the boolean parameter. + * + * Parameters: + * + * edge - <mxCell> that specifies the edge. + * isSource - Boolean indicating which end of the edge should be returned. + */ +mxGraphModel.prototype.getTerminal = function(edge, isSource) +{ + return (edge != null) ? edge.getTerminal(isSource) : null; +}; + +/** + * Function: setTerminal + * + * Sets the source or target terminal of the given <mxCell> using + * <mxTerminalChange> and adds the change to the current transaction. + * This implementation updates the parent of the edge using <updateEdgeParent> + * if required. + * + * Parameters: + * + * edge - <mxCell> that specifies the edge. + * terminal - <mxCell> that specifies the new terminal. + * isSource - Boolean indicating if the terminal is the new source or + * target terminal of the edge. + */ +mxGraphModel.prototype.setTerminal = function(edge, terminal, isSource) +{ + var terminalChanged = terminal != this.getTerminal(edge, isSource); + this.execute(new mxTerminalChange(this, edge, terminal, isSource)); + + if (this.maintainEdgeParent && terminalChanged) + { + this.updateEdgeParent(edge, this.getRoot()); + } + + return terminal; +}; + +/** + * Function: setTerminals + * + * Sets the source and target <mxCell> of the given <mxCell> in a single + * transaction using <setTerminal> for each end of the edge. + * + * Parameters: + * + * edge - <mxCell> that specifies the edge. + * source - <mxCell> that specifies the new source terminal. + * target - <mxCell> that specifies the new target terminal. + */ +mxGraphModel.prototype.setTerminals = function(edge, source, target) +{ + this.beginUpdate(); + try + { + this.setTerminal(edge, source, true); + this.setTerminal(edge, target, false); + } + finally + { + this.endUpdate(); + } +}; + +/** + * Function: terminalForCellChanged + * + * Inner helper function to update the terminal of the edge using + * <mxCell.insertEdge> and return the previous terminal. + * + * Parameters: + * + * edge - <mxCell> that specifies the edge to be updated. + * terminal - <mxCell> that specifies the new terminal. + * isSource - Boolean indicating if the terminal is the new source or + * target terminal of the edge. + */ +mxGraphModel.prototype.terminalForCellChanged = function(edge, terminal, isSource) +{ + var previous = this.getTerminal(edge, isSource); + + if (terminal != null) + { + terminal.insertEdge(edge, isSource); + } + else if (previous != null) + { + previous.removeEdge(edge, isSource); + } + + return previous; +}; + +/** + * Function: getEdgeCount + * + * Returns the number of distinct edges connected to the given cell. + * + * Parameters: + * + * cell - <mxCell> that represents the vertex. + */ +mxGraphModel.prototype.getEdgeCount = function(cell) +{ + return (cell != null) ? cell.getEdgeCount() : 0; +}; + +/** + * Function: getEdgeAt + * + * Returns the edge of cell at the given index. + * + * Parameters: + * + * cell - <mxCell> that specifies the vertex. + * index - Integer that specifies the index of the edge + * to return. + */ +mxGraphModel.prototype.getEdgeAt = function(cell, index) +{ + return (cell != null) ? cell.getEdgeAt(index) : null; +}; + +/** + * Function: getDirectedEdgeCount + * + * Returns the number of incoming or outgoing edges, ignoring the given + * edge. + * + * Parameters: + * + * cell - <mxCell> whose edge count should be returned. + * outgoing - Boolean that specifies if the number of outgoing or + * incoming edges should be returned. + * ignoredEdge - <mxCell> that represents an edge to be ignored. + */ +mxGraphModel.prototype.getDirectedEdgeCount = function(cell, outgoing, ignoredEdge) +{ + var count = 0; + var edgeCount = this.getEdgeCount(cell); + + for (var i = 0; i < edgeCount; i++) + { + var edge = this.getEdgeAt(cell, i); + + if (edge != ignoredEdge && this.getTerminal(edge, outgoing) == cell) + { + count++; + } + } + + return count; +}; + +/** + * Function: getConnections + * + * Returns all edges of the given cell without loops. + * + * Parameters: + * + * cell - <mxCell> whose edges should be returned. + * + */ +mxGraphModel.prototype.getConnections = function(cell) +{ + return this.getEdges(cell, true, true, false); +}; + +/** + * Function: getIncomingEdges + * + * Returns the incoming edges of the given cell without loops. + * + * Parameters: + * + * cell - <mxCell> whose incoming edges should be returned. + * + */ +mxGraphModel.prototype.getIncomingEdges = function(cell) +{ + return this.getEdges(cell, true, false, false); +}; + +/** + * Function: getOutgoingEdges + * + * Returns the outgoing edges of the given cell without loops. + * + * Parameters: + * + * cell - <mxCell> whose outgoing edges should be returned. + * + */ +mxGraphModel.prototype.getOutgoingEdges = function(cell) +{ + return this.getEdges(cell, false, true, false); +}; + +/** + * Function: getEdges + * + * Returns all distinct edges connected to this cell as a new array of + * <mxCells>. If at least one of incoming or outgoing is true, then loops + * are ignored, otherwise if both are false, then all edges connected to + * the given cell are returned including loops. + * + * Parameters: + * + * cell - <mxCell> that specifies the cell. + * incoming - Optional boolean that specifies if incoming edges should be + * returned. Default is true. + * outgoing - Optional boolean that specifies if outgoing edges should be + * returned. Default is true. + * includeLoops - Optional boolean that specifies if loops should be returned. + * Default is true. + */ +mxGraphModel.prototype.getEdges = function(cell, incoming, outgoing, includeLoops) +{ + incoming = (incoming != null) ? incoming : true; + outgoing = (outgoing != null) ? outgoing : true; + includeLoops = (includeLoops != null) ? includeLoops : true; + + var edgeCount = this.getEdgeCount(cell); + var result = []; + + for (var i = 0; i < edgeCount; i++) + { + var edge = this.getEdgeAt(cell, i); + var source = this.getTerminal(edge, true); + var target = this.getTerminal(edge, false); + + if ((includeLoops && source == target) || ((source != target) && ((incoming && target == cell) || + (outgoing && source == cell)))) + { + result.push(edge); + } + } + + return result; +}; + +/** + * Function: getEdgesBetween + * + * Returns all edges between the given source and target pair. If directed + * is true, then only edges from the source to the target are returned, + * otherwise, all edges between the two cells are returned. + * + * Parameters: + * + * source - <mxCell> that defines the source terminal of the edge to be + * returned. + * target - <mxCell> that defines the target terminal of the edge to be + * returned. + * directed - Optional boolean that specifies if the direction of the + * edge should be taken into account. Default is false. + */ +mxGraphModel.prototype.getEdgesBetween = function(source, target, directed) +{ + directed = (directed != null) ? directed : false; + + var tmp1 = this.getEdgeCount(source); + var tmp2 = this.getEdgeCount(target); + + // Assumes the source has less connected edges + var terminal = source; + var edgeCount = tmp1; + + // Uses the smaller array of connected edges + // for searching the edge + if (tmp2 < tmp1) + { + edgeCount = tmp2; + terminal = target; + } + + var result = []; + + // Checks if the edge is connected to the correct + // cell and returns the first match + for (var i = 0; i < edgeCount; i++) + { + var edge = this.getEdgeAt(terminal, i); + var src = this.getTerminal(edge, true); + var trg = this.getTerminal(edge, false); + var directedMatch = (src == source) && (trg == target); + var oppositeMatch = (trg == source) && (src == target); + + if (directedMatch || (!directed && oppositeMatch)) + { + result.push(edge); + } + } + + return result; +}; + +/** + * Function: getOpposites + * + * Returns all opposite vertices wrt terminal for the given edges, only + * returning sources and/or targets as specified. The result is returned + * as an array of <mxCells>. + * + * Parameters: + * + * edges - Array of <mxCells> that contain the edges to be examined. + * terminal - <mxCell> that specifies the known end of the edges. + * sources - Boolean that specifies if source terminals should be contained + * in the result. Default is true. + * targets - Boolean that specifies if target terminals should be contained + * in the result. Default is true. + */ +mxGraphModel.prototype.getOpposites = function(edges, terminal, sources, targets) +{ + sources = (sources != null) ? sources : true; + targets = (targets != null) ? targets : true; + + var terminals = []; + + if (edges != null) + { + for (var i = 0; i < edges.length; i++) + { + var source = this.getTerminal(edges[i], true); + var target = this.getTerminal(edges[i], false); + + // Checks if the terminal is the source of + // the edge and if the target should be + // stored in the result + if (source == terminal && target != null && target != terminal && targets) + { + terminals.push(target); + } + + // Checks if the terminal is the taget of + // the edge and if the source should be + // stored in the result + else if (target == terminal && source != null && source != terminal && sources) + { + terminals.push(source); + } + } + } + + return terminals; +}; + +/** + * Function: getTopmostCells + * + * Returns the topmost cells of the hierarchy in an array that contains no + * descendants for each <mxCell> that it contains. Duplicates should be + * removed in the cells array to improve performance. + * + * Parameters: + * + * cells - Array of <mxCells> whose topmost ancestors should be returned. + */ +mxGraphModel.prototype.getTopmostCells = function(cells) +{ + var tmp = []; + + for (var i = 0; i < cells.length; i++) + { + var cell = cells[i]; + var topmost = true; + var parent = this.getParent(cell); + + while (parent != null) + { + if (mxUtils.indexOf(cells, parent) >= 0) + { + topmost = false; + break; + } + + parent = this.getParent(parent); + } + + if (topmost) + { + tmp.push(cell); + } + } + + return tmp; +}; + +/** + * Function: isVertex + * + * Returns true if the given cell is a vertex. + * + * Parameters: + * + * cell - <mxCell> that represents the possible vertex. + */ +mxGraphModel.prototype.isVertex = function(cell) +{ + return (cell != null) ? cell.isVertex() : false; +}; + +/** + * Function: isEdge + * + * Returns true if the given cell is an edge. + * + * Parameters: + * + * cell - <mxCell> that represents the possible edge. + */ +mxGraphModel.prototype.isEdge = function(cell) +{ + return (cell != null) ? cell.isEdge() : false; +}; + +/** + * Function: isConnectable + * + * Returns true if the given <mxCell> is connectable. If <edgesConnectable> + * is false, then this function returns false for all edges else it returns + * the return value of <mxCell.isConnectable>. + * + * Parameters: + * + * cell - <mxCell> whose connectable state should be returned. + */ +mxGraphModel.prototype.isConnectable = function(cell) +{ + return (cell != null) ? cell.isConnectable() : false; +}; + +/** + * Function: getValue + * + * Returns the user object of the given <mxCell> using <mxCell.getValue>. + * + * Parameters: + * + * cell - <mxCell> whose user object should be returned. + */ +mxGraphModel.prototype.getValue = function(cell) +{ + return (cell != null) ? cell.getValue() : null; +}; + +/** + * Function: setValue + * + * Sets the user object of then given <mxCell> using <mxValueChange> + * and adds the change to the current transaction. + * + * Parameters: + * + * cell - <mxCell> whose user object should be changed. + * value - Object that defines the new user object. + */ +mxGraphModel.prototype.setValue = function(cell, value) +{ + this.execute(new mxValueChange(this, cell, value)); + + return value; +}; + +/** + * Function: valueForCellChanged + * + * Inner callback to update the user object of the given <mxCell> + * using <mxCell.valueChanged> and return the previous value, + * that is, the return value of <mxCell.valueChanged>. + * + * To change a specific attribute in an XML node, the following code can be + * used. + * + * (code) + * graph.getModel().valueForCellChanged = function(cell, value) + * { + * var previous = cell.value.getAttribute('label'); + * cell.value.setAttribute('label', value); + * + * return previous; + * }; + * (end) + */ +mxGraphModel.prototype.valueForCellChanged = function(cell, value) +{ + return cell.valueChanged(value); +}; + +/** + * Function: getGeometry + * + * Returns the <mxGeometry> of the given <mxCell>. + * + * Parameters: + * + * cell - <mxCell> whose geometry should be returned. + */ +mxGraphModel.prototype.getGeometry = function(cell, geometry) +{ + return (cell != null) ? cell.getGeometry() : null; +}; + +/** + * Function: setGeometry + * + * Sets the <mxGeometry> of the given <mxCell>. The actual update + * of the cell is carried out in <geometryForCellChanged>. The + * <mxGeometryChange> action is used to encapsulate the change. + * + * Parameters: + * + * cell - <mxCell> whose geometry should be changed. + * geometry - <mxGeometry> that defines the new geometry. + */ +mxGraphModel.prototype.setGeometry = function(cell, geometry) +{ + if (geometry != this.getGeometry(cell)) + { + this.execute(new mxGeometryChange(this, cell, geometry)); + } + + return geometry; +}; + +/** + * Function: geometryForCellChanged + * + * Inner callback to update the <mxGeometry> of the given <mxCell> using + * <mxCell.setGeometry> and return the previous <mxGeometry>. + */ +mxGraphModel.prototype.geometryForCellChanged = function(cell, geometry) +{ + var previous = this.getGeometry(cell); + cell.setGeometry(geometry); + + return previous; +}; + +/** + * Function: getStyle + * + * Returns the style of the given <mxCell>. + * + * Parameters: + * + * cell - <mxCell> whose style should be returned. + */ +mxGraphModel.prototype.getStyle = function(cell) +{ + return (cell != null) ? cell.getStyle() : null; +}; + +/** + * Function: setStyle + * + * Sets the style of the given <mxCell> using <mxStyleChange> and + * adds the change to the current transaction. + * + * Parameters: + * + * cell - <mxCell> whose style should be changed. + * style - String of the form [stylename;|key=value;] to specify + * the new cell style. + */ +mxGraphModel.prototype.setStyle = function(cell, style) +{ + if (style != this.getStyle(cell)) + { + this.execute(new mxStyleChange(this, cell, style)); + } + + return style; +}; + +/** + * Function: styleForCellChanged + * + * Inner callback to update the style of the given <mxCell> + * using <mxCell.setStyle> and return the previous style. + * + * Parameters: + * + * cell - <mxCell> that specifies the cell to be updated. + * style - String of the form [stylename;|key=value;] to specify + * the new cell style. + */ +mxGraphModel.prototype.styleForCellChanged = function(cell, style) +{ + var previous = this.getStyle(cell); + cell.setStyle(style); + + return previous; +}; + +/** + * Function: isCollapsed + * + * Returns true if the given <mxCell> is collapsed. + * + * Parameters: + * + * cell - <mxCell> whose collapsed state should be returned. + */ +mxGraphModel.prototype.isCollapsed = function(cell) +{ + return (cell != null) ? cell.isCollapsed() : false; +}; + +/** + * Function: setCollapsed + * + * Sets the collapsed state of the given <mxCell> using <mxCollapseChange> + * and adds the change to the current transaction. + * + * Parameters: + * + * cell - <mxCell> whose collapsed state should be changed. + * collapsed - Boolean that specifies the new collpased state. + */ +mxGraphModel.prototype.setCollapsed = function(cell, collapsed) +{ + if (collapsed != this.isCollapsed(cell)) + { + this.execute(new mxCollapseChange(this, cell, collapsed)); + } + + return collapsed; +}; + +/** + * Function: collapsedStateForCellChanged + * + * Inner callback to update the collapsed state of the + * given <mxCell> using <mxCell.setCollapsed> and return + * the previous collapsed state. + * + * Parameters: + * + * cell - <mxCell> that specifies the cell to be updated. + * collapsed - Boolean that specifies the new collpased state. + */ +mxGraphModel.prototype.collapsedStateForCellChanged = function(cell, collapsed) +{ + var previous = this.isCollapsed(cell); + cell.setCollapsed(collapsed); + + return previous; +}; + +/** + * Function: isVisible + * + * Returns true if the given <mxCell> is visible. + * + * Parameters: + * + * cell - <mxCell> whose visible state should be returned. + */ +mxGraphModel.prototype.isVisible = function(cell) +{ + return (cell != null) ? cell.isVisible() : false; +}; + +/** + * Function: setVisible + * + * Sets the visible state of the given <mxCell> using <mxVisibleChange> and + * adds the change to the current transaction. + * + * Parameters: + * + * cell - <mxCell> whose visible state should be changed. + * visible - Boolean that specifies the new visible state. + */ +mxGraphModel.prototype.setVisible = function(cell, visible) +{ + if (visible != this.isVisible(cell)) + { + this.execute(new mxVisibleChange(this, cell, visible)); + } + + return visible; +}; + +/** + * Function: visibleStateForCellChanged + * + * Inner callback to update the visible state of the + * given <mxCell> using <mxCell.setCollapsed> and return + * the previous visible state. + * + * Parameters: + * + * cell - <mxCell> that specifies the cell to be updated. + * visible - Boolean that specifies the new visible state. + */ +mxGraphModel.prototype.visibleStateForCellChanged = function(cell, visible) +{ + var previous = this.isVisible(cell); + cell.setVisible(visible); + + return previous; +}; + +/** + * Function: execute + * + * Executes the given edit and fires events if required. The edit object + * requires an execute function which is invoked. The edit is added to the + * <currentEdit> between <beginUpdate> and <endUpdate> calls, so that + * events will be fired if this execute is an individual transaction, that + * is, if no previous <beginUpdate> calls have been made without calling + * <endUpdate>. This implementation fires an <execute> event before + * executing the given change. + * + * Parameters: + * + * change - Object that described the change. + */ +mxGraphModel.prototype.execute = function(change) +{ + change.execute(); + this.beginUpdate(); + this.currentEdit.add(change); + this.fireEvent(new mxEventObject(mxEvent.EXECUTE, 'change', change)); + this.endUpdate(); +}; + +/** + * Function: beginUpdate + * + * Increments the <updateLevel> by one. The event notification + * is queued until <updateLevel> reaches 0 by use of + * <endUpdate>. + * + * All changes on <mxGraphModel> are transactional, + * that is, they are executed in a single undoable change + * on the model (without transaction isolation). + * Therefore, if you want to combine any + * number of changes into a single undoable change, + * you should group any two or more API calls that + * modify the graph model between <beginUpdate> + * and <endUpdate> calls as shown here: + * + * (code) + * var model = graph.getModel(); + * var parent = graph.getDefaultParent(); + * var index = model.getChildCount(parent); + * model.beginUpdate(); + * try + * { + * model.add(parent, v1, index); + * model.add(parent, v2, index+1); + * } + * finally + * { + * model.endUpdate(); + * } + * (end) + * + * Of course there is a shortcut for appending a + * sequence of cells into the default parent: + * + * (code) + * graph.addCells([v1, v2]). + * (end) + */ +mxGraphModel.prototype.beginUpdate = function() +{ + this.updateLevel++; + this.fireEvent(new mxEventObject(mxEvent.BEGIN_UPDATE)); +}; + +/** + * Function: endUpdate + * + * Decrements the <updateLevel> by one and fires an <undo> + * event if the <updateLevel> reaches 0. This function + * indirectly fires a <change> event by invoking the notify + * function on the <currentEdit> und then creates a new + * <currentEdit> using <createUndoableEdit>. + * + * The <undo> event is fired only once per edit, whereas + * the <change> event is fired whenever the notify + * function is invoked, that is, on undo and redo of + * the edit. + */ +mxGraphModel.prototype.endUpdate = function() +{ + this.updateLevel--; + + if (!this.endingUpdate) + { + this.endingUpdate = this.updateLevel == 0; + this.fireEvent(new mxEventObject(mxEvent.END_UPDATE, 'edit', this.currentEdit)); + + try + { + if (this.endingUpdate && !this.currentEdit.isEmpty()) + { + this.fireEvent(new mxEventObject(mxEvent.BEFORE_UNDO, 'edit', this.currentEdit)); + var tmp = this.currentEdit; + this.currentEdit = this.createUndoableEdit(); + tmp.notify(); + this.fireEvent(new mxEventObject(mxEvent.UNDO, 'edit', tmp)); + } + } + finally + { + this.endingUpdate = false; + } + } +}; + +/** + * Function: createUndoableEdit + * + * Creates a new <mxUndoableEdit> that implements the + * notify function to fire a <change> and <notify> event + * through the <mxUndoableEdit>'s source. + */ +mxGraphModel.prototype.createUndoableEdit = function() +{ + var edit = new mxUndoableEdit(this, true); + + edit.notify = function() + { + // LATER: Remove changes property (deprecated) + edit.source.fireEvent(new mxEventObject(mxEvent.CHANGE, + 'edit', edit, 'changes', edit.changes)); + edit.source.fireEvent(new mxEventObject(mxEvent.NOTIFY, + 'edit', edit, 'changes', edit.changes)); + }; + + return edit; +}; + +/** + * Function: mergeChildren + * + * Merges the children of the given cell into the given target cell inside + * this model. All cells are cloned unless there is a corresponding cell in + * the model with the same id, in which case the source cell is ignored and + * all edges are connected to the corresponding cell in this model. Edges + * are considered to have no identity and are always cloned unless the + * cloneAllEdges flag is set to false, in which case edges with the same + * id in the target model are reconnected to reflect the terminals of the + * source edges. + */ +mxGraphModel.prototype.mergeChildren = function(from, to, cloneAllEdges) +{ + cloneAllEdges = (cloneAllEdges != null) ? cloneAllEdges : true; + + this.beginUpdate(); + try + { + var mapping = new Object(); + this.mergeChildrenImpl(from, to, cloneAllEdges, mapping); + + // Post-processes all edges in the mapping and + // reconnects the terminals to the corresponding + // cells in the target model + for (var key in mapping) + { + var cell = mapping[key]; + var terminal = this.getTerminal(cell, true); + + if (terminal != null) + { + terminal = mapping[mxCellPath.create(terminal)]; + this.setTerminal(cell, terminal, true); + } + + terminal = this.getTerminal(cell, false); + + if (terminal != null) + { + terminal = mapping[mxCellPath.create(terminal)]; + this.setTerminal(cell, terminal, false); + } + } + } + finally + { + this.endUpdate(); + } +}; + +/** + * Function: mergeChildren + * + * Clones the children of the source cell into the given target cell in + * this model and adds an entry to the mapping that maps from the source + * cell to the target cell with the same id or the clone of the source cell + * that was inserted into this model. + */ +mxGraphModel.prototype.mergeChildrenImpl = function(from, to, cloneAllEdges, mapping) +{ + this.beginUpdate(); + try + { + var childCount = from.getChildCount(); + + for (var i = 0; i < childCount; i++) + { + var cell = from.getChildAt(i); + + if (typeof(cell.getId) == 'function') + { + var id = cell.getId(); + var target = (id != null && (!this.isEdge(cell) || !cloneAllEdges)) ? + this.getCell(id) : null; + + // Clones and adds the child if no cell exists for the id + if (target == null) + { + var clone = cell.clone(); + clone.setId(id); + + // Sets the terminals from the original cell to the clone + // because the lookup uses strings not cells in JS + clone.setTerminal(cell.getTerminal(true), true); + clone.setTerminal(cell.getTerminal(false), false); + + // Do *NOT* use model.add as this will move the edge away + // from the parent in updateEdgeParent if maintainEdgeParent + // is enabled in the target model + target = to.insert(clone); + this.cellAdded(target); + } + + // Stores the mapping for later reconnecting edges + mapping[mxCellPath.create(cell)] = target; + + // Recurses + this.mergeChildrenImpl(cell, target, cloneAllEdges, mapping); + } + } + } + finally + { + this.endUpdate(); + } +}; + +/** + * Function: getParents + * + * Returns an array that represents the set (no duplicates) of all parents + * for the given array of cells. + * + * Parameters: + * + * cells - Array of cells whose parents should be returned. + */ +mxGraphModel.prototype.getParents = function(cells) +{ + var parents = []; + + if (cells != null) + { + var hash = new Object(); + + for (var i = 0; i < cells.length; i++) + { + var parent = this.getParent(cells[i]); + + if (parent != null) + { + var id = mxCellPath.create(parent); + + if (hash[id] == null) + { + hash[id] = parent; + parents.push(parent); + } + } + } + } + + return parents; +}; + +// +// Cell Cloning +// + +/** + * Function: cloneCell + * + * Returns a deep clone of the given <mxCell> (including + * the children) which is created using <cloneCells>. + * + * Parameters: + * + * cell - <mxCell> to be cloned. + */ +mxGraphModel.prototype.cloneCell = function(cell) +{ + if (cell != null) + { + return this.cloneCells([cell], true)[0]; + } + + return null; +}; + +/** + * Function: cloneCells + * + * Returns an array of clones for the given array of <mxCells>. + * Depending on the value of includeChildren, a deep clone is created for + * each cell. Connections are restored based if the corresponding + * cell is contained in the passed in array. + * + * Parameters: + * + * cells - Array of <mxCell> to be cloned. + * includeChildren - Boolean indicating if the cells should be cloned + * with all descendants. + */ +mxGraphModel.prototype.cloneCells = function(cells, includeChildren) +{ + var mapping = new Object(); + var clones = []; + + for (var i = 0; i < cells.length; i++) + { + if (cells[i] != null) + { + clones.push(this.cloneCellImpl(cells[i], mapping, includeChildren)); + } + else + { + clones.push(null); + } + } + + for (var i = 0; i < clones.length; i++) + { + if (clones[i] != null) + { + this.restoreClone(clones[i], cells[i], mapping); + } + } + + return clones; +}; + +/** + * Function: cloneCellImpl + * + * Inner helper method for cloning cells recursively. + */ +mxGraphModel.prototype.cloneCellImpl = function(cell, mapping, includeChildren) +{ + var clone = this.cellCloned(cell); + + // Stores the clone in the lookup under the + // cell path for the original cell + mapping[mxObjectIdentity.get(cell)] = clone; + + if (includeChildren) + { + var childCount = this.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + var cloneChild = this.cloneCellImpl( + this.getChildAt(cell, i), mapping, true); + clone.insert(cloneChild); + } + } + + return clone; +}; + +/** + * Function: cellCloned + * + * Hook for cloning the cell. This returns cell.clone() or + * any possible exceptions. + */ +mxGraphModel.prototype.cellCloned = function(cell) +{ + return cell.clone(); +}; + +/** + * Function: restoreClone + * + * Inner helper method for restoring the connections in + * a network of cloned cells. + */ +mxGraphModel.prototype.restoreClone = function(clone, cell, mapping) +{ + var source = this.getTerminal(cell, true); + + if (source != null) + { + var tmp = mapping[mxObjectIdentity.get(source)]; + + if (tmp != null) + { + tmp.insertEdge(clone, true); + } + } + + var target = this.getTerminal(cell, false); + + if (target != null) + { + var tmp = mapping[mxObjectIdentity.get(target)]; + + if (tmp != null) + { + tmp.insertEdge(clone, false); + } + } + + var childCount = this.getChildCount(clone); + + for (var i = 0; i < childCount; i++) + { + this.restoreClone(this.getChildAt(clone, i), + this.getChildAt(cell, i), mapping); + } +}; + +// +// Atomic changes +// + +/** + * Class: mxRootChange + * + * Action to change the root in a model. + * + * Constructor: mxRootChange + * + * Constructs a change of the root in the + * specified model. + */ +function mxRootChange(model, root) +{ + this.model = model; + this.root = root; + this.previous = root; +}; + +/** + * Function: execute + * + * Carries out a change of the root using + * <mxGraphModel.rootChanged>. + */ +mxRootChange.prototype.execute = function() +{ + this.root = this.previous; + this.previous = this.model.rootChanged(this.previous); +}; + +/** + * Class: mxChildChange + * + * Action to add or remove a child in a model. + * + * Constructor: mxChildChange + * + * Constructs a change of a child in the + * specified model. + */ +function mxChildChange(model, parent, child, index) +{ + this.model = model; + this.parent = parent; + this.previous = parent; + this.child = child; + this.index = index; + this.previousIndex = index; +}; + +/** + * Function: execute + * + * Changes the parent of <child> using + * <mxGraphModel.parentForCellChanged> and + * removes or restores the cell's + * connections. + */ +mxChildChange.prototype.execute = function() +{ + var tmp = this.model.getParent(this.child); + var tmp2 = (tmp != null) ? tmp.getIndex(this.child) : 0; + + if (this.previous == null) + { + this.connect(this.child, false); + } + + tmp = this.model.parentForCellChanged( + this.child, this.previous, this.previousIndex); + + if (this.previous != null) + { + this.connect(this.child, true); + } + + this.parent = this.previous; + this.previous = tmp; + this.index = this.previousIndex; + this.previousIndex = tmp2; +}; + +/** + * Function: disconnect + * + * Disconnects the given cell recursively from its + * terminals and stores the previous terminal in the + * cell's terminals. + */ +mxChildChange.prototype.connect = function(cell, isConnect) +{ + isConnect = (isConnect != null) ? isConnect : true; + + var source = cell.getTerminal(true); + var target = cell.getTerminal(false); + + if (source != null) + { + if (isConnect) + { + this.model.terminalForCellChanged(cell, source, true); + } + else + { + this.model.terminalForCellChanged(cell, null, true); + } + } + + if (target != null) + { + if (isConnect) + { + this.model.terminalForCellChanged(cell, target, false); + } + else + { + this.model.terminalForCellChanged(cell, null, false); + } + } + + cell.setTerminal(source, true); + cell.setTerminal(target, false); + + var childCount = this.model.getChildCount(cell); + + for (var i=0; i<childCount; i++) + { + this.connect(this.model.getChildAt(cell, i), isConnect); + } +}; + +/** + * Class: mxTerminalChange + * + * Action to change a terminal in a model. + * + * Constructor: mxTerminalChange + * + * Constructs a change of a terminal in the + * specified model. + */ +function mxTerminalChange(model, cell, terminal, source) +{ + this.model = model; + this.cell = cell; + this.terminal = terminal; + this.previous = terminal; + this.source = source; +}; + +/** + * Function: execute + * + * Changes the terminal of <cell> to <previous> using + * <mxGraphModel.terminalForCellChanged>. + */ +mxTerminalChange.prototype.execute = function() +{ + this.terminal = this.previous; + this.previous = this.model.terminalForCellChanged( + this.cell, this.previous, this.source); +}; + +/** + * Class: mxValueChange + * + * Action to change a user object in a model. + * + * Constructor: mxValueChange + * + * Constructs a change of a user object in the + * specified model. + */ +function mxValueChange(model, cell, value) +{ + this.model = model; + this.cell = cell; + this.value = value; + this.previous = value; +}; + +/** + * Function: execute + * + * Changes the value of <cell> to <previous> using + * <mxGraphModel.valueForCellChanged>. + */ +mxValueChange.prototype.execute = function() +{ + this.value = this.previous; + this.previous = this.model.valueForCellChanged( + this.cell, this.previous); +}; + +/** + * Class: mxStyleChange + * + * Action to change a cell's style in a model. + * + * Constructor: mxStyleChange + * + * Constructs a change of a style in the + * specified model. + */ +function mxStyleChange(model, cell, style) +{ + this.model = model; + this.cell = cell; + this.style = style; + this.previous = style; +}; + +/** + * Function: execute + * + * Changes the style of <cell> to <previous> using + * <mxGraphModel.styleForCellChanged>. + */ +mxStyleChange.prototype.execute = function() +{ + this.style = this.previous; + this.previous = this.model.styleForCellChanged( + this.cell, this.previous); +}; + +/** + * Class: mxGeometryChange + * + * Action to change a cell's geometry in a model. + * + * Constructor: mxGeometryChange + * + * Constructs a change of a geometry in the + * specified model. + */ +function mxGeometryChange(model, cell, geometry) +{ + this.model = model; + this.cell = cell; + this.geometry = geometry; + this.previous = geometry; +}; + +/** + * Function: execute + * + * Changes the geometry of <cell> ro <previous> using + * <mxGraphModel.geometryForCellChanged>. + */ +mxGeometryChange.prototype.execute = function() +{ + this.geometry = this.previous; + this.previous = this.model.geometryForCellChanged( + this.cell, this.previous); +}; + +/** + * Class: mxCollapseChange + * + * Action to change a cell's collapsed state in a model. + * + * Constructor: mxCollapseChange + * + * Constructs a change of a collapsed state in the + * specified model. + */ +function mxCollapseChange(model, cell, collapsed) +{ + this.model = model; + this.cell = cell; + this.collapsed = collapsed; + this.previous = collapsed; +}; + +/** + * Function: execute + * + * Changes the collapsed state of <cell> to <previous> using + * <mxGraphModel.collapsedStateForCellChanged>. + */ +mxCollapseChange.prototype.execute = function() +{ + this.collapsed = this.previous; + this.previous = this.model.collapsedStateForCellChanged( + this.cell, this.previous); +}; + +/** + * Class: mxVisibleChange + * + * Action to change a cell's visible state in a model. + * + * Constructor: mxVisibleChange + * + * Constructs a change of a visible state in the + * specified model. + */ +function mxVisibleChange(model, cell, visible) +{ + this.model = model; + this.cell = cell; + this.visible = visible; + this.previous = visible; +}; + +/** + * Function: execute + * + * Changes the visible state of <cell> to <previous> using + * <mxGraphModel.visibleStateForCellChanged>. + */ +mxVisibleChange.prototype.execute = function() +{ + this.visible = this.previous; + this.previous = this.model.visibleStateForCellChanged( + this.cell, this.previous); +}; + +/** + * Class: mxCellAttributeChange + * + * Action to change the attribute of a cell's user object. + * There is no method on the graph model that uses this + * action. To use the action, you can use the code shown + * in the example below. + * + * Example: + * + * To change the attributeName in the cell's user object + * to attributeValue, use the following code: + * + * (code) + * model.beginUpdate(); + * try + * { + * var edit = new mxCellAttributeChange( + * cell, attributeName, attributeValue); + * model.execute(edit); + * } + * finally + * { + * model.endUpdate(); + * } + * (end) + * + * Constructor: mxCellAttributeChange + * + * Constructs a change of a attribute of the DOM node + * stored as the value of the given <mxCell>. + */ +function mxCellAttributeChange(cell, attribute, value) +{ + this.cell = cell; + this.attribute = attribute; + this.value = value; + this.previous = value; +}; + +/** + * Function: execute + * + * Changes the attribute of the cell's user object by + * using <mxCell.setAttribute>. + */ +mxCellAttributeChange.prototype.execute = function() +{ + var tmp = this.cell.getAttribute(this.attribute); + + if (this.previous == null) + { + this.cell.value.removeAttribute(this.attribute); + } + else + { + this.cell.setAttribute(this.attribute, this.previous); + } + + this.previous = tmp; +}; diff --git a/src/js/mxClient.js b/src/js/mxClient.js new file mode 100644 index 0000000..a23b5fc --- /dev/null +++ b/src/js/mxClient.js @@ -0,0 +1,643 @@ +/** + * $Id: mxClient.js,v 1.203 2012-07-19 15:19:07 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +var mxClient = +{ + + /** + * Class: mxClient + * + * Bootstrapping mechanism for the mxGraph thin client. The production version + * of this file contains all code required to run the mxGraph thin client, as + * well as global constants to identify the browser and operating system in + * use. You may have to load chrome://global/content/contentAreaUtils.js in + * your page to disable certain security restrictions in Mozilla. + * + * Variable: VERSION + * + * Contains the current version of the mxGraph library. The strings that + * communicate versions of mxGraph use the following format. + * + * versionMajor.versionMinor.buildNumber.revisionNumber + * + * Current version is 1.10.4.1. + */ + VERSION: '1.10.4.1', + + /** + * Variable: IS_IE + * + * True if the current browser is Internet Explorer. + */ + IS_IE: navigator.userAgent.indexOf('MSIE') >= 0, + + /** + * Variable: IS_IE6 + * + * True if the current browser is Internet Explorer 6.x. + */ + IS_IE6: navigator.userAgent.indexOf('MSIE 6') >= 0, + + /** + * Variable: IS_QUIRKS + * + * True if the current browser is Internet Explorer and it is in quirks mode. + */ + IS_QUIRKS: navigator.userAgent.indexOf('MSIE') >= 0 && (document.documentMode == null || document.documentMode == 5), + + /** + * Variable: IS_NS + * + * True if the current browser is Netscape (including Firefox). + */ + IS_NS: navigator.userAgent.indexOf('Mozilla/') >= 0 && + navigator.userAgent.indexOf('MSIE') < 0, + + /** + * Variable: IS_OP + * + * True if the current browser is Opera. + */ + IS_OP: navigator.userAgent.indexOf('Opera/') >= 0, + + /** + * Variable: IS_OT + * + * True if -o-transform is available as a CSS style. This is the case + * for Opera browsers that use Presto/2.5 and later. + */ + IS_OT: navigator.userAgent.indexOf('Presto/2.4.') < 0 && + navigator.userAgent.indexOf('Presto/2.3.') < 0 && + navigator.userAgent.indexOf('Presto/2.2.') < 0 && + navigator.userAgent.indexOf('Presto/2.1.') < 0 && + navigator.userAgent.indexOf('Presto/2.0.') < 0 && + navigator.userAgent.indexOf('Presto/1.') < 0, + + /** + * Variable: IS_SF + * + * True if the current browser is Safari. + */ + IS_SF: navigator.userAgent.indexOf('AppleWebKit/') >= 0 && + navigator.userAgent.indexOf('Chrome/') < 0, + + /** + * Variable: IS_GC + * + * True if the current browser is Google Chrome. + */ + IS_GC: navigator.userAgent.indexOf('Chrome/') >= 0, + + /** + * Variable: IS_MT + * + * True if -moz-transform is available as a CSS style. This is the case + * for all Firefox-based browsers newer than or equal 3, such as Camino, + * Iceweasel, Seamonkey and Iceape. + */ + IS_MT: (navigator.userAgent.indexOf('Firefox/') >= 0 && + navigator.userAgent.indexOf('Firefox/1.') < 0 && + navigator.userAgent.indexOf('Firefox/2.') < 0) || + (navigator.userAgent.indexOf('Iceweasel/') >= 0 && + navigator.userAgent.indexOf('Iceweasel/1.') < 0 && + navigator.userAgent.indexOf('Iceweasel/2.') < 0) || + (navigator.userAgent.indexOf('SeaMonkey/') >= 0 && + navigator.userAgent.indexOf('SeaMonkey/1.') < 0) || + (navigator.userAgent.indexOf('Iceape/') >= 0 && + navigator.userAgent.indexOf('Iceape/1.') < 0), + + /** + * Variable: IS_SVG + * + * True if the browser supports SVG. + */ + IS_SVG: navigator.userAgent.indexOf('Firefox/') >= 0 || // FF and Camino + navigator.userAgent.indexOf('Iceweasel/') >= 0 || // Firefox on Debian + navigator.userAgent.indexOf('Seamonkey/') >= 0 || // Firefox-based + navigator.userAgent.indexOf('Iceape/') >= 0 || // Seamonkey on Debian + navigator.userAgent.indexOf('Galeon/') >= 0 || // Gnome Browser (old) + navigator.userAgent.indexOf('Epiphany/') >= 0 || // Gnome Browser (new) + navigator.userAgent.indexOf('AppleWebKit/') >= 0 || // Safari/Google Chrome + navigator.userAgent.indexOf('Gecko/') >= 0 || // Netscape/Gecko + navigator.userAgent.indexOf('Opera/') >= 0, + + + /** + * Variable: NO_FO + * + * True if foreignObject support is not available. This is the case for + * Opera and older SVG-based browsers. IE does not require this type + * of tag. + */ + NO_FO: navigator.userAgent.indexOf('Firefox/1.') >= 0 || + navigator.userAgent.indexOf('Iceweasel/1.') >= 0 || + navigator.userAgent.indexOf('Firefox/2.') >= 0 || + navigator.userAgent.indexOf('Iceweasel/2.') >= 0 || + navigator.userAgent.indexOf('SeaMonkey/1.') >= 0 || + navigator.userAgent.indexOf('Iceape/1.') >= 0 || + navigator.userAgent.indexOf('Camino/1.') >= 0 || + navigator.userAgent.indexOf('Epiphany/2.') >= 0 || + navigator.userAgent.indexOf('Opera/') >= 0 || + navigator.userAgent.indexOf('MSIE') >= 0 || + navigator.userAgent.indexOf('Mozilla/2.') >= 0, // Safari/Google Chrome + + /** + * Variable: IS_VML + * + * True if the browser supports VML. + */ + IS_VML: navigator.appName.toUpperCase() == 'MICROSOFT INTERNET EXPLORER', + + /** + * Variable: IS_MAC + * + * True if the client is a Mac. + */ + IS_MAC: navigator.userAgent.toUpperCase().indexOf('MACINTOSH') > 0, + + /** + * Variable: IS_TOUCH + * + * True if this client uses a touch interface (no mouse). Currently this + * detects IPads, IPods, IPhones and Android devices. + */ + IS_TOUCH: navigator.userAgent.toUpperCase().indexOf('IPAD') > 0 || + navigator.userAgent.toUpperCase().indexOf('IPOD') > 0 || + navigator.userAgent.toUpperCase().indexOf('IPHONE') > 0 || + navigator.userAgent.toUpperCase().indexOf('ANDROID') > 0, + + /** + * Variable: IS_LOCAL + * + * True if the documents location does not start with http:// or https://. + */ + IS_LOCAL: document.location.href.indexOf('http://') < 0 && + document.location.href.indexOf('https://') < 0, + + /** + * Function: isBrowserSupported + * + * Returns true if the current browser is supported, that is, if + * <mxClient.IS_VML> or <mxClient.IS_SVG> is true. + * + * Example: + * + * (code) + * if (!mxClient.isBrowserSupported()) + * { + * mxUtils.error('Browser is not supported!', 200, false); + * } + * (end) + */ + isBrowserSupported: function() + { + return mxClient.IS_VML || mxClient.IS_SVG; + }, + + /** + * Function: link + * + * Adds a link node to the head of the document. Use this + * to add a stylesheet to the page as follows: + * + * (code) + * mxClient.link('stylesheet', filename); + * (end) + * + * where filename is the (relative) URL of the stylesheet. The charset + * is hardcoded to ISO-8859-1 and the type is text/css. + * + * Parameters: + * + * rel - String that represents the rel attribute of the link node. + * href - String that represents the href attribute of the link node. + * doc - Optional parent document of the link node. + */ + link: function(rel, href, doc) + { + doc = doc || document; + + // Workaround for Operation Aborted in IE6 if base tag is used in head + if (mxClient.IS_IE6) + { + doc.write('<link rel="'+rel+'" href="'+href+'" charset="ISO-8859-1" type="text/css"/>'); + } + else + { + var link = doc.createElement('link'); + + link.setAttribute('rel', rel); + link.setAttribute('href', href); + link.setAttribute('charset', 'ISO-8859-1'); + link.setAttribute('type', 'text/css'); + + var head = doc.getElementsByTagName('head')[0]; + head.appendChild(link); + } + }, + + /** + * Function: include + * + * Dynamically adds a script node to the document header. + * + * In production environments, the includes are resolved in the mxClient.js + * file to reduce the number of requests required for client startup. This + * function should only be used in development environments, but not in + * production systems. + */ + include: function(src) + { + document.write('<script src="'+src+'"></script>'); + }, + + /** + * Function: dispose + * + * Frees up memory in IE by resolving cyclic dependencies between the DOM + * and the JavaScript objects. This is always invoked in IE when the page + * unloads. + */ + dispose: function() + { + // Cleans all objects where listeners have been added + for (var i = 0; i < mxEvent.objects.length; i++) + { + if (mxEvent.objects[i].mxListenerList != null) + { + mxEvent.removeAllListeners(mxEvent.objects[i]); + } + } + } + +}; + +/** + * Variable: mxLoadResources + * + * Optional global config variable to toggle loading of the two resource files + * in <mxGraph> and <mxEditor>. Default is true. NOTE: This is a global variable, + * not a variable of mxClient. + * + * (code) + * <script type="text/javascript"> + * var mxLoadResources = false; + * </script> + * <script type="text/javascript" src="/path/to/core/directory/js/mxClient.js"></script> + * (end) + */ +if (typeof(mxLoadResources) == 'undefined') +{ + mxLoadResources = true; +} + +/** + * Variable: mxLoadStylesheets + * + * Optional global config variable to toggle loading of the CSS files when + * the library is initialized. Default is true. NOTE: This is a global variable, + * not a variable of mxClient. + * + * (code) + * <script type="text/javascript"> + * var mxLoadStylesheets = false; + * </script> + * <script type="text/javascript" src="/path/to/core/directory/js/mxClient.js"></script> + * (end) + */ +if (typeof(mxLoadStylesheets) == 'undefined') +{ + mxLoadStylesheets = true; +} + +/** + * Variable: basePath + * + * Basepath for all URLs in the core without trailing slash. Default is '.'. + * Set mxBasePath prior to loading the mxClient library as follows to override + * this setting: + * + * (code) + * <script type="text/javascript"> + * mxBasePath = '/path/to/core/directory'; + * </script> + * <script type="text/javascript" src="/path/to/core/directory/js/mxClient.js"></script> + * (end) + * + * When using a relative path, the path is relative to the URL of the page that + * contains the assignment. Trailing slashes are automatically removed. + */ +if (typeof(mxBasePath) != 'undefined' && mxBasePath.length > 0) +{ + // Adds a trailing slash if required + if (mxBasePath.substring(mxBasePath.length - 1) == '/') + { + mxBasePath = mxBasePath.substring(0, mxBasePath.length - 1); + } + + mxClient.basePath = mxBasePath; +} +else +{ + mxClient.basePath = '.'; +} + +/** + * Variable: imageBasePath + * + * Basepath for all images URLs in the core without trailing slash. Default is + * <mxClient.basePath> + '/images'. Set mxImageBasePath prior to loading the + * mxClient library as follows to override this setting: + * + * (code) + * <script type="text/javascript"> + * mxImageBasePath = '/path/to/image/directory'; + * </script> + * <script type="text/javascript" src="/path/to/core/directory/js/mxClient.js"></script> + * (end) + * + * When using a relative path, the path is relative to the URL of the page that + * contains the assignment. Trailing slashes are automatically removed. + */ +if (typeof(mxImageBasePath) != 'undefined' && mxImageBasePath.length > 0) +{ + // Adds a trailing slash if required + if (mxImageBasePath.substring(mxImageBasePath.length - 1) == '/') + { + mxImageBasePath = mxImageBasePath.substring(0, mxImageBasePath.length - 1); + } + + mxClient.imageBasePath = mxImageBasePath; +} +else +{ + mxClient.imageBasePath = mxClient.basePath + '/images'; +} + +/** + * Variable: language + * + * Defines the language of the client, eg. en for english, de for german etc. + * The special value 'none' will disable all built-in internationalization and + * resource loading. See <mxResources.getSpecialBundle> for handling identifiers + * with and without a dash. + * + * Set mxLanguage prior to loading the mxClient library as follows to override + * this setting: + * + * (code) + * <script type="text/javascript"> + * mxLanguage = 'en'; + * </script> + * <script type="text/javascript" src="js/mxClient.js"></script> + * (end) + * + * If internationalization is disabled, then the following variables should be + * overridden to reflect the current language of the system. These variables are + * cleared when i18n is disabled. + * <mxEditor.askZoomResource>, <mxEditor.lastSavedResource>, + * <mxEditor.currentFileResource>, <mxEditor.propertiesResource>, + * <mxEditor.tasksResource>, <mxEditor.helpResource>, <mxEditor.outlineResource>, + * <mxElbowEdgeHandler.doubleClickOrientationResource>, <mxUtils.errorResource>, + * <mxUtils.closeResource>, <mxGraphSelectionModel.doneResource>, + * <mxGraphSelectionModel.updatingSelectionResource>, <mxGraphView.doneResource>, + * <mxGraphView.updatingDocumentResource>, <mxCellRenderer.collapseExpandResource>, + * <mxGraph.containsValidationErrorsResource> and + * <mxGraph.alreadyConnectedResource>. + */ +if (typeof(mxLanguage) != 'undefined') +{ + mxClient.language = mxLanguage; +} +else +{ + mxClient.language = (mxClient.IS_IE) ? navigator.userLanguage : navigator.language; +} + +/** + * Variable: defaultLanguage + * + * Defines the default language which is used in the common resource files. Any + * resources for this language will only load the common resource file, but not + * the language-specific resource file. Default is 'en'. + * + * Set mxDefaultLanguage prior to loading the mxClient library as follows to override + * this setting: + * + * (code) + * <script type="text/javascript"> + * mxDefaultLanguage = 'de'; + * </script> + * <script type="text/javascript" src="js/mxClient.js"></script> + * (end) + */ +if (typeof(mxDefaultLanguage) != 'undefined') +{ + mxClient.defaultLanguage = mxDefaultLanguage; +} +else +{ + mxClient.defaultLanguage = 'en'; +} + +// Adds all required stylesheets and namespaces +if (mxLoadStylesheets) +{ + mxClient.link('stylesheet', mxClient.basePath + '/css/common.css'); +} + +/** + * Variable: languages + * + * Defines the optional array of all supported language extensions. The default + * language does not have to be part of this list. See + * <mxResources.isLanguageSupported>. + * + * (code) + * <script type="text/javascript"> + * mxLanguages = ['de', 'it', 'fr']; + * </script> + * <script type="text/javascript" src="js/mxClient.js"></script> + * (end) + * + * This is used to avoid unnecessary requests to language files, ie. if a 404 + * will be returned. + */ +if (typeof(mxLanguages) != 'undefined') +{ + mxClient.languages = mxLanguages; +} + +if (mxClient.IS_IE) +{ + // IE9/10 standards mode uses SVG (VML is broken) + if (document.documentMode >= 9) + { + mxClient.IS_VML = false; + mxClient.IS_SVG = true; + } + else + { + // Enables support for IE8 standards mode. Note that this requires all attributes for VML + // elements to be set using direct notation, ie. node.attr = value. The use of setAttribute + // is not possible. See mxShape.init for more code to handle this specific document mode. + if (document.documentMode == 8) + { + document.namespaces.add('v', 'urn:schemas-microsoft-com:vml', '#default#VML'); + document.namespaces.add('o', 'urn:schemas-microsoft-com:office:office', '#default#VML'); + } + else + { + document.namespaces.add('v', 'urn:schemas-microsoft-com:vml'); + document.namespaces.add('o', 'urn:schemas-microsoft-com:office:office'); + } + + var ss = document.createStyleSheet(); + ss.cssText = 'v\\:*{behavior:url(#default#VML)}o\\:*{behavior:url(#default#VML)}'; + + if (mxLoadStylesheets) + { + mxClient.link('stylesheet', mxClient.basePath + '/css/explorer.css'); + } + } + + // Cleans up resources when the application terminates + window.attachEvent('onunload', mxClient.dispose); +} + +mxClient.include(mxClient.basePath+'/js/util/mxLog.js'); +mxClient.include(mxClient.basePath+'/js/util/mxObjectIdentity.js'); +mxClient.include(mxClient.basePath+'/js/util/mxDictionary.js'); +mxClient.include(mxClient.basePath+'/js/util/mxResources.js'); +mxClient.include(mxClient.basePath+'/js/util/mxPoint.js'); +mxClient.include(mxClient.basePath+'/js/util/mxRectangle.js'); +mxClient.include(mxClient.basePath+'/js/util/mxEffects.js'); +mxClient.include(mxClient.basePath+'/js/util/mxUtils.js'); +mxClient.include(mxClient.basePath+'/js/util/mxConstants.js'); +mxClient.include(mxClient.basePath+'/js/util/mxEventObject.js'); +mxClient.include(mxClient.basePath+'/js/util/mxMouseEvent.js'); +mxClient.include(mxClient.basePath+'/js/util/mxEventSource.js'); +mxClient.include(mxClient.basePath+'/js/util/mxEvent.js'); +mxClient.include(mxClient.basePath+'/js/util/mxXmlRequest.js'); +mxClient.include(mxClient.basePath+'/js/util/mxClipboard.js'); +mxClient.include(mxClient.basePath+'/js/util/mxWindow.js'); +mxClient.include(mxClient.basePath+'/js/util/mxForm.js'); +mxClient.include(mxClient.basePath+'/js/util/mxImage.js'); +mxClient.include(mxClient.basePath+'/js/util/mxDivResizer.js'); +mxClient.include(mxClient.basePath+'/js/util/mxDragSource.js'); +mxClient.include(mxClient.basePath+'/js/util/mxToolbar.js'); +mxClient.include(mxClient.basePath+'/js/util/mxSession.js'); +mxClient.include(mxClient.basePath+'/js/util/mxUndoableEdit.js'); +mxClient.include(mxClient.basePath+'/js/util/mxUndoManager.js'); +mxClient.include(mxClient.basePath+'/js/util/mxUrlConverter.js'); +mxClient.include(mxClient.basePath+'/js/util/mxPanningManager.js'); +mxClient.include(mxClient.basePath+'/js/util/mxPath.js'); +mxClient.include(mxClient.basePath+'/js/util/mxPopupMenu.js'); +mxClient.include(mxClient.basePath+'/js/util/mxAutoSaveManager.js'); +mxClient.include(mxClient.basePath+'/js/util/mxAnimation.js'); +mxClient.include(mxClient.basePath+'/js/util/mxMorphing.js'); +mxClient.include(mxClient.basePath+'/js/util/mxImageBundle.js'); +mxClient.include(mxClient.basePath+'/js/util/mxImageExport.js'); +mxClient.include(mxClient.basePath+'/js/util/mxXmlCanvas2D.js'); +mxClient.include(mxClient.basePath+'/js/util/mxSvgCanvas2D.js'); +mxClient.include(mxClient.basePath+'/js/util/mxGuide.js'); +mxClient.include(mxClient.basePath+'/js/shape/mxShape.js'); +mxClient.include(mxClient.basePath+'/js/shape/mxStencil.js'); +mxClient.include(mxClient.basePath+'/js/shape/mxStencilRegistry.js'); +mxClient.include(mxClient.basePath+'/js/shape/mxStencilShape.js'); +mxClient.include(mxClient.basePath+'/js/shape/mxMarker.js'); +mxClient.include(mxClient.basePath+'/js/shape/mxActor.js'); +mxClient.include(mxClient.basePath+'/js/shape/mxCloud.js'); +mxClient.include(mxClient.basePath+'/js/shape/mxRectangleShape.js'); +mxClient.include(mxClient.basePath+'/js/shape/mxEllipse.js'); +mxClient.include(mxClient.basePath+'/js/shape/mxDoubleEllipse.js'); +mxClient.include(mxClient.basePath+'/js/shape/mxRhombus.js'); +mxClient.include(mxClient.basePath+'/js/shape/mxPolyline.js'); +mxClient.include(mxClient.basePath+'/js/shape/mxArrow.js'); +mxClient.include(mxClient.basePath+'/js/shape/mxText.js'); +mxClient.include(mxClient.basePath+'/js/shape/mxTriangle.js'); +mxClient.include(mxClient.basePath+'/js/shape/mxHexagon.js'); +mxClient.include(mxClient.basePath+'/js/shape/mxLine.js'); +mxClient.include(mxClient.basePath+'/js/shape/mxImageShape.js'); +mxClient.include(mxClient.basePath+'/js/shape/mxLabel.js'); +mxClient.include(mxClient.basePath+'/js/shape/mxCylinder.js'); +mxClient.include(mxClient.basePath+'/js/shape/mxConnector.js'); +mxClient.include(mxClient.basePath+'/js/shape/mxSwimlane.js'); +mxClient.include(mxClient.basePath+'/js/layout/mxGraphLayout.js'); +mxClient.include(mxClient.basePath+'/js/layout/mxStackLayout.js'); +mxClient.include(mxClient.basePath+'/js/layout/mxPartitionLayout.js'); +mxClient.include(mxClient.basePath+'/js/layout/mxCompactTreeLayout.js'); +mxClient.include(mxClient.basePath+'/js/layout/mxFastOrganicLayout.js'); +mxClient.include(mxClient.basePath+'/js/layout/mxCircleLayout.js'); +mxClient.include(mxClient.basePath+'/js/layout/mxParallelEdgeLayout.js'); +mxClient.include(mxClient.basePath+'/js/layout/mxCompositeLayout.js'); +mxClient.include(mxClient.basePath+'/js/layout/mxEdgeLabelLayout.js'); +mxClient.include(mxClient.basePath+'/js/layout/hierarchical/model/mxGraphAbstractHierarchyCell.js'); +mxClient.include(mxClient.basePath+'/js/layout/hierarchical/model/mxGraphHierarchyNode.js'); +mxClient.include(mxClient.basePath+'/js/layout/hierarchical/model/mxGraphHierarchyEdge.js'); +mxClient.include(mxClient.basePath+'/js/layout/hierarchical/model/mxGraphHierarchyModel.js'); +mxClient.include(mxClient.basePath+'/js/layout/hierarchical/stage/mxHierarchicalLayoutStage.js'); +mxClient.include(mxClient.basePath+'/js/layout/hierarchical/stage/mxMedianHybridCrossingReduction.js'); +mxClient.include(mxClient.basePath+'/js/layout/hierarchical/stage/mxMinimumCycleRemover.js'); +mxClient.include(mxClient.basePath+'/js/layout/hierarchical/stage/mxCoordinateAssignment.js'); +mxClient.include(mxClient.basePath+'/js/layout/hierarchical/mxHierarchicalLayout.js'); +mxClient.include(mxClient.basePath+'/js/model/mxGraphModel.js'); +mxClient.include(mxClient.basePath+'/js/model/mxCell.js'); +mxClient.include(mxClient.basePath+'/js/model/mxGeometry.js'); +mxClient.include(mxClient.basePath+'/js/model/mxCellPath.js'); +mxClient.include(mxClient.basePath+'/js/view/mxPerimeter.js'); +mxClient.include(mxClient.basePath+'/js/view/mxPrintPreview.js'); +mxClient.include(mxClient.basePath+'/js/view/mxStylesheet.js'); +mxClient.include(mxClient.basePath+'/js/view/mxCellState.js'); +mxClient.include(mxClient.basePath+'/js/view/mxGraphSelectionModel.js'); +mxClient.include(mxClient.basePath+'/js/view/mxCellEditor.js'); +mxClient.include(mxClient.basePath+'/js/view/mxCellRenderer.js'); +mxClient.include(mxClient.basePath+'/js/view/mxEdgeStyle.js'); +mxClient.include(mxClient.basePath+'/js/view/mxStyleRegistry.js'); +mxClient.include(mxClient.basePath+'/js/view/mxGraphView.js'); +mxClient.include(mxClient.basePath+'/js/view/mxGraph.js'); +mxClient.include(mxClient.basePath+'/js/view/mxCellOverlay.js'); +mxClient.include(mxClient.basePath+'/js/view/mxOutline.js'); +mxClient.include(mxClient.basePath+'/js/view/mxMultiplicity.js'); +mxClient.include(mxClient.basePath+'/js/view/mxLayoutManager.js'); +mxClient.include(mxClient.basePath+'/js/view/mxSpaceManager.js'); +mxClient.include(mxClient.basePath+'/js/view/mxSwimlaneManager.js'); +mxClient.include(mxClient.basePath+'/js/view/mxTemporaryCellStates.js'); +mxClient.include(mxClient.basePath+'/js/view/mxCellStatePreview.js'); +mxClient.include(mxClient.basePath+'/js/view/mxConnectionConstraint.js'); +mxClient.include(mxClient.basePath+'/js/handler/mxGraphHandler.js'); +mxClient.include(mxClient.basePath+'/js/handler/mxPanningHandler.js'); +mxClient.include(mxClient.basePath+'/js/handler/mxCellMarker.js'); +mxClient.include(mxClient.basePath+'/js/handler/mxSelectionCellsHandler.js'); +mxClient.include(mxClient.basePath+'/js/handler/mxConnectionHandler.js'); +mxClient.include(mxClient.basePath+'/js/handler/mxConstraintHandler.js'); +mxClient.include(mxClient.basePath+'/js/handler/mxRubberband.js'); +mxClient.include(mxClient.basePath+'/js/handler/mxVertexHandler.js'); +mxClient.include(mxClient.basePath+'/js/handler/mxEdgeHandler.js'); +mxClient.include(mxClient.basePath+'/js/handler/mxElbowEdgeHandler.js'); +mxClient.include(mxClient.basePath+'/js/handler/mxEdgeSegmentHandler.js'); +mxClient.include(mxClient.basePath+'/js/handler/mxKeyHandler.js'); +mxClient.include(mxClient.basePath+'/js/handler/mxTooltipHandler.js'); +mxClient.include(mxClient.basePath+'/js/handler/mxCellTracker.js'); +mxClient.include(mxClient.basePath+'/js/handler/mxCellHighlight.js'); +mxClient.include(mxClient.basePath+'/js/editor/mxDefaultKeyHandler.js'); +mxClient.include(mxClient.basePath+'/js/editor/mxDefaultPopupMenu.js'); +mxClient.include(mxClient.basePath+'/js/editor/mxDefaultToolbar.js'); +mxClient.include(mxClient.basePath+'/js/editor/mxEditor.js'); +mxClient.include(mxClient.basePath+'/js/io/mxCodecRegistry.js'); +mxClient.include(mxClient.basePath+'/js/io/mxCodec.js'); +mxClient.include(mxClient.basePath+'/js/io/mxObjectCodec.js'); +mxClient.include(mxClient.basePath+'/js/io/mxCellCodec.js'); +mxClient.include(mxClient.basePath+'/js/io/mxModelCodec.js'); +mxClient.include(mxClient.basePath+'/js/io/mxRootChangeCodec.js'); +mxClient.include(mxClient.basePath+'/js/io/mxChildChangeCodec.js'); +mxClient.include(mxClient.basePath+'/js/io/mxTerminalChangeCodec.js'); +mxClient.include(mxClient.basePath+'/js/io/mxGenericChangeCodec.js'); +mxClient.include(mxClient.basePath+'/js/io/mxGraphCodec.js'); +mxClient.include(mxClient.basePath+'/js/io/mxGraphViewCodec.js'); +mxClient.include(mxClient.basePath+'/js/io/mxStylesheetCodec.js'); +mxClient.include(mxClient.basePath+'/js/io/mxDefaultKeyHandlerCodec.js'); +mxClient.include(mxClient.basePath+'/js/io/mxDefaultToolbarCodec.js'); +mxClient.include(mxClient.basePath+'/js/io/mxDefaultPopupMenuCodec.js'); +mxClient.include(mxClient.basePath+'/js/io/mxEditorCodec.js'); diff --git a/src/js/shape/mxActor.js b/src/js/shape/mxActor.js new file mode 100644 index 0000000..e6a0765 --- /dev/null +++ b/src/js/shape/mxActor.js @@ -0,0 +1,183 @@ +/** + * $Id: mxActor.js,v 1.35 2012-07-31 11:46:53 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxActor + * + * Extends <mxShape> to implement an actor shape. If a custom shape with one + * filled area is needed, then this shape's <redrawPath> should be overridden. + * + * Example: + * + * (code) + * function SampleShape() { } + * + * SampleShape.prototype = new mxActor(); + * SampleShape.prototype.constructor = vsAseShape; + * + * mxCellRenderer.prototype.defaultShapes['sample'] = SampleShape; + * SampleShape.prototype.redrawPath = function(path, x, y, w, h) + * { + * path.moveTo(0, 0); + * path.lineTo(w, h); + * // ... + * path.close(); + * } + * (end) + * + * This shape is registered under <mxConstants.SHAPE_ACTOR> in + * <mxCellRenderer>. + * + * Constructor: mxActor + * + * Constructs a new actor shape. + * + * Parameters: + * + * bounds - <mxRectangle> that defines the bounds. This is stored in + * <mxShape.bounds>. + * fill - String that defines the fill color. This is stored in <fill>. + * stroke - String that defines the stroke color. This is stored in <stroke>. + * strokewidth - Optional integer that defines the stroke width. Default is + * 1. This is stored in <strokewidth>. + */ +function mxActor(bounds, fill, stroke, strokewidth) +{ + this.bounds = bounds; + this.fill = fill; + this.stroke = stroke; + this.strokewidth = (strokewidth != null) ? strokewidth : 1; +}; + +/** + * Extends mxShape. + */ +mxActor.prototype = new mxShape(); +mxActor.prototype.constructor = mxActor; + +/** + * Variable: mixedModeHtml + * + * Overrides the parent value with false, meaning it will + * draw in VML in mixed Html mode. + */ +mxActor.prototype.mixedModeHtml = false; + +/** + * Variable: preferModeHtml + * + * Overrides the parent value with false, meaning it will + * draw as VML in prefer Html mode. + */ +mxActor.prototype.preferModeHtml = false; + +/** + * Variable: vmlScale + * + * Renders VML with a scale of 2. + */ +mxActor.prototype.vmlScale = 2; + +/** + * Function: createVml + * + * Creates and returns the VML node(s) to represent this shape. + */ +mxActor.prototype.createVml = function() +{ + var node = document.createElement('v:shape'); + node.style.position = 'absolute'; + this.configureVmlShape(node); + + return node; +}; + +/** + * Function: redrawVml + * + * Updates the VML node(s) to reflect the latest bounds and scale. + */ +mxActor.prototype.redrawVml = function() +{ + this.updateVmlShape(this.node); + this.node.path = this.createPath(); +}; + +/** + * Function: createSvg + * + * Creates and returns the SVG node(s) to represent this shape. + */ +mxActor.prototype.createSvg = function() +{ + return this.createSvgGroup('path'); +}; + +/** + * Function: redrawSvg + * + * Updates the SVG node(s) to reflect the latest bounds and scale. + */ +mxActor.prototype.redrawSvg = function() +{ + var strokeWidth = Math.round(Math.max(1, this.strokewidth * this.scale)); + this.innerNode.setAttribute('stroke-width', strokeWidth); + this.innerNode.setAttribute('stroke-linejoin', 'round'); + + if (this.crisp && (this.rotation == null || this.rotation == 0)) + { + this.innerNode.setAttribute('shape-rendering', 'crispEdges'); + } + else + { + this.innerNode.removeAttribute('shape-rendering'); + } + + var d = this.createPath(); + + if (d.length > 0) + { + this.innerNode.setAttribute('d', d); + + if (this.shadowNode != null) + { + this.shadowNode.setAttribute('transform', this.getSvgShadowTransform() + + (this.innerNode.getAttribute('transform') || '')); + this.shadowNode.setAttribute('stroke-width', strokeWidth); + this.shadowNode.setAttribute('d', d); + } + } + else + { + this.innerNode.removeAttribute('d'); + + if (this.shadowNode != null) + { + this.shadowNode.removeAttribute('d'); + } + } + + if (this.isDashed) + { + var phase = Math.max(1, Math.round(3 * this.scale * this.strokewidth)); + this.innerNode.setAttribute('stroke-dasharray', phase + ' ' + phase); + } +}; + +/** + * Function: redrawPath + * + * Draws the path for this shape. This method uses the <mxPath> + * abstraction to paint the shape for VML and SVG. + */ +mxActor.prototype.redrawPath = function(path, x, y, w, h) +{ + var width = w/3; + path.moveTo(0, h); + path.curveTo(0, 3 * h / 5, 0, 2 * h / 5, w / 2, 2 * h / 5); + path.curveTo(w / 2 - width, 2 * h / 5, w / 2 - width, 0, w / 2, 0); + path.curveTo(w / 2 + width, 0, w / 2 + width, 2 * h / 5, w / 2, 2 * h / 5); + path.curveTo(w, 2 * h / 5, w, 3 * h / 5, w, h); + path.close(); +}; diff --git a/src/js/shape/mxArrow.js b/src/js/shape/mxArrow.js new file mode 100644 index 0000000..93777d8 --- /dev/null +++ b/src/js/shape/mxArrow.js @@ -0,0 +1,226 @@ +/** + * $Id: mxArrow.js,v 1.31 2012-05-23 19:09:22 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxArrow + * + * Extends <mxShape> to implement an arrow shape. (The shape + * is used to represent edges, not vertices.) + * This shape is registered under <mxConstants.SHAPE_ARROW> + * in <mxCellRenderer>. + * + * Constructor: mxArrow + * + * Constructs a new arrow shape. + * + * Parameters: + * + * points - Array of <mxPoints> that define the points. This is stored in + * <mxShape.points>. + * fill - String that defines the fill color. This is stored in <fill>. + * stroke - String that defines the stroke color. This is stored in <stroke>. + * strokewidth - Optional integer that defines the stroke width. Default is + * 1. This is stored in <strokewidth>. + * arrowWidth - Optional integer that defines the arrow width. Default is + * <mxConstants.ARROW_WIDTH>. This is stored in <arrowWidth>. + * spacing - Optional integer that defines the spacing between the arrow shape + * and its endpoints. Default is <mxConstants.ARROW_SPACING>. This is stored in + * <spacing>. + * endSize - Optional integer that defines the size of the arrowhead. Default + * is <mxConstants.ARROW_SIZE>. This is stored in <endSize>. + */ +function mxArrow(points, fill, stroke, strokewidth, arrowWidth, spacing, endSize) +{ + this.points = points; + this.fill = fill; + this.stroke = stroke; + this.strokewidth = (strokewidth != null) ? strokewidth : 1; + this.arrowWidth = (arrowWidth != null) ? arrowWidth : mxConstants.ARROW_WIDTH; + this.spacing = (spacing != null) ? spacing : mxConstants.ARROW_SPACING; + this.endSize = (endSize != null) ? endSize : mxConstants.ARROW_SIZE; +}; + +/** + * Extends <mxActor>. + */ +mxArrow.prototype = new mxActor(); +mxArrow.prototype.constructor = mxArrow; + +/** + * Variable: addPipe + * + * Specifies if a SVG path should be created around any path to increase the + * tolerance for mouse events. Default is false since this shape is filled. + */ +mxArrow.prototype.addPipe = false; + +/** + * Variable: enableFill + * + * Specifies if fill colors should be ignored. This must be set to true for + * shapes that are stroked only. Default is true since this shape is filled. + */ +mxArrow.prototype.enableFill = true; + +/** + * Function: configureTransparentBackground + * + * Overidden to remove transparent background. + */ +mxArrow.prototype.configureTransparentBackground = function(node) +{ + // do nothing +}; + +/** + * Function: updateBoundingBox + * + * Updates the <boundingBox> for this shape. + */ +mxArrow.prototype.augmentBoundingBox = function(bbox) +{ + // FIXME: Fix precision, share math and cache results with painting code + bbox.grow(Math.max(this.arrowWidth / 2, this.endSize / 2) * this.scale); + + mxShape.prototype.augmentBoundingBox.apply(this, arguments); +}; + +/** + * Function: createVml + * + * Extends <mxShape.createVml> to ignore fill if <enableFill> is false. + */ +mxArrow.prototype.createVml = function() +{ + if (!this.enableFill) + { + this.fill = null; + } + + return mxActor.prototype.createVml.apply(this, arguments); +}; + +/** + * Function: createSvg + * + * Extends <mxActor.createSvg> to ignore fill if <enableFill> is false and + * create an event handling shape if <this.addPipe> is true. + */ +mxArrow.prototype.createSvg = function() +{ + if (!this.enableFill) + { + this.fill = null; + } + + var g = mxActor.prototype.createSvg.apply(this, arguments); + + // Creates an invisible shape around the path for easier + // selection with the mouse. Note: Firefox does not ignore + // the value of the stroke attribute for pointer-events: stroke, + // it does, however, ignore the visibility attribute. + if (this.addPipe) + { + this.pipe = this.createSvgPipe(); + g.appendChild(this.pipe); + } + + return g; +}; + +/** + * Function: reconfigure + * + * Extends <mxActor.reconfigure> to ignore fill if <enableFill> is false. + */ +mxArrow.prototype.reconfigure = function() +{ + if (!this.enableFill) + { + this.fill = null; + } + + mxActor.prototype.reconfigure.apply(this, arguments); +}; + +/** + * Function: redrawSvg + * + * Extends <mxActor.redrawSvg> to update the event handling shape if one + * exists. + */ +mxArrow.prototype.redrawSvg = function() +{ + mxActor.prototype.redrawSvg.apply(this, arguments); + + if (this.pipe != null) + { + var d = this.innerNode.getAttribute('d'); + + if (d != null) + { + this.pipe.setAttribute('d', this.innerNode.getAttribute('d')); + var strokeWidth = Math.round(this.strokewidth * this.scale); + this.pipe.setAttribute('stroke-width', strokeWidth + mxShape.prototype.SVG_STROKE_TOLERANCE); + } + } +}; + +/** + * Function: redrawPath + * + * Draws the path for this shape. This method uses the <mxPath> + * abstraction to paint the shape for VML and SVG. + */ +mxArrow.prototype.redrawPath = function(path, x, y, w, h) +{ + // All points are offset + path.translate.x -= x; + path.translate.y -= y; + + // Geometry of arrow + var spacing = this.spacing * this.scale; + var width = this.arrowWidth * this.scale; + var arrow = this.endSize * this.scale; + + // Base vector (between end points) + var p0 = this.points[0]; + var pe = this.points[this.points.length - 1]; + + var dx = pe.x - p0.x; + var dy = pe.y - p0.y; + var dist = Math.sqrt(dx * dx + dy * dy); + var length = dist - 2 * spacing - arrow; + + // Computes the norm and the inverse norm + var nx = dx / dist; + var ny = dy / dist; + var basex = length * nx; + var basey = length * ny; + var floorx = width * ny/3; + var floory = -width * nx/3; + + // Computes points + var p0x = p0.x - floorx / 2 + spacing * nx; + var p0y = p0.y - floory / 2 + spacing * ny; + var p1x = p0x + floorx; + var p1y = p0y + floory; + var p2x = p1x + basex; + var p2y = p1y + basey; + var p3x = p2x + floorx; + var p3y = p2y + floory; + // p4 not necessary + var p5x = p3x - 3 * floorx; + var p5y = p3y - 3 * floory; + + path.moveTo(p0x, p0y); + path.lineTo(p1x, p1y); + path.lineTo(p2x, p2y); + path.lineTo(p3x, p3y); + path.lineTo(pe.x - spacing * nx, pe.y - spacing * ny); + path.lineTo(p5x, p5y); + path.lineTo(p5x + floorx, p5y + floory); + path.lineTo(p0x, p0y); + path.close(); +}; diff --git a/src/js/shape/mxCloud.js b/src/js/shape/mxCloud.js new file mode 100644 index 0000000..3893a1b --- /dev/null +++ b/src/js/shape/mxCloud.js @@ -0,0 +1,56 @@ +/** + * $Id: mxCloud.js,v 1.12 2011-06-24 11:27:30 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxCloud + * + * Extends <mxActor> to implement a cloud shape. + * + * This shape is registered under <mxConstants.SHAPE_CLOUD> in + * <mxCellRenderer>. + * + * Constructor: mxCloud + * + * Constructs a new cloud shape. + * + * Parameters: + * + * bounds - <mxRectangle> that defines the bounds. This is stored in + * <mxShape.bounds>. + * fill - String that defines the fill color. This is stored in <fill>. + * stroke - String that defines the stroke color. This is stored in <stroke>. + * strokewidth - Optional integer that defines the stroke width. Default is + * 1. This is stored in <strokewidth>. + */ +function mxCloud(bounds, fill, stroke, strokewidth) +{ + this.bounds = bounds; + this.fill = fill; + this.stroke = stroke; + this.strokewidth = (strokewidth != null) ? strokewidth : 1; +}; + +/** + * Extends mxActor. + */ +mxCloud.prototype = new mxActor(); +mxCloud.prototype.constructor = mxActor; + +/** + * Function: redrawPath + * + * Draws the path for this shape. This method uses the <mxPath> + * abstraction to paint the shape for VML and SVG. + */ +mxCloud.prototype.redrawPath = function(path, x, y, w, h) +{ + path.moveTo(0.25 * w, 0.25 * h); + path.curveTo(0.05 * w, 0.25 * h, 0, 0.5 * h, 0.16 * w, 0.55 * h); + path.curveTo(0, 0.66 * h, 0.18 * w, 0.9 * h, 0.31 * w, 0.8 * h); + path.curveTo(0.4 * w, h, 0.7 * w, h, 0.8 * w, 0.8 * h); + path.curveTo(w, 0.8 * h, w, 0.6 * h, 0.875 * w, 0.5 * h); + path.curveTo(w, 0.3 * h, 0.8 * w, 0.1 * h, 0.625 * w, 0.2 * h); + path.curveTo(0.5 * w, 0.05 * h, 0.3 * w, 0.05 * h, 0.25 * w, 0.25 * h); + path.close(); +}; diff --git a/src/js/shape/mxConnector.js b/src/js/shape/mxConnector.js new file mode 100644 index 0000000..092bf79 --- /dev/null +++ b/src/js/shape/mxConnector.js @@ -0,0 +1,446 @@ +/** + * $Id: mxConnector.js,v 1.80 2012-05-24 12:00:45 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxConnector + * + * Extends <mxShape> to implement a connector shape. The connector + * shape allows for arrow heads on either side. + * + * This shape is registered under <mxConstants.SHAPE_CONNECTOR> in + * <mxCellRenderer>. + * + * Constructor: mxConnector + * + * Constructs a new connector shape. + * + * Parameters: + * + * points - Array of <mxPoints> that define the points. This is stored in + * <mxShape.points>. + * stroke - String that defines the stroke color. This is stored in <stroke>. + * Default is 'black'. + * strokewidth - Optional integer that defines the stroke width. Default is + * 1. This is stored in <strokewidth>. + */ +function mxConnector(points, stroke, strokewidth) +{ + this.points = points; + this.stroke = stroke; + this.strokewidth = (strokewidth != null) ? strokewidth : 1; +}; + +/** + * Extends mxShape. + */ +mxConnector.prototype = new mxShape(); +mxConnector.prototype.constructor = mxConnector; + +/** + * Variable: vmlNodes + * + * Adds local references to <mxShape.vmlNodes>. + */ +mxConnector.prototype.vmlNodes = mxConnector.prototype.vmlNodes.concat([ + 'shapeNode', 'start', 'end', 'startStroke', 'endStroke', 'startFill', 'endFill']); + +/** + * Variable: mixedModeHtml + * + * Overrides the parent value with false, meaning it will + * draw in VML in mixed Html mode. + */ +mxConnector.prototype.mixedModeHtml = false; + +/** + * Variable: preferModeHtml + * + * Overrides the parent value with false, meaning it will + * draw as VML in prefer Html mode. + */ +mxConnector.prototype.preferModeHtml = false; + +/** + * Variable: allowCrispMarkers + * + * Specifies if <mxShape.crisp> should be allowed for markers. Default is false. + */ +mxConnector.prototype.allowCrispMarkers = false; + +/** + * Variable: addPipe + * + * Specifies if a SVG path should be created around any path to increase the + * tolerance for mouse events. Default is false since this shape is filled. + */ +mxConnector.prototype.addPipe = true; + +/** + * Function: configureHtmlShape + * + * Overrides <mxShape.configureHtmlShape> to clear the border and background. + */ +mxConnector.prototype.configureHtmlShape = function(node) +{ + mxShape.prototype.configureHtmlShape.apply(this, arguments); + node.style.borderStyle = ''; + node.style.background = ''; +}; + +/** + * Function: createVml + * + * Creates and returns the VML node to represent this shape. + */ +mxConnector.prototype.createVml = function() +{ + var node = document.createElement('v:group'); + node.style.position = 'absolute'; + this.shapeNode = document.createElement('v:shape'); + this.updateVmlStrokeColor(this.shapeNode); + this.updateVmlStrokeNode(this.shapeNode); + node.appendChild(this.shapeNode); + this.shapeNode.filled = 'false'; + + if (this.isShadow) + { + this.createVmlShadow(this.shapeNode); + } + + // Creates the start arrow as an additional child path + if (this.startArrow != null) + { + this.start = document.createElement('v:shape'); + this.start.style.position = 'absolute'; + + // Only required for opacity and joinstyle + this.startStroke = document.createElement('v:stroke'); + this.startStroke.joinstyle = 'miter'; + this.start.appendChild(this.startStroke); + + this.startFill = document.createElement('v:fill'); + this.start.appendChild(this.startFill); + + node.appendChild(this.start); + } + + // Creates the end arrows as an additional child path + if (this.endArrow != null) + { + this.end = document.createElement('v:shape'); + this.end.style.position = 'absolute'; + + // Only required for opacity and joinstyle + this.endStroke = document.createElement('v:stroke'); + this.endStroke.joinstyle = 'miter'; + this.end.appendChild(this.endStroke); + + this.endFill = document.createElement('v:fill'); + this.end.appendChild(this.endFill); + + node.appendChild(this.end); + } + + this.updateVmlMarkerOpacity(); + + return node; +}; + +/** + * Function: updateVmlMarkerOpacity + * + * Updates the opacity for the markers in VML. + */ +mxConnector.prototype.updateVmlMarkerOpacity = function() +{ + var op = (this.opacity != null) ? (this.opacity + '%') : '100%'; + + if (this.start != null) + { + this.startFill.opacity = op; + this.startStroke.opacity = op; + } + + if (this.end != null) + { + this.endFill.opacity = op; + this.endStroke.opacity = op; + } +}; + +/** + * Function: redrawVml + * + * Redraws this VML shape by invoking <updateVmlShape> on this.node. + */ +mxConnector.prototype.reconfigure = function() +{ + // Never fill a connector + this.fill = null; + + if (mxUtils.isVml(this.node)) + { + // Updates the style of the given shape + // LATER: Check if this can be replaced with redrawVml and + // updating the color, dash pattern and shadow. + this.node.style.visibility = 'hidden'; + this.configureVmlShape(this.shapeNode); + this.updateVmlMarkerOpacity(); + this.node.style.visibility = 'visible'; + } + else + { + mxShape.prototype.reconfigure.apply(this, arguments); + } +}; + +/** + * Function: redrawVml + * + * Redraws this VML shape by invoking <updateVmlShape> on this.node. + */ +mxConnector.prototype.redrawVml = function() +{ + if (this.node != null && this.points != null && this.bounds != null && + !isNaN(this.bounds.x) && !isNaN(this.bounds.y) && + !isNaN(this.bounds.width) && !isNaN(this.bounds.height)) + { + var w = Math.max(0, Math.round(this.bounds.width)); + var h = Math.max(0, Math.round(this.bounds.height)); + var cs = w + ',' + h; + w += 'px'; + h += 'px'; + + // Computes the marker paths before the main path is updated so + // that offsets can be taken into account + if (this.start != null) + { + this.start.style.width = w; + this.start.style.height = h; + this.start.coordsize = cs; + + var p0 = this.points[1]; + var pe = this.points[0]; + + var size = mxUtils.getNumber(this.style, mxConstants.STYLE_STARTSIZE, mxConstants.DEFAULT_MARKERSIZE); + this.startOffset = this.redrawMarker(this.start, this.startArrow, p0, pe, this.stroke, size); + } + + if (this.end != null) + { + this.end.style.width = w; + this.end.style.height = h; + this.end.coordsize = cs; + + var n = this.points.length; + var p0 = this.points[n - 2]; + var pe = this.points[n - 1]; + + var size = mxUtils.getNumber(this.style, mxConstants.STYLE_ENDSIZE, mxConstants.DEFAULT_MARKERSIZE); + this.endOffset = this.redrawMarker(this.end, this.endArrow, p0, pe, this.stroke, size); + } + + this.updateVmlShape(this.node); + this.updateVmlShape(this.shapeNode); + this.shapeNode.filled = 'false'; + + // Adds custom dash pattern + if (this.isDashed) + { + var pat = mxUtils.getValue(this.style, 'dashStyle', null); + + if (pat != null) + { + this.strokeNode.dashstyle = pat; + } + + if (this.shadowStrokeNode != null) + { + this.shadowStrokeNode.dashstyle = this.strokeNode.dashstyle; + } + } + } +}; + +/** + * Function: createSvg + * + * Creates and returns the SVG node to represent this shape. + */ +mxConnector.prototype.createSvg = function() +{ + this.fill = null; + var g = this.createSvgGroup('path'); + + // Creates the start arrow as an additional child path + if (this.startArrow != null) + { + this.start = document.createElementNS(mxConstants.NS_SVG, 'path'); + g.appendChild(this.start); + } + + // Creates the end arrows as an additional child path + if (this.endArrow != null) + { + this.end = document.createElementNS(mxConstants.NS_SVG, 'path'); + g.appendChild(this.end); + } + + // Creates an invisible shape around the path for easier + // selection with the mouse. Note: Firefox does not ignore + // the value of the stroke attribute for pointer-events: stroke, + // it does, however, ignore the visibility attribute. + if (this.addPipe) + { + this.pipe = this.createSvgPipe(); + g.appendChild(this.pipe); + } + + return g; +}; + +/** + * Function: redrawSvg + * + * Updates the SVG node(s) to reflect the latest bounds and scale. + */ +mxConnector.prototype.redrawSvg = function() +{ + // Computes the markers first which modifies the coordinates of the + // endpoints to not overlap with the painted marker then updates the actual + // shape for the edge to take the modified endpoints into account. + if (this.points != null && this.points[0] != null) + { + var color = this.innerNode.getAttribute('stroke'); + + // Draws the start marker + if (this.start != null) + { + var p0 = this.points[1]; + var pe = this.points[0]; + + var size = mxUtils.getNumber(this.style, mxConstants.STYLE_STARTSIZE, + mxConstants.DEFAULT_MARKERSIZE); + this.startOffset = this.redrawMarker(this.start, + this.startArrow, p0, pe, color, size); + + if (this.allowCrispMarkers && this.crisp) + { + this.start.setAttribute('shape-rendering', 'crispEdges'); + } + else + { + this.start.removeAttribute('shape-rendering'); + } + } + + // Draws the end marker + if (this.end != null) + { + var n = this.points.length; + + var p0 = this.points[n - 2]; + var pe = this.points[n - 1]; + + var size = mxUtils.getNumber(this.style, mxConstants.STYLE_ENDSIZE, + mxConstants.DEFAULT_MARKERSIZE); + this.endOffset = this.redrawMarker(this.end, + this.endArrow, p0, pe, color, size); + + if (this.allowCrispMarkers && this.crisp) + { + this.end.setAttribute('shape-rendering', 'crispEdges'); + } + else + { + this.end.removeAttribute('shape-rendering'); + } + } + } + + this.updateSvgShape(this.innerNode); + var d = this.innerNode.getAttribute('d'); + + if (d != null) + { + var strokeWidth = Math.round(this.strokewidth * this.scale); + + // Updates the tolerance of the invisible shape for event handling + if (this.pipe != null) + { + this.pipe.setAttribute('d', this.innerNode.getAttribute('d')); + this.pipe.setAttribute('stroke-width', strokeWidth + mxShape.prototype.SVG_STROKE_TOLERANCE); + } + + // Updates the shadow + if (this.shadowNode != null) + { + this.shadowNode.setAttribute('transform', this.getSvgShadowTransform()); + this.shadowNode.setAttribute('d', d); + this.shadowNode.setAttribute('stroke-width', strokeWidth); + } + } + + // Adds custom dash pattern + if (this.isDashed) + { + var pat = this.createDashPattern(this.scale * this.strokewidth); + + if (pat != null) + { + this.innerNode.setAttribute('stroke-dasharray', pat); + } + } + + // Updates the shadow + if (this.shadowNode != null) + { + var pat = this.innerNode.getAttribute('stroke-dasharray'); + + if (pat != null) + { + this.shadowNode.setAttribute('stroke-dasharray', pat); + } + } +}; + +/** + * Function: createDashPattern + * + * Creates a dash pattern for the given factor. + */ +mxConnector.prototype.createDashPattern = function(factor) +{ + var value = mxUtils.getValue(this.style, 'dashPattern', null); + + if (value != null) + { + var tmp = value.split(' '); + var pat = []; + + for (var i = 0; i < tmp.length; i++) + { + if (tmp[i].length > 0) + { + pat.push(Math.round(Number(tmp[i]) * factor)); + } + } + + return pat.join(' '); + } + + return null; +}; + +/** + * Function: redrawMarker + * + * Updates the given SVG or VML marker. + */ +mxConnector.prototype.redrawMarker = function(node, type, p0, pe, color, size) +{ + return mxMarker.paintMarker(node, type, p0, pe, color, this.strokewidth, + size, this.scale, this.bounds.x, this.bounds.y, this.start == node, + this.style); +}; diff --git a/src/js/shape/mxCylinder.js b/src/js/shape/mxCylinder.js new file mode 100644 index 0000000..9a45760 --- /dev/null +++ b/src/js/shape/mxCylinder.js @@ -0,0 +1,319 @@ +/** + * $Id: mxCylinder.js,v 1.38 2012-07-31 11:46:53 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxCylinder + * + * Extends <mxShape> to implement an cylinder shape. If a + * custom shape with one filled area and an overlay path is + * needed, then this shape's <redrawPath> should be overridden. + * This shape is registered under <mxConstants.SHAPE_CYLINDER> + * in <mxCellRenderer>. + * + * Constructor: mxCylinder + * + * Constructs a new cylinder shape. + * + * Parameters: + * + * bounds - <mxRectangle> that defines the bounds. This is stored in + * <mxShape.bounds>. + * fill - String that defines the fill color. This is stored in <fill>. + * stroke - String that defines the stroke color. This is stored in <stroke>. + * strokewidth - Optional integer that defines the stroke width. Default is + * 1. This is stored in <strokewidth>. + */ +function mxCylinder(bounds, fill, stroke, strokewidth) +{ + this.bounds = bounds; + this.fill = fill; + this.stroke = stroke; + this.strokewidth = (strokewidth != null) ? strokewidth : 1; +}; + +/** + * Extends mxShape. + */ +mxCylinder.prototype = new mxShape(); +mxCylinder.prototype.constructor = mxCylinder; + +/** + * Variable: vmlNodes + * + * Adds local references to <mxShape.vmlNodes>. + */ +mxCylinder.prototype.vmlNodes = mxCylinder.prototype.vmlNodes.concat(['background', 'foreground']); + +/** + * Variable: mixedModeHtml + * + * Overrides the parent value with false, meaning it will + * draw in VML in mixed Html mode. + */ +mxCylinder.prototype.mixedModeHtml = false; + +/** + * Variable: preferModeHtml + * + * Overrides the parent value with false, meaning it will + * draw as VML in prefer Html mode. + */ +mxCylinder.prototype.preferModeHtml = false; + +/** + * Variable: addPipe + * + * Specifies if a SVG path should be created around the background for better + * hit detection. Default is false. + */ +mxCylinder.prototype.addPipe = false; + +/** + * Variable: strokedBackground + * + * Specifies if the background should be stroked. Default is true. + */ +mxCylinder.prototype.strokedBackground = true; + +/** + * Variable: maxHeight + * + * Defines the maximum height of the top and bottom part + * of the cylinder shape. + */ +mxCylinder.prototype.maxHeight = 40; + +/** + * Variable: vmlScale + * + * Renders VML with a scale of 2. + */ +mxCylinder.prototype.vmlScale = 2; + +/** + * Function: create + * + * Overrides the method to make sure the <stroke> is never + * null. If it is null is will be assigned the <fill> color. + */ +mxCylinder.prototype.create = function(container) +{ + if (this.stroke == null) + { + this.stroke = this.fill; + } + + // Calls superclass implementation of create + return mxShape.prototype.create.apply(this, arguments); +}; + +/** + * Function: reconfigure + * + * Overrides the method to make sure the <stroke> is applied to the foreground. + */ +mxCylinder.prototype.reconfigure = function() +{ + if (this.dialect == mxConstants.DIALECT_SVG) + { + this.configureSvgShape(this.foreground); + this.foreground.setAttribute('fill', 'none'); + } + else if (mxUtils.isVml(this.node)) + { + this.configureVmlShape(this.background); + this.configureVmlShape(this.foreground); + } + + mxShape.prototype.reconfigure.apply(this); +}; + +/** + * Function: createVml + * + * Creates and returns the VML node to represent this shape. + */ +mxCylinder.prototype.createVml = function() +{ + var node = document.createElement('v:group'); + + // Draws the background + this.background = document.createElement('v:shape'); + this.label = this.background; + this.configureVmlShape(this.background); + node.appendChild(this.background); + + // Ignores values that only apply to the background + this.fill = null; + this.isShadow = false; + this.configureVmlShape(node); + + // Draws the foreground + this.foreground = document.createElement('v:shape'); + this.configureVmlShape(this.foreground); + + // To match SVG defaults jointsyle miter, miterlimit 4 + this.fgStrokeNode = document.createElement('v:stroke'); + this.fgStrokeNode.joinstyle = 'miter'; + this.fgStrokeNode.miterlimit = 4; + this.foreground.appendChild(this.fgStrokeNode); + + node.appendChild(this.foreground); + + return node; +}; + +/** + * Function: redrawVml + * + * Updates the VML node(s) to reflect the latest bounds and scale. + */ +mxCylinder.prototype.redrawVml = function() +{ + this.updateVmlShape(this.node); + this.updateVmlShape(this.background); + this.updateVmlShape(this.foreground); + this.background.path = this.createPath(false); + this.foreground.path = this.createPath(true); + + this.fgStrokeNode.dashstyle = this.strokeNode.dashstyle; +}; + +/** + * Function: createSvg + * + * Creates and returns the SVG node(s) to represent this shape. + */ +mxCylinder.prototype.createSvg = function() +{ + var g = this.createSvgGroup('path'); + this.foreground = document.createElementNS(mxConstants.NS_SVG, 'path'); + + if (this.stroke != null && this.stroke != mxConstants.NONE) + { + this.foreground.setAttribute('stroke', this.stroke); + } + else + { + this.foreground.setAttribute('stroke', 'none'); + } + + this.foreground.setAttribute('fill', 'none'); + g.appendChild(this.foreground); + + if (this.addPipe) + { + this.pipe = this.createSvgPipe(); + g.appendChild(this.pipe); + } + + return g; +}; + +/** + * Function: redrawSvg + * + * Updates the SVG node(s) to reflect the latest bounds and scale. + */ +mxCylinder.prototype.redrawSvg = function() +{ + var strokeWidth = Math.round(Math.max(1, this.strokewidth * this.scale)); + this.innerNode.setAttribute('stroke-width', strokeWidth); + + if (this.crisp && (this.rotation == null || this.rotation == 0)) + { + this.innerNode.setAttribute('shape-rendering', 'crispEdges'); + this.foreground.setAttribute('shape-rendering', 'crispEdges'); + } + else + { + this.innerNode.removeAttribute('shape-rendering'); + this.foreground.removeAttribute('shape-rendering'); + } + + // Paints background + var d = this.createPath(false); + + if (d.length > 0) + { + this.innerNode.setAttribute('d', d); + + // Updates event handling element + if (this.pipe != null) + { + this.pipe.setAttribute('d', d); + this.pipe.setAttribute('stroke-width', strokeWidth + mxShape.prototype.SVG_STROKE_TOLERANCE); + this.pipe.setAttribute('transform', (this.innerNode.getAttribute('transform') || '')); + } + } + else + { + this.innerNode.removeAttribute('d'); + + // Updates event handling element + if (this.pipe != null) + { + this.pipe.removeAttribute('d'); + } + } + + // Stroked background + if (!this.strokedBackground) + { + this.innerNode.setAttribute('stroke', 'none'); + } + + // Paints shadow + if (this.shadowNode != null) + { + this.shadowNode.setAttribute('stroke-width', strokeWidth); + this.shadowNode.setAttribute('d', d); + this.shadowNode.setAttribute('transform', this.getSvgShadowTransform()); + } + + // Paints foreground + d = this.createPath(true); + + if (d.length > 0) + { + this.foreground.setAttribute('stroke-width', strokeWidth); + this.foreground.setAttribute('d', d); + } + else + { + this.foreground.removeAttribute('d'); + } + + if (this.isDashed) + { + var phase = Math.max(1, Math.round(3 * this.scale * this.strokewidth)); + this.innerNode.setAttribute('stroke-dasharray', phase + ' ' + phase); + this.foreground.setAttribute('stroke-dasharray', phase + ' ' + phase); + } +}; + +/** + * Function: redrawPath + * + * Draws the path for this shape. This method uses the <mxPath> + * abstraction to paint the shape for VML and SVG. + */ +mxCylinder.prototype.redrawPath = function(path, x, y, w, h, isForeground) +{ + var dy = Math.min(this.maxHeight, Math.round(h / 5)); + + if (isForeground) + { + path.moveTo(0, dy); + path.curveTo(0, 2 * dy, w, 2 * dy, w, dy); + } + else + { + path.moveTo(0, dy); + path.curveTo(0, -dy / 3, w, -dy / 3, w, dy); + path.lineTo(w, h - dy); + path.curveTo(w, h + dy / 3, 0, h + dy / 3, 0, h - dy); + path.close(); + } +}; diff --git a/src/js/shape/mxDoubleEllipse.js b/src/js/shape/mxDoubleEllipse.js new file mode 100644 index 0000000..7854851 --- /dev/null +++ b/src/js/shape/mxDoubleEllipse.js @@ -0,0 +1,203 @@ +/** + * $Id: mxDoubleEllipse.js,v 1.19 2012-05-21 18:27:17 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxDoubleEllipse + * + * Extends <mxShape> to implement a double ellipse shape. + * This shape is registered under <mxConstants.SHAPE_DOUBLE_ELLIPSE> + * in <mxCellRenderer>. + * + * Constructor: mxDoubleEllipse + * + * Constructs a new ellipse shape. + * + * Parameters: + * + * bounds - <mxRectangle> that defines the bounds. This is stored in + * <mxShape.bounds>. + * fill - String that defines the fill color. This is stored in <fill>. + * stroke - String that defines the stroke color. This is stored in <stroke>. + * strokewidth - Optional integer that defines the stroke width. Default is + * 1. This is stored in <strokewidth>. + */ +function mxDoubleEllipse(bounds, fill, stroke, strokewidth) +{ + this.bounds = bounds; + this.fill = fill; + this.stroke = stroke; + this.strokewidth = (strokewidth != null) ? strokewidth : 1; +}; + +/** + * Extends mxShape. + */ +mxDoubleEllipse.prototype = new mxShape(); +mxDoubleEllipse.prototype.constructor = mxDoubleEllipse; + +/** + * Variable: vmlNodes + * + * Adds local references to <mxShape.vmlNodes>. + */ +mxDoubleEllipse.prototype.vmlNodes = mxDoubleEllipse.prototype.vmlNodes.concat(['background', 'foreground']); + +/** + * Variable: mixedModeHtml + * + * Overrides the parent value with false, meaning it will + * draw in VML in mixed Html mode. + */ +mxDoubleEllipse.prototype.mixedModeHtml = false; + +/** + * Variable: preferModeHtml + * + * Overrides the parent value with false, meaning it will + * draw as VML in prefer Html mode. + */ +mxDoubleEllipse.prototype.preferModeHtml = false; + +/** + * Variable: vmlScale + * + * Renders VML with a scale of 2. + */ +mxDoubleEllipse.prototype.vmlScale = 2; + +/** + * Function: createVml + * + * Creates and returns the VML node to represent this shape. + */ +mxDoubleEllipse.prototype.createVml = function() +{ + var node = document.createElement('v:group'); + + // Draws the background + this.background = document.createElement('v:arc'); + this.background.startangle = '0'; + this.background.endangle = '360'; + this.configureVmlShape(this.background); + + node.appendChild(this.background); + + // Ignores values that only apply to the background + this.label = this.background; + this.isShadow = false; + this.fill = null; + + // Draws the foreground + this.foreground = document.createElement('v:oval'); + this.configureVmlShape(this.foreground); + + node.appendChild(this.foreground); + + this.stroke = null; + this.configureVmlShape(node); + + return node; +}; + +/** + * Function: redrawVml + * + * Updates the VML node(s) to reflect the latest bounds and scale. + */ +mxDoubleEllipse.prototype.redrawVml = function() +{ + this.updateVmlShape(this.node); + this.updateVmlShape(this.background); + this.updateVmlShape(this.foreground); + + var inset = Math.round((this.strokewidth + 3) * this.scale) * this.vmlScale; + var w = Math.round(this.bounds.width * this.vmlScale); + var h = Math.round(this.bounds.height * this.vmlScale); + + this.foreground.style.top = inset + 'px'; // relative + this.foreground.style.left = inset + 'px'; // relative + this.foreground.style.width = Math.max(0, w - 2 * inset) + 'px'; + this.foreground.style.height = Math.max(0, h - 2 * inset) + 'px'; +}; + +/** + * Function: createSvg + * + * Creates and returns the SVG node(s) to represent this shape. + */ +mxDoubleEllipse.prototype.createSvg = function() +{ + var g = this.createSvgGroup('ellipse'); + this.foreground = document.createElementNS(mxConstants.NS_SVG, 'ellipse'); + + if (this.stroke != null) + { + this.foreground.setAttribute('stroke', this.stroke); + } + else + { + this.foreground.setAttribute('stroke', 'none'); + } + + this.foreground.setAttribute('fill', 'none'); + g.appendChild(this.foreground); + + return g; +}; + +/** + * Function: redrawSvg + * + * Updates the SVG node(s) to reflect the latest bounds and scale. + */ +mxDoubleEllipse.prototype.redrawSvg = function() +{ + if (this.crisp) + { + this.innerNode.setAttribute('shape-rendering', 'crispEdges'); + this.foreground.setAttribute('shape-rendering', 'crispEdges'); + } + else + { + this.innerNode.removeAttribute('shape-rendering'); + this.foreground.removeAttribute('shape-rendering'); + } + + this.updateSvgNode(this.innerNode); + this.updateSvgNode(this.shadowNode); + this.updateSvgNode(this.foreground, (this.strokewidth + 3) * this.scale); + + if (this.isDashed) + { + var phase = Math.max(1, Math.round(3 * this.scale * this.strokewidth)); + this.innerNode.setAttribute('stroke-dasharray', phase + ' ' + phase); + } +}; + +/** + * Function: updateSvgNode + * + * Updates the given node to reflect the new <bounds> and <scale>. + */ +mxDoubleEllipse.prototype.updateSvgNode = function(node, inset) +{ + inset = (inset != null) ? inset : 0; + + if (node != null) + { + var strokeWidth = Math.round(Math.max(1, this.strokewidth * this.scale)); + node.setAttribute('stroke-width', strokeWidth); + + node.setAttribute('cx', this.bounds.x + this.bounds.width / 2); + node.setAttribute('cy', this.bounds.y + this.bounds.height / 2); + node.setAttribute('rx', Math.max(0, this.bounds.width / 2 - inset)); + node.setAttribute('ry', Math.max(0, this.bounds.height / 2 - inset)); + + // Updates the transform of the shadow + if (this.shadowNode != null) + { + this.shadowNode.setAttribute('transform', this.getSvgShadowTransform()); + } + } +}; diff --git a/src/js/shape/mxEllipse.js b/src/js/shape/mxEllipse.js new file mode 100644 index 0000000..f3882cf --- /dev/null +++ b/src/js/shape/mxEllipse.js @@ -0,0 +1,132 @@ +/** + * $Id: mxEllipse.js,v 1.20 2012-04-04 07:34:50 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxEllipse + * + * Extends <mxShape> to implement an ellipse shape. + * This shape is registered under <mxConstants.SHAPE_ELLIPSE> + * in <mxCellRenderer>. + * + * Constructor: mxEllipse + * + * Constructs a new ellipse shape. + * + * Parameters: + * + * bounds - <mxRectangle> that defines the bounds. This is stored in + * <mxShape.bounds>. + * fill - String that defines the fill color. This is stored in <fill>. + * stroke - String that defines the stroke color. This is stored in <stroke>. + * strokewidth - Optional integer that defines the stroke width. Default is + * 1. This is stored in <strokewidth>. + */ +function mxEllipse(bounds, fill, stroke, strokewidth) +{ + this.bounds = bounds; + this.fill = fill; + this.stroke = stroke; + this.strokewidth = (strokewidth != null) ? strokewidth : 1; +}; + +/** + * Extends mxShape. + */ +mxEllipse.prototype = new mxShape(); +mxEllipse.prototype.constructor = mxEllipse; + +/** + * Variable: mixedModeHtml + * + * Overrides the parent value with false, meaning it will + * draw in VML in mixed Html mode. + */ +mxEllipse.prototype.mixedModeHtml = false; + +/** + * Variable: preferModeHtml + * + * Overrides the parent value with false, meaning it will + * draw as VML in prefer Html mode. + */ +mxEllipse.prototype.preferModeHtml = false; + +/** + * Function: createVml + * + * Creates and returns the VML node to represent this shape. + */ +mxEllipse.prototype.createVml = function() +{ + // Uses an arc not an oval to make sure the + // textbox fills out the outer bounds of the + // circle, not just the inner rectangle + var node = document.createElement('v:arc'); + node.startangle = '0'; + node.endangle = '360'; + this.configureVmlShape(node); + + return node; +}; + +/** + * Function: createSvg + * + * Creates and returns the SVG node(s) to represent this shape. + */ +mxEllipse.prototype.createSvg = function() +{ + return this.createSvgGroup('ellipse'); +}; + +/** + * Function: redrawSvg + * + * Updates the SVG node(s) to reflect the latest bounds and scale. + */ +mxEllipse.prototype.redrawSvg = function() +{ + if (this.crisp) + { + this.innerNode.setAttribute('shape-rendering', 'crispEdges'); + } + else + { + this.innerNode.removeAttribute('shape-rendering'); + } + + this.updateSvgNode(this.innerNode); + this.updateSvgNode(this.shadowNode); +}; + +/** + * Function: updateSvgNode + * + * Updates the given node to reflect the new <bounds> and <scale>. + */ +mxEllipse.prototype.updateSvgNode = function(node) +{ + if (node != null) + { + var strokeWidth = Math.round(Math.max(1, this.strokewidth * this.scale)); + node.setAttribute('stroke-width', strokeWidth); + + node.setAttribute('cx', this.bounds.x + this.bounds.width / 2); + node.setAttribute('cy', this.bounds.y + this.bounds.height / 2); + node.setAttribute('rx', this.bounds.width / 2); + node.setAttribute('ry', this.bounds.height / 2); + + // Updates the shadow offset + if (this.shadowNode != null) + { + this.shadowNode.setAttribute('transform', this.getSvgShadowTransform()); + } + + if (this.isDashed) + { + var phase = Math.max(1, Math.round(3 * this.scale * this.strokewidth)); + node.setAttribute('stroke-dasharray', phase + ' ' + phase); + } + } +}; diff --git a/src/js/shape/mxHexagon.js b/src/js/shape/mxHexagon.js new file mode 100644 index 0000000..7fa45a3 --- /dev/null +++ b/src/js/shape/mxHexagon.js @@ -0,0 +1,37 @@ +/** + * $Id: mxHexagon.js,v 1.8 2011-09-02 10:01:00 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxHexagon + * + * Implementation of the hexagon shape. + * + * Constructor: mxHexagon + * + * Constructs a new hexagon shape. + */ +function mxHexagon() { }; + +/** + * Extends <mxActor>. + */ +mxHexagon.prototype = new mxActor(); +mxHexagon.prototype.constructor = mxHexagon; + +/** + * Function: redrawPath + * + * Draws the path for this shape. This method uses the <mxPath> + * abstraction to paint the shape for VML and SVG. + */ +mxHexagon.prototype.redrawPath = function(path, x, y, w, h) +{ + path.moveTo(0.25 * w, 0); + path.lineTo(0.75 * w, 0); + path.lineTo(w, 0.5 * h); + path.lineTo(0.75 * w, h); + path.lineTo(0.25 * w, h); + path.lineTo(0, 0.5 * h); + path.close(); +}; diff --git a/src/js/shape/mxImageShape.js b/src/js/shape/mxImageShape.js new file mode 100644 index 0000000..2f1eab0 --- /dev/null +++ b/src/js/shape/mxImageShape.js @@ -0,0 +1,405 @@ +/** + * $Id: mxImageShape.js,v 1.67 2012-04-22 10:16:23 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxImageShape + * + * Extends <mxShape> to implement an image shape. This shape is registered + * under <mxConstants.SHAPE_IMAGE> in <mxCellRenderer>. + * + * Constructor: mxImageShape + * + * Constructs a new image shape. + * + * Parameters: + * + * bounds - <mxRectangle> that defines the bounds. This is stored in + * <mxShape.bounds>. + * image - String that specifies the URL of the image. This is stored in + * <image>. + * fill - String that defines the fill color. This is stored in <fill>. + * stroke - String that defines the stroke color. This is stored in <stroke>. + * strokewidth - Optional integer that defines the stroke width. Default is + * 0. This is stored in <strokewidth>. + */ +function mxImageShape(bounds, image, fill, stroke, strokewidth) +{ + this.bounds = bounds; + this.image = (image != null) ? image : ''; + this.fill = fill; + this.stroke = stroke; + this.strokewidth = (strokewidth != null) ? strokewidth : 1; + this.isShadow = false; +}; + +/** + * Extends mxShape. + */ +mxImageShape.prototype = new mxShape(); +mxImageShape.prototype.constructor = mxImageShape; + +/** + * Variable: crisp + * + * Disables crisp rendering via attributes. Image quality defines the rendering + * quality. Default is false. + */ +mxImageShape.prototype.crisp = false; + +/** + * Variable: preserveImageAspect + * + * Switch to preserve image aspect. Default is true. + */ +mxImageShape.prototype.preserveImageAspect = true; + +/** + * Function: apply + * + * Overrides <mxShape.apply> to replace the fill and stroke colors with the + * respective values from <mxConstants.STYLE_IMAGE_BACKGROUND> and + * <mxConstants.STYLE_IMAGE_BORDER>. + * + * Applies the style of the given <mxCellState> to the shape. This + * implementation assigns the following styles to local fields: + * + * - <mxConstants.STYLE_IMAGE_BACKGROUND> => fill + * - <mxConstants.STYLE_IMAGE_BORDER> => stroke + * + * Parameters: + * + * state - <mxCellState> of the corresponding cell. + */ +mxImageShape.prototype.apply = function(state) +{ + mxShape.prototype.apply.apply(this, arguments); + + this.fill = null; + this.stroke = null; + + if (this.style != null) + { + this.fill = mxUtils.getValue(this.style, mxConstants.STYLE_IMAGE_BACKGROUND); + this.stroke = mxUtils.getValue(this.style, mxConstants.STYLE_IMAGE_BORDER); + this.preserveImageAspect = mxUtils.getNumber(this.style, mxConstants.STYLE_IMAGE_ASPECT, 1) == 1; + this.gradient = null; + } +}; + +/** + * Function: create + * + * Override to create HTML regardless of gradient and + * rounded property. + */ +mxImageShape.prototype.create = function() +{ + var node = null; + + if (this.dialect == mxConstants.DIALECT_SVG) + { + // Workaround: To avoid control-click on images in Firefox to + // open the image in a new window, this image needs to be placed + // inside a group with a rectangle in the foreground which has a + // fill property but no visibility and absorbs all events. + // The image in turn must have all pointer-events disabled. + node = this.createSvgGroup('rect'); + this.innerNode.setAttribute('visibility', 'hidden'); + this.innerNode.setAttribute('pointer-events', 'fill'); + + this.imageNode = document.createElementNS(mxConstants.NS_SVG, 'image'); + this.imageNode.setAttributeNS(mxConstants.NS_XLINK, 'xlink:href', this.image); + this.imageNode.setAttribute('style', 'pointer-events:none'); + this.configureSvgShape(this.imageNode); + + // Removes invalid attributes on the image node + this.imageNode.removeAttribute('stroke'); + this.imageNode.removeAttribute('fill'); + node.insertBefore(this.imageNode, this.innerNode); + + // Inserts node for background and border color rendering + if ((this.fill != null && this.fill != mxConstants.NONE) || + (this.stroke != null && this.stroke != mxConstants.NONE)) + { + this.bg = document.createElementNS(mxConstants.NS_SVG, 'rect'); + node.insertBefore(this.bg, node.firstChild); + } + + // Preserves image aspect as default + if (!this.preserveImageAspect) + { + this.imageNode.setAttribute('preserveAspectRatio', 'none'); + } + } + else + { + // Uses VML image for all non-embedded images in IE to support better + // image flipping quality and avoid workarounds for event redirection + var flipH = mxUtils.getValue(this.style, mxConstants.STYLE_IMAGE_FLIPH, 0) == 1; + var flipV = mxUtils.getValue(this.style, mxConstants.STYLE_IMAGE_FLIPV, 0) == 1; + var img = this.image.toUpperCase(); + + // Handles non-flipped embedded images in IE6 + if (mxClient.IS_IE && !flipH && !flipV && img.substring(0, 6) == 'MHTML:') + { + // LATER: Check if outer DIV is required or if aspect can be implemented + // by adding an offset to the image loading or the background via CSS. + this.imageNode = document.createElement('DIV'); + this.imageNode.style.filter = 'progid:DXImageTransform.Microsoft.AlphaImageLoader ' + + '(src=\'' + this.image + '\', sizingMethod=\'scale\')'; + + node = document.createElement('DIV'); + this.configureHtmlShape(node); + node.appendChild(this.imageNode); + } + // Handles all data URL images and HTML images for IE9 with no VML support (in SVG mode) + else if (!mxClient.IS_IE || img.substring(0, 5) == 'DATA:' || document.documentMode >= 9) + { + this.imageNode = document.createElement('img'); + this.imageNode.setAttribute('src', this.image); + this.imageNode.setAttribute('border', '0'); + this.imageNode.style.position = 'absolute'; + this.imageNode.style.width = '100%'; + this.imageNode.style.height = '100%'; + + node = document.createElement('DIV'); + this.configureHtmlShape(node); + node.appendChild(this.imageNode); + } + else + { + this.imageNode = document.createElement('v:image'); + this.imageNode.style.position = 'absolute'; + this.imageNode.src = this.image; + + // Needed to draw the background and border but known + // to cause problems in print preview with https + node = document.createElement('DIV'); + this.configureHtmlShape(node); + + // Workaround for cropped images in IE7/8 + node.style.overflow = 'visible'; + node.appendChild(this.imageNode); + } + } + + return node; +}; + +/** + * Function: updateAspect + * + * Updates the aspect of the image for the given image width and height. + */ +mxImageShape.prototype.updateAspect = function(w, h) +{ + var s = Math.min(this.bounds.width / w, this.bounds.height / h); + w = Math.max(0, Math.round(w * s)); + h = Math.max(0, Math.round(h * s)); + var x0 = Math.max(0, Math.round((this.bounds.width - w) / 2)); + var y0 = Math.max(0, Math.round((this.bounds.height - h) / 2)); + var st = this.imageNode.style; + + // Positions the child node relative to the parent node + if (this.imageNode.parentNode == this.node) + { + // Workaround for duplicate offset in VML in IE8 is + // to use parent padding instead of left and top + this.node.style.paddingLeft = x0 + 'px'; + this.node.style.paddingTop = y0 + 'px'; + } + else + { + st.left = (Math.round(this.bounds.x) + x0) + 'px'; + st.top = (Math.round(this.bounds.y) + y0) + 'px'; + } + + st.width = w + 'px'; + st.height = h + 'px'; +}; + +/** + * Function: scheduleUpdateAspect + * + * Schedules an asynchronous <updateAspect> using the current <image>. + */ +mxImageShape.prototype.scheduleUpdateAspect = function() +{ + var img = new Image(); + + img.onload = mxUtils.bind(this, function() + { + mxImageShape.prototype.updateAspect.call(this, img.width, img.height); + }); + + img.src = this.image; +}; + +/** + * Function: redraw + * + * Overrides <mxShape.redraw> to preserve the aspect ratio of images. + */ +mxImageShape.prototype.redraw = function() +{ + mxShape.prototype.redraw.apply(this, arguments); + + if (this.imageNode != null && this.bounds != null) + { + // Horizontal and vertical flipping + var flipH = mxUtils.getValue(this.style, mxConstants.STYLE_IMAGE_FLIPH, 0) == 1; + var flipV = mxUtils.getValue(this.style, mxConstants.STYLE_IMAGE_FLIPV, 0) == 1; + + if (this.dialect == mxConstants.DIALECT_SVG) + { + var sx = 1; + var sy = 1; + var dx = 0; + var dy = 0; + + if (flipH) + { + sx = -1; + dx = -this.bounds.width - 2 * this.bounds.x; + } + + if (flipV) + { + sy = -1; + dy = -this.bounds.height - 2 * this.bounds.y; + } + + // Adds image tansformation to existing transforms + var transform = (this.imageNode.getAttribute('transform') || '') + + ' scale('+sx+' '+sy+')'+ ' translate('+dx+' '+dy+')'; + this.imageNode.setAttribute('transform', transform); + } + else + { + // Sets default size (no aspect) + if (this.imageNode.nodeName != 'DIV') + { + this.imageNode.style.width = Math.max(0, Math.round(this.bounds.width)) + 'px'; + this.imageNode.style.height = Math.max(0, Math.round(this.bounds.height)) + 'px'; + } + + // Preserves image aspect + if (this.preserveImageAspect) + { + this.scheduleUpdateAspect(); + } + + if (flipH || flipV) + { + if (mxUtils.isVml(this.imageNode)) + { + if (flipH && flipV) + { + this.imageNode.style.rotation = '180'; + } + else if (flipH) + { + this.imageNode.style.flip = 'x'; + } + else + { + this.imageNode.style.flip = 'y'; + } + } + else + { + var filter = (this.imageNode.nodeName == 'DIV') ? 'progid:DXImageTransform.Microsoft.AlphaImageLoader ' + + '(src=\'' + this.image + '\', sizingMethod=\'scale\')' : ''; + + if (flipH && flipV) + { + filter += 'progid:DXImageTransform.Microsoft.BasicImage(rotation=2)'; + } + else if (flipH) + { + filter += 'progid:DXImageTransform.Microsoft.BasicImage(mirror=1)'; + } + else + { + filter += 'progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)'; + } + + if (this.imageNode.style.filter != filter) + { + this.imageNode.style.filter = filter; + } + } + } + } + } +}; + +/** + * Function: configureTransparentBackground + * + * Workaround for security warning in IE if this is used in the overlay pane + * of a diagram. + */ +mxImageShape.prototype.configureTransparentBackground = function(node) +{ + // do nothing +}; + +/** + * Function: redrawSvg + * + * Updates the SVG node(s) to reflect the latest bounds and scale. + */ +mxImageShape.prototype.redrawSvg = function() +{ + this.updateSvgShape(this.innerNode); + this.updateSvgShape(this.imageNode); + + if (this.bg != null) + { + this.updateSvgShape(this.bg); + + if (this.fill != null) + { + this.bg.setAttribute('fill', this.fill); + } + else + { + this.bg.setAttribute('fill', 'none'); + } + + if (this.stroke != null) + { + this.bg.setAttribute('stroke', this.stroke); + } + else + { + this.bg.setAttribute('stroke', 'none'); + } + + this.bg.setAttribute('shape-rendering', 'crispEdges'); + } +}; + +/** + * Function: configureSvgShape + * + * Extends method to set opacity on images. + */ +mxImageShape.prototype.configureSvgShape = function(node) +{ + mxShape.prototype.configureSvgShape.apply(this, arguments); + + if (this.imageNode != null) + { + if (this.opacity != null) + { + this.imageNode.setAttribute('opacity', this.opacity / 100); + } + else + { + this.imageNode.removeAttribute('opacity'); + } + } +}; diff --git a/src/js/shape/mxLabel.js b/src/js/shape/mxLabel.js new file mode 100644 index 0000000..c31f3bf --- /dev/null +++ b/src/js/shape/mxLabel.js @@ -0,0 +1,427 @@ +/** + * $Id: mxLabel.js,v 1.40 2012-05-22 16:10:12 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxLabel + * + * Extends <mxShape> to implement an image shape with a label. + * This shape is registered under <mxConstants.SHAPE_LABEL> in + * <mxCellRenderer>. + * + * Constructor: mxLabel + * + * Constructs a new label shape. + * + * Parameters: + * + * bounds - <mxRectangle> that defines the bounds. This is stored in + * <mxShape.bounds>. + * fill - String that defines the fill color. This is stored in <fill>. + * stroke - String that defines the stroke color. This is stored in <stroke>. + * strokewidth - Optional integer that defines the stroke width. Default is + * 1. This is stored in <strokewidth>. + */ +function mxLabel(bounds, fill, stroke, strokewidth) +{ + this.bounds = bounds; + this.fill = fill; + this.stroke = stroke; + this.strokewidth = (strokewidth != null) ? strokewidth : 1; +}; + +/** + * Extends mxShape. + */ +mxLabel.prototype = new mxShape(); +mxLabel.prototype.constructor = mxLabel; + +/** + * Variable: vmlNodes + * + * Adds local references to <mxShape.vmlNodes>. + */ +mxLabel.prototype.vmlNodes = mxLabel.prototype.vmlNodes.concat(['label', 'imageNode', 'indicatorImageNode', 'rectNode']); + +/** + * Variable: imageSize + * + * Default width and height for the image. Default is + * <mxConstants.DEFAULT_IMAGESIZE>. + */ +mxLabel.prototype.imageSize = mxConstants.DEFAULT_IMAGESIZE; + +/** + * Variable: spacing + * + * Default value for spacing. Default is 2. + */ +mxLabel.prototype.spacing = 2; + +/** + * Variable: indicatorSize + * + * Default width and height for the indicicator. Default is + * 10. + */ +mxLabel.prototype.indicatorSize = 10; + +/** + * Variable: indicatorSpacing + * + * Default spacing between image and indicator. Default is 2. + */ +mxLabel.prototype.indicatorSpacing = 2; + +/** + * Variable: opaqueVmlImages + * + * Specifies if all VML images should be rendered without transparency, that + * is, if the current opacity should be ignored for images. Default is false. + */ +mxLabel.prototype.opaqueVmlImages = false; + +/** + * Function: init + * + * Initializes the shape and adds it to the container. This function is + * overridden so that the node is already in the DOM when the indicator + * is added. This is required to access the ownerSVGelement of the + * container in the init function of the indicator. + */ +mxLabel.prototype.init = function(container) +{ + mxShape.prototype.init.apply(this, arguments); + + // Creates the indicator shape after the node was added to the DOM + if (this.indicatorColor != null && this.indicatorShape != null) + { + this.indicator = new this.indicatorShape(); + this.indicator.dialect = this.dialect; + this.indicator.bounds = this.bounds; + this.indicator.fill = this.indicatorColor; + this.indicator.stroke = this.indicatorColor; + this.indicator.gradient = this.indicatorGradientColor; + this.indicator.direction = this.indicatorDirection; + this.indicator.init(this.node); + this.indicatorShape = null; + } +}; + +/** + * Function: reconfigure + * + * Reconfigures this shape. This will update the colors of the indicator + * and reconfigure it if required. + */ +mxLabel.prototype.reconfigure = function() +{ + mxShape.prototype.reconfigure.apply(this); + + if (this.indicator != null) + { + this.indicator.fill = this.indicatorColor; + this.indicator.stroke = this.indicatorColor; + this.indicator.gradient = this.indicatorGradientColor; + this.indicator.direction = this.indicatorDirection; + this.indicator.reconfigure(); + } +}; + +/** + * Function: createHtml + * + * Creates and returns the HTML node to represent this shape. + */ +mxLabel.prototype.createHtml = function() +{ + var name = 'DIV'; + var node = document.createElement(name); + this.configureHtmlShape(node); + + // Adds a small subshape inside this shape + if (this.indicatorImage != null) + { + this.indicatorImageNode = mxUtils.createImage(this.indicatorImage); + this.indicatorImageNode.style.position = 'absolute'; + node.appendChild(this.indicatorImageNode); + } + + // Adds an image node to the div + if (this.image != null) + { + this.imageNode = mxUtils.createImage(this.image); + this.stroke = null; + this.configureHtmlShape(this.imageNode); + mxUtils.setOpacity(this.imageNode, '100'); + node.appendChild(this.imageNode); + } + + return node; +}; + +/** + * Function: createVml + * + * Creates and returns the VML node to represent this shape. + */ +mxLabel.prototype.createVml = function() +{ + var node = document.createElement('v:group'); + + // Background + var name = (this.isRounded) ? 'v:roundrect' : 'v:rect'; + this.rectNode = document.createElement(name); + this.configureVmlShape(this.rectNode); + + // Disables the shadow and configures the enclosing group + this.isShadow = false; + this.configureVmlShape(node); + node.coordorigin = '0,0'; + node.appendChild(this.rectNode); + + // Adds a small subshape inside this shape + if (this.indicatorImage != null) + { + this.indicatorImageNode = this.createVmlImage(this.indicatorImage, (this.opaqueVmlImages) ? null : this.opacity); + node.appendChild(this.indicatorImageNode); + } + + // Adds an image node to the div + if (this.image != null) + { + this.imageNode = this.createVmlImage(this.image, (this.opaqueVmlImages) ? null : this.opacity); + node.appendChild(this.imageNode); + } + + // Container for the label on top of everything + this.label = document.createElement('v:rect'); + this.label.style.top = '0px'; // relative + this.label.style.left = '0px'; // relative + this.label.filled = 'false'; + this.label.stroked = 'false'; + node.appendChild(this.label); + + return node; +}; + +/** + * Function: createVmlImage + * + * Creates an image node for the given image src and opacity to be used in VML. + */ +mxLabel.prototype.createVmlImage = function(src, opacity) +{ + var result = null; + + // Workaround for data URIs not supported in VML and for added + // border around images if opacity is used (not needed in IE9, + // but IMG node is probably better and faster anyway). + if (src.substring(0, 5) == 'data:' || opacity != null) + { + result = document.createElement('img'); + mxUtils.setOpacity(result, opacity); + result.setAttribute('border', '0'); + result.style.position = 'absolute'; + result.setAttribute('src', src); + } + else + { + result = document.createElement('v:image'); + result.src = src; + } + + return result; +}; + +/** + * Function: createSvg + * + * Creates and returns the SVG node to represent this shape. + */ +mxLabel.prototype.createSvg = function() +{ + var g = this.createSvgGroup('rect'); + + // Adds a small subshape to the svg group + if (this.indicatorImage != null) + { + this.indicatorImageNode = document.createElementNS(mxConstants.NS_SVG, 'image'); + this.indicatorImageNode.setAttributeNS(mxConstants.NS_XLINK, 'href', this.indicatorImage); + g.appendChild(this.indicatorImageNode); + + if (this.opacity != null) + { + this.indicatorImageNode.setAttribute('opacity', this.opacity / 100); + } + } + + // Adds an image to the svg group + if (this.image != null) + { + this.imageNode = document.createElementNS(mxConstants.NS_SVG, 'image'); + this.imageNode.setAttributeNS(mxConstants.NS_XLINK, 'href', this.image); + + if (this.opacity != null) + { + this.imageNode.setAttribute('opacity', this.opacity / 100); + } + + // Disables control-click and alt-click in Firefox + this.imageNode.setAttribute('style', 'pointer-events:none'); + this.configureSvgShape(this.imageNode); + g.appendChild(this.imageNode); + } + + return g; +}; + +/** + * Function: redraw + * + * Overrides redraw to define a unified implementation for redrawing + * all supported dialects. + */ +mxLabel.prototype.redraw = function() +{ + this.updateBoundingBox(); + var isSvg = (this.dialect == mxConstants.DIALECT_SVG); + var isVml = mxUtils.isVml(this.node); + + // Updates the bounds of the outermost shape + if (isSvg) + { + this.updateSvgShape(this.innerNode); + + if (this.shadowNode != null) + { + this.updateSvgShape(this.shadowNode); + } + + this.updateSvgGlassPane(); + } + else if (isVml) + { + this.updateVmlShape(this.node); + this.updateVmlShape(this.rectNode); + this.label.style.width = this.node.style.width; + this.label.style.height = this.node.style.height; + + this.updateVmlGlassPane(); + } + else + { + this.updateHtmlShape(this.node); + } + + // Updates the imagewidth and imageheight + var imageWidth = 0; + var imageHeight = 0; + + if (this.imageNode != null) + { + imageWidth = (this.style[mxConstants.STYLE_IMAGE_WIDTH] || + this.imageSize) * this.scale; + imageHeight = (this.style[mxConstants.STYLE_IMAGE_HEIGHT] || + this.imageSize) * this.scale; + } + + // Updates the subshape size and location + var indicatorSpacing = 0; + var indicatorWidth = 0; + var indicatorHeight = 0; + + if (this.indicator != null || this.indicatorImageNode != null) + { + indicatorSpacing = (this.style[mxConstants.STYLE_INDICATOR_SPACING] || + this.indicatorSpacing) * this.scale; + indicatorWidth = (this.style[mxConstants.STYLE_INDICATOR_WIDTH] || + this.indicatorSize) * this.scale; + indicatorHeight = (this.style[mxConstants.STYLE_INDICATOR_HEIGHT] || + this.indicatorSize) * this.scale; + } + + var align = this.style[mxConstants.STYLE_IMAGE_ALIGN]; + var valign = this.style[mxConstants.STYLE_IMAGE_VERTICAL_ALIGN]; + + var inset = this.spacing * this.scale + 5; + var width = Math.max(imageWidth, indicatorWidth); + var height = imageHeight + indicatorSpacing + indicatorHeight; + + var x = (isSvg) ? this.bounds.x : 0; + + if (align == mxConstants.ALIGN_RIGHT) + { + x += this.bounds.width - width - inset; + } + else if (align == mxConstants.ALIGN_CENTER) + { + x += (this.bounds.width - width) / 2; + } + else // default is left + { + x += inset; + } + + var y = (isSvg) ? this.bounds.y : 0; + + if (valign == mxConstants.ALIGN_BOTTOM) + { + y += this.bounds.height - height - inset; + } + else if (valign == mxConstants.ALIGN_TOP) + { + y += inset; + } + else // default is middle + { + y += (this.bounds.height - height) / 2; + } + + // Updates the imagenode + if (this.imageNode != null) + { + if (isSvg) + { + this.imageNode.setAttribute('x', (x + (width - imageWidth) / 2) + 'px'); + this.imageNode.setAttribute('y', y + 'px'); + this.imageNode.setAttribute('width', imageWidth + 'px'); + this.imageNode.setAttribute('height', imageHeight + 'px'); + } + else + { + this.imageNode.style.left = (x + width - imageWidth) + 'px'; + this.imageNode.style.top = y + 'px'; + this.imageNode.style.width = imageWidth + 'px'; + this.imageNode.style.height = imageHeight + 'px'; + this.imageNode.stroked = 'false'; + } + } + + // Updates the subshapenode (aka. indicator) + if (this.indicator != null) + { + this.indicator.bounds = new mxRectangle( + x + (width - indicatorWidth) / 2, + y + imageHeight + indicatorSpacing, + indicatorWidth, indicatorHeight); + this.indicator.redraw(); + } + else if (this.indicatorImageNode != null) + { + if (isSvg) + { + this.indicatorImageNode.setAttribute('x', (x + (width - indicatorWidth) / 2) + 'px'); + this.indicatorImageNode.setAttribute('y', (y + imageHeight + indicatorSpacing) + 'px'); + this.indicatorImageNode.setAttribute('width', indicatorWidth + 'px'); + this.indicatorImageNode.setAttribute('height', indicatorHeight + 'px'); + } + else + { + this.indicatorImageNode.style.left = (x + (width - indicatorWidth) / 2) + 'px'; + this.indicatorImageNode.style.top = (y + imageHeight + indicatorSpacing) + 'px'; + this.indicatorImageNode.style.width = indicatorWidth + 'px'; + this.indicatorImageNode.style.height = indicatorHeight + 'px'; + } + } +}; diff --git a/src/js/shape/mxLine.js b/src/js/shape/mxLine.js new file mode 100644 index 0000000..5ef3eb0 --- /dev/null +++ b/src/js/shape/mxLine.js @@ -0,0 +1,217 @@ +/** + * $Id: mxLine.js,v 1.36 2012-03-30 04:44:59 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxLine + * + * Extends <mxShape> to implement a horizontal line shape. + * This shape is registered under <mxConstants.SHAPE_LINE> in + * <mxCellRenderer>. + * + * Constructor: mxLine + * + * Constructs a new line shape. + * + * Parameters: + * + * bounds - <mxRectangle> that defines the bounds. This is stored in + * <mxShape.bounds>. + * stroke - String that defines the stroke color. Default is 'black'. This is + * stored in <stroke>. + * strokewidth - Optional integer that defines the stroke width. Default is + * 1. This is stored in <strokewidth>. + */ +function mxLine(bounds, stroke, strokewidth) +{ + this.bounds = bounds; + this.stroke = stroke; + this.strokewidth = (strokewidth != null) ? strokewidth : 1; +}; + +/** + * Extends mxShape. + */ +mxLine.prototype = new mxShape(); +mxLine.prototype.constructor = mxLine; + +/** + * Variable: vmlNodes + * + * Adds local references to <mxShape.vmlNodes>. + */ +mxLine.prototype.vmlNodes = mxLine.prototype.vmlNodes.concat(['label', 'innerNode']); + +/** + * Variable: mixedModeHtml + * + * Overrides the parent value with false, meaning it will + * draw in VML in mixed Html mode. + */ +mxLine.prototype.mixedModeHtml = false; + +/** + * Variable: preferModeHtml + * + * Overrides the parent value with false, meaning it will + * draw as VML in prefer Html mode. + */ +mxLine.prototype.preferModeHtml = false; + +/** + * Function: clone + * + * Overrides the clone method to add special fields. + */ +mxLine.prototype.clone = function() +{ + var clone = new mxLine(this.bounds, + this.stroke, this.strokewidth); + clone.isDashed = this.isDashed; + + return clone; +}; + +/** + * Function: createVml + * + * Creates and returns the VML node to represent this shape. + */ +mxLine.prototype.createVml = function() +{ + var node = document.createElement('v:group'); + node.style.position = 'absolute'; + + // Represents the text label container + this.label = document.createElement('v:rect'); + this.label.style.position = 'absolute'; + this.label.stroked = 'false'; + this.label.filled = 'false'; + node.appendChild(this.label); + + // Represents the straight line shape + this.innerNode = document.createElement('v:shape'); + this.configureVmlShape(this.innerNode); + node.appendChild(this.innerNode); + + return node; +}; + +/** + * Function: redrawVml + * + * Redraws this VML shape by invoking <updateVmlShape> on this.node. + */ +mxLine.prototype.reconfigure = function() +{ + if (mxUtils.isVml(this.node)) + { + this.configureVmlShape(this.innerNode); + } + else + { + mxShape.prototype.reconfigure.apply(this, arguments); + } +}; + +/** + * Function: redrawVml + * + * Updates the VML node(s) to reflect the latest bounds and scale. + */ +mxLine.prototype.redrawVml = function() +{ + this.updateVmlShape(this.node); + this.updateVmlShape(this.label); + + this.innerNode.coordsize = this.node.coordsize; + this.innerNode.strokeweight = (this.strokewidth * this.scale) + 'px'; + this.innerNode.style.width = this.node.style.width; + this.innerNode.style.height = this.node.style.height; + + var w = this.bounds.width; + var h =this.bounds.height; + + if (this.direction == mxConstants.DIRECTION_NORTH || + this.direction == mxConstants.DIRECTION_SOUTH) + { + this.innerNode.path = 'm ' + Math.round(w / 2) + ' 0' + + ' l ' + Math.round(w / 2) + ' ' + Math.round(h) + ' e'; + } + else + { + this.innerNode.path = 'm 0 ' + Math.round(h / 2) + + ' l ' + Math.round(w) + ' ' + Math.round(h / 2) + ' e'; + } +}; + +/** + * Function: createSvg + * + * Creates and returns the SVG node(s) to represent this shape. + */ +mxLine.prototype.createSvg = function() +{ + var g = this.createSvgGroup('path'); + + // Creates an invisible shape around the path for easier + // selection with the mouse. Note: Firefox does not ignore + // the value of the stroke attribute for pointer-events: stroke. + // It does, however, ignore the visibility attribute. + this.pipe = this.createSvgPipe(); + g.appendChild(this.pipe); + + return g; +}; + +/** + * Function: redrawSvg + * + * Updates the SVG node(s) to reflect the latest bounds and scale. + */ +mxLine.prototype.redrawSvg = function() +{ + var strokeWidth = Math.round(Math.max(1, this.strokewidth * this.scale)); + this.innerNode.setAttribute('stroke-width', strokeWidth); + + if (this.bounds != null) + { + var x = this.bounds.x; + var y = this.bounds.y; + var w = this.bounds.width; + var h = this.bounds.height; + + var d = null; + + if (this.direction == mxConstants.DIRECTION_NORTH || this.direction == mxConstants.DIRECTION_SOUTH) + { + d = 'M ' + Math.round(x + w / 2) + ' ' + Math.round(y) + ' L ' + Math.round(x + w / 2) + ' ' + Math.round(y + h); + } + else + { + d = 'M ' + Math.round(x) + ' ' + Math.round(y + h / 2) + ' L ' + Math.round(x + w) + ' ' + Math.round(y + h / 2); + } + + this.innerNode.setAttribute('d', d); + this.pipe.setAttribute('d', d); + this.pipe.setAttribute('stroke-width', this.strokewidth + mxShape.prototype.SVG_STROKE_TOLERANCE); + + this.updateSvgTransform(this.innerNode, false); + this.updateSvgTransform(this.pipe, false); + + if (this.crisp) + { + this.innerNode.setAttribute('shape-rendering', 'crispEdges'); + } + else + { + this.innerNode.removeAttribute('shape-rendering'); + } + + if (this.isDashed) + { + var phase = Math.max(1, Math.round(3 * this.scale * this.strokewidth)); + this.innerNode.setAttribute('stroke-dasharray', phase + ' ' + phase); + } + } +}; diff --git a/src/js/shape/mxMarker.js b/src/js/shape/mxMarker.js new file mode 100644 index 0000000..cfd6f66 --- /dev/null +++ b/src/js/shape/mxMarker.js @@ -0,0 +1,267 @@ +/** + * $Id: mxMarker.js,v 1.19 2012-03-30 12:51:58 david Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +var mxMarker = +{ + /** + * Class: mxMarker + * + * A static class that implements all markers for VML and SVG using a + * registry. NOTE: The signatures in this class will change. + * + * Variable: markers + * + * Maps from markers names to functions to paint the markers. + */ + markers: [], + + /** + * Function: paintMarker + * + * Paints the given marker. + */ + paintMarker: function(node, type, p0, pe, color, strokewidth, size, scale, x0, y0, source, style) + { + var marker = mxMarker.markers[type]; + var result = null; + + if (marker != null) + { + var isVml = mxUtils.isVml(node); + + // Computes the norm and the inverse norm + var dx = pe.x - p0.x; + var dy = pe.y - p0.y; + + if (isNaN(dx) || isNaN(dy)) + { + return; + } + + var dist = Math.max(1, Math.sqrt(dx * dx + dy * dy)); + var nx = dx * scale / dist; + var ny = dy * scale / dist; + + pe = pe.clone(); + + if (isVml) + { + pe.x -= x0; + pe.y -= y0; + } + + // Handles start-/endFill style + var filled = true; + var key = (source) ? mxConstants.STYLE_STARTFILL : mxConstants.STYLE_ENDFILL; + + if (style[key] == 0) + { + filled = false; + } + + if (isVml) + { + // Opacity is updated in reconfigure, use nf in path for no fill + node.strokecolor = color; + + if (filled) + { + node.fillcolor = color; + } + else + { + node.filled = 'false'; + } + } + else + { + node.setAttribute('stroke', color); + + var op = (style.opacity != null) ? style.opacity / 100 : 1; + node.setAttribute('stroke-opacity', op); + + if (filled) + { + node.setAttribute('fill', color); + node.setAttribute('fill-opacity', op); + } + else + { + node.setAttribute('fill', 'none'); + } + } + + result = marker.call(this, node, type, pe, nx, ny, strokewidth, size, scale, isVml); + } + + return result; + } + +}; + +(function() +{ + /** + * Drawing of the classic and block arrows. + */ + var tmp = function(node, type, pe, nx, ny, strokewidth, size, scale, isVml) + { + // The angle of the forward facing arrow sides against the x axis is + // 26.565 degrees, 1/sin(26.565) = 2.236 / 2 = 1.118 ( / 2 allows for + // only half the strokewidth is processed ). + var endOffsetX = nx * strokewidth * 1.118; + var endOffsetY = ny * strokewidth * 1.118; + pe.x -= endOffsetX; + pe.y -= endOffsetY; + + nx = nx * (size + strokewidth); + ny = ny * (size + strokewidth); + + if (isVml) + { + node.path = 'm' + Math.round(pe.x) + ',' + Math.round(pe.y) + + ' l' + Math.round(pe.x - nx - ny / 2) + ' ' + Math.round(pe.y - ny + nx / 2) + + ((type != mxConstants.ARROW_CLASSIC) ? '' : + ' ' + Math.round(pe.x - nx * 3 / 4) + ' ' + Math.round(pe.y - ny * 3 / 4)) + + ' ' + Math.round(pe.x + ny / 2 - nx) + ' ' + Math.round(pe.y - ny - nx / 2) + + ' x e'; + node.setAttribute('strokeweight', (strokewidth * scale) + 'px'); + } + else + { + node.setAttribute('d', 'M ' + pe.x + ' ' + pe.y + + ' L ' + (pe.x - nx - ny / 2) + ' ' + (pe.y - ny + nx / 2) + + ((type != mxConstants.ARROW_CLASSIC) ? '' : + ' L ' + (pe.x - nx * 3 / 4) + ' ' + (pe.y - ny * 3 / 4)) + + ' L ' + (pe.x + ny / 2 - nx) + ' ' + (pe.y - ny - nx / 2) + + ' z'); + node.setAttribute('stroke-width', strokewidth * scale); + } + + var f = (type != mxConstants.ARROW_CLASSIC) ? 1 : 3 / 4; + return new mxPoint(-nx * f - endOffsetX, -ny * f - endOffsetY); + }; + + mxMarker.markers[mxConstants.ARROW_CLASSIC] = tmp; + mxMarker.markers[mxConstants.ARROW_BLOCK] = tmp; +}()); + +mxMarker.markers[mxConstants.ARROW_OPEN] = function(node, type, pe, nx, ny, strokewidth, size, scale, isVml) +{ + // The angle of the forward facing arrow sides against the x axis is + // 26.565 degrees, 1/sin(26.565) = 2.236 / 2 = 1.118 ( / 2 allows for + // only half the strokewidth is processed ). + var endOffsetX = nx * strokewidth * 1.118; + var endOffsetY = ny * strokewidth * 1.118; + pe.x -= endOffsetX; + pe.y -= endOffsetY; + + nx = nx * (size + strokewidth); + ny = ny * (size + strokewidth); + + if (isVml) + { + node.path = 'm' + Math.round(pe.x - nx - ny / 2) + ' ' + Math.round(pe.y - ny + nx / 2) + + ' l' + Math.round(pe.x) + ' ' + Math.round(pe.y) + + ' ' + Math.round(pe.x + ny / 2 - nx) + ' ' + Math.round(pe.y - ny - nx / 2) + + ' e nf'; + node.setAttribute('strokeweight', (strokewidth * scale) + 'px'); + } + else + { + node.setAttribute('d', 'M ' + (pe.x - nx - ny / 2) + ' ' + (pe.y - ny + nx / 2) + + ' L ' + (pe.x) + ' ' + (pe.y) + + ' L ' + (pe.x + ny / 2 - nx) + ' ' + (pe.y - ny - nx / 2)); + node.setAttribute('stroke-width', strokewidth * scale); + node.setAttribute('fill', 'none'); + } + + return new mxPoint(-endOffsetX * 2, -endOffsetY * 2); +}; + +mxMarker.markers[mxConstants.ARROW_OVAL] = function(node, type, pe, nx, ny, strokewidth, size, scale, isVml) +{ + nx *= size; + ny *= size; + + nx *= 0.5 + strokewidth / 2; + ny *= 0.5 + strokewidth / 2; + + var absSize = size * scale; + var radius = absSize / 2; + + if (isVml) + { + node.path = 'm' + Math.round(pe.x + radius) + ' ' + Math.round(pe.y) + + ' at ' + Math.round(pe.x - radius) + ' ' + Math.round(pe.y - radius) + + ' ' + Math.round(pe.x + radius) + ' ' + Math.round(pe.y + radius) + + ' ' + Math.round(pe.x + radius) + ' ' + Math.round(pe.y) + + ' ' + Math.round(pe.x + radius) + ' ' + Math.round(pe.y) + + ' x e'; + + node.setAttribute('strokeweight', (strokewidth * scale) + 'px'); + } + else + { + node.setAttribute('d', 'M ' + (pe.x - radius) + ' ' + (pe.y) + + ' a ' + (radius) + ' ' + (radius) + + ' 0 1,1 ' + (absSize) + ' 0' + + ' a ' + (radius) + ' ' + (radius) + + ' 0 1,1 ' + (-absSize) + ' 0 z'); + node.setAttribute('stroke-width', strokewidth * scale); + } + + return new mxPoint(-nx / (2 + strokewidth), -ny / (2 + strokewidth)); +}; + +(function() + { + /** + * Drawing of the diamond and thin diamond markers + */ + var tmp_diamond = function(node, type, pe, nx, ny, strokewidth, size, scale, isVml) + { + // The angle of the forward facing arrow sides against the x axis is + // 45 degrees, 1/sin(45) = 1.4142 / 2 = 0.7071 ( / 2 allows for + // only half the strokewidth is processed ). Or 0.9862 for thin diamond. + // Note these values and the tk variable below are dependent, update + // both together (saves trig hard coding it). + var swFactor = (type == mxConstants.ARROW_DIAMOND) ? 0.7071 : 0.9862; + var endOffsetX = nx * strokewidth * swFactor; + var endOffsetY = ny * strokewidth * swFactor; + + nx = nx * (size + strokewidth); + ny = ny * (size + strokewidth); + + pe.x -= endOffsetX + nx / 2; + pe.y -= endOffsetY + ny / 2; + + // thickness factor for diamond + var tk = ((type == mxConstants.ARROW_DIAMOND) ? 2 : 3.4); + + if (isVml) + { + node.path = 'm' + Math.round(pe.x + nx / 2) + ' ' + Math.round(pe.y + ny / 2) + + ' l' + Math.round(pe.x - ny / tk) + ' ' + Math.round(pe.y + nx / tk) + + ' ' + Math.round(pe.x - nx / 2) + ' ' + Math.round(pe.y - ny / 2) + + ' ' + Math.round(pe.x + ny / tk) + ' ' + Math.round(pe.y - nx / tk) + + ' x e'; + node.setAttribute('strokeweight', (strokewidth * scale) + 'px'); + } + else + { + node.setAttribute('d', 'M ' + (pe.x + nx / 2) + ' ' + (pe.y + ny / 2) + + ' L ' + (pe.x - ny / tk) + ' ' + (pe.y + nx / tk) + + ' L ' + (pe.x - nx / 2) + ' ' + (pe.y - ny / 2) + + ' L ' + (pe.x + ny / tk) + ' ' + (pe.y - nx / tk) + + ' z'); + node.setAttribute('stroke-width', strokewidth * scale); + } + + return new mxPoint(-endOffsetX - nx, -endOffsetY - ny); + }; + + mxMarker.markers[mxConstants.ARROW_DIAMOND] = tmp_diamond; + mxMarker.markers[mxConstants.ARROW_DIAMOND_THIN] = tmp_diamond; + }()); diff --git a/src/js/shape/mxPolyline.js b/src/js/shape/mxPolyline.js new file mode 100644 index 0000000..2d64323 --- /dev/null +++ b/src/js/shape/mxPolyline.js @@ -0,0 +1,146 @@ +/** + * $Id: mxPolyline.js,v 1.31 2012-05-24 12:00:45 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxPolyline + * + * Extends <mxShape> to implement a polyline (a line with multiple points). + * This shape is registered under <mxConstants.SHAPE_POLYLINE> in + * <mxCellRenderer>. + * + * Constructor: mxPolyline + * + * Constructs a new polyline shape. + * + * Parameters: + * + * points - Array of <mxPoints> that define the points. This is stored in + * <mxShape.points>. + * stroke - String that defines the stroke color. Default is 'black'. This is + * stored in <stroke>. + * strokewidth - Optional integer that defines the stroke width. Default is + * 1. This is stored in <strokewidth>. + */ +function mxPolyline(points, stroke, strokewidth) +{ + this.points = points; + this.stroke = stroke; + this.strokewidth = (strokewidth != null) ? strokewidth : 1; +}; + +/** + * Extends mxShape. + */ +mxPolyline.prototype = new mxShape(); +mxPolyline.prototype.constructor = mxPolyline; + +/** + * Variable: addPipe + * + * Specifies if a SVG path should be created around any path to increase the + * tolerance for mouse events. Default is false since this shape is filled. + */ +mxPolyline.prototype.addPipe = true; + +/** + * Function: create + * + * Override to create HTML regardless of gradient and + * rounded property. + */ +mxPolyline.prototype.create = function() +{ + var node = null; + + if (this.dialect == mxConstants.DIALECT_SVG) + { + node = this.createSvg(); + } + else if (this.dialect == mxConstants.DIALECT_STRICTHTML || + (this.dialect == mxConstants.DIALECT_PREFERHTML && + this.points != null && this.points.length > 0)) + { + node = document.createElement('DIV'); + this.configureHtmlShape(node); + node.style.borderStyle = ''; + node.style.background = ''; + } + else + { + node = document.createElement('v:shape'); + this.configureVmlShape(node); + var strokeNode = document.createElement('v:stroke'); + + if (this.opacity != null) + { + strokeNode.opacity = this.opacity + '%'; + } + + node.appendChild(strokeNode); + } + + return node; +}; + +/** + * Function: redrawVml + * + * Overrides the method to update the bounds if they have not been + * assigned. + */ +mxPolyline.prototype.redrawVml = function() +{ + // Updates the bounds based on the points + if (this.points != null && this.points.length > 0 && this.points[0] != null) + { + this.bounds = new mxRectangle(this.points[0].x,this.points[0].y, 0, 0); + + for (var i = 1; i < this.points.length; i++) + { + this.bounds.add(new mxRectangle(this.points[i].x,this.points[i].y, 0, 0)); + } + } + + mxShape.prototype.redrawVml.apply(this, arguments); +}; + +/** + * Function: createSvg + * + * Creates and returns the SVG node(s) to represent this shape. + */ +mxPolyline.prototype.createSvg = function() +{ + var g = this.createSvgGroup('path'); + + // Creates an invisible shape around the path for easier + // selection with the mouse. Note: Firefox does not ignore + // the value of the stroke attribute for pointer-events: stroke, + // it does, however, ignore the visibility attribute. + if (this.addPipe) + { + this.pipe = this.createSvgPipe(); + g.appendChild(this.pipe); + } + + return g; +}; + +/** + * Function: redrawSvg + * + * Updates the SVG node(s) to reflect the latest bounds and scale. + */ +mxPolyline.prototype.redrawSvg = function() +{ + this.updateSvgShape(this.innerNode); + var d = this.innerNode.getAttribute('d'); + + if (d != null && this.pipe != null) + { + this.pipe.setAttribute('d', d); + var strokeWidth = Math.round(Math.max(1, this.strokewidth * this.scale)); + this.pipe.setAttribute('stroke-width', strokeWidth + mxShape.prototype.SVG_STROKE_TOLERANCE); + } +}; diff --git a/src/js/shape/mxRectangleShape.js b/src/js/shape/mxRectangleShape.js new file mode 100644 index 0000000..26688c3 --- /dev/null +++ b/src/js/shape/mxRectangleShape.js @@ -0,0 +1,61 @@ +/** + * $Id: mxRectangleShape.js,v 1.17 2012-09-26 07:51:29 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxRectangleShape + * + * Extends <mxShape> to implement a rectangle shape. + * This shape is registered under <mxConstants.SHAPE_RECTANGLE> + * in <mxCellRenderer>. + * + * Constructor: mxRectangleShape + * + * Constructs a new rectangle shape. + * + * Parameters: + * + * bounds - <mxRectangle> that defines the bounds. This is stored in + * <mxShape.bounds>. + * fill - String that defines the fill color. This is stored in <fill>. + * stroke - String that defines the stroke color. This is stored in <stroke>. + * strokewidth - Optional integer that defines the stroke width. Default is + * 1. This is stored in <strokewidth>. + */ +function mxRectangleShape(bounds, fill, stroke, strokewidth) +{ + this.bounds = bounds; + this.fill = fill; + this.stroke = stroke; + this.strokewidth = (strokewidth != null) ? strokewidth : 1; +}; + +/** + * Extends mxShape. + */ +mxRectangleShape.prototype = new mxShape(); +mxRectangleShape.prototype.constructor = mxRectangleShape; + +/** + * Function: createVml + * + * Creates and returns the VML node to represent this shape. + */ +mxRectangleShape.prototype.createVml = function() +{ + var name = (this.isRounded) ? 'v:roundrect' : 'v:rect'; + var node = document.createElement(name); + this.configureVmlShape(node); + + return node; +}; + +/** + * Function: createSvg + * + * Creates and returns the SVG node to represent this shape. + */ +mxRectangleShape.prototype.createSvg = function() +{ + return this.createSvgGroup('rect'); +}; diff --git a/src/js/shape/mxRhombus.js b/src/js/shape/mxRhombus.js new file mode 100644 index 0000000..37e35ec --- /dev/null +++ b/src/js/shape/mxRhombus.js @@ -0,0 +1,172 @@ +/** + * $Id: mxRhombus.js,v 1.25 2012-04-04 07:34:50 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxRhombus + * + * Extends <mxShape> to implement a rhombus (aka diamond) shape. + * This shape is registered under <mxConstants.SHAPE_RHOMBUS> + * in <mxCellRenderer>. + * + * Constructor: mxRhombus + * + * Constructs a new rhombus shape. + * + * Parameters: + * + * bounds - <mxRectangle> that defines the bounds. This is stored in + * <mxShape.bounds>. + * fill - String that defines the fill color. This is stored in <fill>. + * stroke - String that defines the stroke color. This is stored in <stroke>. + * strokewidth - Optional integer that defines the stroke width. Default is + * 1. This is stored in <strokewidth>. + */ +function mxRhombus(bounds, fill, stroke, strokewidth) +{ + this.bounds = bounds; + this.fill = fill; + this.stroke = stroke; + this.strokewidth = (strokewidth != null) ? strokewidth : 1; +}; + +/** + * Extends mxShape. + */ +mxRhombus.prototype = new mxShape(); +mxRhombus.prototype.constructor = mxRhombus; + +/** + * Variable: mixedModeHtml + * + * Overrides the parent value with false, meaning it will + * draw in VML in mixed Html mode. + */ +mxRhombus.prototype.mixedModeHtml = false; + +/** + * Variable: preferModeHtml + * + * Overrides the parent value with false, meaning it will + * draw as VML in prefer Html mode. + */ +mxRhombus.prototype.preferModeHtml = false; + +/** + * Function: createHtml + * + * Creates and returns the HTML node to represent this shape. + */ +mxRhombus.prototype.createHtml = function() +{ + var node = document.createElement('DIV'); + this.configureHtmlShape(node); + + return node; +}; + +/** + * Function: createVml + * + * Creates and returns the VML node(s) to represent this shape. + */ +mxRhombus.prototype.createVml = function() +{ + var node = document.createElement('v:shape'); + this.configureVmlShape(node); + + return node; +}; + +/** + * Function: createSvg + * + * Creates and returns the SVG node(s) to represent this shape. + */ +mxRhombus.prototype.createSvg = function() +{ + return this.createSvgGroup('path'); +}; + +// TODO: When used as an indicator, this.node.points is null +// so we use a path object for building general diamonds. +//mxRhombus.prototype.redraw = function() { +// this.node.setAttribute('strokeweight', (this.strokewidth * this.scale) + 'px'); +// var x = this.bounds.x; +// var y = this.bounds.y; +// var w = this.bounds.width; +// var h = this.bounds.height; +// this.node.points.value = (x+w/2)+','+y+' '+(x+w)+','+(y+h/2)+ +// ' '+(x+w/2)+','+(y+h)+' '+x+','+(y+h/2)+' '+ +// (x+w/2)+','+y; +//} + +/** + * Function: redrawVml + * + * Updates the VML node(s) to reflect the latest bounds and scale. + */ +mxRhombus.prototype.redrawVml = function() +{ + this.updateVmlShape(this.node); + var x = 0; + var y = 0; + var w = Math.round(this.bounds.width); + var h = Math.round(this.bounds.height); + + this.node.path = 'm ' + Math.round(x + w / 2) + ' ' + y + + ' l ' + (x + w) + ' ' + Math.round(y + h / 2) + + ' l ' + Math.round(x + w / 2) + ' ' + (y + h) + + ' l ' + x + ' ' + Math.round(y + h / 2) + ' x e'; +}; + +/** + * Function: redrawHtml + * + * Updates the HTML node(s) to reflect the latest bounds and scale. + */ +mxRhombus.prototype.redrawHtml = function() +{ + this.updateHtmlShape(this.node); +}; + +/** + * Function: redrawSvg + * + * Updates the SVG node(s) to reflect the latest bounds and scale. + */ +mxRhombus.prototype.redrawSvg = function() +{ + this.updateSvgNode(this.innerNode); + + if (this.shadowNode != null) + { + this.updateSvgNode(this.shadowNode); + } +}; + +/** + * Function: createSvgSpan + * + * Updates the path for the given SVG node. + */ +mxRhombus.prototype.updateSvgNode = function(node) +{ + var strokeWidth = Math.round(Math.max(1, this.strokewidth * this.scale)); + node.setAttribute('stroke-width', strokeWidth); + var x = this.bounds.x; + var y = this.bounds.y; + var w = this.bounds.width; + var h = this.bounds.height; + var d = 'M ' + Math.round(x + w / 2) + ' ' + Math.round(y) + ' L ' + Math.round(x + w) + ' ' + Math.round(y + h / 2) + + ' L ' + Math.round(x + w / 2) + ' ' + Math.round(y + h) + ' L ' + Math.round(x) + ' ' + Math.round(y + h / 2) + + ' Z '; + node.setAttribute('d', d); + this.updateSvgTransform(node, node == this.shadowNode); + + if (this.isDashed) + { + var phase = Math.max(1, Math.round(3 * this.scale * this.strokewidth)); + node.setAttribute('stroke-dasharray', phase + ' ' + phase); + } +}; diff --git a/src/js/shape/mxShape.js b/src/js/shape/mxShape.js new file mode 100644 index 0000000..44ba3e7 --- /dev/null +++ b/src/js/shape/mxShape.js @@ -0,0 +1,2045 @@ +/** + * $Id: mxShape.js,v 1.173 2012-07-31 11:46:53 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxShape + * + * Base class for all shapes. A shape in mxGraph is a + * separate implementation for SVG, VML and HTML. Which + * implementation to use is controlled by the <dialect> + * property which is assigned from within the <mxCellRenderer> + * when the shape is created. The dialect must be assigned + * for a shape, and it does normally depend on the browser and + * the confiuration of the graph (see <mxGraph> rendering hint). + * + * For each supported shape in SVG and VML, a corresponding + * shape exists in mxGraph, namely for text, image, rectangle, + * rhombus, ellipse and polyline. The other shapes are a + * combination of these shapes (eg. label and swimlane) + * or they consist of one or more (filled) path objects + * (eg. actor and cylinder). The HTML implementation is + * optional but may be required for a HTML-only view of + * the graph. + * + * Custom Shapes: + * + * To extend from this class, the basic code looks as follows. + * In the special case where the custom shape consists only of + * one filled region or one filled region and an additional stroke + * the <mxActor> and <mxCylinder> should be subclassed, + * respectively. These implement <redrawPath> in order to create + * the path expression for VML and SVG via a unified API (see + * <mxPath>). <mxCylinder.redrawPath> has an additional boolean + * argument to draw the foreground and background separately. + * + * (code) + * function CustomShape() { } + * + * CustomShape.prototype = new mxShape(); + * CustomShape.prototype.constructor = CustomShape; + * (end) + * + * To register a custom shape in an existing graph instance, + * one must register the shape under a new name in the graph's + * cell renderer as follows: + * + * (code) + * graph.cellRenderer.registerShape('customShape', CustomShape); + * (end) + * + * The second argument is the name of the constructor. + * + * In order to use the shape you can refer to the given name above + * in a stylesheet. For example, to change the shape for the default + * vertex style, the following code is used: + * + * (code) + * var style = graph.getStylesheet().getDefaultVertexStyle(); + * style[mxConstants.STYLE_SHAPE] = 'customShape'; + * (end) + * + * Constructor: mxShape + * + * Constructs a new shape. + */ +function mxShape() { }; + +/** + * Variable: SVG_STROKE_TOLERANCE + * + * Event-tolerance for SVG strokes (in px). Default is 8. + */ +mxShape.prototype.SVG_STROKE_TOLERANCE = 8; + +/** + * Variable: scale + * + * Holds the scale in which the shape is being painted. + */ +mxShape.prototype.scale = 1; + +/** + * Variable: dialect + * + * Holds the dialect in which the shape is to be painted. + * This can be one of the DIALECT constants in <mxConstants>. + */ +mxShape.prototype.dialect = null; + +/** + * Variable: crisp + * + * Special attribute for SVG rendering to set the shape-rendering attribute to + * crispEdges in the output. This is ignored in IE. Default is false. To + * disable antialias in IE, the explorer.css file can be changed as follows: + * + * [code] + * v\:* { + * behavior: url(#default#VML); + * antialias: false; + * } + * [/code] + */ +mxShape.prototype.crisp = false; + +/** + * Variable: roundedCrispSvg + * + * Specifies if crisp rendering should be enabled for rounded shapes. + * Default is true. + */ +mxShape.prototype.roundedCrispSvg = true; + +/** + * Variable: mixedModeHtml + * + * Specifies if <createHtml> should be used in mixed Html mode. + * Default is true. + */ +mxShape.prototype.mixedModeHtml = true; + +/** + * Variable: preferModeHtml + * + * Specifies if <createHtml> should be used in prefer Html mode. + * Default is true. + */ +mxShape.prototype.preferModeHtml = true; + +/** + * Variable: bounds + * + * Holds the <mxRectangle> that specifies the bounds of this shape. + */ +mxShape.prototype.bounds = null; + +/** + * Variable: points + * + * Holds the array of <mxPoints> that specify the points of this shape. + */ +mxShape.prototype.points = null; + +/** + * Variable: node + * + * Holds the outermost DOM node that represents this shape. + */ +mxShape.prototype.node = null; + +/** + * Variable: label + * + * Reference to the DOM node that should contain the label. This is null + * if the label should be placed inside <node> or <innerNode>. + */ +mxShape.prototype.label = null; + +/** + * Variable: innerNode + * + * Holds the DOM node that graphically represents this shape. This may be + * null if the outermost DOM <node> represents this shape. + */ +mxShape.prototype.innerNode = null; + +/** + * Variable: style + * + * Holds the style of the cell state that corresponds to this shape. This may + * be null if the shape is used directly, without a cell state. + */ +mxShape.prototype.style = null; + +/** + * Variable: startOffset + * + * Specifies the offset in pixels from the first point in <points> and + * the actual start of the shape. + */ +mxShape.prototype.startOffset = null; + +/** + * Variable: endOffset + * + * Specifies the offset in pixels from the last point in <points> and + * the actual start of the shape. + */ +mxShape.prototype.endOffset = null; + +/** + * Variable: boundingBox + * + * Contains the bounding box of the shape, that is, the smallest rectangle + * that includes all pixels of the shape. + */ +mxShape.prototype.boundingBox = null; + +/** + * Variable: vmlNodes + * + * Array if VML node names to fix in IE8 standards mode. + */ +mxShape.prototype.vmlNodes = ['node', 'strokeNode', 'fillNode', 'shadowNode']; + +/** + * Variable: vmlScale + * + * Internal scaling for VML using coordsize for better precision. + */ +mxShape.prototype.vmlScale = 1; + +/** + * Variable: strokewidth + * + * Holds the current strokewidth. Default is 1. + */ +mxShape.prototype.strokewidth = 1; + +/** + * Function: setCursor + * + * Sets the cursor on the given shape. + * + * Parameters: + * + * cursor - The cursor to be used. + */ +mxShape.prototype.setCursor = function(cursor) +{ + if (cursor == null) + { + cursor = ''; + } + + this.cursor = cursor; + + if (this.innerNode != null) + { + this.innerNode.style.cursor = cursor; + } + + if (this.node != null) + { + this.node.style.cursor = cursor; + } + + if (this.pipe != null) + { + this.pipe.style.cursor = cursor; + } +}; + +/** + * Function: getCursor + * + * Returns the current cursor. + */ +mxShape.prototype.getCursor = function() +{ + return this.cursor; +}; + +/** + * Function: init + * + * Initializes the shape by creaing the DOM node using <create> + * and adding it into the given container. + * + * Parameters: + * + * container - DOM node that will contain the shape. + */ +mxShape.prototype.init = function(container) +{ + if (this.node == null) + { + this.node = this.create(container); + + if (container != null) + { + container.appendChild(this.node); + + // Workaround for broken VML in IE8 standards mode. This gives an ID to + // each element that is referenced from this instance. After adding the + // DOM to the document, the outerHTML is overwritten to fix the VML + // rendering and the references are restored. + if (document.documentMode == 8 && mxUtils.isVml(this.node)) + { + this.reparseVml(); + } + } + } + + // Gradients are inserted late when the owner SVG element is known + if (this.insertGradientNode != null) + { + this.insertGradient(this.insertGradientNode); + this.insertGradientNode = null; + } +}; + +/** + * Function: reparseVml + * + * Forces a parsing of the outerHTML of this node and restores all references specified in <vmlNodes>. + * This is a workaround for the VML rendering bug in IE8 standards mode. + */ +mxShape.prototype.reparseVml = function() +{ + // Assigns temporary IDs to VML nodes so that references can be restored when + // inserted into the DOM as a string + for (var i = 0; i < this.vmlNodes.length; i++) + { + if (this[this.vmlNodes[i]] != null) + { + this[this.vmlNodes[i]].setAttribute('id', 'mxTemporaryReference-' + this.vmlNodes[i]); + } + } + + this.node.outerHTML = this.node.outerHTML; + + // Restores references to the actual DOM nodes + for (var i = 0; i < this.vmlNodes.length; i++) + { + if (this[this.vmlNodes[i]] != null) + { + this[this.vmlNodes[i]] = this.node.ownerDocument.getElementById('mxTemporaryReference-' + this.vmlNodes[i]); + this[this.vmlNodes[i]].removeAttribute('id'); + } + } +}; + +/** + * Function: insertGradient + * + * Inserts the given gradient node. + */ +mxShape.prototype.insertGradient = function(node) +{ + // Gradients are inserted late when the owner SVG element is known + if (node != null) + { + // Checks if the given gradient already exists inside the SVG element + // that also contains the node that represents this shape. If the gradient + // with the same ID exists in another SVG element, then this will add + // a copy of the gradient with a different ID to the SVG element and update + // the reference accordingly. This is required in Firefox because if the + // referenced fill element is removed from the DOM the shape appears black. + var count = 0; + var id = node.getAttribute('id'); + var gradient = document.getElementById(id); + + while (gradient != null && gradient.ownerSVGElement != this.node.ownerSVGElement) + { + count++; + id = node.getAttribute('id') + '-' + count; + gradient = document.getElementById(id); + } + + // According to specification, gradients should be put in a defs + // section in the first child of the owner SVG element. However, + // it turns out that gradients only work when added as follows. + if (gradient == null) + { + node.setAttribute('id', id); + this.node.ownerSVGElement.appendChild(node); + gradient = node; + } + + if (gradient != null) + { + var ref = 'url(#' + id + ')'; + var tmp = (this.innerNode != null) ? this.innerNode : this.node; + + if (tmp != null && tmp.getAttribute('fill') != ref) + { + tmp.setAttribute('fill', ref); + } + } + } +}; + +/** + * Function: isMixedModeHtml + * + * Used to determine if a shape can be rendered using <createHtml> in mixed + * mode Html without compromising the display accuracy. The default + * implementation will check if the shape is not rounded or rotated and has + * no gradient, and will use a DIV if that is the case. It will also check + * if <mxShape.mixedModeHtml> is true, which is the default settings. + * Subclassers can either override <mixedModeHtml> or this function if the + * result depends on dynamic values. The graph's dialect is available via + * <dialect>. + */ +mxShape.prototype.isMixedModeHtml = function() +{ + return this.mixedModeHtml && !this.isRounded && !this.isShadow && this.gradient == null && + mxUtils.getValue(this.style, mxConstants.STYLE_GLASS, 0) == 0 && + mxUtils.getValue(this.style, mxConstants.STYLE_ROTATION, 0) == 0; +}; + +/** + * Function: create + * + * Creates and returns the DOM node(s) for the shape in + * the given container. This implementation invokes + * <createSvg>, <createHtml> or <createVml> depending + * on the <dialect> and style settings. + * + * Parameters: + * + * container - DOM node that will contain the shape. + */ +mxShape.prototype.create = function(container) +{ + var node = null; + + if (this.dialect == mxConstants.DIALECT_SVG) + { + node = this.createSvg(); + } + else if (this.dialect == mxConstants.DIALECT_STRICTHTML || + (this.preferModeHtml && this.dialect == mxConstants.DIALECT_PREFERHTML) || + (this.isMixedModeHtml() && this.dialect == mxConstants.DIALECT_MIXEDHTML)) + { + node = this.createHtml(); + } + else + { + node = this.createVml(); + } + + return node; +}; + +/** + * Function: createHtml + * + * Creates and returns the HTML DOM node(s) to represent + * this shape. This implementation falls back to <createVml> + * so that the HTML creation is optional. + */ +mxShape.prototype.createHtml = function() +{ + var node = document.createElement('DIV'); + this.configureHtmlShape(node); + + return node; +}; + +/** + * Function: destroy + * + * Destroys the shape by removing it from the DOM and releasing the DOM + * node associated with the shape using <mxEvent.release>. + */ +mxShape.prototype.destroy = function() +{ + if (this.node != null) + { + mxEvent.release(this.node); + + if (this.node.parentNode != null) + { + this.node.parentNode.removeChild(this.node); + } + + if (this.node.glassOverlay) + { + this.node.glassOverlay.parentNode.removeChild(this.node.glassOverlay); + this.node.glassOverlay = null; + } + + this.node = null; + } +}; + +/** + * Function: apply + * + * Applies the style of the given <mxCellState> to the shape. This + * implementation assigns the following styles to local fields: + * + * - <mxConstants.STYLE_FILLCOLOR> => fill + * - <mxConstants.STYLE_GRADIENTCOLOR> => gradient + * - <mxConstants.STYLE_GRADIENT_DIRECTION> => gradientDirection + * - <mxConstants.STYLE_OPACITY> => opacity + * - <mxConstants.STYLE_STROKECOLOR> => stroke + * - <mxConstants.STYLE_STROKEWIDTH> => strokewidth + * - <mxConstants.STYLE_SHADOW> => isShadow + * - <mxConstants.STYLE_DASHED> => isDashed + * - <mxConstants.STYLE_SPACING> => spacing + * - <mxConstants.STYLE_STARTSIZE> => startSize + * - <mxConstants.STYLE_ENDSIZE> => endSize + * - <mxConstants.STYLE_ROUNDED> => isRounded + * - <mxConstants.STYLE_STARTARROW> => startArrow + * - <mxConstants.STYLE_ENDARROW> => endArrow + * - <mxConstants.STYLE_ROTATION> => rotation + * - <mxConstants.STYLE_DIRECTION> => direction + * + * This keeps a reference to the <style>. If you need to keep a reference to + * the cell, you can override this method and store a local reference to + * state.cell or the <mxCellState> itself. + * + * Parameters: + * + * state - <mxCellState> of the corresponding cell. + */ +mxShape.prototype.apply = function(state) +{ + var style = state.style; + this.style = style; + + if (style != null) + { + this.fill = mxUtils.getValue(style, mxConstants.STYLE_FILLCOLOR, this.fill); + this.gradient = mxUtils.getValue(style, mxConstants.STYLE_GRADIENTCOLOR, this.gradient); + this.gradientDirection = mxUtils.getValue(style, mxConstants.STYLE_GRADIENT_DIRECTION, this.gradientDirection); + this.opacity = mxUtils.getValue(style, mxConstants.STYLE_OPACITY, this.opacity); + this.stroke = mxUtils.getValue(style, mxConstants.STYLE_STROKECOLOR, this.stroke); + this.strokewidth = mxUtils.getNumber(style, mxConstants.STYLE_STROKEWIDTH, this.strokewidth); + this.isShadow = mxUtils.getValue(style, mxConstants.STYLE_SHADOW, this.isShadow); + this.isDashed = mxUtils.getValue(style, mxConstants.STYLE_DASHED, this.isDashed); + this.spacing = mxUtils.getValue(style, mxConstants.STYLE_SPACING, this.spacing); + this.startSize = mxUtils.getNumber(style, mxConstants.STYLE_STARTSIZE, this.startSize); + this.endSize = mxUtils.getNumber(style, mxConstants.STYLE_ENDSIZE, this.endSize); + this.isRounded = mxUtils.getValue(style, mxConstants.STYLE_ROUNDED, this.isRounded); + this.startArrow = mxUtils.getValue(style, mxConstants.STYLE_STARTARROW, this.startArrow); + this.endArrow = mxUtils.getValue(style, mxConstants.STYLE_ENDARROW, this.endArrow); + this.rotation = mxUtils.getValue(style, mxConstants.STYLE_ROTATION, this.rotation); + this.direction = mxUtils.getValue(style, mxConstants.STYLE_DIRECTION, this.direction); + + if (this.fill == 'none') + { + this.fill = null; + } + + if (this.gradient == 'none') + { + this.gradient = null; + } + + if (this.stroke == 'none') + { + this.stroke = null; + } + } +}; + +/** + * Function: createSvgGroup + * + * Creates a SVG group element and adds the given shape as a child of the + * element. The child is stored in <innerNode> for later access. + */ +mxShape.prototype.createSvgGroup = function(shape) +{ + var g = document.createElementNS(mxConstants.NS_SVG, 'g'); + + // Creates the shape inside an svg group + this.innerNode = document.createElementNS(mxConstants.NS_SVG, shape); + this.configureSvgShape(this.innerNode); + + // Avoids anti-aliasing for non-rounded rectangles with a + // strokewidth of 1 or more pixels + if (shape == 'rect' && this.strokewidth * this.scale >= 1 && !this.isRounded) + { + this.innerNode.setAttribute('shape-rendering', 'optimizeSpeed'); + } + + // Creates the shadow + this.shadowNode = this.createSvgShadow(this.innerNode); + + if (this.shadowNode != null) + { + g.appendChild(this.shadowNode); + } + + // Appends the main shape after the shadow + g.appendChild(this.innerNode); + + return g; +}; + +/** + * Function: createSvgShadow + * + * Creates a clone of the given node and configures the node's color + * to use <mxConstants.SHADOWCOLOR>. + */ +mxShape.prototype.createSvgShadow = function(node) +{ + if (this.isShadow) + { + var shadow = node.cloneNode(true); + shadow.setAttribute('opacity', mxConstants.SHADOW_OPACITY); + + if (this.fill != null && this.fill != mxConstants.NONE) + { + shadow.setAttribute('fill', mxConstants.SHADOWCOLOR); + } + + if (this.stroke != null && this.stroke != mxConstants.NONE) + { + shadow.setAttribute('stroke', mxConstants.SHADOWCOLOR); + } + + return shadow; + } + + return null; +}; + +/** + * Function: configureHtmlShape + * + * Configures the specified HTML node by applying the current color, + * bounds, shadow, opacity etc. + */ +mxShape.prototype.configureHtmlShape = function(node) +{ + if (mxUtils.isVml(node)) + { + this.configureVmlShape(node); + } + else + { + node.style.position = 'absolute'; + node.style.overflow = 'hidden'; + var color = this.stroke; + + if (color != null && color != mxConstants.NONE) + { + node.style.borderColor = color; + + if (this.isDashed) + { + node.style.borderStyle = 'dashed'; + } + else if (this.strokewidth > 0) + { + node.style.borderStyle = 'solid'; + } + + node.style.borderWidth = Math.ceil(this.strokewidth * this.scale) + 'px'; + } + else + { + node.style.borderWidth = '0px'; + } + + color = this.fill; + node.style.background = ''; + + if (color != null && color != mxConstants.NONE) + { + node.style.backgroundColor = color; + } + else if (this.points == null) + { + this.configureTransparentBackground(node); + } + + if (this.opacity != null) + { + mxUtils.setOpacity(node, this.opacity); + } + } +}; + +/** + * Function: updateVmlFill + * + * Updates the given VML fill node. + */ +mxShape.prototype.updateVmlFill = function(node, c1, c2, dir, alpha) +{ + node.color = c1; + + if (alpha != null && alpha != 100) + { + node.opacity = alpha + '%'; + + if (c2 != null) + { + // LATER: Set namespaced attribute without using setAttribute + // which is required for updating the value in IE8 standards. + node.setAttribute('o:opacity2', alpha + '%'); + } + } + + if (c2 != null) + { + node.type = 'gradient'; + node.color2 = c2; + var angle = '180'; + + if (this.gradientDirection == mxConstants.DIRECTION_EAST) + { + angle = '270'; + } + else if (this.gradientDirection == mxConstants.DIRECTION_WEST) + { + angle = '90'; + } + else if (this.gradientDirection == mxConstants.DIRECTION_NORTH) + { + angle = '0'; + } + + node.angle = angle; + } +}; + +/** + * Function: updateVmlStrokeNode + * + * Creates the stroke node for VML. + */ +mxShape.prototype.updateVmlStrokeNode = function(parent) +{ + // Stroke node is always needed to specify defaults that match SVG output + if (this.strokeNode == null) + { + this.strokeNode = document.createElement('v:stroke'); + + // To math SVG defaults jointsyle miter and miterlimit 4 + this.strokeNode.joinstyle = 'miter'; + this.strokeNode.miterlimit = 4; + + parent.appendChild(this.strokeNode); + } + + if (this.opacity != null) + { + this.strokeNode.opacity = this.opacity + '%'; + } + + this.updateVmlDashStyle(); +}; + +/** + * Function: updateVmlStrokeColor + * + * Updates the VML stroke color for the given node. + */ +mxShape.prototype.updateVmlStrokeColor = function(node) +{ + var color = this.stroke; + + if (color != null && color != mxConstants.NONE) + { + node.stroked = 'true'; + node.strokecolor = color; + } + else + { + node.stroked = 'false'; + } +}; + +/** + * Function: configureVmlShape + * + * Configures the specified VML node by applying the current color, + * bounds, shadow, opacity etc. + */ +mxShape.prototype.configureVmlShape = function(node) +{ + node.style.position = 'absolute'; + this.updateVmlStrokeColor(node); + node.style.background = ''; + var color = this.fill; + + if (color != null && color != mxConstants.NONE) + { + if (this.fillNode == null) + { + this.fillNode = document.createElement('v:fill'); + node.appendChild(this.fillNode); + } + + this.updateVmlFill(this.fillNode, color, this.gradient, this.gradientDirection, this.opacity); + } + else + { + node.filled = 'false'; + + if (this.points == null) + { + this.configureTransparentBackground(node); + } + } + + this.updateVmlStrokeNode(node); + + if (this.isShadow) + { + this.createVmlShadow(node); + } + + // Fixes possible hang in IE when arcsize is set on non-rects + if (node.nodeName == 'roundrect') + { + // Workaround for occasional "member not found" error + try + { + var f = mxConstants.RECTANGLE_ROUNDING_FACTOR * 100; + + if (this.style != null) + { + f = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, f); + } + + node.setAttribute('arcsize', String(f) + '%'); + } + catch (e) + { + // ignore + } + } +}; + +/** + * Function: createVmlShadow + * + * Creates the VML shadow node. + */ +mxShape.prototype.createVmlShadow = function(node) +{ + // Adds a shadow only once per shape + if (this.shadowNode == null) + { + this.shadowNode = document.createElement('v:shadow'); + this.shadowNode.on = 'true'; + this.shadowNode.color = mxConstants.SHADOWCOLOR; + this.shadowNode.opacity = (mxConstants.SHADOW_OPACITY * 100) + '%'; + + this.shadowStrokeNode = document.createElement('v:stroke'); + this.shadowNode.appendChild(this.shadowStrokeNode); + + node.appendChild(this.shadowNode); + } +}; + +/** + * Function: configureTransparentBackground + * + * Hook to make the background of a shape transparent. This hook was added as + * a workaround for the "display non secure items" warning dialog in IE which + * appears if the background:url(transparent.gif) is used in the overlay pane + * of a diagram. Since only mxImageShapes currently exist in the overlay pane + * this function is only overridden in mxImageShape. + */ +mxShape.prototype.configureTransparentBackground = function(node) +{ + node.style.background = 'url(\'' + mxClient.imageBasePath + '/transparent.gif\')'; +}; + +/** + * Function: configureSvgShape + * + * Configures the specified SVG node by applying the current color, + * bounds, shadow, opacity etc. + */ +mxShape.prototype.configureSvgShape = function(node) +{ + var color = this.stroke; + + if (color != null && color != mxConstants.NONE) + { + node.setAttribute('stroke', color); + } + else + { + node.setAttribute('stroke', 'none'); + } + + color = this.fill; + + if (color != null && color != mxConstants.NONE) + { + // Fetches a reference to a shared gradient + if (this.gradient != null) + { + var id = this.getGradientId(color, this.gradient); + + if (this.gradientNode != null && this.gradientNode.getAttribute('id') != id) + { + this.gradientNode = null; + node.setAttribute('fill', ''); + } + + if (this.gradientNode == null) + { + this.gradientNode = this.createSvgGradient(id, + color, this.gradient, node); + node.setAttribute('fill', 'url(#'+id+')'); + } + } + else + { + // TODO: Remove gradient from document if no longer shared + this.gradientNode = null; + node.setAttribute('fill', color); + } + } + else + { + node.setAttribute('fill', 'none'); + } + + if (this.opacity != null) + { + // Improves opacity performance in Firefox + node.setAttribute('fill-opacity', this.opacity / 100); + node.setAttribute('stroke-opacity', this.opacity / 100); + } +}; + +/** + * Function: getGradientId + * + * Creates a unique ID for the gradient of this shape. + */ +mxShape.prototype.getGradientId = function(start, end) +{ + // Removes illegal characters from gradient ID + if (start.charAt(0) == '#') + { + start = start.substring(1); + } + + if (end.charAt(0) == '#') + { + end = end.substring(1); + } + + // Workaround for gradient IDs not working in Safari 5 / Chrome 6 + // if they contain uppercase characters + start = start.toLowerCase(); + end = end.toLowerCase(); + + var dir = null; + + if (this.gradientDirection == null || + this.gradientDirection == mxConstants.DIRECTION_SOUTH) + { + dir = 'south'; + } + else if (this.gradientDirection == mxConstants.DIRECTION_EAST) + { + dir = 'east'; + } + else + { + var tmp = start; + start = end; + end = tmp; + + if (this.gradientDirection == mxConstants.DIRECTION_NORTH) + { + dir = 'south'; + } + else if (this.gradientDirection == mxConstants.DIRECTION_WEST) + { + dir = 'east'; + } + } + + return 'mx-gradient-'+start+'-'+end+'-'+dir; +}; + +/** + * Function: createSvgPipe + * + * Creates an invisible path which is used to increase the hit detection for + * edges in SVG. + */ +mxShape.prototype.createSvgPipe = function(id, start, end, node) +{ + var pipe = document.createElementNS(mxConstants.NS_SVG, 'path'); + pipe.setAttribute('pointer-events', 'stroke'); + pipe.setAttribute('fill', 'none'); + pipe.setAttribute('visibility', 'hidden'); + // Workaround for Opera ignoring the visiblity attribute above while + // other browsers need a stroke color to perform the hit-detection but + // do not ignore the visibility attribute. Side-effect is that Opera's + // hit detection for horizontal/vertical edges seems to ignore the pipe. + pipe.setAttribute('stroke', (mxClient.IS_OP) ? 'none' : 'white'); + + return pipe; +}; + +/** + * Function: createSvgGradient + * + * Creates a gradient object for SVG using the specified startcolor, + * endcolor and opacity. + */ +mxShape.prototype.createSvgGradient = function(id, start, end, node) +{ + var gradient = this.insertGradientNode; + + if (gradient == null) + { + gradient = document.createElementNS(mxConstants.NS_SVG, 'linearGradient'); + gradient.setAttribute('id', id); + gradient.setAttribute('x1', '0%'); + gradient.setAttribute('y1', '0%'); + gradient.setAttribute('x2', '0%'); + gradient.setAttribute('y2', '0%'); + + if (this.gradientDirection == null || + this.gradientDirection == mxConstants.DIRECTION_SOUTH) + { + gradient.setAttribute('y2', '100%'); + } + else if (this.gradientDirection == mxConstants.DIRECTION_EAST) + { + gradient.setAttribute('x2', '100%'); + } + else if (this.gradientDirection == mxConstants.DIRECTION_NORTH) + { + gradient.setAttribute('y1', '100%'); + } + else if (this.gradientDirection == mxConstants.DIRECTION_WEST) + { + gradient.setAttribute('x1', '100%'); + } + + var stop = document.createElementNS(mxConstants.NS_SVG, 'stop'); + stop.setAttribute('offset', '0%'); + stop.setAttribute('style', 'stop-color:'+start); + gradient.appendChild(stop); + + stop = document.createElementNS(mxConstants.NS_SVG, 'stop'); + stop.setAttribute('offset', '100%'); + stop.setAttribute('style', 'stop-color:'+end); + gradient.appendChild(stop); + } + + // Inserted later when the owner SVG element is known + this.insertGradientNode = gradient; + + return gradient; +}; + +/** + * Function: createPoints + * + * Creates a path expression using the specified commands for this.points. + * If <isRounded> is true, then the path contains curves for the corners. + */ +mxShape.prototype.createPoints = function(moveCmd, lineCmd, curveCmd, isRelative) +{ + var offsetX = (isRelative) ? this.bounds.x : 0; + var offsetY = (isRelative) ? this.bounds.y : 0; + + // Workaround for crisp shape-rendering in IE9 + var crisp = (this.crisp && this.dialect == mxConstants.DIALECT_SVG && mxClient.IS_IE) ? 0.5 : 0; + + if (isNaN(this.points[0].x) || isNaN(this.points[0].y)) + { + return null; + } + + var size = mxConstants.LINE_ARCSIZE * this.scale; + var p0 = this.points[0]; + + if (this.startOffset != null) + { + p0 = p0.clone(); + p0.x += this.startOffset.x; + p0.y += this.startOffset.y; + } + + var points = moveCmd + ' ' + (Math.round(p0.x - offsetX) + crisp) + ' ' + + (Math.round(p0.y - offsetY) + crisp) + ' '; + + for (var i = 1; i < this.points.length; i++) + { + p0 = this.points[i - 1]; + var pt = this.points[i]; + + if (isNaN(pt.x) || isNaN(pt.y)) + { + return null; + } + + if (i == this.points.length - 1 && this.endOffset != null) + { + pt = pt.clone(); + pt.x += this.endOffset.x; + pt.y += this.endOffset.y; + } + + var dx = p0.x - pt.x; + var dy = p0.y - pt.y; + + if ((this.isRounded && i < this.points.length - 1) && + (dx != 0 || dy != 0) && this.scale > 0.3) + { + // Draws a line from the last point to the current point with a spacing + // of size off the current point into direction of the last point + var dist = Math.sqrt(dx * dx + dy * dy); + var nx1 = dx * Math.min(size, dist / 2) / dist; + var ny1 = dy * Math.min(size, dist / 2) / dist; + points += lineCmd + ' ' + (Math.round(pt.x + nx1 - offsetX) + crisp) + ' ' + + (Math.round(pt.y + ny1 - offsetY) + crisp) + ' '; + + // Draws a curve from the last point to the current point with a spacing + // of size off the current point into direction of the next point + var pe = this.points[i+1]; + dx = pe.x - pt.x; + dy = pe.y - pt.y; + + dist = Math.max(1, Math.sqrt(dx * dx + dy * dy)); + + if (dist != 0) + { + var nx2 = dx * Math.min(size, dist / 2) / dist; + var ny2 = dy * Math.min(size, dist / 2) / dist; + + points += curveCmd + ' ' + Math.round(pt.x - offsetX) + ' '+ + Math.round(pt.y - offsetY) + ' ' + Math.round(pt.x - offsetX) + ',' + + Math.round(pt.y - offsetY) + ' ' + (Math.round(pt.x + nx2 - offsetX) + crisp) + ' ' + + (Math.round(pt.y + ny2 - offsetY) + crisp) + ' '; + } + } + else + { + points += lineCmd + ' ' + (Math.round(pt.x - offsetX) + crisp) + ' ' + (Math.round(pt.y - offsetY) + crisp) + ' '; + } + } + + return points; +}; + +/** + * Function: updateHtmlShape + * + * Updates the bounds or points of the specified HTML node and + * updates the inner children to reflect the changes. + */ +mxShape.prototype.updateHtmlShape = function(node) +{ + if (node != null) + { + if (mxUtils.isVml(node)) + { + this.updateVmlShape(node); + } + else + { + var sw = Math.ceil(this.strokewidth * this.scale); + node.style.borderWidth = Math.max(1, sw) + 'px'; + + if (this.bounds != null && !isNaN(this.bounds.x) && !isNaN(this.bounds.y) && + !isNaN(this.bounds.width) && !isNaN(this.bounds.height)) + { + node.style.left = Math.round(this.bounds.x - sw / 2) + 'px'; + node.style.top = Math.round(this.bounds.y - sw / 2) + 'px'; + + if (document.compatMode == 'CSS1Compat') + { + sw = -sw; + } + + node.style.width = Math.round(Math.max(0, this.bounds.width + sw)) + 'px'; + node.style.height = Math.round(Math.max(0, this.bounds.height + sw)) + 'px'; + + if (this.bounds.width == 0 || this.bounds.height == 0) + { + node.style.visibility = 'hidden'; + } + else + { + node.style.visibility = 'visible'; + } + } + } + + if (this.points != null && this.bounds != null && !mxUtils.isVml(node)) + { + if (this.divContainer == null) + { + this.divContainer = node; + } + + while (this.divContainer.firstChild != null) + { + mxEvent.release(this.divContainer.firstChild); + this.divContainer.removeChild(this.divContainer.firstChild); + } + + node.style.borderStyle = ''; + node.style.background = ''; + + if (this.points.length == 2) + { + var p0 = this.points[0]; + var pe = this.points[1]; + + var dx = pe.x - p0.x; + var dy = pe.y - p0.y; + + if (dx == 0 || dy == 0) + { + node.style.borderStyle = 'solid'; + } + else + { + node.style.width = Math.round(this.bounds.width + 1) + 'px'; + node.style.height = Math.round(this.bounds.height + 1) + 'px'; + + var length = Math.sqrt(dx * dx + dy * dy); + var dotCount = 1 + (length / (8 * this.scale)); + + var nx = dx / dotCount; + var ny = dy / dotCount; + var x = p0.x - this.bounds.x; + var y = p0.y - this.bounds.y; + + for (var i = 0; i < dotCount; i++) + { + var tmp = document.createElement('DIV'); + + tmp.style.position = 'absolute'; + tmp.style.overflow = 'hidden'; + + tmp.style.left = Math.round(x) + 'px'; + tmp.style.top = Math.round(y) + 'px'; + tmp.style.width = Math.max(1, 2 * this.scale) + 'px'; + tmp.style.height = Math.max(1, 2 * this.scale) + 'px'; + + tmp.style.backgroundColor = this.stroke; + this.divContainer.appendChild(tmp); + + x += nx; + y += ny; + } + } + } + else if (this.points.length == 3) + { + var mid = this.points[1]; + + var n = '0'; + var s = '1'; + var w = '0'; + var e = '1'; + + if (mid.x == this.bounds.x) + { + e = '0'; + w = '1'; + } + + if (mid.y == this.bounds.y) + { + n = '1'; + s = '0'; + } + + node.style.borderStyle = 'solid'; + node.style.borderWidth = n + ' ' + e + ' ' + s + ' ' + w + 'px'; + } + else + { + node.style.width = Math.round(this.bounds.width + 1) + 'px'; + node.style.height = Math.round(this.bounds.height + 1) + 'px'; + var last = this.points[0]; + + for (var i = 1; i < this.points.length; i++) + { + var next = this.points[i]; + + // TODO: Use one div for multiple lines + var tmp = document.createElement('DIV'); + + tmp.style.position = 'absolute'; + tmp.style.overflow = 'hidden'; + + tmp.style.borderColor = this.stroke; + tmp.style.borderStyle = 'solid'; + tmp.style.borderWidth = '1 0 0 1px'; + + var x = Math.min(next.x, last.x) - this.bounds.x; + var y = Math.min(next.y, last.y) - this.bounds.y; + var w = Math.max(1, Math.abs(next.x - last.x)); + var h = Math.max(1, Math.abs(next.y - last.y)); + + tmp.style.left = x + 'px'; + tmp.style.top = y + 'px'; + tmp.style.width = w + 'px'; + tmp.style.height = h + 'px'; + + this.divContainer.appendChild(tmp); + last = next; + } + } + } + } +}; + +/** + * Function: updateVmlDashStyle + * + * Updates the dashstyle in the stroke node. + */ +mxShape.prototype.updateVmlDashStyle = function() +{ + if (this.isDashed) + { + if (this.strokeNode.dashstyle != 'dash') + { + this.strokeNode.dashstyle = 'dash'; + } + } + else if (this.strokeNode.dashstyle != 'solid') + { + this.strokeNode.dashstyle = 'solid'; + } +}; + +/** + * Function: updateVmlShape + * + * Updates the bounds or points of the specified VML node and + * updates the inner children to reflect the changes. + */ +mxShape.prototype.updateVmlShape = function(node) +{ + node.strokeweight = (this.strokewidth * this.scale) + 'px'; + + // Dash pattern needs updating as it depends on strokeweight in VML + if (this.strokeNode != null) + { + this.updateVmlDashStyle(); + } + + // Updates the offset of the shadow + if (this.shadowNode != null) + { + var dx = Math.round(mxConstants.SHADOW_OFFSET_X * this.scale); + var dy = Math.round(mxConstants.SHADOW_OFFSET_Y * this.scale); + this.shadowNode.offset = dx + 'px,' + dy + 'px'; + } + + if (this.bounds != null && !isNaN(this.bounds.x) && !isNaN(this.bounds.y) && + !isNaN(this.bounds.width) && !isNaN(this.bounds.height)) + { + var f = 1; + + var w = Math.max(0, Math.round(this.bounds.width)); + var h = Math.max(0, Math.round(this.bounds.height)); + + // Groups and shapes need a coordsize + if (this.points != null || node.nodeName == 'shape' || node.nodeName == 'group') + { + var tmp = (node.parentNode.nodeName == 'group') ? 1 : this.vmlScale; + node.coordsize = (w * tmp) + ',' + (h * tmp); + } + else if (node.parentNode.nodeName == 'group') + { + f = this.vmlScale; + } + + // Only top-level nodes are non-relative and rotated + if (node.parentNode != this.node) + { + node.style.left = Math.round(this.bounds.x * f) + 'px'; + node.style.top = Math.round(this.bounds.y * f) + 'px'; + + if (this.points == null) + { + if (this.rotation != null && this.rotation != 0) + { + node.style.rotation = this.rotation; + } + else if (node.style.rotation != null) + { + node.style.rotation = ''; + } + } + } + + node.style.width = (w * f) + 'px'; + node.style.height = (h * f) + 'px'; + } + + if (this.points != null && node.nodeName != 'group') + { + if (node.nodeName == 'polyline' && node.points != null) + { + var points = ''; + + for (var i = 0; i < this.points.length; i++) + { + points += this.points[i].x + ',' + this.points[i].y + ' '; + } + + node.points.value = points; + + node.style.left = null; + node.style.top = null; + node.style.width = null; + node.style.height = null; + } + else if (this.bounds != null) + { + var points = this.createPoints('m', 'l', 'c', true); + + // Smooth style for VML (experimental) + if (this.style != null && this.style[mxConstants.STYLE_SMOOTH]) + { + var pts = this.points; + var n = pts.length; + + if (n > 3) + { + var x0 = this.bounds.x; + var y0 = this.bounds.y; + points = 'm ' + Math.round(pts[0].x - x0) + ' ' + Math.round(pts[0].y - y0) + ' qb'; + + for (var i = 1; i < n - 1; i++) + { + points += ' ' + Math.round(pts[i].x - x0) + ' ' + Math.round(pts[i].y - y0); + } + + points += ' nf l ' + Math.round(pts[n - 1].x - x0) + ' ' + Math.round(pts[n - 1].y - y0); + } + } + + node.path = points + ' e'; + } + } +}; + +/** + * Function: updateSvgBounds + * + * Updates the bounds of the given node using <bounds>. + */ +mxShape.prototype.updateSvgBounds = function(node) +{ + var w = this.bounds.width; + var h = this.bounds.height; + + if (this.isRounded && !(this.crisp && mxClient.IS_IE)) + { + node.setAttribute('x', this.bounds.x); + node.setAttribute('y', this.bounds.y); + node.setAttribute('width', w); + node.setAttribute('height', h); + } + else + { + // Workaround for crisp shape-rendering in IE9 + var dd = (this.crisp && mxClient.IS_IE) ? 0.5 : 0; + node.setAttribute('x', Math.round(this.bounds.x) + dd); + node.setAttribute('y', Math.round(this.bounds.y) + dd); + + w = Math.round(w); + h = Math.round(h); + + node.setAttribute('width', w); + node.setAttribute('height', h); + } + + if (this.isRounded) + { + var f = mxConstants.RECTANGLE_ROUNDING_FACTOR * 100; + + if (this.style != null) + { + f = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, f) / 100; + } + + var r = Math.min(w * f, h * f); + node.setAttribute('rx', r); + node.setAttribute('ry', r); + } + + this.updateSvgTransform(node, node == this.shadowNode); +}; + +/** + * Function: updateSvgPath + * + * Updates the path of the given node using <points>. + */ +mxShape.prototype.updateSvgPath = function(node) +{ + var d = this.createPoints('M', 'L', 'C', false); + + if (d != null) + { + node.setAttribute('d', d); + + // Smooth style for SVG (experimental) + if (this.style != null && this.style[mxConstants.STYLE_SMOOTH]) + { + var pts = this.points; + var n = pts.length; + + if (n > 3) + { + var points = 'M '+pts[0].x+' '+pts[0].y+' '; + points += ' Q '+pts[1].x + ' ' + pts[1].y + ' ' + + ' '+pts[2].x + ' ' + pts[2].y; + + for (var i = 3; i < n; i++) + { + points += ' T ' + pts[i].x + ' ' + pts[i].y; + } + + node.setAttribute('d', points); + } + } + + node.removeAttribute('x'); + node.removeAttribute('y'); + node.removeAttribute('width'); + node.removeAttribute('height'); + } +}; + +/** + * Function: updateSvgScale + * + * Updates the properties of the given node that depend on the scale and checks + * the crisp rendering attribute. + */ +mxShape.prototype.updateSvgScale = function(node) +{ + node.setAttribute('stroke-width', Math.round(Math.max(1, this.strokewidth * this.scale))); + + if (this.isDashed) + { + var phase = Math.max(1, Math.round(3 * this.scale * this.strokewidth)); + node.setAttribute('stroke-dasharray', phase + ' ' + phase); + } + + if (this.crisp && (this.roundedCrispSvg || this.isRounded != true) && + (this.rotation == null || this.rotation == 0)) + { + node.setAttribute('shape-rendering', 'crispEdges'); + } + else + { + node.removeAttribute('shape-rendering'); + } +}; + +/** + * Function: updateSvgShape + * + * Updates the bounds or points of the specified SVG node and + * updates the inner children to reflect the changes. + */ +mxShape.prototype.updateSvgShape = function(node) +{ + if (this.points != null && this.points[0] != null) + { + this.updateSvgPath(node); + } + else if (this.bounds != null) + { + this.updateSvgBounds(node); + } + + this.updateSvgScale(node); +}; + +/** + * Function: getSvgShadowTransform + * + * Returns the current transformation for SVG shadows. + */ +mxShape.prototype.getSvgShadowTransform = function(node, shadow) +{ + var dx = mxConstants.SHADOW_OFFSET_X * this.scale; + var dy = mxConstants.SHADOW_OFFSET_Y * this.scale; + + return 'translate(' + dx + ' ' + dy + ')'; +}; + +/** + * Function: updateSvgTransform + * + * Updates the tranform of the given node. + */ +mxShape.prototype.updateSvgTransform = function(node, shadow) +{ + var st = (shadow) ? this.getSvgShadowTransform() : ''; + + if (this.rotation != null && this.rotation != 0) + { + var cx = this.bounds.x + this.bounds.width / 2; + var cy = this.bounds.y + this.bounds.height / 2; + node.setAttribute('transform', 'rotate(' + this.rotation + ',' + cx + ',' + cy + ') ' + st); + } + else + { + if (shadow) + { + node.setAttribute('transform', st); + } + else + { + node.removeAttribute('transform'); + } + } +}; + +/** + * Function: reconfigure + * + * Reconfigures this shape. This will update the colors etc in + * addition to the bounds or points. + */ +mxShape.prototype.reconfigure = function() +{ + if (this.dialect == mxConstants.DIALECT_SVG) + { + if (this.innerNode != null) + { + this.configureSvgShape(this.innerNode); + } + else + { + this.configureSvgShape(this.node); + } + + if (this.insertGradientNode != null) + { + this.insertGradient(this.insertGradientNode); + this.insertGradientNode = null; + } + } + else if (mxUtils.isVml(this.node)) + { + this.node.style.visibility = 'hidden'; + this.configureVmlShape(this.node); + this.node.style.visibility = 'visible'; + } + else + { + this.node.style.visibility = 'hidden'; + this.configureHtmlShape(this.node); + this.node.style.visibility = 'visible'; + } +}; + +/** + * Function: redraw + * + * Invokes <redrawSvg>, <redrawVml> or <redrawHtml> depending on the + * dialect of the shape. + */ +mxShape.prototype.redraw = function() +{ + this.updateBoundingBox(); + + if (this.dialect == mxConstants.DIALECT_SVG) + { + this.redrawSvg(); + } + else if (mxUtils.isVml(this.node)) + { + this.node.style.visibility = 'hidden'; + this.redrawVml(); + this.node.style.visibility = 'visible'; + } + else + { + this.redrawHtml(); + } +}; + +/** + * Function: updateBoundingBox + * + * Updates the <boundingBox> for this shape using <createBoundingBox> and + * <augmentBoundingBox> and stores the result in <boundingBox>. + */ +mxShape.prototype.updateBoundingBox = function() +{ + if (this.bounds != null) + { + var bbox = this.createBoundingBox(); + this.augmentBoundingBox(bbox); + + var rot = Number(mxUtils.getValue(this.style, mxConstants.STYLE_ROTATION, 0)); + + if (rot != 0) + { + bbox = mxUtils.getBoundingBox(bbox, rot); + } + + bbox.x = Math.floor(bbox.x); + bbox.y = Math.floor(bbox.y); + // TODO: Fix rounding errors + bbox.width = Math.ceil(bbox.width); + bbox.height = Math.ceil(bbox.height); + + this.boundingBox = bbox; + } +}; + +/** + * Function: createBoundingBox + * + * Returns a new rectangle that represents the bounding box of the bare shape + * with no shadows or strokewidths. + */ +mxShape.prototype.createBoundingBox = function() +{ + return this.bounds.clone(); +}; + +/** + * Function: augmentBoundingBox + * + * Augments the bounding box with the strokewidth and shadow offsets. + */ +mxShape.prototype.augmentBoundingBox = function(bbox) +{ + if (this.isShadow) + { + bbox.width += Math.ceil(mxConstants.SHADOW_OFFSET_X * this.scale); + bbox.height += Math.ceil(mxConstants.SHADOW_OFFSET_Y * this.scale); + } + + // Adds strokeWidth + var sw = Math.ceil(this.strokewidth * this.scale); + bbox.grow(Math.ceil(sw / 2)); +}; + +/** + * Function: redrawSvg + * + * Redraws this SVG shape by invoking <updateSvgShape> on this.node, + * this.innerNode and this.shadowNode. + */ +mxShape.prototype.redrawSvg = function() +{ + if (this.innerNode != null) + { + this.updateSvgShape(this.innerNode); + + if (this.shadowNode != null) + { + this.updateSvgShape(this.shadowNode); + } + } + else + { + this.updateSvgShape(this.node); + + // Updates the transform of the shadow + if (this.shadowNode != null) + { + this.shadowNode.setAttribute('transform', this.getSvgShadowTransform()); + } + } + + this.updateSvgGlassPane(); +}; + +/** + * Function: updateVmlGlassPane + * + * Draws the glass overlay if mxConstants.STYLE_GLASS is 1. + */ +mxShape.prototype.updateVmlGlassPane = function() +{ + // Currently only used in mxLabel. Most shapes would have to be changed to use + // a group node in VML which might affect performance for glass-less cells. + if (this.bounds != null && this.node.nodeName == 'group' && this.style != null && + mxUtils.getValue(this.style, mxConstants.STYLE_GLASS, 0) == 1) + { + // Glass overlay + if (this.node.glassOverlay == null) + { + // Creates glass overlay + this.node.glassOverlay = document.createElement('v:shape'); + this.node.glassOverlay.setAttribute('filled', 'true'); + this.node.glassOverlay.setAttribute('fillcolor', 'white'); + this.node.glassOverlay.setAttribute('stroked', 'false'); + + var fillNode = document.createElement('v:fill'); + fillNode.setAttribute('type', 'gradient'); + fillNode.setAttribute('color', 'white'); + fillNode.setAttribute('color2', 'white'); + fillNode.setAttribute('opacity', '90%'); + fillNode.setAttribute('o:opacity2', '15%'); + fillNode.setAttribute('angle', '180'); + + this.node.glassOverlay.appendChild(fillNode); + this.node.appendChild(this.node.glassOverlay); + } + + var size = 0.4; + + // TODO: Mask with rectangle or rounded rectangle of label + var b = this.bounds; + var sw = Math.ceil(this.strokewidth * this.scale / 2 + 1); + var d = 'm ' + (-sw) + ' ' + (-sw) + ' l ' + (-sw) + ' ' + Math.round(b.height * size) + + ' c ' + Math.round(b.width * 0.3) + ' ' + Math.round(b.height * 0.6) + + ' ' + Math.round(b.width * 0.7) + ' ' + Math.round(b.height * 0.6) + + ' ' + Math.round(b.width + sw) + ' ' + Math.round(b.height * size) + + ' l '+Math.round(b.width + sw)+' ' + (-sw) + ' x e'; + this.node.glassOverlay.style.position = 'absolute'; + this.node.glassOverlay.style.width = b.width + 'px'; + this.node.glassOverlay.style.height = b.height + 'px'; + this.node.glassOverlay.setAttribute('coordsize', + Math.round(this.bounds.width) + ',' + + Math.round(this.bounds.height)); + this.node.glassOverlay.setAttribute('path', d); + } + else if (this.node.glassOverlay != null) + { + this.node.glassOverlay.parentNode.removeChild(this.node.glassOverlay); + this.node.glassOverlay = null; + } +}; + +/** + * Function: updateSvgGlassPane + * + * Draws the glass overlay if mxConstants.STYLE_GLASS is 1. + */ +mxShape.prototype.updateSvgGlassPane = function() +{ + if (this.node.nodeName == 'g' && this.style != null && + mxUtils.getValue(this.style, mxConstants.STYLE_GLASS, 0) == 1) + { + // Glass overlay + if (this.node.glassOverlay == null) + { + // Glass overlay gradient + if (this.node.ownerSVGElement.glassGradient == null) + { + // Creates glass overlay gradient + var glassGradient = document.createElementNS(mxConstants.NS_SVG, 'linearGradient'); + glassGradient.setAttribute('x1', '0%'); + glassGradient.setAttribute('y1', '0%'); + glassGradient.setAttribute('x2', '0%'); + glassGradient.setAttribute('y2', '100%'); + + var stop1 = document.createElementNS(mxConstants.NS_SVG, 'stop'); + stop1.setAttribute('offset', '0%'); + stop1.setAttribute('style', 'stop-color:#ffffff;stop-opacity:0.9'); + glassGradient.appendChild(stop1); + + var stop2 = document.createElementNS(mxConstants.NS_SVG, 'stop'); + stop2.setAttribute('offset', '100%'); + stop2.setAttribute('style', 'stop-color:#ffffff;stop-opacity:0.1'); + glassGradient.appendChild(stop2); + + // Finds a unique ID for the gradient + var prefix = 'mx-glass-gradient-'; + var counter = 0; + + while (document.getElementById(prefix+counter) != null) + { + counter++; + } + + glassGradient.setAttribute('id', prefix+counter); + this.node.ownerSVGElement.appendChild(glassGradient); + this.node.ownerSVGElement.glassGradient = glassGradient; + } + + // Creates glass overlay + this.node.glassOverlay = document.createElementNS(mxConstants.NS_SVG, 'path'); + // LATER: Not sure what the behaviour is for mutiple SVG elements in page. + // Probably its possible that this points to an element in another SVG + // node which when removed will result in an undefined background. + var id = this.node.ownerSVGElement.glassGradient.getAttribute('id'); + this.node.glassOverlay.setAttribute('style', 'fill:url(#'+id+');'); + this.node.appendChild(this.node.glassOverlay); + } + + var size = 0.4; + + // TODO: Mask with rectangle or rounded rectangle of label + var b = this.bounds; + var sw = Math.ceil(this.strokewidth * this.scale / 2); + var d = 'm ' + (b.x - sw) + ',' + (b.y - sw) + + ' L ' + (b.x - sw) + ',' + (b.y + b.height * size) + + ' Q '+ (b.x + b.width * 0.5) + ',' + (b.y + b.height * 0.7) + ' '+ + (b.x + b.width + sw) + ',' + (b.y + b.height * size) + + ' L ' + (b.x + b.width + sw) + ',' + (b.y - sw) + ' z'; + this.node.glassOverlay.setAttribute('d', d); + } + else if (this.node.glassOverlay != null) + { + this.node.glassOverlay.parentNode.removeChild(this.node.glassOverlay); + this.node.glassOverlay = null; + } +}; + +/** + * Function: redrawVml + * + * Redraws this VML shape by invoking <updateVmlShape> on this.node. + */ +mxShape.prototype.redrawVml = function() +{ + this.node.style.visibility = 'hidden'; + this.updateVmlShape(this.node); + this.updateVmlGlassPane(); + this.node.style.visibility = 'visible'; +}; + +/** + * Function: redrawHtml + * + * Redraws this HTML shape by invoking <updateHtmlShape> on this.node. + */ +mxShape.prototype.redrawHtml = function() +{ + this.updateHtmlShape(this.node); +}; + +/** + * Function: getRotation + * + * Returns the current rotation including direction. + */ +mxShape.prototype.getRotation = function() +{ + var rot = this.rotation || 0; + + // Default direction is east (ignored if rotation exists) + if (this.direction != null) + { + if (this.direction == 'north') + { + rot += 270; + } + else if (this.direction == 'west') + { + rot += 180; + } + else if (this.direction == 'south') + { + rot += 90; + } + } + + return rot; +}; + +/** + * Function: createPath + * + * Creates an <mxPath> for the specified format and origin. The path object is + * then passed to <redrawPath> and <mxPath.getPath> is returned. + */ +mxShape.prototype.createPath = function(arg) +{ + var x = this.bounds.x; + var y = this.bounds.y; + var w = this.bounds.width; + var h = this.bounds.height; + var dx = 0; + var dy = 0; + + // Inverts bounds for stencils which are rotated 90 or 270 degrees + if (this.direction == 'north' || this.direction == 'south') + { + dx = (w - h) / 2; + dy = (h - w) / 2; + x += dx; + y += dy; + var tmp = w; + w = h; + h = tmp; + } + + var rotation = this.getRotation(); + var path = null; + + if (this.dialect == mxConstants.DIALECT_SVG) + { + path = new mxPath('svg'); + path.setTranslate(x, y); + + // Adds rotation as a separate transform + if (rotation != 0) + { + var cx = this.bounds.getCenterX(); + var cy = this.bounds.getCenterY(); + var transform = 'rotate(' + rotation + ' ' + cx + ' ' + cy + ')'; + + if (this.innerNode != null) + { + this.innerNode.setAttribute('transform', transform); + } + + if (this.foreground != null) + { + this.foreground.setAttribute('transform', transform); + } + + // Shadow needs different transform so that it ends up on the correct side + if (this.shadowNode != null) + { + this.shadowNode.setAttribute('transform', this.getSvgShadowTransform() + ' ' + transform); + } + } + } + else + { + path = new mxPath('vml'); + path.setTranslate(dx, -dx); + path.scale = this.vmlScale; + + if (rotation != 0) + { + this.node.style.rotation = rotation; + } + } + + this.redrawPath(path, x, y, w, h, arg); + + return path.getPath(); +}; + +/** + * Function: redrawPath + * + * Draws the path for this shape. This implementation is empty. See + * <mxActor> and <mxCylinder> for implementations. + */ +mxShape.prototype.redrawPath = function(path, x, y, w, h) +{ + // do nothing +}; diff --git a/src/js/shape/mxStencil.js b/src/js/shape/mxStencil.js new file mode 100644 index 0000000..d0e1a63 --- /dev/null +++ b/src/js/shape/mxStencil.js @@ -0,0 +1,1585 @@ +/** + * $Id: mxStencil.js,v 1.91 2012-07-16 10:22:44 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxStencil + * + * Implements a generic shape which is based on a XML node as a description. + * The node contains a background and a foreground node, which contain the + * definition to render the respective part of the shape. Note that the + * fill, stroke or fillstroke of the background is be the first statement + * of the foreground. This is because the content of the background node + * maybe used to not only render the shape itself, but also its shadow and + * other elements which do not require a fill, stroke or fillstroke. + * + * The shape uses a coordinate system with a width of 100 and a height of + * 100 by default. This can be changed by setting the w and h attribute of + * the shape element. The aspect attribute can be set to "variable" (default) + * or "fixed". If fixed is used, then the aspect which is defined via the w + * and h attribute is kept constant while the shape is scaled. + * + * The possible contents of the background and foreground elements are rect, + * ellipse, roundrect, text, image, include-shape or paths. A path element + * contains move, line, curve, quad, arc and close elements. The rect, ellipse + * and roundrect elements may be thought of as special path elements. All these + * path elements must be followed by either fill, stroke or fillstroke (note + * that text, image and include-shape or not path elements). + * + * The background element can be empty or contain at most one path element. It + * should not contain a text, image or include-shape element. If the background + * element is empty, then no shadow or glass effect will be rendered. If the + * background element is non-empty, then the corresponding fill, stroke or + * fillstroke should be the first element in the subsequent foreground element. + * + * The format of the XML is "a simplified HTML 5 Canvas". Each command changes + * the "current" state, so eg. a linecap, linejoin will be used for all + * subsequent line drawing, unless a save/restore appears, which saves/restores + * a state in a stack. + * + * The connections section contains the fixed connection points for a stencil. + * The perimeter attribute of the constraint element should have a value of 0 + * or 1 (default), where 1 (true) specifies that the given point should be + * projected into the perimeter of the given shape. + * + * The x- and y-coordinates are typically between 0 and 1 and define the + * location of the connection point relative to the width and height of the + * shape. + * + * The dashpattern directive sets the current dashpattern. The format for the + * pattern attribute is a space-separated sequence of numbers, eg. 5 5 5 5, + * that specifies the lengths of alternating dashes and spaces in dashed lines. + * The dashpattern should be used together with the dashed directive to + * enabled/disable the dashpattern. The default dashpattern is 3 3. + * + * The strokewidth attribute defines a fixed strokewidth for the shape. It + * can contain a numeric value or the keyword "inherit", which means that the + * strokeWidth from the cell's style will be used and muliplied with the shape's + * scale. If numeric values are used, those are multiplied with the minimum + * scale used to render the stencil inside the shape's bounds. + * + * Constructor: mxStencilShape + * + * Constructs a new generic shape by setting <desc> to the given XML node and + * invoking <parseDescription> and <parseConstraints>. + * + * Parameters: + * + * desc - XML node that contains the stencil description. + */ +function mxStencil(desc) +{ + this.desc = desc; + this.parseDescription(); + this.parseConstraints(); +}; + +/** + * Variable: desc + * + * Holds the XML node with the stencil description. + */ +mxStencil.prototype.desc = null; + +/** + * Variable: constraints + * + * Holds an array of <mxConnectionConstraints> as defined in the shape. + */ +mxStencil.prototype.constraints = null; + +/** + * Variable: aspect + * + * Holds the aspect of the shape. Default is 'auto'. + */ +mxStencil.prototype.aspect = null; + +/** + * Variable: w0 + * + * Holds the width of the shape. Default is 100. + */ +mxStencil.prototype.w0 = null; + +/** + * Variable: h0 + * + * Holds the height of the shape. Default is 100. + */ +mxStencil.prototype.h0 = null; + +/** + * Variable: bgNodes + * + * Holds the XML node with the stencil description. + */ +mxStencil.prototype.bgNode = null; + +/** + * Variable: fgNodes + * + * Holds the XML node with the stencil description. + */ +mxStencil.prototype.fgNode = null; + +/** + * Variable: strokewidth + * + * Holds the strokewidth direction from the description. + */ +mxStencil.prototype.strokewidth = null; + +/** + * Function: parseDescription + * + * Reads <w0>, <h0>, <aspect>, <bgNodes> and <fgNodes> from <desc>. + */ +mxStencil.prototype.parseDescription = function() +{ + // LATER: Preprocess nodes for faster painting + this.fgNode = this.desc.getElementsByTagName('foreground')[0]; + this.bgNode = this.desc.getElementsByTagName('background')[0]; + this.w0 = Number(this.desc.getAttribute('w') || 100); + this.h0 = Number(this.desc.getAttribute('h') || 100); + + // Possible values for aspect are: variable and fixed where + // variable means fill the available space and fixed means + // use w0 and h0 to compute the aspect. + var aspect = this.desc.getAttribute('aspect'); + this.aspect = (aspect != null) ? aspect : 'variable'; + + // Possible values for strokewidth are all numbers and "inherit" + // where the inherit means take the value from the style (ie. the + // user-defined stroke-width). Note that the strokewidth is scaled + // by the minimum scaling that is used to draw the shape (sx, sy). + var sw = this.desc.getAttribute('strokewidth'); + this.strokewidth = (sw != null) ? sw : '1'; +}; + +/** + * Function: parseConstraints + * + * Reads the constraints from <desc> into <constraints> using + * <parseConstraint>. + */ +mxStencil.prototype.parseConstraints = function() +{ + var conns = this.desc.getElementsByTagName('connections')[0]; + + if (conns != null) + { + var tmp = mxUtils.getChildNodes(conns); + + if (tmp != null && tmp.length > 0) + { + this.constraints = []; + + for (var i = 0; i < tmp.length; i++) + { + this.constraints.push(this.parseConstraint(tmp[i])); + } + } + } +}; + +/** + * Function: parseConstraint + * + * Parses the given XML node and returns its <mxConnectionConstraint>. + */ +mxStencil.prototype.parseConstraint = function(node) +{ + var x = Number(node.getAttribute('x')); + var y = Number(node.getAttribute('y')); + var perimeter = node.getAttribute('perimeter') == '1'; + + return new mxConnectionConstraint(new mxPoint(x, y), perimeter); +}; + +/** + * Function: evaluateAttribute + * + * Gets the attribute for the given name from the given node. If the attribute + * does not exist then the text content of the node is evaluated and if it is + * a function it is invoked with <state> as the only argument and the return + * value is used as the attribute value to be returned. + */ +mxStencil.prototype.evaluateAttribute = function(node, attribute, state) +{ + var result = node.getAttribute(attribute); + + if (result == null) + { + var text = mxUtils.getTextContent(node); + + if (text != null) + { + var funct = mxUtils.eval(text); + + if (typeof(funct) == 'function') + { + result = funct(state); + } + } + } + + return result; +}; + +/** + * Function: renderDom + * + * Updates the SVG or VML shape. + */ +mxStencil.prototype.renderDom = function(shape, bounds, parentNode, state) +{ + var vml = shape.dialect != mxConstants.DIALECT_SVG; + var vmlScale = (document.documentMode == 8) ? 1 : shape.vmlScale; + var rotation = shape.rotation || 0; + var inverse = false; + + // New styles for shape flipping the stencil + var flipH = shape.style[mxConstants.STYLE_STENCIL_FLIPH]; + var flipV = shape.style[mxConstants.STYLE_STENCIL_FLIPV]; + + if (flipH ? !flipV : flipV) + { + rotation *= -1; + } + + // Default direction is east (ignored if rotation exists) + if (shape.direction != null) + { + if (shape.direction == 'north') + { + rotation += 270; + } + else if (shape.direction == 'west') + { + rotation += 180; + } + else if (shape.direction == 'south') + { + rotation += 90; + } + + inverse = (shape.direction == 'north' || shape.direction == 'south'); + } + + if (flipH && flipV) + { + rotation += 180; + flipH = false; + flipV = false; + } + + // SVG transform should be applied on all child shapes + var svgTransform = ''; + + // Implements direction style and vertical/horizontal flip + // via container transformation. + if (vml) + { + if (flipH) + { + parentNode.style.flip = 'x'; + } + else if (flipV) + { + parentNode.style.flip = 'y'; + } + + if (rotation != 0) + { + parentNode.style.rotation = rotation; + } + } + else + { + if (flipH || flipV) + { + var sx = 1; + var sy = 1; + var dx = 0; + var dy = 0; + + if (flipH) + { + sx = -1; + dx = -bounds.width - 2 * bounds.x; + } + + if (flipV) + { + sy = -1; + dy = -bounds.height - 2 * bounds.y; + } + + svgTransform = 'scale(' + sx + ' ' + sy + ') translate(' + dx + ' ' + dy + ')'; + } + + // Adds rotation as a separate transform + if (rotation != 0) + { + var cx = bounds.getCenterX(); + var cy = bounds.getCenterY(); + svgTransform += ' rotate(' + rotation + ' ' + cx + ' ' + cy + ')'; + } + } + + var background = (state == null); + + if (this.bgNode != null || this.fgNode != null) + { + var x0 = (vml && state == null) ? 0 : bounds.x; + var y0 = (vml && state == null) ? 0 : bounds.y; + var sx = bounds.width / this.w0; + var sy = bounds.height / this.h0; + + // Stores current location inside path + this.lastMoveX = 0; + this.lastMoveY = 0; + + if (inverse) + { + sy = bounds.width / this.h0; + sx = bounds.height / this.w0; + + var delta = (bounds.width - bounds.height) / 2; + + x0 += delta; + y0 -= delta; + } + + if (this.aspect == 'fixed') + { + sy = Math.min(sx, sy); + sx = sy; + + // Centers the shape inside the available space + if (inverse) + { + x0 += (bounds.height - this.w0 * sx) / 2; + y0 += (bounds.width - this.h0 * sy) / 2; + } + else + { + x0 += (bounds.width - this.w0 * sx) / 2; + y0 += (bounds.height - this.h0 * sy) / 2; + } + } + + // Workaround to improve VML rendering precision. + if (vml) + { + sx *= vmlScale; + sy *= vmlScale; + x0 *= vmlScale; + y0 *= vmlScale; + } + + var minScale = Math.min(sx, sy); + + // Stack of states for save/restore ops + var stack = []; + + var currentState = (state != null) ? state : + { + fillColorAssigned: false, + fill: shape.fill, + stroke: shape.stroke, + strokeWidth: (this.strokewidth == 'inherit') ? + Number(shape.strokewidth) * shape.scale : + Number(this.strokewidth) * minScale / ((vml) ? vmlScale : 1), + dashed: shape.isDashed, + dashpattern: [3, 3], + alpha: shape.opacity, + linejoin: 'miter', + fontColor: '#000000', + fontSize: mxConstants.DEFAULT_FONTSIZE, + fontFamily: mxConstants.DEFAULT_FONTFAMILY, + fontStyle: 0 + }; + + var currentPath = null; + var currentPoints = null; + + var configurePath = function(path, state) + { + var sw = Math.max(1, state.strokeWidth); + + if (vml) + { + path.strokeweight = Math.round(sw) + 'px'; + + if (state.fill != null) + { + // Gradient in foregrounds not supported because special gradients + // with bounds must be created for each element in graphics-canvases + var gradient = (!state.fillColorAssigned) ? shape.gradient : null; + var fill = document.createElement('v:fill'); + shape.updateVmlFill(fill, state.fill, gradient, shape.gradientDirection, state.alpha); + path.appendChild(fill); + } + else + { + path.filled = 'false'; + } + + if (state.stroke != null) + { + path.stroked = 'true'; + path.strokecolor = state.stroke; + } + else + { + path.stroked = 'false'; + } + + path.style.position = 'absolute'; + } + else + { + path.setAttribute('stroke-width', sw); + + if (state.fill != null && state.fillColorAssigned) + { + path.setAttribute('fill', state.fill); + } + + if (state.stroke != null) + { + path.setAttribute('stroke', state.stroke); + } + } + }; + + var addToPath = function(s) + { + if (currentPath != null && currentPoints != null) + { + currentPoints.push(s); + } + }; + + var round = function(value) + { + return (vml) ? Math.round(value) : value; + }; + + // Will be moved to a hook later for example to set text values + var renderNode = function(node) + { + var name = node.nodeName; + + var fillOp = name == 'fill'; + var strokeOp = name == 'stroke'; + var fillStrokeOp = name == 'fillstroke'; + + if (name == 'save') + { + stack.push(currentState); + currentState = mxUtils.clone(currentState); + } + else if (name == 'restore') + { + currentState = stack.pop(); + } + else if (name == 'path') + { + currentPoints = []; + + if (vml) + { + currentPath = document.createElement('v:shape'); + configurePath.call(this, currentPath, currentState); + var w = Math.round(bounds.width) * vmlScale; + var h = Math.round(bounds.height) * vmlScale; + currentPath.style.width = w + 'px'; + currentPath.style.height = h + 'px'; + currentPath.coordsize = w + ',' + h; + } + else + { + currentPath = document.createElementNS(mxConstants.NS_SVG, 'path'); + configurePath.call(this, currentPath, currentState); + + if (svgTransform.length > 0) + { + currentPath.setAttribute('transform', svgTransform); + } + + if (node.getAttribute('crisp') == '1') + { + currentPath.setAttribute('shape-rendering', 'crispEdges'); + } + } + + // Renders the elements inside the given path + var childNode = node.firstChild; + + while (childNode != null) + { + if (childNode.nodeType == mxConstants.NODETYPE_ELEMENT) + { + renderNode.call(this, childNode); + } + + childNode = childNode.nextSibling; + } + + // Ends the current path + if (vml) + { + addToPath('e'); + currentPath.path = currentPoints.join(''); + } + else + { + currentPath.setAttribute('d', currentPoints.join('')); + } + } + else if (name == 'move') + { + var op = (vml) ? 'm' : 'M'; + this.lastMoveX = round(x0 + Number(node.getAttribute('x')) * sx); + this.lastMoveY = round(y0 + Number(node.getAttribute('y')) * sy); + addToPath(op + ' ' + this.lastMoveX + ' ' + this.lastMoveY); + } + else if (name == 'line') + { + var op = (vml) ? 'l' : 'L'; + this.lastMoveX = round(x0 + Number(node.getAttribute('x')) * sx); + this.lastMoveY = round(y0 + Number(node.getAttribute('y')) * sy); + addToPath(op + ' ' + this.lastMoveX + ' ' + this.lastMoveY); + } + else if (name == 'quad') + { + if (vml) + { + var cpx0 = this.lastMoveX; + var cpy0 = this.lastMoveY; + var qpx1 = x0 + Number(node.getAttribute('x1')) * sx; + var qpy1 = y0 + Number(node.getAttribute('y1')) * sy; + var cpx3 = x0 + Number(node.getAttribute('x2')) * sx; + var cpy3 = y0 + Number(node.getAttribute('y2')) * sy; + + var cpx1 = cpx0 + 2/3 * (qpx1 - cpx0); + var cpy1 = cpy0 + 2/3 * (qpy1 - cpy0); + + var cpx2 = cpx3 + 2/3 * (qpx1 - cpx3); + var cpy2 = cpy3 + 2/3 * (qpy1 - cpy3); + + addToPath('c ' + Math.round(cpx1) + ' ' + Math.round(cpy1) + ' ' + + Math.round(cpx2) + ' ' + Math.round(cpy2) + ' ' + + Math.round(cpx3) + ' ' + Math.round(cpy3)); + + this.lastMoveX = cpx3; + this.lastMoveY = cpy3; + } + else + { + this.lastMoveX = x0 + Number(node.getAttribute('x2')) * sx; + this.lastMoveY = y0 + Number(node.getAttribute('y2')) * sy; + + addToPath('Q ' + (x0 + Number(node.getAttribute('x1')) * sx) + ' ' + + (y0 + Number(node.getAttribute('y1')) * sy) + ' ' + + this.lastMoveX + ' ' + this.lastMoveY); + } + } + else if (name == 'curve') + { + var op = (vml) ? 'c' : 'C'; + this.lastMoveX = round(x0 + Number(node.getAttribute('x3')) * sx); + this.lastMoveY = round(y0 + Number(node.getAttribute('y3')) * sy); + + addToPath(op + ' ' + round(x0 + Number(node.getAttribute('x1')) * sx) + ' ' + + round(y0 + Number(node.getAttribute('y1')) * sy) + ' ' + + round(x0 + Number(node.getAttribute('x2')) * sx) + ' ' + + round(y0 + Number(node.getAttribute('y2')) * sy) + ' ' + + this.lastMoveX + ' ' + this.lastMoveY); + } + else if (name == 'close') + { + addToPath((vml) ? 'x' : 'Z'); + } + else if (name == 'rect' || name == 'roundrect') + { + var rounded = name == 'roundrect'; + var x = round(x0 + Number(node.getAttribute('x')) * sx); + var y = round(y0 + Number(node.getAttribute('y')) * sy); + var w = round(Number(node.getAttribute('w')) * sx); + var h = round(Number(node.getAttribute('h')) * sy); + + var arcsize = node.getAttribute('arcsize'); + + if (arcsize == 0) + { + arcsize = mxConstants.RECTANGLE_ROUNDING_FACTOR * 100; + } + + if (vml) + { + // LATER: Use HTML for non-rounded, gradientless rectangles + currentPath = document.createElement((rounded) ? 'v:roundrect' : 'v:rect'); + currentPath.style.left = x + 'px'; + currentPath.style.top = y + 'px'; + currentPath.style.width = w + 'px'; + currentPath.style.height = h + 'px'; + + if (rounded) + { + currentPath.setAttribute('arcsize', String(arcsize) + '%'); + } + } + else + { + currentPath = document.createElementNS(mxConstants.NS_SVG, 'rect'); + currentPath.setAttribute('x', x); + currentPath.setAttribute('y', y); + currentPath.setAttribute('width', w); + currentPath.setAttribute('height', h); + + if (rounded) + { + var factor = Number(arcsize) / 100; + var r = Math.min(w * factor, h * factor); + currentPath.setAttribute('rx', r); + currentPath.setAttribute('ry', r); + } + + if (svgTransform.length > 0) + { + currentPath.setAttribute('transform', svgTransform); + } + + if (node.getAttribute('crisp') == '1') + { + currentPath.setAttribute('shape-rendering', 'crispEdges'); + } + } + + configurePath.call(this, currentPath, currentState); + } + else if (name == 'ellipse') + { + var x = round(x0 + Number(node.getAttribute('x')) * sx); + var y = round(y0 + Number(node.getAttribute('y')) * sy); + var w = round(Number(node.getAttribute('w')) * sx); + var h = round(Number(node.getAttribute('h')) * sy); + + if (vml) + { + currentPath = document.createElement('v:arc'); + currentPath.startangle = '0'; + currentPath.endangle = '360'; + currentPath.style.left = x + 'px'; + currentPath.style.top = y + 'px'; + currentPath.style.width = w + 'px'; + currentPath.style.height = h + 'px'; + } + else + { + currentPath = document.createElementNS(mxConstants.NS_SVG, 'ellipse'); + currentPath.setAttribute('cx', x + w / 2); + currentPath.setAttribute('cy', y + h / 2); + currentPath.setAttribute('rx', w / 2); + currentPath.setAttribute('ry', h / 2); + + if (svgTransform.length > 0) + { + currentPath.setAttribute('transform', svgTransform); + } + } + + configurePath.call(this, currentPath, currentState); + } + else if (name == 'arc') + { + var r1 = Number(node.getAttribute('rx')) * sx; + var r2 = Number(node.getAttribute('ry')) * sy; + var angle = Number(node.getAttribute('x-axis-rotation')); + var largeArcFlag = Number(node.getAttribute('large-arc-flag')); + var sweepFlag = Number(node.getAttribute('sweep-flag')); + var x = x0 + Number(node.getAttribute('x')) * sx; + var y = y0 + Number(node.getAttribute('y')) * sy; + + if (vml) + { + var curves = mxUtils.arcToCurves(this.lastMoveX, this.lastMoveY, r1, r2, angle, largeArcFlag, sweepFlag, x, y); + + for (var i = 0; i < curves.length; i += 6) + { + addToPath('c' + ' ' + Math.round(curves[i]) + ' ' + Math.round(curves[i + 1]) + ' ' + + Math.round(curves[i + 2]) + ' ' + Math.round(curves[i + 3]) + ' ' + + Math.round(curves[i + 4]) + ' ' + Math.round(curves[i + 5])); + + this.lastMoveX = curves[i + 4]; + this.lastMoveY = curves[i + 5]; + } + } + else + { + addToPath('A ' + r1 + ',' + r2 + ' ' + angle + ' ' + largeArcFlag + ',' + sweepFlag + ' ' + x + ',' + y); + this.lastMoveX = x0 + x; + this.lastMoveY = y0 + y; + } + } + else if (name == 'image') + { + var src = this.evaluateAttribute(node, 'src', shape.state); + + if (src != null) + { + var x = round(x0 + Number(node.getAttribute('x')) * sx); + var y = round(y0 + Number(node.getAttribute('y')) * sy); + var w = round(Number(node.getAttribute('w')) * sx); + var h = round(Number(node.getAttribute('h')) * sy); + + // TODO: _Not_ providing an aspect in the shapes format has the advantage + // of not needing a callback to adjust the image in VML. Since the shape + // developer can specify the aspect via width and height this should OK. + //var aspect = node.getAttribute('aspect') != '0'; + var aspect = false; + var flipH = node.getAttribute('flipH') == '1'; + var flipV = node.getAttribute('flipV') == '1'; + + if (vml) + { + currentPath = document.createElement('v:image'); + currentPath.style.filter = 'alpha(opacity=' + currentState.alpha + ')'; + currentPath.style.left = x + 'px'; + currentPath.style.top = y + 'px'; + currentPath.style.width = w + 'px'; + currentPath.style.height = h + 'px'; + currentPath.src = src; + + if (flipH && flipV) + { + currentPath.style.rotation = '180'; + } + else if (flipH) + { + currentPath.style.flip = 'x'; + } + else if (flipV) + { + currentPath.style.flip = 'y'; + } + } + else + { + currentPath = document.createElementNS(mxConstants.NS_SVG, 'image'); + currentPath.setAttributeNS(mxConstants.NS_XLINK, 'xlink:href', src); + currentPath.setAttribute('opacity', currentState.alpha / 100); + currentPath.setAttribute('x', x); + currentPath.setAttribute('y', y); + currentPath.setAttribute('width', w); + currentPath.setAttribute('height', h); + + if (!aspect) + { + currentPath.setAttribute('preserveAspectRatio', 'none'); + } + + if (flipH || flipV) + { + var scx = 1; + var scy = 1; + var dx = 0; + var dy = 0; + + if (flipH) + { + scx = -1; + dx = -w - 2 * x; + } + + if (flipV) + { + scy = -1; + dy = -h - 2 * y; + } + + currentPath.setAttribute('transform', svgTransform + 'scale(' + scx + ' ' + scy + ')' + + ' translate('+dx+' '+dy+') '); + } + else + { + currentPath.setAttribute('transform', svgTransform); + } + } + + parentNode.appendChild(currentPath); + } + } + else if (name == 'include-shape') + { + var stencil = mxStencilRegistry.getStencil(node.getAttribute('name')); + + if (stencil != null) + { + var x = x0 + Number(node.getAttribute('x')) * sx; + var y = y0 + Number(node.getAttribute('y')) * sy; + var w = Number(node.getAttribute('w')) * sx; + var h = Number(node.getAttribute('h')) * sy; + + stencil.renderDom(shape, new mxRectangle(x, y, w, h), parentNode, currentState); + } + } + // Additional labels are currently disabled. Needs fixing of VML + // text positon, SVG text rotation and ignored baseline in FF + else if (name == 'text') + { + var str = this.evaluateAttribute(node, 'str', shape.state); + + if (str != null) + { + var x = round(x0 + Number(node.getAttribute('x')) * sx); + var y = round(y0 + Number(node.getAttribute('y')) * sy); + var align = node.getAttribute('align') || 'left'; + var valign = node.getAttribute('valign') || 'top'; + + if (vml) + { + // Renders a single line of text with full rotation support + currentPath = document.createElement('v:shape'); + currentPath.style.position = 'absolute'; + currentPath.style.width = '1px'; + currentPath.style.height = '1px'; + currentPath.style.left = x + 'px'; + currentPath.style.top = y + 'px'; + + var fill = document.createElement('v:fill'); + fill.color = currentState.fontColor; + fill.on = 'true'; + currentPath.appendChild(fill); + + var stroke = document.createElement('v:stroke'); + stroke.on = 'false'; + currentPath.appendChild(stroke); + + var path = document.createElement('v:path'); + path.textpathok = 'true'; + path.v = 'm ' + x + ' ' + y + ' l ' + (x + 1) + ' ' + y; + + currentPath.appendChild(path); + + var tp = document.createElement('v:textpath'); + tp.style.cssText = 'v-text-align:' + align; + tp.style.fontSize = Math.round(currentState.fontSize / vmlScale) + 'px'; + + // FIXME: Font-family seems to be ignored for textpath + tp.style.fontFamily = currentState.fontFamily; + tp.string = str; + tp.on = 'true'; + + // Bold + if ((currentState.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD) + { + tp.style.fontWeight = 'bold'; + } + + // Italic + if ((currentState.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC) + { + tp.style.fontStyle = 'italic'; + } + + // FIXME: Text decoration not supported in textpath + if ((currentState.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE) + { + tp.style.textDecoration = 'underline'; + } + + // LATER: Find vertical center for div via CSS if possible + if (valign == 'top') + { + currentPath.style.top = (y + currentState.fontSize / 2) + 'px'; + } + else if (valign == 'bottom') + { + currentPath.style.top = (y - currentState.fontSize / 3) + 'px'; + } + + currentPath.appendChild(tp); + } + else + { + currentPath = document.createElementNS(mxConstants.NS_SVG, 'text'); + currentPath.setAttribute('fill', currentState.fontColor); + currentPath.setAttribute('font-family', currentState.fontFamily); + currentPath.setAttribute('font-size', currentState.fontSize); + currentPath.setAttribute('stroke', 'none'); + currentPath.setAttribute('x', x); + currentPath.appendChild(document.createTextNode(str)); + + // Bold + if ((currentState.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD) + { + currentPath.setAttribute('font-weight', 'bold'); + } + + // Italic + if ((currentState.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC) + { + currentPath.setAttribute('font-style', 'italic'); + } + + // Underline + if ((currentState.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE) + { + currentPath.setAttribute('text-decoration', uline); + } + + // Horizontal alignment + if (align == 'left') + { + align = 'start'; + } + else if (align == 'center') + { + align = 'middle'; + } + else if (align == 'right') + { + align = 'end'; + } + + currentPath.setAttribute('text-anchor', align); + + // Vertical alignment + // Uses dy because FF ignores alignment-baseline + if (valign == 'top') + { + currentPath.setAttribute('y', y + currentState.fontSize / 5); + currentPath.setAttribute('dy', '1ex'); + } + else if (valign == 'middle') + { + currentPath.setAttribute('y', y + currentState.fontSize / 8); + currentPath.setAttribute('dy', '0.5ex'); + } + else + { + currentPath.setAttribute('y', y); + } + + if (svgTransform.length > 0) + { + currentPath.setAttribute('transform', svgTransform); + } + } + + parentNode.appendChild(currentPath); + } + } + else if (fillOp || strokeOp || fillStrokeOp) + { + if (currentPath != null) + { + var pattern = null; + + if (currentState.dashed) + { + var f = (vml) ? minScale : Number(currentPath.getAttribute('stroke-width')); + var pat = []; + + for (var i = 0; i < currentState.dashpattern.length; i++) + { + pat.push(Math.max(1, Math.round(Number(currentState.dashpattern[i]) * f))); + } + + pattern = pat.join(' '); + } + + if (strokeOp || fillStrokeOp) + { + if (vml) + { + var stroke = document.createElement('v:stroke'); + stroke.endcap = currentState.linecap || 'flat'; + stroke.joinstyle = currentState.linejoin || 'miter'; + stroke.miterlimit = currentState.miterlimit || '10'; + currentPath.appendChild(stroke); + + // TODO: Dashpattern support in VML is limited, we should + // map this to VML or allow for a separate VML dashstyle. + if (pattern != null) + { + stroke.dashstyle = pattern; + } + } + else + { + if (currentState.linejoin != null) + { + currentPath.setAttribute('stroke-linejoin', currentState.linejoin); + } + + if (currentState.linecap != null) + { + // flat is called butt in SVG + var value = currentState.linecap; + + if (value == 'flat') + { + value = 'butt'; + } + + currentPath.setAttribute('stroke-linecap', value); + } + + if (currentState.miterlimit != null) + { + currentPath.setAttribute('stroke-miterlimit', currentState.miterlimit); + } + + // Handles dash pattern + if (pattern != null) + { + currentPath.setAttribute('stroke-dasharray', pattern); + } + } + } + + // Adds the shadow + if (background && shape.isShadow) + { + var dx = mxConstants.SHADOW_OFFSET_X * shape.scale; + var dy = mxConstants.SHADOW_OFFSET_Y * shape.scale; + + // Adds the shadow + if (vml) + { + var shadow = document.createElement('v:shadow'); + shadow.setAttribute('on', 'true'); + shadow.setAttribute('color', mxConstants.SHADOWCOLOR); + shadow.setAttribute('offset', Math.round(dx) + 'px,' + Math.round(dy) + 'px'); + shadow.setAttribute('opacity', (mxConstants.SHADOW_OPACITY * 100) + '%'); + + var stroke = document.createElement('v:stroke'); + stroke.endcap = currentState.linecap || 'flat'; + stroke.joinstyle = currentState.linejoin || 'miter'; + stroke.miterlimit = currentState.miterlimit || '10'; + + if (pattern != null) + { + stroke.dashstyle = pattern; + } + + shadow.appendChild(stroke); + currentPath.appendChild(shadow); + } + else + { + var shadow = currentPath.cloneNode(true); + shadow.setAttribute('stroke', mxConstants.SHADOWCOLOR); + + if (currentState.fill != null && (fillOp || fillStrokeOp)) + { + shadow.setAttribute('fill', mxConstants.SHADOWCOLOR); + } + else + { + shadow.setAttribute('fill', 'none'); + } + + shadow.setAttribute('transform', 'translate(' + dx + ' ' + dy + ') ' + + (shadow.getAttribute('transform') || '')); + shadow.setAttribute('opacity', mxConstants.SHADOW_OPACITY); + parentNode.appendChild(shadow); + } + } + + if (fillOp) + { + if (vml) + { + currentPath.stroked = 'false'; + } + else + { + currentPath.setAttribute('stroke', 'none'); + } + } + else if (strokeOp) + { + if (vml) + { + currentPath.filled = 'false'; + } + else + { + currentPath.setAttribute('fill', 'none'); + } + } + + parentNode.appendChild(currentPath); + } + + // Background was painted + if (background) + { + background = false; + } + } + else if (name == 'linecap') + { + currentState.linecap = node.getAttribute('cap'); + } + else if (name == 'linejoin') + { + currentState.linejoin = node.getAttribute('join'); + } + else if (name == 'miterlimit') + { + currentState.miterlimit = node.getAttribute('limit'); + } + else if (name == 'dashed') + { + currentState.dashed = node.getAttribute('dashed') == '1'; + } + else if (name == 'dashpattern') + { + var value = node.getAttribute('pattern'); + + if (value != null && value.length > 0) + { + currentState.dashpattern = value.split(' '); + } + } + else if (name == 'strokewidth') + { + currentState.strokeWidth = node.getAttribute('width') * minScale; + + if (vml) + { + currentState.strokeWidth /= vmlScale; + } + } + else if (name == 'strokecolor') + { + currentState.stroke = node.getAttribute('color'); + } + else if (name == 'fillcolor') + { + currentState.fill = node.getAttribute('color'); + currentState.fillColorAssigned = true; + } + else if (name == 'alpha') + { + currentState.alpha = Number(node.getAttribute('alpha')); + } + else if (name == 'fontcolor') + { + currentState.fontColor = node.getAttribute('color'); + } + else if (name == 'fontsize') + { + currentState.fontSize = Number(node.getAttribute('size')) * minScale; + } + else if (name == 'fontfamily') + { + currentState.fontFamily = node.getAttribute('family'); + } + else if (name == 'fontstyle') + { + currentState.fontStyle = Number(node.getAttribute('style')); + } + }; + + // Adds a transparent rectangle in the background for hit-detection in SVG + if (!vml) + { + var rect = document.createElementNS(mxConstants.NS_SVG, 'rect'); + rect.setAttribute('x', bounds.x); + rect.setAttribute('y', bounds.y); + rect.setAttribute('width', bounds.width); + rect.setAttribute('height', bounds.height); + rect.setAttribute('fill', 'none'); + rect.setAttribute('stroke', 'none'); + parentNode.appendChild(rect); + } + + // Background switches to false after fill/stroke of the background + if (this.bgNode != null) + { + var tmp = this.bgNode.firstChild; + + while (tmp != null) + { + if (tmp.nodeType == mxConstants.NODETYPE_ELEMENT) + { + renderNode.call(this, tmp); + } + + tmp = tmp.nextSibling; + } + } + else + { + background = false; + } + + if (this.fgNode != null) + { + var tmp = this.fgNode.firstChild; + + while (tmp != null) + { + if (tmp.nodeType == mxConstants.NODETYPE_ELEMENT) + { + renderNode.call(this, tmp); + } + + tmp = tmp.nextSibling; + } + } + } +}; + +/** + * Function: drawShape + * + * Draws this stencil inside the given bounds. + */ +mxStencil.prototype.drawShape = function(canvas, state, bounds, background) +{ + // TODO: Unify with renderDom, check performance of pluggable shape, + // internal structure (array of special structs?), relative and absolute + // coordinates (eg. note shape, process vs star, actor etc.), text rendering + // and non-proportional scaling, how to implement pluggable edge shapes + // (start, segment, end blocks), pluggable markers, how to implement + // swimlanes (title area) with this API, add icon, horizontal/vertical + // label, indicator for all shapes, rotation + var node = (background) ? this.bgNode : this.fgNode; + + if (node != null) + { + var direction = mxUtils.getValue(state.style, mxConstants.STYLE_DIRECTION, null); + var aspect = this.computeAspect(state, bounds, direction); + var minScale = Math.min(aspect.width, aspect.height); + var sw = (this.strokewidth == 'inherit') ? + Number(mxUtils.getNumber(state.style, mxConstants.STYLE_STROKEWIDTH, 1)) * state.view.scale : + Number(this.strokewidth) * minScale; + this.lastMoveX = 0; + this.lastMoveY = 0; + canvas.setStrokeWidth(sw); + + var tmp = node.firstChild; + + while (tmp != null) + { + if (tmp.nodeType == mxConstants.NODETYPE_ELEMENT) + { + this.drawNode(canvas, state, tmp, aspect); + } + + tmp = tmp.nextSibling; + } + + return true; + } + + return false; +}; + +/** + * Function: computeAspect + * + * Returns a rectangle that contains the offset in x and y and the horizontal + * and vertical scale in width and height used to draw this shape inside the + * given <mxRectangle>. + * + * Parameters: + * + * state - <mxCellState> for which the shape should be drawn. + * bounds - <mxRectangle> that should contain the stencil. + * direction - Optional direction of the shape to be darwn. + */ +mxStencil.prototype.computeAspect = function(state, bounds, direction) +{ + var x0 = bounds.x; + var y0 = bounds.y; + var sx = bounds.width / this.w0; + var sy = bounds.height / this.h0; + + var inverse = (direction == 'north' || direction == 'south'); + + if (inverse) + { + sy = bounds.width / this.h0; + sx = bounds.height / this.w0; + + var delta = (bounds.width - bounds.height) / 2; + + x0 += delta; + y0 -= delta; + } + + if (this.aspect == 'fixed') + { + sy = Math.min(sx, sy); + sx = sy; + + // Centers the shape inside the available space + if (inverse) + { + x0 += (bounds.height - this.w0 * sx) / 2; + y0 += (bounds.width - this.h0 * sy) / 2; + } + else + { + x0 += (bounds.width - this.w0 * sx) / 2; + y0 += (bounds.height - this.h0 * sy) / 2; + } + } + + return new mxRectangle(x0, y0, sx, sy); +}; + +/** + * Function: drawNode + * + * Draws this stencil inside the given bounds. + */ +mxStencil.prototype.drawNode = function(canvas, state, node, aspect) +{ + var name = node.nodeName; + var x0 = aspect.x; + var y0 = aspect.y; + var sx = aspect.width; + var sy = aspect.height; + var minScale = Math.min(sx, sy); + + // LATER: Move to lookup table + if (name == 'save') + { + canvas.save(); + } + else if (name == 'restore') + { + canvas.restore(); + } + else if (name == 'path') + { + canvas.begin(); + + // Renders the elements inside the given path + var childNode = node.firstChild; + + while (childNode != null) + { + if (childNode.nodeType == mxConstants.NODETYPE_ELEMENT) + { + this.drawNode(canvas, state, childNode, aspect); + } + + childNode = childNode.nextSibling; + } + } + else if (name == 'close') + { + canvas.close(); + } + else if (name == 'move') + { + this.lastMoveX = x0 + Number(node.getAttribute('x')) * sx; + this.lastMoveY = y0 + Number(node.getAttribute('y')) * sy; + canvas.moveTo(this.lastMoveX, this.lastMoveY); + } + else if (name == 'line') + { + this.lastMoveX = x0 + Number(node.getAttribute('x')) * sx; + this.lastMoveY = y0 + Number(node.getAttribute('y')) * sy; + canvas.lineTo(this.lastMoveX, this.lastMoveY); + } + else if (name == 'quad') + { + this.lastMoveX = x0 + Number(node.getAttribute('x2')) * sx; + this.lastMoveY = y0 + Number(node.getAttribute('y2')) * sy; + canvas.quadTo(x0 + Number(node.getAttribute('x1')) * sx, + y0 + Number(node.getAttribute('y1')) * sy, + this.lastMoveX, this.lastMoveY); + } + else if (name == 'curve') + { + this.lastMoveX = x0 + Number(node.getAttribute('x3')) * sx; + this.lastMoveY = y0 + Number(node.getAttribute('y3')) * sy; + canvas.curveTo(x0 + Number(node.getAttribute('x1')) * sx, + y0 + Number(node.getAttribute('y1')) * sy, + x0 + Number(node.getAttribute('x2')) * sx, + y0 + Number(node.getAttribute('y2')) * sy, + this.lastMoveX, this.lastMoveY); + } + else if (name == 'arc') + { + // Arc from stencil is turned into curves in image output + var r1 = Number(node.getAttribute('rx')) * sx; + var r2 = Number(node.getAttribute('ry')) * sy; + var angle = Number(node.getAttribute('x-axis-rotation')); + var largeArcFlag = Number(node.getAttribute('large-arc-flag')); + var sweepFlag = Number(node.getAttribute('sweep-flag')); + var x = x0 + Number(node.getAttribute('x')) * sx; + var y = y0 + Number(node.getAttribute('y')) * sy; + + var curves = mxUtils.arcToCurves(this.lastMoveX, this.lastMoveY, r1, r2, angle, largeArcFlag, sweepFlag, x, y); + + for (var i = 0; i < curves.length; i += 6) + { + canvas.curveTo(curves[i], curves[i + 1], curves[i + 2], + curves[i + 3], curves[i + 4], curves[i + 5]); + + this.lastMoveX = curves[i + 4]; + this.lastMoveY = curves[i + 5]; + } + } + else if (name == 'rect') + { + canvas.rect(x0 + Number(node.getAttribute('x')) * sx, + y0 + Number(node.getAttribute('y')) * sy, + Number(node.getAttribute('w')) * sx, + Number(node.getAttribute('h')) * sy); + } + else if (name == 'roundrect') + { + var arcsize = node.getAttribute('arcsize'); + + if (arcsize == 0) + { + arcsize = mxConstants.RECTANGLE_ROUNDING_FACTOR * 100; + } + + var w = Number(node.getAttribute('w')) * sx; + var h = Number(node.getAttribute('h')) * sy; + var factor = Number(arcsize) / 100; + var r = Math.min(w * factor, h * factor); + + canvas.roundrect(x0 + Number(node.getAttribute('x')) * sx, + y0 + Number(node.getAttribute('y')) * sy, + w, h, r, r); + } + else if (name == 'ellipse') + { + canvas.ellipse(x0 + Number(node.getAttribute('x')) * sx, + y0 + Number(node.getAttribute('y')) * sy, + Number(node.getAttribute('w')) * sx, + Number(node.getAttribute('h')) * sy); + } + else if (name == 'image') + { + var src = this.evaluateAttribute(node, 'src', state); + + canvas.image(x0 + Number(node.getAttribute('x')) * sx, + y0 + Number(node.getAttribute('y')) * sy, + Number(node.getAttribute('w')) * sx, + Number(node.getAttribute('h')) * sy, + src, false, node.getAttribute('flipH') == '1', + node.getAttribute('flipV') == '1'); + } + else if (name == 'text') + { + var str = this.evaluateAttribute(node, 'str', state); + + canvas.text(x0 + Number(node.getAttribute('x')) * sx, + y0 + Number(node.getAttribute('y')) * sy, + 0, 0, str, node.getAttribute('align'), + node.getAttribute('valign'), + node.getAttribute('vertical')); + } + else if (name == 'include-shape') + { + var stencil = mxStencilRegistry.getStencil(node.getAttribute('name')); + + if (stencil != null) + { + var x = x0 + Number(node.getAttribute('x')) * sx; + var y = y0 + Number(node.getAttribute('y')) * sy; + var w = Number(node.getAttribute('w')) * sx; + var h = Number(node.getAttribute('h')) * sy; + + var tmp = new mxRectangle(x, y, w, h); + stencil.drawShape(canvas, state, tmp, true); + stencil.drawShape(canvas, state, tmp, false); + } + } + else if (name == 'fillstroke') + { + canvas.fillAndStroke(); + } + else if (name == 'fill') + { + canvas.fill(); + } + else if (name == 'stroke') + { + canvas.stroke(); + } + else if (name == 'strokewidth') + { + canvas.setStrokeWidth(Number(node.getAttribute('width')) * minScale); + } + else if (name == 'dashed') + { + canvas.setDashed(node.getAttribute('dashed') == '1'); + } + else if (name == 'dashpattern') + { + var value = node.getAttribute('pattern'); + + if (value != null) + { + var tmp = value.split(' '); + var pat = []; + + for (var i = 0; i < tmp.length; i++) + { + if (tmp[i].length > 0) + { + pat.push(Number(tmp[i]) * minScale); + } + } + + value = pat.join(' '); + canvas.setDashPattern(value); + } + } + else if (name == 'strokecolor') + { + canvas.setStrokeColor(node.getAttribute('color')); + } + else if (name == 'linecap') + { + canvas.setLineCap(node.getAttribute('cap')); + } + else if (name == 'linejoin') + { + canvas.setLineJoin(node.getAttribute('join')); + } + else if (name == 'miterlimit') + { + canvas.setMiterLimit(Number(node.getAttribute('limit'))); + } + else if (name == 'fillcolor') + { + canvas.setFillColor(node.getAttribute('color')); + } + else if (name == 'fontcolor') + { + canvas.setFontColor(node.getAttribute('color')); + } + else if (name == 'fontstyle') + { + canvas.setFontStyle(node.getAttribute('style')); + } + else if (name == 'fontfamily') + { + canvas.setFontFamily(node.getAttribute('family')); + } + else if (name == 'fontsize') + { + canvas.setFontSize(Number(node.getAttribute('size')) * minScale); + } +}; diff --git a/src/js/shape/mxStencilRegistry.js b/src/js/shape/mxStencilRegistry.js new file mode 100644 index 0000000..7621573 --- /dev/null +++ b/src/js/shape/mxStencilRegistry.js @@ -0,0 +1,53 @@ +/** + * $Id: mxStencilRegistry.js,v 1.2 2011-07-15 12:57:50 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + * + * Code to add stencils. + * + * (code) + * var req = mxUtils.load('test/stencils.xml'); + * var root = req.getDocumentElement(); + * var shape = root.firstChild; + * + * while (shape != null) + * { + * if (shape.nodeType == mxConstants.NODETYPE_ELEMENT) + * { + * mxStencilRegistry.addStencil(shape.getAttribute('name'), new mxStencil(shape)); + * } + * + * shape = shape.nextSibling; + * } + * (end) + */ +var mxStencilRegistry = +{ + /** + * Class: mxStencilRegistry + * + * A singleton class that provides a registry for stencils and the methods + * for painting those stencils onto a canvas or into a DOM. + */ + stencils: [], + + /** + * Function: addStencil + * + * Adds the given <mxStencil>. + */ + addStencil: function(name, stencil) + { + mxStencilRegistry.stencils[name] = stencil; + }, + + /** + * Function: getStencil + * + * Returns the <mxStencil> for the given name. + */ + getStencil: function(name) + { + return mxStencilRegistry.stencils[name]; + } + +}; diff --git a/src/js/shape/mxStencilShape.js b/src/js/shape/mxStencilShape.js new file mode 100644 index 0000000..9c95f8b --- /dev/null +++ b/src/js/shape/mxStencilShape.js @@ -0,0 +1,209 @@ +/** + * $Id: mxStencilShape.js,v 1.10 2012-07-16 10:22:44 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxStencilShape + * + * Implements a shape based on a <mxStencil>. + * + * Constructor: mxStencilShape + * + * Constructs a new generic shape. + */ +function mxStencilShape(stencil) +{ + this.stencil = stencil; +}; + +/** + * Extends mxShape. + */ +mxStencilShape.prototype = new mxShape(); +mxStencilShape.prototype.constructor = mxStencilShape; + +/** + * Variable: mixedModeHtml + * + * Always prefers VML in mixed mode for stencil shapes. Default is false. + */ +mxStencilShape.prototype.mixedModeHtml = false; + +/** + * Variable: preferModeHtml + * + * Always prefers VML in prefer HTML mode for stencil shapes. Default is false. + */ +mxStencilShape.prototype.preferModeHtml = false; + +/** + * Variable: stencil + * + * Holds the <mxStencil> that defines the shape. + */ +mxStencilShape.prototype.stencil = null; + +/** + * Variable: state + * + * Holds the <mxCellState> associated with this shape. + */ +mxStencilShape.prototype.state = null; + +/** + * Variable: vmlScale + * + * Renders VML with a scale of 4. + */ +mxStencilShape.prototype.vmlScale = 4; + +/** + * Function: apply + * + * Extends <mxShape> apply to keep a reference to the <mxCellState>. + * + * Parameters: + * + * state - <mxCellState> of the corresponding cell. + */ +mxStencilShape.prototype.apply = function(state) +{ + this.state = state; + mxShape.prototype.apply.apply(this, arguments); +}; + +/** + * Function: createSvg + * + * Creates and returns the SVG node(s) to represent this shape. + */ +mxStencilShape.prototype.createSvg = function() +{ + var node = document.createElementNS(mxConstants.NS_SVG, 'g'); + this.configureSvgShape(node); + + return node; +}; + +/** + * Function: configureHtmlShape + * + * Overrides method to set the overflow style to visible. + */ +mxStencilShape.prototype.configureHtmlShape = function(node) +{ + mxShape.prototype.configureHtmlShape.apply(this, arguments); + + if (!mxUtils.isVml(node)) + { + node.style.overflow = 'visible'; + } +}; + +/** + * Function: createVml + * + * Creates and returns the VML node to represent this shape. + */ +mxStencilShape.prototype.createVml = function() +{ + var name = (document.documentMode == 8) ? 'div' : 'v:group'; + var node = document.createElement(name); + this.configureTransparentBackground(node); + node.style.position = 'absolute'; + + return node; +}; + +/** + * Function: configureVmlShape + * + * Configures the specified VML node by applying the current color, + * bounds, shadow, opacity etc. + */ +mxStencilShape.prototype.configureVmlShape = function(node) +{ + // do nothing +}; + +/** + * Function: redraw + * + * Creates and returns the SVG node(s) to represent this shape. + */ +mxStencilShape.prototype.redraw = function() +{ + this.updateBoundingBox(); + + if (this.dialect == mxConstants.DIALECT_SVG) + { + this.redrawShape(); + } + else + { + this.node.style.visibility = 'hidden'; + this.redrawShape(); + this.node.style.visibility = 'visible'; + } +}; + +/** + * Function: redrawShape + * + * Updates the SVG or VML shape. + */ +mxStencilShape.prototype.redrawShape = function() +{ + // LATER: Update existing DOM nodes to improve repaint performance + if (this.dialect != mxConstants.DIALECT_SVG) + { + this.node.style.left = Math.round(this.bounds.x) + 'px'; + this.node.style.top = Math.round(this.bounds.y) + 'px'; + var w = Math.round(this.bounds.width); + var h = Math.round(this.bounds.height); + this.node.style.width = w + 'px'; + this.node.style.height = h + 'px'; + + var node = this.node; + + // Workaround for VML rendering bug in IE8 standards mode where all VML must be + // parsed via assigning the innerHTML of the parent HTML node to keep all event + // handlers referencing node and support rotation via v:group parent element. + if (this.node.nodeName == 'DIV') + { + node = document.createElement('v:group'); + node.style.position = 'absolute'; + node.style.left = '0px'; + node.style.top = '0px'; + node.style.width = w + 'px'; + node.style.height = h + 'px'; + } + else + { + node.innerHTML = ''; + } + + if (mxUtils.isVml(node)) + { + var s = (document.documentMode != 8) ? this.vmlScale : 1; + node.coordsize = (w * s) + ',' + (h * s); + } + + this.stencil.renderDom(this, this.bounds, node); + + if(this.node != node) + { + // Forces parsing in IE8 standards mode + this.node.innerHTML = node.outerHTML; + } + } + else + { + while (this.node.firstChild != null) + { + this.node.removeChild(this.node.firstChild); + } + + this.stencil.renderDom(this, this.bounds, this.node); + } +}; diff --git a/src/js/shape/mxSwimlane.js b/src/js/shape/mxSwimlane.js new file mode 100644 index 0000000..22720fd --- /dev/null +++ b/src/js/shape/mxSwimlane.js @@ -0,0 +1,553 @@ +/** + * $Id: mxSwimlane.js,v 1.43 2011-11-04 13:54:50 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxSwimlane + * + * Extends <mxShape> to implement a swimlane shape. + * This shape is registered under <mxConstants.SHAPE_SWIMLANE> + * in <mxCellRenderer>. + * + * Constructor: mxSwimlane + * + * Constructs a new swimlane shape. + * + * Parameters: + * + * bounds - <mxRectangle> that defines the bounds. This is stored in + * <mxShape.bounds>. + * fill - String that defines the fill color. This is stored in <fill>. + * stroke - String that defines the stroke color. This is stored in <stroke>. + * strokewidth - Optional integer that defines the stroke width. Default is + * 1. This is stored in <strokewidth>. + */ +function mxSwimlane(bounds, fill, stroke, strokewidth) +{ + this.bounds = bounds; + this.fill = fill; + this.stroke = stroke; + this.strokewidth = (strokewidth != null) ? strokewidth : 1; +}; + +/** + * Extends mxShape. + */ +mxSwimlane.prototype = new mxShape(); +mxSwimlane.prototype.constructor = mxSwimlane; + +/** + * Variable: vmlNodes + * + * Adds local references to <mxShape.vmlNodes>. + */ +mxSwimlane.prototype.vmlNodes = mxSwimlane.prototype.vmlNodes.concat(['label', 'content', 'imageNode', 'separator']); + +/** + * Variable: imageSize + * + * Default imagewidth and imageheight if an image but no imagewidth + * and imageheight are defined in the style. Value is 16. + */ +mxSwimlane.prototype.imageSize = 16; + +/** + * Variable: mixedModeHtml + * + * Overrides the parent value with false, meaning it will + * draw in VML in mixed Html mode. This is for better + * handling of event-transparency of the content area. + */ +mxSwimlane.prototype.mixedModeHtml = false; + +/** + * Variable: preferModeHtml + * + * Overrides the parent value with false, meaning it will + * draw as VML in prefer Html mode. This is for better + * handling of event-transparency of the content area. + */ +mxRhombus.prototype.preferModeHtml = false; + +/** + * Function: createHtml + * + * Creates and returns the HTML node to represent this shape. + */ +mxSwimlane.prototype.createHtml = function() +{ + var node = document.createElement('DIV'); + this.configureHtmlShape(node); + node.style.background = ''; + node.style.backgroundColor = ''; + node.style.borderStyle = 'none'; + + // Adds a node that will contain the text label + this.label = document.createElement('DIV'); + this.configureHtmlShape(this.label); + node.appendChild(this.label); + + // Adds a node for the content area of the swimlane + this.content = document.createElement('DIV'); + this.configureHtmlShape(this.content); + this.content.style.backgroundColor = ''; + + // Sets border styles depending on orientation + if (mxUtils.getValue(this.style, mxConstants.STYLE_HORIZONTAL, true)) + { + this.content.style.borderTopStyle = 'none'; + } + else + { + this.content.style.borderLeftStyle = 'none'; + } + + this.content.style.cursor = 'default'; + node.appendChild(this.content); + + // Adds a node for the separator + var color = this.style[mxConstants.STYLE_SEPARATORCOLOR]; + + if (color != null) + { + this.separator = document.createElement('DIV'); + this.separator.style.borderColor = color; + this.separator.style.borderLeftStyle = 'dashed'; + node.appendChild(this.separator); + } + + // Adds a node for the image + if (this.image != null) + { + this.imageNode = mxUtils.createImage(this.image); + this.configureHtmlShape(this.imageNode); + this.imageNode.style.borderStyle = 'none'; + node.appendChild(this.imageNode); + } + + return node; +}; + +/** + * Function: reconfigure + * + * Overrides to avoid filled content area in HTML and updates the shadow + * in SVG. + */ +mxSwimlane.prototype.reconfigure = function(node) +{ + mxShape.prototype.reconfigure.apply(this, arguments); + + if (this.dialect == mxConstants.DIALECT_SVG) + { + if (this.shadowNode != null) + { + this.updateSvgShape(this.shadowNode); + + if (mxUtils.getValue(this.style, mxConstants.STYLE_HORIZONTAL, true)) + { + this.shadowNode.setAttribute('height', this.startSize*this.scale); + } + else + { + this.shadowNode.setAttribute('width', this.startSize*this.scale); + } + } + } + else if (!mxUtils.isVml(this.node)) + { + this.node.style.background = ''; + this.node.style.backgroundColor = ''; + } +}; + +/** + * Function: redrawHtml + * + * Updates the HTML node(s) to reflect the latest bounds and scale. + */ +mxSwimlane.prototype.redrawHtml = function() +{ + this.updateHtmlShape(this.node); + this.node.style.background = ''; + this.node.style.backgroundColor = ''; + this.startSize = parseInt(mxUtils.getValue(this.style, + mxConstants.STYLE_STARTSIZE, mxConstants.DEFAULT_STARTSIZE)); + this.updateHtmlShape(this.label); + this.label.style.top = '0px'; + this.label.style.left = '0px'; + + if (mxUtils.getValue(this.style, mxConstants.STYLE_HORIZONTAL, true)) + { + this.startSize = Math.min(this.startSize, this.bounds.height); + this.label.style.height = (this.startSize * this.scale)+'px'; // relative + this.updateHtmlShape(this.content); + this.content.style.background = ''; + this.content.style.backgroundColor = ''; + + var h = this.startSize*this.scale; + + this.content.style.top = h+'px'; + this.content.style.left = '0px'; + this.content.style.height = Math.max(1, this.bounds.height - h)+'px'; + + if (this.separator != null) + { + this.separator.style.left = Math.round(this.bounds.width)+'px'; + this.separator.style.top = Math.round(this.startSize*this.scale)+'px'; + this.separator.style.width = '1px'; + this.separator.style.height = Math.round(this.bounds.height)+'px'; + this.separator.style.borderWidth = Math.round(this.scale)+'px'; + } + + if (this.imageNode != null) + { + this.imageNode.style.left = (this.bounds.width-this.imageSize-4)+'px'; + this.imageNode.style.top = '0px'; + // TODO: Use imageWidth and height from style if available + this.imageNode.style.width = Math.round(this.imageSize*this.scale)+'px'; + this.imageNode.style.height = Math.round(this.imageSize*this.scale)+'px'; + } + } + else + { + this.startSize = Math.min(this.startSize, this.bounds.width); + this.label.style.width = (this.startSize * this.scale)+'px'; // relative + this.updateHtmlShape(this.content); + this.content.style.background = ''; + this.content.style.backgroundColor = ''; + + var w = this.startSize*this.scale; + + this.content.style.top = '0px'; + this.content.style.left = w+'px'; + this.content.style.width = Math.max(0, this.bounds.width - w)+'px'; + + if (this.separator != null) + { + this.separator.style.left = Math.round(this.startSize*this.scale)+'px'; + this.separator.style.top = Math.round(this.bounds.height)+'px'; + this.separator.style.width = Math.round(this.bounds.width)+'px'; + this.separator.style.height = '1px'; + } + + if (this.imageNode != null) + { + this.imageNode.style.left = (this.bounds.width-this.imageSize-4)+'px'; + this.imageNode.style.top = '0px'; + this.imageNode.style.width = this.imageSize*this.scale+'px'; + this.imageNode.style.height = this.imageSize*this.scale+'px'; + } + } +}; + +/** + * Function: createVml + * + * Creates and returns the VML node(s) to represent this shape. + */ +mxSwimlane.prototype.createVml = function() +{ + var node = document.createElement('v:group'); + var name = (this.isRounded) ? 'v:roundrect' : 'v:rect'; + this.label = document.createElement(name); + + // First configure the label with all settings + this.configureVmlShape(this.label); + + if (this.isRounded) + { + this.label.setAttribute('arcsize', '20%'); + } + + // Disables stuff and configures the rest + this.isShadow = false; + this.configureVmlShape(node); + node.coordorigin = '0,0'; + node.appendChild(this.label); + + this.content = document.createElement(name); + + var tmp = this.fill; + this.fill = null; + + this.configureVmlShape(this.content); + node.style.background = ''; + + if (this.isRounded) + { + this.content.setAttribute('arcsize', '4%'); + } + + this.fill = tmp; + this.content.style.borderBottom = '0px'; + + node.appendChild(this.content); + + var color = this.style[mxConstants.STYLE_SEPARATORCOLOR]; + + if (color != null) + { + this.separator = document.createElement('v:shape'); + this.separator.style.position = 'absolute'; + this.separator.strokecolor = color; + + var strokeNode = document.createElement('v:stroke'); + strokeNode.dashstyle = '2 2'; + this.separator.appendChild(strokeNode); + + node.appendChild(this.separator); + } + + if (this.image != null) + { + this.imageNode = document.createElement('v:image'); + this.imageNode.src = this.image; + this.configureVmlShape(this.imageNode); + this.imageNode.stroked = 'false'; + + node.appendChild(this.imageNode); + } + + return node; +}; + +/** + * Function: redrawVml + * + * Updates the VML node(s) to reflect the latest bounds and scale. + */ +mxSwimlane.prototype.redrawVml = function() +{ + var x = Math.round(this.bounds.x); + var y = Math.round(this.bounds.y); + var w = Math.round(this.bounds.width); + var h = Math.round(this.bounds.height); + + this.updateVmlShape(this.node); + this.node.coordsize = w + ',' + h; + + this.updateVmlShape(this.label); + this.label.style.top = '0px'; + this.label.style.left = '0px'; + this.label.style.rotation = null; + + this.startSize = parseInt(mxUtils.getValue(this.style, + mxConstants.STYLE_STARTSIZE, mxConstants.DEFAULT_STARTSIZE)); + var start = Math.round(this.startSize * this.scale); + + if (this.separator != null) + { + this.separator.coordsize = w + ',' + h; + this.separator.style.left = x + 'px'; + this.separator.style.top = y + 'px'; + this.separator.style.width = w + 'px'; + this.separator.style.height = h + 'px'; + } + + if (mxUtils.getValue(this.style, mxConstants.STYLE_HORIZONTAL, true)) + { + start = Math.min(start, this.bounds.height); + this.label.style.height = start + 'px'; // relative + this.updateVmlShape(this.content); + this.content.style.background = ''; + this.content.style.top = start + 'px'; + this.content.style.left = '0px'; + this.content.style.height = Math.max(0, h - start)+'px'; + + if (this.separator != null) + { + var d = 'm ' + (w - x) + ' ' + (start - y) + + ' l ' + (w - x) + ' ' + (h - y) + ' e'; + this.separator.path = d; + } + + if (this.imageNode != null) + { + var img = Math.round(this.imageSize*this.scale); + + this.imageNode.style.left = (w-img-4)+'px'; + this.imageNode.style.top = '0px'; + this.imageNode.style.width = img + 'px'; + this.imageNode.style.height = img + 'px'; + } + } + else + { + start = Math.min(start, this.bounds.width); + this.label.style.width = start + 'px'; // relative + this.updateVmlShape(this.content); + this.content.style.background = ''; + this.content.style.top = '0px'; + this.content.style.left = start + 'px'; + this.content.style.width = Math.max(0, w - start) + 'px'; + + if (this.separator != null) + { + var d = 'm ' + (start - x) + ' ' + (h - y) + + ' l ' + (w - x) + ' ' + (h - y) + ' e'; + this.separator.path = d; + } + + if (this.imageNode != null) + { + var img = Math.round(this.imageSize * this.scale); + + this.imageNode.style.left = (w - img - 4)+'px'; + this.imageNode.style.top = '0px'; + this.imageNode.style.width = img + 'px'; + this.imageNode.style.height = img + 'px'; + } + } + + this.content.style.rotation = null; +}; + +/** + * Function: createSvg + * + * Creates and returns the SVG node(s) to represent this shape. + */ +mxSwimlane.prototype.createSvg = function() +{ + var node = this.createSvgGroup('rect'); + + if (this.isRounded) + { + this.innerNode.setAttribute('rx', 10); + this.innerNode.setAttribute('ry', 10); + } + + this.content = document.createElementNS(mxConstants.NS_SVG, 'path'); + this.configureSvgShape(this.content); + this.content.setAttribute('fill', 'none'); + + if (this.isRounded) + { + this.content.setAttribute('rx', 10); + this.content.setAttribute('ry', 10); + } + + node.appendChild(this.content); + var color = this.style[mxConstants.STYLE_SEPARATORCOLOR]; + + if (color != null) + { + this.separator = document.createElementNS(mxConstants.NS_SVG, 'line'); + + this.separator.setAttribute('stroke', color); + this.separator.setAttribute('fill', 'none'); + this.separator.setAttribute('stroke-dasharray', '2, 2'); + + node.appendChild(this.separator); + } + + if (this.image != null) + { + this.imageNode = document.createElementNS(mxConstants.NS_SVG, 'image'); + + this.imageNode.setAttributeNS(mxConstants.NS_XLINK, 'href', this.image); + this.configureSvgShape(this.imageNode); + + node.appendChild(this.imageNode); + } + + return node; +}; + +/** + * Function: redrawSvg + * + * Updates the SVG node(s) to reflect the latest bounds and scale. + */ +mxSwimlane.prototype.redrawSvg = function() +{ + var tmp = this.isRounded; + this.isRounded = false; + + this.updateSvgShape(this.innerNode); + this.updateSvgShape(this.content); + var horizontal = mxUtils.getValue(this.style, mxConstants.STYLE_HORIZONTAL, true); + this.startSize = parseInt(mxUtils.getValue(this.style, + mxConstants.STYLE_STARTSIZE, mxConstants.DEFAULT_STARTSIZE)); + var ss = this.startSize * this.scale; + + // Updates the size of the shadow node + if (this.shadowNode != null) + { + this.updateSvgShape(this.shadowNode); + + if (horizontal) + { + this.shadowNode.setAttribute('height', ss); + } + else + { + this.shadowNode.setAttribute('width', ss); + } + } + + this.isRounded = tmp; + + this.content.removeAttribute('x'); + this.content.removeAttribute('y'); + this.content.removeAttribute('width'); + this.content.removeAttribute('height'); + + var crisp = (this.crisp && mxClient.IS_IE) ? 0.5 : 0; + var x = Math.round(this.bounds.x) + crisp; + var y = Math.round(this.bounds.y) + crisp; + var w = Math.round(this.bounds.width); + var h = Math.round(this.bounds.height); + + if (horizontal) + { + ss = Math.min(ss, h); + this.innerNode.setAttribute('height', ss); + var points = 'M ' + x + ' ' + (y + ss) + + ' l 0 ' + (h - ss) + ' l ' + w + ' 0' + + ' l 0 ' + (ss - h); + this.content.setAttribute('d', points); + + if (this.separator != null) + { + this.separator.setAttribute('x1', x + w); + this.separator.setAttribute('y1', y + ss); + this.separator.setAttribute('x2', x + w); + this.separator.setAttribute('y2', y + h); + } + + if (this.imageNode != null) + { + this.imageNode.setAttribute('x', x + w - this.imageSize - 4); + this.imageNode.setAttribute('y', y); + this.imageNode.setAttribute('width', this.imageSize * this.scale + 'px'); + this.imageNode.setAttribute('height', this.imageSize * this.scale + 'px'); + } + } + else + { + ss = Math.min(ss, w); + this.innerNode.setAttribute('width', ss); + var points = 'M ' + (x + ss) + ' ' + y + + ' l ' + (w - ss) + ' 0' + ' l 0 ' + h + + ' l ' + (ss - w) + ' 0'; + this.content.setAttribute('d', points); + + if (this.separator != null) + { + this.separator.setAttribute('x1', x + ss); + this.separator.setAttribute('y1', y + h); + this.separator.setAttribute('x2', x + w); + this.separator.setAttribute('y2', y + h); + } + + if (this.imageNode != null) + { + this.imageNode.setAttribute('x', x + w - this.imageSize - 4); + this.imageNode.setAttribute('y', y); + this.imageNode.setAttribute('width', this.imageSize * this.scale + 'px'); + this.imageNode.setAttribute('height', this.imageSize * this.scale + 'px'); + } + } +}; diff --git a/src/js/shape/mxText.js b/src/js/shape/mxText.js new file mode 100644 index 0000000..de44b59 --- /dev/null +++ b/src/js/shape/mxText.js @@ -0,0 +1,1811 @@ +/** + * $Id: mxText.js,v 1.174 2012-09-27 10:20:30 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxText + * + * Extends <mxShape> to implement a text shape. To change vertical text from + * bottom to top to top to bottom, the following code can be used: + * + * (code) + * mxText.prototype.ieVerticalFilter = 'progid:DXImageTransform.Microsoft.BasicImage(rotation=1)'; + * mxText.prototype.verticalTextDegree = 90; + * + * mxText.prototype.getVerticalOffset = function(offset) + * { + * return new mxPoint(-offset.y, offset.x); + * }; + * (end) + * + * Constructor: mxText + * + * Constructs a new text shape. + * + * Parameters: + * + * value - String that represents the text to be displayed. This is stored in + * <value>. + * bounds - <mxRectangle> that defines the bounds. This is stored in + * <mxShape.bounds>. + * align - Specifies the horizontal alignment. Default is ''. This is stored in + * <align>. + * valign - Specifies the vertical alignment. Default is ''. This is stored in + * <valign>. + * color - String that specifies the text color. Default is 'black'. This is + * stored in <color>. + * family - String that specifies the font family. Default is + * <mxConstants.DEFAULT_FONTFAMILY>. This is stored in <family>. + * size - Integer that specifies the font size. Default is + * <mxConstants.DEFAULT_FONTSIZE>. This is stored in <size>. + * fontStyle - Specifies the font style. Default is 0. This is stored in + * <fontStyle>. + * spacing - Integer that specifies the global spacing. Default is 2. This is + * stored in <spacing>. + * spacingTop - Integer that specifies the top spacing. Default is 0. The + * sum of the spacing and this is stored in <spacingTop>. + * spacingRight - Integer that specifies the right spacing. Default is 0. The + * sum of the spacing and this is stored in <spacingRight>. + * spacingBottom - Integer that specifies the bottom spacing. Default is 0.The + * sum of the spacing and this is stored in <spacingBottom>. + * spacingLeft - Integer that specifies the left spacing. Default is 0. The + * sum of the spacing and this is stored in <spacingLeft>. + * horizontal - Boolean that specifies if the label is horizontal. Default is + * true. This is stored in <horizontal>. + * background - String that specifies the background color. Default is null. + * This is stored in <background>. + * border - String that specifies the label border color. Default is null. + * This is stored in <border>. + * wrap - Specifies if word-wrapping should be enabled. Default is false. + * This is stored in <wrap>. + * clipped - Specifies if the label should be clipped. Default is false. + * This is stored in <clipped>. + * overflow - Value of the overflow style. Default is 'visible'. + */ +function mxText(value, bounds, align, valign, color, + family, size, fontStyle, spacing, spacingTop, spacingRight, + spacingBottom, spacingLeft, horizontal, background, border, + wrap, clipped, overflow, labelPadding) +{ + this.value = value; + this.bounds = bounds; + this.color = (color != null) ? color : 'black'; + this.align = (align != null) ? align : ''; + this.valign = (valign != null) ? valign : ''; + this.family = (family != null) ? family : mxConstants.DEFAULT_FONTFAMILY; + this.size = (size != null) ? size : mxConstants.DEFAULT_FONTSIZE; + this.fontStyle = (fontStyle != null) ? fontStyle : 0; + this.spacing = parseInt(spacing || 2); + this.spacingTop = this.spacing + parseInt(spacingTop || 0); + this.spacingRight = this.spacing + parseInt(spacingRight || 0); + this.spacingBottom = this.spacing + parseInt(spacingBottom || 0); + this.spacingLeft = this.spacing + parseInt(spacingLeft || 0); + this.horizontal = (horizontal != null) ? horizontal : true; + this.background = background; + this.border = border; + this.wrap = (wrap != null) ? wrap : false; + this.clipped = (clipped != null) ? clipped : false; + this.overflow = (overflow != null) ? overflow : 'visible'; + this.labelPadding = (labelPadding != null) ? labelPadding : 0; +}; + +/** + * Extends mxShape. + */ +mxText.prototype = new mxShape(); +mxText.prototype.constructor = mxText; + +/** + * Variable: replaceLinefeeds + * + * Specifies if linefeeds in HTML labels should be replaced with BR tags. + * Default is true. This is also used in <mxImageExport> to export the label. + */ +mxText.prototype.replaceLinefeeds = true; + +/** + * Variable: ieVerticalFilter + * + * Holds the filter definition for vertical text in IE. Default is + * progid:DXImageTransform.Microsoft.BasicImage(rotation=3). + */ +mxText.prototype.ieVerticalFilter = 'progid:DXImageTransform.Microsoft.BasicImage(rotation=3)'; + +/** + * Variable: verticalTextDegree + * + * Specifies the degree to be used for vertical text. Default is -90. + */ +mxText.prototype.verticalTextDegree = -90; + +/** + * Variable: forceIgnoreStringSize + * + * Specifies if the string size should always be ignored. Default is false. + * This can be used to improve rendering speed in slow browsers. This can be + * used if all labels are smaller than the vertex width. String sizes are + * ignored by default for labels which are left aligned with no background and + * border or if the overflow is set to fill. + */ +mxText.prototype.forceIgnoreStringSize = false; + +/** + * Function: isStyleSet + * + * Returns true if the given font style (bold, italic etc) + * is true in this shape's fontStyle. + * + * Parameters: + * + * style - Fontstyle constant from <mxConstants>. + */ +mxText.prototype.isStyleSet = function(style) +{ + return (this.fontStyle & style) == style; +}; + +/** + * Function: create + * + * Override to create HTML regardless of gradient and + * rounded property. + */ +mxText.prototype.create = function(container) +{ + var node = null; + + if (this.dialect == mxConstants.DIALECT_SVG) + { + node = this.createSvg(); + } + else if (this.dialect == mxConstants.DIALECT_STRICTHTML || + this.dialect == mxConstants.DIALECT_PREFERHTML || + !mxUtils.isVml(container)) + { + if (mxClient.IS_SVG && !mxClient.NO_FO) + { + node = this.createForeignObject(); + } + else + { + node = this.createHtml(); + } + } + else + { + node = this.createVml(); + } + + return node; +}; + +/** + * Function: updateBoundingBox + * + * Overrides method to do nothing. + */ +mxText.prototype.updateBoundingBox = function() +{ + // do nothing +}; + +/** + * Function: createForeignObject + * + * Creates and returns the foreignObject node to represent this shape. + */ +mxText.prototype.createForeignObject = function() +{ + var node = document.createElementNS(mxConstants.NS_SVG, 'g'); + + var fo = document.createElementNS(mxConstants.NS_SVG, 'foreignObject'); + fo.setAttribute('pointer-events', 'fill'); + + // Ignored in FF + if (this.overflow == 'hidden') + { + fo.style.overflow = 'hidden'; + } + else + { + // Fill and default are visible + fo.style.overflow = 'visible'; + } + + var body = document.createElement('div'); + body.style.margin = '0px'; + body.style.height = '100%'; + + fo.appendChild(body); + node.appendChild(fo); + + return node; +}; + +/** + * Function: createHtml + * + * Creates and returns the HTML node to represent this shape. + */ +mxText.prototype.createHtml = function() +{ + var table = this.createHtmlTable(); + table.style.position = 'absolute'; + + return table; +}; + +/** + * Function: createVml + * + * Creates and returns the VML node(s) to represent this shape. + */ +mxText.prototype.createVml = function() +{ + return document.createElement('v:textbox'); +}; + +/** + * Function: redrawHtml + * + * Updates the HTML node(s) to reflect the latest bounds and scale. + */ +mxText.prototype.redrawHtml = function() +{ + this.redrawVml(); +}; + +/** + * Function: getOffset + * + * Returns the description of the space between the <bounds> size and the label + * size as an <mxPoint>. + */ +mxText.prototype.getOffset = function(outerWidth, outerHeight, actualWidth, actualHeight, horizontal) +{ + horizontal = (horizontal != null) ? horizontal : this.horizontal; + + var tmpalign = (horizontal) ? this.align : this.valign; + var tmpvalign = (horizontal) ? this.valign : this.align; + var dx = actualWidth - outerWidth; + var dy = actualHeight - outerHeight; + + if (tmpalign == mxConstants.ALIGN_CENTER || tmpalign == mxConstants.ALIGN_MIDDLE) + { + dx = Math.round(dx / 2); + } + else if (tmpalign == mxConstants.ALIGN_LEFT || tmpalign === mxConstants.ALIGN_TOP) + { + dx = (horizontal) ? 0 : (actualWidth - actualHeight) / 2; + } + else if (!horizontal) // BOTTOM + { + dx = (actualWidth + actualHeight) / 2 - outerWidth; + } + + if (tmpvalign == mxConstants.ALIGN_MIDDLE || tmpvalign == mxConstants.ALIGN_CENTER) + { + dy = Math.round(dy / 2); + } + else if (tmpvalign == mxConstants.ALIGN_TOP || tmpvalign == mxConstants.ALIGN_LEFT) + { + dy = (horizontal) ? 0 : (actualHeight + actualWidth) / 2 - outerHeight; + } + else if (!horizontal) // RIGHT + { + dy = (actualHeight - actualWidth) / 2; + } + + return new mxPoint(dx, dy); +}; + +/** + * Function: getSpacing + * + * Returns the spacing as an <mxPoint>. + */ +mxText.prototype.getSpacing = function(horizontal) +{ + horizontal = (horizontal != null) ? horizontal : this.horizontal; + + var dx = 0; + var dy = 0; + + if (this.align == mxConstants.ALIGN_CENTER) + { + dx = (this.spacingLeft - this.spacingRight) / 2; + } + else if (this.align == mxConstants.ALIGN_RIGHT) + { + dx = -this.spacingRight; + } + else + { + dx = this.spacingLeft; + } + + if (this.valign == mxConstants.ALIGN_MIDDLE) + { + dy = (this.spacingTop - this.spacingBottom) / 2; + } + else if (this.valign == mxConstants.ALIGN_BOTTOM) + { + dy = -this.spacingBottom; + } + else + { + dy = this.spacingTop; + } + + return (horizontal) ? new mxPoint(dx, dy) : new mxPoint(dy, dx); +}; + +/** + * Function: createHtmlTable + * + * Creates and returns a HTML table with a table body and a single row with a + * single cell. + */ +mxText.prototype.createHtmlTable = function() +{ + var table = document.createElement('table'); + table.style.borderCollapse = 'collapse'; + var tbody = document.createElement('tbody'); + var tr = document.createElement('tr'); + var td = document.createElement('td'); + + // Workaround for ignored table height in IE9 standards mode + if (document.documentMode >= 9) + { + // FIXME: Ignored in print preview for IE9 standards mode + td.style.height = '100%'; + } + + tr.appendChild(td); + tbody.appendChild(tr); + table.appendChild(tbody); + + return table; +}; + +/** + * Function: updateTableStyle + * + * Updates the style of the given HTML table and the value + * within the table. + */ +mxText.prototype.updateHtmlTable = function(table, scale) +{ + scale = (scale != null) ? scale : 1; + var td = table.firstChild.firstChild.firstChild; + + // Reset of width required to measure actual width after word wrap + if (this.wrap) + { + table.style.width = ''; + } + + // Updates the value + if (mxUtils.isNode(this.value)) + { + if (td.firstChild != this.value) + { + if (td.firstChild != null) + { + td.removeChild(td.firstChild); + } + + td.appendChild(this.value); + } + } + else + { + if (this.lastValue != this.value) + { + td.innerHTML = (this.replaceLinefeeds) ? this.value.replace(/\n/g, '<br/>') : this.value; + this.lastValue = this.value; + } + } + + // Font style + var fontSize = Math.round(this.size * scale); + + if (fontSize <= 0) + { + table.style.visibility = 'hidden'; + } + else + { + // Do not use visible here as it will clone + // all labels while panning in IE + table.style.visibility = ''; + } + + table.style.fontSize = fontSize + 'px'; + table.style.color = this.color; + table.style.fontFamily = this.family; + + // Bold + if (this.isStyleSet(mxConstants.FONT_BOLD)) + { + table.style.fontWeight = 'bold'; + } + else + { + table.style.fontWeight = 'normal'; + } + + // Italic + if (this.isStyleSet(mxConstants.FONT_ITALIC)) + { + table.style.fontStyle = 'italic'; + } + else + { + table.style.fontStyle = ''; + } + + // Underline + if (this.isStyleSet(mxConstants.FONT_UNDERLINE)) + { + table.style.textDecoration = 'underline'; + } + else + { + table.style.textDecoration = ''; + } + + // Font shadow (only available in IE) + if (mxClient.IS_IE) + { + if (this.isStyleSet(mxConstants.FONT_SHADOW)) + { + td.style.filter = 'Shadow(Color=#666666,'+'Direction=135,Strength=%)'; + } + else + { + td.style.removeAttribute('filter'); + } + } + + // Horizontal and vertical alignment + td.style.textAlign = + (this.align == mxConstants.ALIGN_RIGHT) ? 'right' : + ((this.align == mxConstants.ALIGN_CENTER) ? 'center' : + 'left'); + td.style.verticalAlign = + (this.valign == mxConstants.ALIGN_BOTTOM) ? 'bottom' : + ((this.valign == mxConstants.ALIGN_MIDDLE) ? 'middle' : + 'top'); + + // Background style (Must use TD not TABLE for Firefox when rotated) + if (this.value.length > 0 && this.background != null) + { + td.style.background = this.background; + } + else + { + td.style.background = ''; + } + + td.style.padding = this.labelPadding + 'px'; + + if (this.value.length > 0 && this.border != null) + { + table.style.borderColor = this.border; + table.style.borderWidth = '1px'; + table.style.borderStyle = 'solid'; + } + else + { + table.style.borderStyle = 'none'; + } +}; + +/** + * Function: getTableSize + * + * Returns the actual size of the table. + */ +mxText.prototype.getTableSize = function(table) +{ + return new mxRectangle(0, 0, table.offsetWidth, table.offsetHeight); +}; + +/** + * Function: updateTableWidth + * + * Updates the width of the given HTML table. + */ +mxText.prototype.updateTableWidth = function(table) +{ + var td = table.firstChild.firstChild.firstChild; + + // Word-wrap for vertices (not edges) and only if not + // just getting the bounding box in SVG + if (this.wrap && this.bounds.width > 0 && this.dialect != mxConstants.DIALECT_SVG) + { + // Makes sure the label is not wrapped when measuring full length + td.style.whiteSpace = 'nowrap'; + var size = this.getTableSize(table); + var space = Math.min(size.width, ((this.horizontal || mxUtils.isVml(this.node)) ? + this.bounds.width : this.bounds.height) / this.scale); + + // Opera needs the new width to be scaled + if (mxClient.IS_OP) + { + space *= this.scale; + } + + table.style.width = Math.round(space) + 'px'; + td.style.whiteSpace = 'normal'; + } + else + { + table.style.width = ''; + } + + if (!this.wrap) + { + td.style.whiteSpace = 'nowrap'; + } + else + { + td.style.whiteSpace = 'normal'; + } +}; + +/** + * Function: redrawVml + * + * Updates the VML node(s) to reflect the latest bounds and scale. + */ +mxText.prototype.redrawVml = function() +{ + if (this.node.nodeName == 'g') + { + this.redrawForeignObject(); + } + else if (mxUtils.isVml(this.node)) + { + this.redrawTextbox(); + } + else + { + this.redrawHtmlTable(); + } +}; + +/** + * Function: redrawTextbox + * + * Redraws the textbox for this text. This is only used in IE in exact + * rendering mode. + */ +mxText.prototype.redrawTextbox = function() +{ + // Gets VML textbox + var textbox = this.node; + + // Creates HTML container on the fly + if (textbox.firstChild == null) + { + textbox.appendChild(this.createHtmlTable()); + } + + // Updates the table style and value + var table = textbox.firstChild; + this.updateHtmlTable(table); + this.updateTableWidth(table); + + // Opacity + if (this.opacity != null) + { + mxUtils.setOpacity(table, this.opacity); + } + + table.style.filter = ''; + textbox.inset = '0px,0px,0px,0px'; + + if (this.overflow != 'fill') + { + // Only tables can be used to work out the actual size of the markup + var size = this.getTableSize(table); + var w = size.width * this.scale; + var h = size.height * this.scale; + var offset = this.getOffset(this.bounds.width, this.bounds.height, w, h); + + // Rotates the label (IE only) + if (!this.horizontal) + { + table.style.filter = this.ieVerticalFilter; + } + + // Adds horizontal/vertical spacing + var spacing = this.getSpacing(); + var x = this.bounds.x - offset.x + spacing.x * this.scale; + var y = this.bounds.y - offset.y + spacing.y * this.scale; + + // Textboxes are always relative to their parent shape's top, left corner so + // we use the inset for absolute positioning as they allow negative values + // except for edges where the bounds are used to find the shape center + var x0 = this.bounds.x; + var y0 = this.bounds.y; + var ow = this.bounds.width; + var oh = this.bounds.height; + + // Insets are given as left, top, right, bottom + if (this.horizontal) + { + var tx = Math.round(x - x0); + var ty = Math.round(y - y0); + + var r = Math.min(0, Math.round(x0 + ow - x - w - 1)); + var b = Math.min(0, Math.round(y0 + oh - y - h - 1)); + textbox.inset = tx + 'px,' + ty + 'px,' + r + 'px,' + b + 'px'; + } + else + { + var t = 0; + var l = 0; + var r = 0; + var b = 0; + + if (this.align == mxConstants.ALIGN_CENTER) + { + t = (oh - w) / 2; + b = t; + } + else if (this.align == mxConstants.ALIGN_LEFT) + { + t = oh - w; + } + else + { + b = oh - w; + } + + if (this.valign == mxConstants.ALIGN_MIDDLE) + { + l = (ow - h) / 2; + r = l; + } + else if (this.valign == mxConstants.ALIGN_BOTTOM) + { + l = ow - h; + } + else + { + r = ow - h; + } + + textbox.inset = l + 'px,' + t + 'px,' + r + 'px,' + b + 'px'; + } + + textbox.style.zoom = this.scale; + + // Clipping + if (this.clipped && this.bounds.width > 0 && this.bounds.height > 0) + { + this.boundingBox = this.bounds.clone(); + var dx = Math.round(x0 - x); + var dy = Math.round(y0 - y); + + textbox.style.clip = 'rect(' + (dy / this.scale) + ' ' + + ((dx + this.bounds.width) / this.scale) + ' ' + + ((dy + this.bounds.height) / this.scale) + ' ' + + (dx / this.scale) + ')'; + } + else + { + this.boundingBox = new mxRectangle(x, y, w, h); + } + } + else + { + this.boundingBox = this.bounds.clone(); + } +}; + +/** + * Function: redrawHtmlTable + * + * Redraws the HTML table. This is used for HTML labels in all modes except + * exact in IE and if NO_FO is false for the browser. + */ +mxText.prototype.redrawHtmlTable = function() +{ + if (isNaN(this.bounds.x) || isNaN(this.bounds.y) || + isNaN(this.bounds.width) || isNaN(this.bounds.height)) + { + return; + } + + // Gets table + var table = this.node; + var td = table.firstChild.firstChild.firstChild; + + // Un-rotates for computing the actual size + // TODO: Check if the result can be tweaked instead in getActualSize + // and only do this if actual rotation did change + var oldBrowser = false; + var fallbackScale = 1; + + if (mxClient.IS_IE) + { + table.style.removeAttribute('filter'); + } + else if (mxClient.IS_SF || mxClient.IS_GC) + { + table.style.WebkitTransform = ''; + } + else if (mxClient.IS_MT) + { + table.style.MozTransform = ''; + td.style.MozTransform = ''; + } + else + { + if (mxClient.IS_OT) + { + table.style.OTransform = ''; + } + + fallbackScale = this.scale; + oldBrowser = true; + } + + // Resets the current zoom for text measuring + td.style.zoom = ''; + + // Updates the table style and value + this.updateHtmlTable(table, fallbackScale); + this.updateTableWidth(table); + + // Opacity + if (this.opacity != null) + { + mxUtils.setOpacity(table, this.opacity); + } + + // Resets the bounds for computing the actual size + table.style.left = ''; + table.style.top = ''; + table.style.height = ''; + + // Workaround for multiple zoom even if CSS style is reset here + var currentZoom = parseFloat(td.style.zoom) || 1; + + // Only tables can be used to work out the actual size of the markup + // NOTE: offsetWidth and offsetHeight are very slow in quirks and IE 8 standards mode + var w = this.bounds.width; + var h = this.bounds.height; + + var ignoreStringSize = this.forceIgnoreStringSize || this.overflow == 'fill' || + (this.align == mxConstants.ALIGN_LEFT && this.background == null && this.border == null); + + if (!ignoreStringSize) + { + var size = this.getTableSize(table); + w = size.width / currentZoom; + h = size.height / currentZoom; + } + + var offset = this.getOffset(this.bounds.width / this.scale, + this.bounds.height / this.scale, w, h, + oldBrowser || this.horizontal); + + // Adds horizontal/vertical spacing + var spacing = this.getSpacing(oldBrowser || this.horizontal); + var x = this.bounds.x / this.scale - offset.x + spacing.x; + var y = this.bounds.y / this.scale - offset.y + spacing.y; + + // Updates the table bounds and stores the scale to be used for + // defining the table width and height, as well as an offset + var s = this.scale; + var s2 = 1; + var shiftX = 0; + var shiftY = 0; + + // Rotates the label and adds offset + if (!this.horizontal) + { + if (mxClient.IS_IE && mxClient.IS_SVG) + { + table.style.msTransform = 'rotate(' + this.verticalTextDegree + 'deg)'; + } + else if (mxClient.IS_IE) + { + table.style.filter = this.ieVerticalFilter; + shiftX = (w - h) / 2; + shiftY = -shiftX; + } + else if (mxClient.IS_SF || mxClient.IS_GC) + { + table.style.WebkitTransform = 'rotate(' + this.verticalTextDegree + 'deg)'; + } + else if (mxClient.IS_OT) + { + table.style.OTransform = 'rotate(' + this.verticalTextDegree + 'deg)'; + } + else if (mxClient.IS_MT) + { + // Firefox paints background and border only if background is on TD + // and border is on TABLE and both are rotated, just the TD with a + // rotation of zero (don't remove the 0-rotate CSS style) + table.style.MozTransform = 'rotate(' + this.verticalTextDegree + 'deg)'; + td.style.MozTransform = 'rotate(0deg)'; + + s2 = 1 / this.scale; + s = 1; + } + } + + // Sets the zoom + var correction = true; + + if (mxClient.IS_MT || oldBrowser) + { + if (mxClient.IS_MT) + { + table.style.MozTransform += ' scale(' + this.scale + ')'; + s2 = 1 / this.scale; + } + else if (mxClient.IS_OT) + { + td.style.OTransform = 'scale(' + this.scale + ')'; + table.style.borderWidth = Math.round(this.scale * parseInt(table.style.borderWidth)) + 'px'; + } + } + else if (!oldBrowser) + { + // Workaround for unsupported zoom CSS in IE9 standards mode + if (document.documentMode >= 9) + { + td.style.msTransform = 'scale(' + this.scale + ')'; + } + // Uses transform in Webkit for better HTML scaling + else if (mxClient.IS_SF || mxClient.IS_GC) + { + td.style.WebkitTransform = 'scale(' + this.scale + ')'; + } + else + { + td.style.zoom = this.scale; + + // Fixes scaling of border width + if (table.style.borderWidth != '' && document.documentMode != 8) + { + table.style.borderWidth = Math.round(this.scale * parseInt(table.style.borderWidth)) + 'px'; + } + + // Workaround for wrong scale in IE8 standards mode + if (document.documentMode == 8 || !mxClient.IS_IE) + { + s = 1; + } + + correction = false; + } + } + + if (correction) + { + // Workaround for scaled TD position + shiftX = (this.scale - 1) * w / (2 * this.scale); + shiftY = (this.scale - 1) * h / (2 * this.scale); + s = 1; + } + + if (this.overflow != 'fill') + { + var rect = new mxRectangle(Math.round((x + shiftX) * this.scale), + Math.round((y + shiftY) * this.scale), Math.round(w * s), Math.round(h * s)); + table.style.left = rect.x + 'px'; + table.style.top = rect.y + 'px'; + table.style.width = rect.width + 'px'; + table.style.height = rect.height + 'px'; + + // Workaround for wrong scale in border and background rendering for table and td in IE8/9 standards mode + if ((this.background != null || this.border != null) && document.documentMode >= 8) + { + var html = (this.replaceLinefeeds) ? this.value.replace(/\n/g, '<br/>') : this.value; + td.innerHTML = '<div style="padding:' + this.labelPadding + 'px;background:' + td.style.background + ';border:' + table.style.border + '">' + html + '</div>'; + td.style.padding = '0px'; + td.style.background = ''; + table.style.border = ''; + } + + // Clipping + if (this.clipped && this.bounds.width > 0 && this.bounds.height > 0) + { + this.boundingBox = this.bounds.clone(); + + // Clipping without rotation or for older browsers + if (this.horizontal || (oldBrowser && !mxClient.IS_OT)) + { + var dx = Math.max(0, offset.x * s); + var dy = Math.max(0, offset.y * s); + + // TODO: Fix clipping for Opera + table.style.clip = 'rect(' + (dy) + 'px ' + (dx + this.bounds.width * s2) + + 'px ' + (dy + this.bounds.height * s2) + 'px ' + (dx) + 'px)'; + } + else + { + // Workaround for IE clip using top, right, bottom, left (un-rotated) + if (mxClient.IS_IE) + { + var uw = this.bounds.width; + var uh = this.bounds.height; + var dx = 0; + var dy = 0; + + if (this.align == mxConstants.ALIGN_LEFT) + { + dx = Math.max(0, w - uh / this.scale) * this.scale; + } + else if (this.align == mxConstants.ALIGN_CENTER) + { + dx = Math.max(0, w - uh / this.scale) * this.scale / 2; + } + + if (this.valign == mxConstants.ALIGN_BOTTOM) + { + dy = Math.max(0, h - uw / this.scale) * this.scale; + } + else if (this.valign == mxConstants.ALIGN_MIDDLE) + { + dy = Math.max(0, h - uw / this.scale) * this.scale / 2; + } + + table.style.clip = 'rect(' + (dx) + 'px ' + (dy + uw - 1) + + 'px ' + (dx + uh - 1) + 'px ' + (dy) + 'px)'; + } + else + { + var uw = this.bounds.width / this.scale; + var uh = this.bounds.height / this.scale; + + if (mxClient.IS_OT) + { + uw = this.bounds.width; + uh = this.bounds.height; + } + + var dx = 0; + var dy = 0; + + if (this.align == mxConstants.ALIGN_RIGHT) + { + dx = Math.max(0, w - uh); + } + else if (this.align == mxConstants.ALIGN_CENTER) + { + dx = Math.max(0, w - uh) / 2; + } + + if (this.valign == mxConstants.ALIGN_BOTTOM) + { + dy = Math.max(0, h - uw); + } + else if (this.valign == mxConstants.ALIGN_MIDDLE) + { + dy = Math.max(0, h - uw) / 2; + } + + if (mxClient.IS_GC || mxClient.IS_SF) + { + dx *= this.scale; + dy *= this.scale; + uw *= this.scale; + uh *= this.scale; + } + + table.style.clip = 'rect(' + (dy) + ' ' + (dx + uh) + + ' ' + (dy + uw) + ' ' + (dx) + ')'; + } + } + } + else + { + this.boundingBox = rect; + } + } + else + { + this.boundingBox = this.bounds.clone(); + + if (document.documentMode >= 9 || mxClient.IS_SVG) + { + table.style.left = Math.round(this.bounds.x + this.scale / 2 + shiftX) + 'px'; + table.style.top = Math.round(this.bounds.y + this.scale / 2 + shiftY) + 'px'; + table.style.width = Math.round((this.bounds.width - this.scale) / this.scale) + 'px'; + table.style.height = Math.round((this.bounds.height - this.scale) / this.scale) + 'px'; + } + else + { + s = (document.documentMode == 8) ? this.scale : 1; + table.style.left = Math.round(this.bounds.x + this.scale / 2) + 'px'; + table.style.top = Math.round(this.bounds.y + this.scale / 2) + 'px'; + table.style.width = Math.round((this.bounds.width - this.scale) / s) + 'px'; + table.style.height = Math.round((this.bounds.height - this.scale) / s) + 'px'; + } + } +}; + +/** + * Function: getVerticalOffset + * + * Returns the factors for the offset to be added to the text vertical + * text rotation. This implementation returns (offset.y, -offset.x). + */ +mxText.prototype.getVerticalOffset = function(offset) +{ + return new mxPoint(offset.y, -offset.x); +}; + +/** + * Function: redrawForeignObject + * + * Redraws the foreign object for this text. + */ +mxText.prototype.redrawForeignObject = function() +{ + // Gets SVG group with foreignObject + var group = this.node; + var fo = group.firstChild; + + // Searches the table which appears behind the background + while (fo == this.backgroundNode) + { + fo = fo.nextSibling; + } + + var body = fo.firstChild; + + // Creates HTML container on the fly + if (body.firstChild == null) + { + body.appendChild(this.createHtmlTable()); + } + + // Updates the table style and value + var table = body.firstChild; + this.updateHtmlTable(table); + + // Workaround for bug in Google Chrome where the text is moved to origin if opacity + // is set on the table, so we set the opacity on the foreignObject instead. + if (this.opacity != null) + { + fo.setAttribute('opacity', this.opacity / 100); + } + + // Workaround for table background not appearing above the shape that is + // behind the label in Safari. To solve this, we add a background rect that + // paints the background instead. + if (mxClient.IS_SF) + { + table.style.borderStyle = 'none'; + table.firstChild.firstChild.firstChild.style.background = ''; + + if (this.backgroundNode == null && (this.background != null || this.border != null)) + { + this.backgroundNode = document.createElementNS(mxConstants.NS_SVG, 'rect'); + group.insertBefore(this.backgroundNode, group.firstChild); + } + else if (this.backgroundNode != null && this.background == null && this.border == null) + { + this.backgroundNode.parentNode.removeChild(this.backgroundNode); + this.backgroundNode = null; + } + + if (this.backgroundNode != null) + { + if (this.background != null) + { + this.backgroundNode.setAttribute('fill', this.background); + } + else + { + this.backgroundNode.setAttribute('fill', 'none'); + } + + if (this.border != null) + { + this.backgroundNode.setAttribute('stroke', this.border); + } + else + { + this.backgroundNode.setAttribute('stroke', 'none'); + } + } + } + + var tr = ''; + + if (this.overflow != 'fill') + { + // Resets the bounds for computing the actual size + fo.removeAttribute('width'); + fo.removeAttribute('height'); + fo.style.width = ''; + fo.style.height = ''; + fo.style.clip = ''; + + // Workaround for size of table not updated if inside foreignObject + if (this.wrap || (!mxClient.IS_GC && !mxClient.IS_SF)) + { + document.body.appendChild(table); + } + + this.updateTableWidth(table); + + // Only tables can be used to work out the actual size of the markup + var size = this.getTableSize(table); + var w = size.width; + var h = size.height; + + if (table.parentNode != body) + { + body.appendChild(table); + } + + // Adds horizontal/vertical spacing + var spacing = this.getSpacing(); + + var x = this.bounds.x / this.scale + spacing.x; + var y = this.bounds.y / this.scale + spacing.y; + var uw = this.bounds.width / this.scale; + var uh = this.bounds.height / this.scale; + var offset = this.getOffset(uw, uh, w, h); + + // Rotates the label and adds offset + if (this.horizontal) + { + x -= offset.x; + y -= offset.y; + + tr = 'scale(' + this.scale + ')'; + } + else + { + var x0 = x + w / 2; + var y0 = y + h / 2; + + tr = 'scale(' + this.scale + ') rotate(' + this.verticalTextDegree + ' ' + x0 + ' ' + y0 + ')'; + + var tmp = this.getVerticalOffset(offset); + x += tmp.x; + y += tmp.y; + } + + // Must use translate instead of x- and y-attribute on FO for iOS + tr += ' translate(' + x + ' ' + y + ')'; + + // Updates the bounds of the background node in Webkit + if (this.backgroundNode != null) + { + this.backgroundNode.setAttribute('width', w); + this.backgroundNode.setAttribute('height', h); + } + + // Updates the foreignObject size + fo.setAttribute('width', w); + fo.setAttribute('height', h); + + // Clipping + // TODO: Fix/check clipping for foreignObjects in Chrome 5.0 - if clipPath + // is used in the group then things can no longer be moved around + if (this.clipped && this.bounds.width > 0 && this.bounds.height > 0) + { + this.boundingBox = this.bounds.clone(); + var dx = Math.max(0, offset.x); + var dy = Math.max(0, offset.y); + + if (this.horizontal) + { + fo.style.clip = 'rect(' + dy + 'px,' + (dx + uw) + + 'px,' + (dy + uh) + 'px,' + (dx) + 'px)'; + } + else + { + var dx = 0; + var dy = 0; + + if (this.align == mxConstants.ALIGN_RIGHT) + { + dx = Math.max(0, w - uh); + } + else if (this.align == mxConstants.ALIGN_CENTER) + { + dx = Math.max(0, w - uh) / 2; + } + + if (this.valign == mxConstants.ALIGN_BOTTOM) + { + dy = Math.max(0, h - uw); + } + else if (this.valign == mxConstants.ALIGN_MIDDLE) + { + dy = Math.max(0, h - uw) / 2; + } + + fo.style.clip = 'rect(' + (dy) + 'px,' + (dx + uh) + + 'px,' + (dy + uw) + 'px,' + (dx) + 'px)'; + } + + // Clipping for the background node in Chrome + if (this.backgroundNode != null) + { + x = this.bounds.x / this.scale; + y = this.bounds.y / this.scale; + + if (!this.horizontal) + { + x += (h + w) / 2 - uh; + y += (h - w) / 2; + + var tmp = uw; + uw = uh; + uh = tmp; + } + + // No clipping in Chome available due to bug + if (!mxClient.IS_GC) + { + var clip = this.getSvgClip(this.node.ownerSVGElement, x, y, uw, uh); + + if (clip != this.clip) + { + this.releaseSvgClip(); + this.clip = clip; + clip.refCount++; + } + + this.backgroundNode.setAttribute('clip-path', 'url(#' + clip.getAttribute('id') + ')'); + } + } + } + else + { + // Removes clipping from background and cleans up the clip + this.releaseSvgClip(); + + if (this.backgroundNode != null) + { + this.backgroundNode.removeAttribute('clip-path'); + } + + if (this.horizontal) + { + this.boundingBox = new mxRectangle(x * this.scale, y * this.scale, w * this.scale, h * this.scale); + } + else + { + this.boundingBox = new mxRectangle(x * this.scale, y * this.scale, h * this.scale, w * this.scale); + } + } + } + else + { + this.boundingBox = this.bounds.clone(); + + var s = this.scale; + var w = this.bounds.width / s; + var h = this.bounds.height / s; + + // Updates the foreignObject and table bounds + fo.setAttribute('width', w); + fo.setAttribute('height', h); + table.style.width = w + 'px'; + table.style.height = h + 'px'; + + // Updates the bounds of the background node in Webkit + if (this.backgroundNode != null) + { + this.backgroundNode.setAttribute('width', table.clientWidth); + this.backgroundNode.setAttribute('height', table.offsetHeight); + } + + // Must use translate instead of x- and y-attribute on FO for iOS + tr = 'scale(' + s + ') translate(' + (this.bounds.x / s) + + ' ' + (this.bounds.y / s) + ')'; + + if (!this.wrap) + { + var td = table.firstChild.firstChild.firstChild; + td.style.whiteSpace = 'nowrap'; + } + } + + group.setAttribute('transform', tr); +}; + +/** + * Function: createSvg + * + * Creates and returns the SVG node(s) to represent this shape. + */ +mxText.prototype.createSvg = function() +{ + // Creates a group so that shapes inside are rendered properly, if this is + // a text node then the background rectangle is not rendered in Webkit. + var node = document.createElementNS(mxConstants.NS_SVG, 'g'); + + var uline = this.isStyleSet(mxConstants.FONT_UNDERLINE) ? 'underline' : 'none'; + var weight = this.isStyleSet(mxConstants.FONT_BOLD) ? 'bold' : 'normal'; + var s = this.isStyleSet(mxConstants.FONT_ITALIC) ? 'italic' : null; + + // Underline is not implemented in FF, see + // https://bugzilla.mozilla.org/show_bug.cgi?id=317196 + node.setAttribute('text-decoration', uline); + node.setAttribute('font-family', this.family); + node.setAttribute('font-weight', weight); + node.setAttribute('font-size', Math.round(this.size * this.scale) + 'px'); + node.setAttribute('fill', this.color); + var align = (this.align == mxConstants.ALIGN_RIGHT) ? 'end' : + (this.align == mxConstants.ALIGN_CENTER) ? 'middle' : + 'start'; + node.setAttribute('text-anchor', align); + + if (s != null) + { + node.setAttribute('font-style', s); + } + + // Adds a rectangle for the background color + if (this.background != null || this.border != null) + { + this.backgroundNode = document.createElementNS(mxConstants.NS_SVG, 'rect'); + this.backgroundNode.setAttribute('shape-rendering', 'crispEdges'); + + if (this.background != null) + { + this.backgroundNode.setAttribute('fill', this.background); + } + else + { + this.backgroundNode.setAttribute('fill', 'none'); + } + + if (this.border != null) + { + this.backgroundNode.setAttribute('stroke', this.border); + } + else + { + this.backgroundNode.setAttribute('stroke', 'none'); + } + } + + this.updateSvgValue(node); + + return node; +}; + +/** + * Updates the text represented by the SVG DOM nodes. + */ +mxText.prototype.updateSvgValue = function(node) +{ + if (this.currentValue != this.value) + { + // Removes all existing children + while (node.firstChild != null) + { + node.removeChild(node.firstChild); + } + + if (this.value != null) + { + // Adds tspan elements for the lines + var uline = this.isStyleSet(mxConstants.FONT_UNDERLINE) ? 'underline' : 'none'; + var lines = this.value.split('\n'); + + // Workaround for empty lines breaking the return value of getBBox + // for the enclosing g element so we avoid adding empty lines + // but still count them as a linefeed + this.textNodes = new Array(lines.length); + + for (var i = 0; i < lines.length; i++) + { + if (!this.isEmptyString(lines[i])) + { + var tspan = this.createSvgSpan(lines[i]); + node.appendChild(tspan); + this.textNodes[i] = tspan; + + // Requires either 'inherit' in Webkit or explicit setting + // to work in Webkit and IE9 standards mode. Both, inherit + // and underline do not work in FF. This is a known bug in + // FF (see above). + tspan.setAttribute('text-decoration', uline); + } + else + { + this.textNodes[i] = null; + } + } + } + + this.currentValue = this.value; + } +}; + +/** + * Function: redrawSvg + * + * Updates the SVG node(s) to reflect the latest bounds and scale. + */ +mxText.prototype.redrawSvg = function() +{ + if (this.node.nodeName == 'foreignObject') + { + this.redrawHtml(); + + return; + } + + var fontSize = Math.round(this.size * this.scale); + + if (fontSize <= 0) + { + this.node.setAttribute('visibility', 'hidden'); + } + else + { + this.node.removeAttribute('visibility'); + } + + this.updateSvgValue(this.node); + this.node.setAttribute('font-size', fontSize + 'px'); + + if (this.opacity != null) + { + // Improves opacity performance in Firefox + this.node.setAttribute('fill-opacity', this.opacity/100); + this.node.setAttribute('stroke-opacity', this.opacity/100); + } + + // Workaround to avoid the use of getBBox to find the size + // of the label. A temporary HTML table is created instead. + var previous = this.value; + var table = this.createHtmlTable(); + + // Makes sure the table is updated and replaces all HTML entities + this.lastValue = null; + this.value = mxUtils.htmlEntities(this.value, false); + this.updateHtmlTable(table); + + // Adds the table to the DOM to find the actual size + document.body.appendChild(table); + var w = table.offsetWidth * this.scale; + var h = table.offsetHeight * this.scale; + + // Cleans up the DOM and restores the original value + table.parentNode.removeChild(table); + this.value = previous; + + // Sets the bounding box for the unclipped case so that + // the full background can be painted using it, the initial + // value for dx and the +4 in the width below are for + // error correction of the HTML and SVG text width + var dx = 2 * this.scale; + + if (this.align == mxConstants.ALIGN_CENTER) + { + dx += w / 2; + } + else if (this.align == mxConstants.ALIGN_RIGHT) + { + dx += w; + } + + var dy = Math.round(fontSize * 1.3); + var childCount = this.node.childNodes.length; + var lineCount = (this.textNodes != null) ? this.textNodes.length : 0; + + if (this.backgroundNode != null) + { + childCount--; + } + + var x = this.bounds.x; + var y = this.bounds.y; + + x += (this.align == mxConstants.ALIGN_RIGHT) ? + ((this.horizontal) ? this.bounds.width : this.bounds.height)- + this.spacingRight * this.scale : + (this.align == mxConstants.ALIGN_CENTER) ? + this.spacingLeft * this.scale + + (((this.horizontal) ? this.bounds.width : this.bounds.height) - + this.spacingLeft * this.scale - this.spacingRight * this.scale) / 2 : + this.spacingLeft * this.scale + 1; + + // Makes sure the alignment is like in VML and HTML + y += (this.valign == mxConstants.ALIGN_BOTTOM) ? + ((this.horizontal) ? this.bounds.height : this.bounds.width) - + (lineCount - 1) * dy - this.spacingBottom * this.scale - 4 : + (this.valign == mxConstants.ALIGN_MIDDLE) ? + (this.spacingTop * this.scale + + ((this.horizontal) ? this.bounds.height : this.bounds.width) - + this.spacingBottom * this.scale - + (lineCount - 1.5) * dy) / 2 : + this.spacingTop * this.scale + dy; + + if (this.overflow == 'fill') + { + if (this.align == mxConstants.ALIGN_CENTER) + { + x = Math.max(this.bounds.x + w / 2, x); + } + + y = Math.max(this.bounds.y + fontSize, y); + + this.boundingBox = new mxRectangle(x - dx, y - dy, + w + 4 * this.scale, h + 1 * this.scale); + this.boundingBox.x = Math.min(this.bounds.x, this.boundingBox.x); + this.boundingBox.y = Math.min(this.bounds.y, this.boundingBox.y); + this.boundingBox.width = Math.max(this.bounds.width, this.boundingBox.width); + this.boundingBox.height = Math.max(this.bounds.height, this.boundingBox.height); + } + else + { + this.boundingBox = new mxRectangle(x - dx, y - dy, + w + 4 * this.scale, h + 1 * this.scale); + } + + if (!this.horizontal) + { + var cx = this.bounds.x + this.bounds.width / 2; + var cy = this.bounds.y + this.bounds.height / 2; + + var offsetX = (this.bounds.width - this.bounds.height) / 2; + var offsetY = (this.bounds.height - this.bounds.width) / 2; + + this.node.setAttribute('transform', + 'rotate(' + this.verticalTextDegree + ' ' + cx + ' ' + cy + ') ' + + 'translate(' + (-offsetY) + ' ' + (-offsetX) + ')'); + } + + // TODO: Font-shadow + this.redrawSvgTextNodes(x, y, dy); + + /* + * FIXME: Bounding box is not rotated. This seems to be a problem for + * all vertical text boxes. Workaround is in mxImageExport. + if (!this.horizontal) + { + var b = this.bounds.y + this.bounds.height; + var cx = this.boundingBox.getCenterX() - this.bounds.x; + var cy = this.boundingBox.getCenterY() - this.bounds.y; + + var y = b - cx - this.bounds.height / 2; + this.boundingBox.x = this.bounds.x + cy - this.boundingBox.width / 2; + this.boundingBox.y = y; + } + */ + + // Updates the bounds of the background node if one exists + if (this.value.length > 0 && this.backgroundNode != null && this.node.firstChild != null) + { + if (this.node.firstChild != this.backgroundNode) + { + this.node.insertBefore(this.backgroundNode, this.node.firstChild); + } + + // FIXME: For larger font sizes the linespacing between HTML and SVG + // seems to be different and hence the bounding box isn't accurate. + // Also in Firefox the background box is slighly offset. + this.backgroundNode.setAttribute('x', this.boundingBox.x + this.scale / 2 + 1 * this.scale); + this.backgroundNode.setAttribute('y', this.boundingBox.y + this.scale / 2 + 2 * this.scale - this.labelPadding); + this.backgroundNode.setAttribute('width', this.boundingBox.width - this.scale - 2 * this.scale); + this.backgroundNode.setAttribute('height', this.boundingBox.height - this.scale); + + var strokeWidth = Math.round(Math.max(1, this.scale)); + this.backgroundNode.setAttribute('stroke-width', strokeWidth); + } + + // Adds clipping and updates the bounding box + if (this.clipped && this.bounds.width > 0 && this.bounds.height > 0) + { + this.boundingBox = this.bounds.clone(); + + if (!this.horizontal) + { + this.boundingBox.width = this.bounds.height; + this.boundingBox.height = this.bounds.width; + } + + x = this.bounds.x; + y = this.bounds.y; + + if (this.horizontal) + { + w = this.bounds.width; + h = this.bounds.height; + } + else + { + w = this.bounds.height; + h = this.bounds.width; + } + + var clip = this.getSvgClip(this.node.ownerSVGElement, x, y, w, h); + + if (clip != this.clip) + { + this.releaseSvgClip(); + this.clip = clip; + clip.refCount++; + } + + this.node.setAttribute('clip-path', 'url(#' + clip.getAttribute('id') + ')'); + } + else + { + this.releaseSvgClip(); + this.node.removeAttribute('clip-path'); + } +}; + +/** + * Function: redrawSvgTextNodes + * + * Hook to update the position of the SVG text nodes. + */ +mxText.prototype.redrawSvgTextNodes = function(x, y, dy) +{ + if (this.textNodes != null) + { + var currentY = y; + + for (var i = 0; i < this.textNodes.length; i++) + { + var node = this.textNodes[i]; + + if (node != null) + { + node.setAttribute('x', x); + node.setAttribute('y', currentY); + + // Triggers an update in Firefox 1.5.0.x (don't add a semicolon!) + node.setAttribute('style', 'pointer-events: all'); + } + + currentY += dy; + } + } +}; + +/** + * Function: releaseSvgClip + * + * Releases the given SVG clip removing it from the DOM if required. + */ +mxText.prototype.releaseSvgClip = function() +{ + if (this.clip != null) + { + this.clip.refCount--; + + if (this.clip.refCount == 0) + { + this.clip.parentNode.removeChild(this.clip); + } + + this.clip = null; + } +}; + +/** + * Function: getSvgClip + * + * Returns a new or existing SVG clip path which is a descendant of the given + * SVG node with a unique ID. + */ +mxText.prototype.getSvgClip = function(svg, x, y, w, h) +{ + x = Math.round(x); + y = Math.round(y); + w = Math.round(w); + h = Math.round(h); + + var id = 'mx-clip-' + x + '-' + y + '-' + w + '-' + h; + + // Quick access + if (this.clip != null && this.clip.ident == id) + { + return this.clip; + } + + var counter = 0; + var tmp = id + '-' + counter; + var clip = document.getElementById(tmp); + + // Tries to find an existing clip in the given SVG + while (clip != null) + { + if (clip.ownerSVGElement == svg) + { + return clip; + } + + counter++; + tmp = id + '-' + counter; + clip = document.getElementById(tmp); + } + + // Creates a new clip node and adds it to the DOM + if (clip != null) + { + clip = clip.cloneNode(true); + counter++; + } + else + { + clip = document.createElementNS(mxConstants.NS_SVG, 'clipPath'); + + var rect = document.createElementNS(mxConstants.NS_SVG, 'rect'); + rect.setAttribute('x', x); + rect.setAttribute('y', y); + rect.setAttribute('width', w); + rect.setAttribute('height', h); + + clip.appendChild(rect); + } + + clip.setAttribute('id', id + '-' + counter); + clip.ident = id; // For quick access above + svg.appendChild(clip); + clip.refCount = 0; + + return clip; +}; + +/** + * Function: isEmptyString + * + * Returns true if the given string is empty or + * contains only whitespace. + */ +mxText.prototype.isEmptyString = function(text) +{ + return text.replace(/ /g, '').length == 0; +}; + +/** + * Function: createSvgSpan + * + * Creats an SVG tspan node for the given text. + */ +mxText.prototype.createSvgSpan = function(text) +{ + // Creates a text node since there is no enclosing text element but + // rather a group, which is required to render the background rectangle + // in Webkit. This can be changed to tspan if the enclosing node is + // a text but this leads to an hidden background in Webkit. + var node = document.createElementNS(mxConstants.NS_SVG, 'text'); + // Needed to preserve multiple white spaces, but ignored in IE9 plus white-space:pre + // is ignored in HTML output for VML, so better to not use this for SVG labels + // node.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve') + // Alternative idea is to replace all spaces with to fix HTML in IE, but + // IE9/10 with SVG will still ignore the xml:space preserve tag as discussed here: + // http://stackoverflow.com/questions/8086292/significant-whitespace-in-svg-embedded-in-html + // Could replace spaces with in text but HTML tags must be scaped first. + mxUtils.write(node, text); + + return node; +}; + +/** + * Function: destroy + * + * Extends destroy to remove any allocated SVG clips. + */ +mxText.prototype.destroy = function() +{ + this.releaseSvgClip(); + mxShape.prototype.destroy.apply(this, arguments); +}; diff --git a/src/js/shape/mxTriangle.js b/src/js/shape/mxTriangle.js new file mode 100644 index 0000000..3a48db2 --- /dev/null +++ b/src/js/shape/mxTriangle.js @@ -0,0 +1,34 @@ +/** + * $Id: mxTriangle.js,v 1.10 2011-09-02 10:01:00 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxTriangle + * + * Implementation of the triangle shape. + * + * Constructor: mxTriangle + * + * Constructs a new triangle shape. + */ +function mxTriangle() { }; + +/** + * Extends <mxActor>. + */ +mxTriangle.prototype = new mxActor(); +mxTriangle.prototype.constructor = mxTriangle; + +/** + * Function: redrawPath + * + * Draws the path for this shape. This method uses the <mxPath> + * abstraction to paint the shape for VML and SVG. + */ +mxTriangle.prototype.redrawPath = function(path, x, y, w, h) +{ + path.moveTo(0, 0); + path.lineTo(w, 0.5 * h); + path.lineTo(0, h); + path.close(); +}; diff --git a/src/js/util/mxAnimation.js b/src/js/util/mxAnimation.js new file mode 100644 index 0000000..80901ef --- /dev/null +++ b/src/js/util/mxAnimation.js @@ -0,0 +1,82 @@ +/** + * $Id: mxAnimation.js,v 1.2 2010-03-19 12:53:29 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * + * Class: mxAnimation + * + * Implements a basic animation in JavaScript. + * + * Constructor: mxAnimation + * + * Constructs an animation. + * + * Parameters: + * + * graph - Reference to the enclosing <mxGraph>. + */ +function mxAnimation(delay) +{ + this.delay = (delay != null) ? delay : 20; +}; + +/** + * Extends mxEventSource. + */ +mxAnimation.prototype = new mxEventSource(); +mxAnimation.prototype.constructor = mxAnimation; + +/** + * Variable: delay + * + * Specifies the delay between the animation steps. Defaul is 30ms. + */ +mxAnimation.prototype.delay = null; + +/** + * Variable: thread + * + * Reference to the thread while the animation is running. + */ +mxAnimation.prototype.thread = null; + +/** + * Function: startAnimation + * + * Starts the animation by repeatedly invoking updateAnimation. + */ +mxAnimation.prototype.startAnimation = function() +{ + if (this.thread == null) + { + this.thread = window.setInterval(mxUtils.bind(this, this.updateAnimation), this.delay); + } +}; + +/** + * Function: updateAnimation + * + * Hook for subclassers to implement the animation. Invoke stopAnimation + * when finished, startAnimation to resume. This is called whenever the + * timer fires and fires an mxEvent.EXECUTE event with no properties. + */ +mxAnimation.prototype.updateAnimation = function() +{ + this.fireEvent(new mxEventObject(mxEvent.EXECUTE)); +}; + +/** + * Function: stopAnimation + * + * Stops the animation by deleting the timer and fires an <mxEvent.DONE>. + */ +mxAnimation.prototype.stopAnimation = function() +{ + if (this.thread != null) + { + window.clearInterval(this.thread); + this.thread = null; + this.fireEvent(new mxEventObject(mxEvent.DONE)); + } +}; diff --git a/src/js/util/mxAutoSaveManager.js b/src/js/util/mxAutoSaveManager.js new file mode 100644 index 0000000..85c23dc --- /dev/null +++ b/src/js/util/mxAutoSaveManager.js @@ -0,0 +1,213 @@ +/** + * $Id: mxAutoSaveManager.js,v 1.9 2010-09-16 09:10:21 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxAutoSaveManager + * + * Manager for automatically saving diagrams. The <save> hook must be + * implemented. + * + * Example: + * + * (code) + * var mgr = new mxAutoSaveManager(editor.graph); + * mgr.save = function() + * { + * mxLog.show(); + * mxLog.debug('save'); + * }; + * (end) + * + * Constructor: mxAutoSaveManager + * + * Constructs a new automatic layout for the given graph. + * + * Arguments: + * + * graph - Reference to the enclosing graph. + */ +function mxAutoSaveManager(graph) +{ + // Notifies the manager of a change + this.changeHandler = mxUtils.bind(this, function(sender, evt) + { + if (this.isEnabled()) + { + this.graphModelChanged(evt.getProperty('edit').changes); + } + }); + + this.setGraph(graph); +}; + +/** + * Extends mxEventSource. + */ +mxAutoSaveManager.prototype = new mxEventSource(); +mxAutoSaveManager.prototype.constructor = mxAutoSaveManager; + +/** + * Variable: graph + * + * Reference to the enclosing <mxGraph>. + */ +mxAutoSaveManager.prototype.graph = null; + +/** + * Variable: autoSaveDelay + * + * Minimum amount of seconds between two consecutive autosaves. Eg. a + * value of 1 (s) means the graph is not stored more than once per second. + * Default is 10. + */ +mxAutoSaveManager.prototype.autoSaveDelay = 10; + +/** + * Variable: autoSaveThrottle + * + * Minimum amount of seconds between two consecutive autosaves triggered by + * more than <autoSaveThreshhold> changes within a timespan of less than + * <autoSaveDelay> seconds. Eg. a value of 1 (s) means the graph is not + * stored more than once per second even if there are more than + * <autoSaveThreshold> changes within that timespan. Default is 2. + */ +mxAutoSaveManager.prototype.autoSaveThrottle = 2; + +/** + * Variable: autoSaveThreshold + * + * Minimum amount of ignored changes before an autosave. Eg. a value of 2 + * means after 2 change of the graph model the autosave will trigger if the + * condition below is true. Default is 5. + */ +mxAutoSaveManager.prototype.autoSaveThreshold = 5; + +/** + * Variable: ignoredChanges + * + * Counter for ignored changes in autosave. + */ +mxAutoSaveManager.prototype.ignoredChanges = 0; + +/** + * Variable: lastSnapshot + * + * Used for autosaving. See <autosave>. + */ +mxAutoSaveManager.prototype.lastSnapshot = 0; + +/** + * Variable: enabled + * + * Specifies if event handling is enabled. Default is true. + */ +mxAutoSaveManager.prototype.enabled = true; + +/** + * Variable: changeHandler + * + * Holds the function that handles graph model changes. + */ +mxAutoSaveManager.prototype.changeHandler = null; + +/** + * Function: isEnabled + * + * Returns true if events are handled. This implementation + * returns <enabled>. + */ +mxAutoSaveManager.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Enables or disables event handling. This implementation + * updates <enabled>. + * + * Parameters: + * + * enabled - Boolean that specifies the new enabled state. + */ +mxAutoSaveManager.prototype.setEnabled = function(value) +{ + this.enabled = value; +}; + +/** + * Function: setGraph + * + * Sets the graph that the layouts operate on. + */ +mxAutoSaveManager.prototype.setGraph = function(graph) +{ + if (this.graph != null) + { + this.graph.getModel().removeListener(this.changeHandler); + } + + this.graph = graph; + + if (this.graph != null) + { + this.graph.getModel().addListener(mxEvent.CHANGE, this.changeHandler); + } +}; + +/** + * Function: save + * + * Empty hook that is called if the graph should be saved. + */ +mxAutoSaveManager.prototype.save = function() +{ + // empty +}; + +/** + * Function: graphModelChanged + * + * Invoked when the graph model has changed. + */ +mxAutoSaveManager.prototype.graphModelChanged = function(changes) +{ + var now = new Date().getTime(); + var dt = (now - this.lastSnapshot) / 1000; + + if (dt > this.autoSaveDelay || + (this.ignoredChanges >= this.autoSaveThreshold && + dt > this.autoSaveThrottle)) + { + this.save(); + this.reset(); + } + else + { + // Increments the number of ignored changes + this.ignoredChanges++; + } +}; + +/** + * Function: reset + * + * Resets all counters. + */ +mxAutoSaveManager.prototype.reset = function() +{ + this.lastSnapshot = new Date().getTime(); + this.ignoredChanges = 0; +}; + +/** + * Function: destroy + * + * Removes all handlers from the <graph> and deletes the reference to it. + */ +mxAutoSaveManager.prototype.destroy = function() +{ + this.setGraph(null); +}; diff --git a/src/js/util/mxClipboard.js b/src/js/util/mxClipboard.js new file mode 100644 index 0000000..e9fec6b --- /dev/null +++ b/src/js/util/mxClipboard.js @@ -0,0 +1,144 @@ +/** + * $Id: mxClipboard.js,v 1.29 2010-01-02 09:45:14 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +var mxClipboard = +{ + /** + * Class: mxClipboard + * + * Singleton that implements a clipboard for graph cells. + * + * Example: + * + * (code) + * mxClipboard.copy(graph); + * mxClipboard.paste(graph2); + * (end) + * + * This copies the selection cells from the graph to the + * clipboard and pastes them into graph2. + * + * For fine-grained control of the clipboard data the <mxGraph.canExportCell> + * and <mxGraph.canImportCell> functions can be overridden. + * + * Variable: STEPSIZE + * + * Defines the step size to offset the cells + * after each paste operation. Default is 10. + */ + STEPSIZE: 10, + + /** + * Variable: insertCount + * + * Counts the number of times the clipboard data has been inserted. + */ + insertCount: 1, + + /** + * Variable: cells + * + * Holds the array of <mxCells> currently in the clipboard. + */ + cells: null, + + /** + * Function: isEmpty + * + * Returns true if the clipboard currently has not data stored. + */ + isEmpty: function() + { + return mxClipboard.cells == null; + }, + + /** + * Function: cut + * + * Cuts the given array of <mxCells> from the specified graph. + * If cells is null then the selection cells of the graph will + * be used. Returns the cells that have been cut from the graph. + * + * Parameters: + * + * graph - <mxGraph> that contains the cells to be cut. + * cells - Optional array of <mxCells> to be cut. + */ + cut: function(graph, cells) + { + cells = mxClipboard.copy(graph, cells); + mxClipboard.insertCount = 0; + mxClipboard.removeCells(graph, cells); + + return cells; + }, + + /** + * Function: removeCells + * + * Hook to remove the given cells from the given graph after + * a cut operation. + * + * Parameters: + * + * graph - <mxGraph> that contains the cells to be cut. + * cells - Array of <mxCells> to be cut. + */ + removeCells: function(graph, cells) + { + graph.removeCells(cells); + }, + + /** + * Function: copy + * + * Copies the given array of <mxCells> from the specified + * graph to <cells>.Returns the original array of cells that has + * been cloned. + * + * Parameters: + * + * graph - <mxGraph> that contains the cells to be copied. + * cells - Optional array of <mxCells> to be copied. + */ + copy: function(graph, cells) + { + cells = cells || graph.getSelectionCells(); + var result = graph.getExportableCells(cells); + mxClipboard.insertCount = 1; + mxClipboard.cells = graph.cloneCells(result); + + return result; + }, + + /** + * Function: paste + * + * Pastes the <cells> into the specified graph restoring + * the relation to <parents>, if possible. If the parents + * are no longer in the graph or invisible then the + * cells are added to the graph's default or into the + * swimlane under the cell's new location if one exists. + * The cells are added to the graph using <mxGraph.importCells>. + * + * Parameters: + * + * graph - <mxGraph> to paste the <cells> into. + */ + paste: function(graph) + { + if (mxClipboard.cells != null) + { + var cells = graph.getImportableCells(mxClipboard.cells); + var delta = mxClipboard.insertCount * mxClipboard.STEPSIZE; + var parent = graph.getDefaultParent(); + cells = graph.importCells(cells, delta, delta, parent); + + // Increments the counter and selects the inserted cells + mxClipboard.insertCount++; + graph.setSelectionCells(cells); + } + } + +}; diff --git a/src/js/util/mxConstants.js b/src/js/util/mxConstants.js new file mode 100644 index 0000000..8d11dc1 --- /dev/null +++ b/src/js/util/mxConstants.js @@ -0,0 +1,1911 @@ +/** + * $Id: mxConstants.js,v 1.127 2012-11-20 09:06:07 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ + var mxConstants = + { + /** + * Class: mxConstants + * + * Defines various global constants. + * + * Variable: DEFAULT_HOTSPOT + * + * Defines the portion of the cell which is to be used as a connectable + * region. Default is 0.3. Possible values are 0 < x <= 1. + */ + DEFAULT_HOTSPOT: 0.3, + + /** + * Variable: MIN_HOTSPOT_SIZE + * + * Defines the minimum size in pixels of the portion of the cell which is + * to be used as a connectable region. Default is 8. + */ + MIN_HOTSPOT_SIZE: 8, + + /** + * Variable: MAX_HOTSPOT_SIZE + * + * Defines the maximum size in pixels of the portion of the cell which is + * to be used as a connectable region. Use 0 for no maximum. Default is 0. + */ + MAX_HOTSPOT_SIZE: 0, + + /** + * Variable: RENDERING_HINT_EXACT + * + * Defines the exact rendering hint. + */ + RENDERING_HINT_EXACT: 'exact', + + /** + * Variable: RENDERING_HINT_FASTER + * + * Defines the faster rendering hint. + */ + RENDERING_HINT_FASTER: 'faster', + + /** + * Variable: RENDERING_HINT_FASTEST + * + * Defines the fastest rendering hint. + */ + RENDERING_HINT_FASTEST: 'fastest', + + /** + * Variable: DIALECT_SVG + * + * Defines the SVG display dialect name. + */ + DIALECT_SVG: 'svg', + + /** + * Variable: DIALECT_VML + * + * Defines the VML display dialect name. + */ + DIALECT_VML: 'vml', + + /** + * Variable: DIALECT_MIXEDHTML + * + * Defines the mixed HTML display dialect name. + */ + DIALECT_MIXEDHTML: 'mixedHtml', + + /** + * Variable: DIALECT_PREFERHTML + * + * Defines the preferred HTML display dialect name. + */ + DIALECT_PREFERHTML: 'preferHtml', + + /** + * Variable: DIALECT_STRICTHTML + * + * Defines the strict HTML display dialect. + */ + DIALECT_STRICTHTML: 'strictHtml', + + /** + * Variable: NS_SVG + * + * Defines the SVG namespace. + */ + NS_SVG: 'http://www.w3.org/2000/svg', + + /** + * Variable: NS_XHTML + * + * Defines the XHTML namespace. + */ + NS_XHTML: 'http://www.w3.org/1999/xhtml', + + /** + * Variable: NS_XLINK + * + * Defines the XLink namespace. + */ + NS_XLINK: 'http://www.w3.org/1999/xlink', + + /** + * Variable: SHADOWCOLOR + * + * Defines the color to be used to draw shadows in shapes and windows. + * Default is gray. + */ + SHADOWCOLOR: 'gray', + + /** + * Variable: SHADOW_OFFSET_X + * + * Specifies the x-offset of the shadow. Default is 2. + */ + SHADOW_OFFSET_X: 2, + + /** + * Variable: SHADOW_OFFSET_Y + * + * Specifies the y-offset of the shadow. Default is 3. + */ + SHADOW_OFFSET_Y: 3, + + /** + * Variable: SHADOW_OPACITY + * + * Defines the opacity for shadows. Default is 1. + */ + SHADOW_OPACITY: 1, + + /** + * Variable: NODETYPE_ELEMENT + * + * DOM node of type ELEMENT. + */ + NODETYPE_ELEMENT: 1, + + /** + * Variable: NODETYPE_ATTRIBUTE + * + * DOM node of type ATTRIBUTE. + */ + NODETYPE_ATTRIBUTE: 2, + + /** + * Variable: NODETYPE_TEXT + * + * DOM node of type TEXT. + */ + NODETYPE_TEXT: 3, + + /** + * Variable: NODETYPE_CDATA + * + * DOM node of type CDATA. + */ + NODETYPE_CDATA: 4, + + /** + * Variable: NODETYPE_ENTITY_REFERENCE + * + * DOM node of type ENTITY_REFERENCE. + */ + NODETYPE_ENTITY_REFERENCE: 5, + + /** + * Variable: NODETYPE_ENTITY + * + * DOM node of type ENTITY. + */ + NODETYPE_ENTITY: 6, + + /** + * Variable: NODETYPE_PROCESSING_INSTRUCTION + * + * DOM node of type PROCESSING_INSTRUCTION. + */ + NODETYPE_PROCESSING_INSTRUCTION: 7, + + /** + * Variable: NODETYPE_COMMENT + * + * DOM node of type COMMENT. + */ + NODETYPE_COMMENT: 8, + + /** + * Variable: NODETYPE_DOCUMENT + * + * DOM node of type DOCUMENT. + */ + NODETYPE_DOCUMENT: 9, + + /** + * Variable: NODETYPE_DOCUMENTTYPE + * + * DOM node of type DOCUMENTTYPE. + */ + NODETYPE_DOCUMENTTYPE: 10, + + /** + * Variable: NODETYPE_DOCUMENT_FRAGMENT + * + * DOM node of type DOCUMENT_FRAGMENT. + */ + NODETYPE_DOCUMENT_FRAGMENT: 11, + + /** + * Variable: NODETYPE_NOTATION + * + * DOM node of type NOTATION. + */ + NODETYPE_NOTATION: 12, + + /** + * Variable: TOOLTIP_VERTICAL_OFFSET + * + * Defines the vertical offset for the tooltip. + * Default is 16. + */ + TOOLTIP_VERTICAL_OFFSET: 16, + + /** + * Variable: DEFAULT_VALID_COLOR + * + * Specifies the default valid colorr. Default is #0000FF. + */ + DEFAULT_VALID_COLOR: '#00FF00', + + /** + * Variable: DEFAULT_INVALID_COLOR + * + * Specifies the default invalid color. Default is #FF0000. + */ + DEFAULT_INVALID_COLOR: '#FF0000', + + /** + * Variable: HIGHLIGHT_STROKEWIDTH + * + * Defines the strokewidth to be used for the highlights. + * Default is 3. + */ + HIGHLIGHT_STROKEWIDTH: 3, + + /** + * Variable: CURSOR_MOVABLE_VERTEX + * + * Defines the cursor for a movable vertex. Default is 'move'. + */ + CURSOR_MOVABLE_VERTEX: 'move', + + /** + * Variable: CURSOR_MOVABLE_EDGE + * + * Defines the cursor for a movable edge. Default is 'move'. + */ + CURSOR_MOVABLE_EDGE: 'move', + + /** + * Variable: CURSOR_LABEL_HANDLE + * + * Defines the cursor for a movable label. Default is 'default'. + */ + CURSOR_LABEL_HANDLE: 'default', + + /** + * Variable: CURSOR_BEND_HANDLE + * + * Defines the cursor for a movable bend. Default is 'pointer'. + */ + CURSOR_BEND_HANDLE: 'pointer', + + /** + * Variable: CURSOR_CONNECT + * + * Defines the cursor for a connectable state. Default is 'pointer'. + */ + CURSOR_CONNECT: 'pointer', + + /** + * Variable: HIGHLIGHT_COLOR + * + * Defines the color to be used for the cell highlighting. + * Use 'none' for no color. Default is #00FF00. + */ + HIGHLIGHT_COLOR: '#00FF00', + + /** + * Variable: TARGET_HIGHLIGHT_COLOR + * + * Defines the color to be used for highlighting a target cell for a new + * or changed connection. Note that this may be either a source or + * target terminal in the graph. Use 'none' for no color. + * Default is #0000FF. + */ + CONNECT_TARGET_COLOR: '#0000FF', + + /** + * Variable: INVALID_CONNECT_TARGET_COLOR + * + * Defines the color to be used for highlighting a invalid target cells + * for a new or changed connections. Note that this may be either a source + * or target terminal in the graph. Use 'none' for no color. Default is + * #FF0000. + */ + INVALID_CONNECT_TARGET_COLOR: '#FF0000', + + /** + * Variable: DROP_TARGET_COLOR + * + * Defines the color to be used for the highlighting target parent cells + * (for drag and drop). Use 'none' for no color. Default is #0000FF. + */ + DROP_TARGET_COLOR: '#0000FF', + + /** + * Variable: VALID_COLOR + * + * Defines the color to be used for the coloring valid connection + * previews. Use 'none' for no color. Default is #FF0000. + */ + VALID_COLOR: '#00FF00', + + /** + * Variable: INVALID_COLOR + * + * Defines the color to be used for the coloring invalid connection + * previews. Use 'none' for no color. Default is #FF0000. + */ + INVALID_COLOR: '#FF0000', + + /** + * Variable: EDGE_SELECTION_COLOR + * + * Defines the color to be used for the selection border of edges. Use + * 'none' for no color. Default is #00FF00. + */ + EDGE_SELECTION_COLOR: '#00FF00', + + /** + * Variable: VERTEX_SELECTION_COLOR + * + * Defines the color to be used for the selection border of vertices. Use + * 'none' for no color. Default is #00FF00. + */ + VERTEX_SELECTION_COLOR: '#00FF00', + + /** + * Variable: VERTEX_SELECTION_STROKEWIDTH + * + * Defines the strokewidth to be used for vertex selections. + * Default is 1. + */ + VERTEX_SELECTION_STROKEWIDTH: 1, + + /** + * Variable: EDGE_SELECTION_STROKEWIDTH + * + * Defines the strokewidth to be used for edge selections. + * Default is 1. + */ + EDGE_SELECTION_STROKEWIDTH: 1, + + /** + * Variable: SELECTION_DASHED + * + * Defines the dashed state to be used for the vertex selection + * border. Default is true. + */ + VERTEX_SELECTION_DASHED: true, + + /** + * Variable: SELECTION_DASHED + * + * Defines the dashed state to be used for the edge selection + * border. Default is true. + */ + EDGE_SELECTION_DASHED: true, + + /** + * Variable: GUIDE_COLOR + * + * Defines the color to be used for the guidelines in mxGraphHandler. + * Default is #FF0000. + */ + GUIDE_COLOR: '#FF0000', + + /** + * Variable: GUIDE_STROKEWIDTH + * + * Defines the strokewidth to be used for the guidelines in mxGraphHandler. + * Default is 1. + */ + GUIDE_STROKEWIDTH: 1, + + /** + * Variable: OUTLINE_COLOR + * + * Defines the color to be used for the outline rectangle + * border. Use 'none' for no color. Default is #0099FF. + */ + OUTLINE_COLOR: '#0099FF', + + /** + * Variable: OUTLINE_STROKEWIDTH + * + * Defines the strokewidth to be used for the outline rectangle + * stroke width. Default is 3. + */ + OUTLINE_STROKEWIDTH: (mxClient.IS_IE) ? 2 : 3, + + /** + * Variable: HANDLE_SIZE + * + * Defines the default size for handles. Default is 7. + */ + HANDLE_SIZE: 7, + + /** + * Variable: LABEL_HANDLE_SIZE + * + * Defines the default size for label handles. Default is 4. + */ + LABEL_HANDLE_SIZE: 4, + + /** + * Variable: HANDLE_FILLCOLOR + * + * Defines the color to be used for the handle fill color. Use 'none' for + * no color. Default is #00FF00 (green). + */ + HANDLE_FILLCOLOR: '#00FF00', + + /** + * Variable: HANDLE_STROKECOLOR + * + * Defines the color to be used for the handle stroke color. Use 'none' for + * no color. Default is black. + */ + HANDLE_STROKECOLOR: 'black', + + /** + * Variable: LABEL_HANDLE_FILLCOLOR + * + * Defines the color to be used for the label handle fill color. Use 'none' + * for no color. Default is yellow. + */ + LABEL_HANDLE_FILLCOLOR: 'yellow', + + /** + * Variable: CONNECT_HANDLE_FILLCOLOR + * + * Defines the color to be used for the connect handle fill color. Use + * 'none' for no color. Default is #0000FF (blue). + */ + CONNECT_HANDLE_FILLCOLOR: '#0000FF', + + /** + * Variable: LOCKED_HANDLE_FILLCOLOR + * + * Defines the color to be used for the locked handle fill color. Use + * 'none' for no color. Default is #FF0000 (red). + */ + LOCKED_HANDLE_FILLCOLOR: '#FF0000', + + /** + * Variable: OUTLINE_HANDLE_FILLCOLOR + * + * Defines the color to be used for the outline sizer fill color. Use + * 'none' for no color. Default is #00FFFF. + */ + OUTLINE_HANDLE_FILLCOLOR: '#00FFFF', + + /** + * Variable: OUTLINE_HANDLE_STROKECOLOR + * + * Defines the color to be used for the outline sizer stroke color. Use + * 'none' for no color. Default is #0033FF. + */ + OUTLINE_HANDLE_STROKECOLOR: '#0033FF', + + /** + * Variable: DEFAULT_FONTFAMILY + * + * Defines the default family for all fonts in points. Default is + * Arial,Helvetica. + */ + DEFAULT_FONTFAMILY: 'Arial,Helvetica', + + /** + * Variable: DEFAULT_FONTSIZE + * + * Defines the default size for all fonts in points. Default is 11. + */ + DEFAULT_FONTSIZE: 11, + + /** + * Variable: DEFAULT_STARTSIZE + * + * Defines the default start size for swimlanes. Default is 40. + */ + DEFAULT_STARTSIZE: 40, + + /** + * Variable: DEFAULT_MARKERSIZE + * + * Defines the default size for all markers. Default is 6. + */ + DEFAULT_MARKERSIZE: 6, + + /** + * Variable: DEFAULT_IMAGESIZE + * + * Defines the default width and height for images used in the + * label shape. Default is 24. + */ + DEFAULT_IMAGESIZE: 24, + + /** + * Variable: ENTITY_SEGMENT + * + * Defines the length of the horizontal segment of an Entity Relation. + * This can be overridden using <mxConstants.STYLE_SEGMENT> style. + * Default is 30. + */ + ENTITY_SEGMENT: 30, + + /** + * Variable: RECTANGLE_ROUNDING_FACTOR + * + * Defines the rounding factor for rounded rectangles in percent between + * 0 and 1. Values should be smaller than 0.5. Default is 0.15. + */ + RECTANGLE_ROUNDING_FACTOR: 0.15, + + /** + * Variable: LINE_ARCSIZE + * + * Defines the size of the arcs for rounded edges. Default is 20. + */ + LINE_ARCSIZE: 20, + + /** + * Variable: ARROW_SPACING + * + * Defines the spacing between the arrow shape and its terminals. Default + * is 10. + */ + ARROW_SPACING: 10, + + /** + * Variable: ARROW_WIDTH + * + * Defines the width of the arrow shape. Default is 30. + */ + ARROW_WIDTH: 30, + + /** + * Variable: ARROW_SIZE + * + * Defines the size of the arrowhead in the arrow shape. Default is 30. + */ + ARROW_SIZE: 30, + + /** + * Variable: PAGE_FORMAT_A4_PORTRAIT + * + * Defines the rectangle for the A4 portrait page format. The dimensions + * of this page format are 826x1169 pixels. + */ + PAGE_FORMAT_A4_PORTRAIT: new mxRectangle(0, 0, 826, 1169), + + /** + * Variable: PAGE_FORMAT_A4_PORTRAIT + * + * Defines the rectangle for the A4 portrait page format. The dimensions + * of this page format are 826x1169 pixels. + */ + PAGE_FORMAT_A4_LANDSCAPE: new mxRectangle(0, 0, 1169, 826), + + /** + * Variable: PAGE_FORMAT_LETTER_PORTRAIT + * + * Defines the rectangle for the Letter portrait page format. The + * dimensions of this page format are 850x1100 pixels. + */ + PAGE_FORMAT_LETTER_PORTRAIT: new mxRectangle(0, 0, 850, 1100), + + /** + * Variable: PAGE_FORMAT_LETTER_PORTRAIT + * + * Defines the rectangle for the Letter portrait page format. The dimensions + * of this page format are 850x1100 pixels. + */ + PAGE_FORMAT_LETTER_LANDSCAPE: new mxRectangle(0, 0, 1100, 850), + + /** + * Variable: NONE + * + * Defines the value for none. Default is "none". + */ + NONE: 'none', + + /** + * Variable: STYLE_PERIMETER + * + * Defines the key for the perimeter style. This is a function that defines + * the perimeter around a particular shape. Possible values are the + * functions defined in <mxPerimeter>. Alternatively, the constants in this + * class that start with <code>PERIMETER_</code> may be used to access + * perimeter styles in <mxStyleRegistry>. + */ + STYLE_PERIMETER: 'perimeter', + + /** + * Variable: STYLE_SOURCE_PORT + * + * Defines the ID of the cell that should be used for computing the + * perimeter point of the source for an edge. This allows for graphically + * connecting to a cell while keeping the actual terminal of the edge. + */ + STYLE_SOURCE_PORT: 'sourcePort', + + /** + * Variable: STYLE_TARGET_PORT + * + * Defines the ID of the cell that should be used for computing the + * perimeter point of the target for an edge. This allows for graphically + * connecting to a cell while keeping the actual terminal of the edge. + */ + STYLE_TARGET_PORT: 'targetPort', + + /** + * Variable: STYLE_PORT_CONSTRAINT + * + * Defines the direction(s) that edges are allowed to connect to cells in. + * Possible values are <code>DIRECTION_NORTH, DIRECTION_SOUTH, + * DIRECTION_EAST</code> and <code>DIRECTION_WEST</code>. + */ + STYLE_PORT_CONSTRAINT: 'portConstraint', + + /** + * Variable: STYLE_OPACITY + * + * Defines the key for the opacity style. The type of the value is + * numeric and the possible range is 0-100. + */ + STYLE_OPACITY: 'opacity', + + /** + * Variable: STYLE_TEXT_OPACITY + * + * Defines the key for the text opacity style. The type of the value is + * numeric and the possible range is 0-100. + */ + STYLE_TEXT_OPACITY: 'textOpacity', + + /** + * Variable: STYLE_OVERFLOW + * + * Defines the key for the overflow style. Possible values are 'visible', + * 'hidden' and 'fill'. The default value is 'visible'. This value + * specifies how overlapping vertex labels are handled. A value of + * 'visible' will show the complete label. A value of 'hidden' will clip + * the label so that it does not overlap the vertex bounds. A value of + * 'fill' will use the vertex bounds for the label. See + * <mxGraph.isLabelClipped>. + */ + STYLE_OVERFLOW: 'overflow', + + /** + * Variable: STYLE_ORTHOGONAL + * + * Defines if the connection points on either end of the edge should be + * computed so that the edge is vertical or horizontal if possible and + * if the point is not at a fixed location. Default is false. This is + * used in <mxGraph.isOrthogonal>, which also returns true if the edgeStyle + * of the edge is an elbow or entity. + */ + STYLE_ORTHOGONAL: 'orthogonal', + + /** + * Variable: STYLE_EXIT_X + * + * Defines the key for the horizontal relative coordinate connection point + * of an edge with its source terminal. + */ + STYLE_EXIT_X: 'exitX', + + /** + * Variable: STYLE_EXIT_Y + * + * Defines the key for the vertical relative coordinate connection point + * of an edge with its source terminal. + */ + STYLE_EXIT_Y: 'exitY', + + /** + * Variable: STYLE_EXIT_PERIMETER + * + * Defines if the perimeter should be used to find the exact entry point + * along the perimeter of the source. Possible values are 0 (false) and + * 1 (true). Default is 1 (true). + */ + STYLE_EXIT_PERIMETER: 'exitPerimeter', + + /** + * Variable: STYLE_ENTRY_X + * + * Defines the key for the horizontal relative coordinate connection point + * of an edge with its target terminal. + */ + STYLE_ENTRY_X: 'entryX', + + /** + * Variable: STYLE_ENTRY_Y + * + * Defines the key for the vertical relative coordinate connection point + * of an edge with its target terminal. + */ + STYLE_ENTRY_Y: 'entryY', + + /** + * Variable: STYLE_ENTRY_PERIMETER + * + * Defines if the perimeter should be used to find the exact entry point + * along the perimeter of the target. Possible values are 0 (false) and + * 1 (true). Default is 1 (true). + */ + STYLE_ENTRY_PERIMETER: 'entryPerimeter', + + /** + * Variable: STYLE_WHITE_SPACE + * + * Defines the key for the white-space style. Possible values are 'nowrap' + * and 'wrap'. The default value is 'nowrap'. This value specifies how + * white-space inside a HTML vertex label should be handled. A value of + * 'nowrap' means the text will never wrap to the next line until a + * linefeed is encountered. A value of 'wrap' means text will wrap when + * necessary. This style is only used for HTML labels. + * See <mxGraph.isWrapping>. + */ + STYLE_WHITE_SPACE: 'whiteSpace', + + /** + * Variable: STYLE_ROTATION + * + * Defines the key for the rotation style. The type of the value is + * numeric and the possible range is 0-360. + */ + STYLE_ROTATION: 'rotation', + + /** + * Variable: STYLE_FILLCOLOR + * + * Defines the key for the fill color. Possible values are all HTML color + * names or HEX codes, as well as special keywords such as 'swimlane, + * 'inherit' or 'indicated' to use the color code of a related cell or the + * indicator shape. + */ + STYLE_FILLCOLOR: 'fillColor', + + /** + * Variable: STYLE_GRADIENTCOLOR + * + * Defines the key for the gradient color. Possible values are all HTML color + * names or HEX codes, as well as special keywords such as 'swimlane, + * 'inherit' or 'indicated' to use the color code of a related cell or the + * indicator shape. This is ignored if no fill color is defined. + */ + STYLE_GRADIENTCOLOR: 'gradientColor', + + /** + * Variable: STYLE_GRADIENT_DIRECTION + * + * Defines the key for the gradient direction. Possible values are + * <DIRECTION_EAST>, <DIRECTION_WEST>, <DIRECTION_NORTH> and + * <DIRECTION_SOUTH>. Default is <DIRECTION_SOUTH>. Generally, and by + * default in mxGraph, gradient painting is done from the value of + * <STYLE_FILLCOLOR> to the value of <STYLE_GRADIENTCOLOR>. Taking the + * example of <DIRECTION_NORTH>, this means <STYLE_FILLCOLOR> color at the + * bottom of paint pattern and <STYLE_GRADIENTCOLOR> at top, with a + * gradient in-between. + */ + STYLE_GRADIENT_DIRECTION: 'gradientDirection', + + /** + * Variable: STYLE_STROKECOLOR + * + * Defines the key for the strokeColor style. Possible values are all HTML + * color names or HEX codes, as well as special keywords such as 'swimlane, + * 'inherit', 'indicated' to use the color code of a related cell or the + * indicator shape or 'none' for no color. + */ + STYLE_STROKECOLOR: 'strokeColor', + + /** + * Variable: STYLE_SEPARATORCOLOR + * + * Defines the key for the separatorColor style. Possible values are all + * HTML color names or HEX codes. This style is only used for + * <SHAPE_SWIMLANE> shapes. + */ + STYLE_SEPARATORCOLOR: 'separatorColor', + + /** + * Variable: STYLE_STROKEWIDTH + * + * Defines the key for the strokeWidth style. The type of the value is + * numeric and the possible range is any non-negative value larger or equal + * to 1. The value defines the stroke width in pixels. Note: To hide a + * stroke use strokeColor none. + */ + STYLE_STROKEWIDTH: 'strokeWidth', + + /** + * Variable: STYLE_ALIGN + * + * Defines the key for the align style. Possible values are <ALIGN_LEFT>, + * <ALIGN_CENTER> and <ALIGN_RIGHT>. This value defines how the lines of + * the label are horizontally aligned. <ALIGN_LEFT> mean label text lines + * are aligned to left of the label bounds, <ALIGN_RIGHT> to the right of + * the label bounds and <ALIGN_CENTER> means the center of the text lines + * are aligned in the center of the label bounds. Note this value doesn't + * affect the positioning of the overall label bounds relative to the + * vertex, to move the label bounds horizontally, use + * <STYLE_LABEL_POSITION>. + */ + STYLE_ALIGN: 'align', + + /** + * Variable: STYLE_VERTICAL_ALIGN + * + * Defines the key for the verticalAlign style. Possible values are + * <ALIGN_TOP>, <ALIGN_MIDDLE> and <ALIGN_BOTTOM>. This value defines how + * the lines of the label are vertically aligned. <ALIGN_TOP> means the + * topmost label text line is aligned against the top of the label bounds, + * <ALIGN_BOTTOM> means the bottom-most label text line is aligned against + * the bottom of the label bounds and <ALIGN_MIDDLE> means there is equal + * spacing between the topmost text label line and the top of the label + * bounds and the bottom-most text label line and the bottom of the label + * bounds. Note this value doesn't affect the positioning of the overall + * label bounds relative to the vertex, to move the label bounds + * vertically, use <STYLE_VERTICAL_LABEL_POSITION>. + */ + STYLE_VERTICAL_ALIGN: 'verticalAlign', + + /** + * Variable: STYLE_LABEL_POSITION + * + * Defines the key for the horizontal label position of vertices. Possible + * values are <ALIGN_LEFT>, <ALIGN_CENTER> and <ALIGN_RIGHT>. Default is + * <ALIGN_CENTER>. The label align defines the position of the label + * relative to the cell. <ALIGN_LEFT> means the entire label bounds is + * placed completely just to the left of the vertex, <ALIGN_RIGHT> means + * adjust to the right and <ALIGN_CENTER> means the label bounds are + * vertically aligned with the bounds of the vertex. Note this value + * doesn't affect the positioning of label within the label bounds, to move + * the label horizontally within the label bounds, use <STYLE_ALIGN>. + */ + STYLE_LABEL_POSITION: 'labelPosition', + + /** + * Variable: STYLE_VERTICAL_LABEL_POSITION + * + * Defines the key for the vertical label position of vertices. Possible + * values are <ALIGN_TOP>, <ALIGN_BOTTOM> and <ALIGN_MIDDLE>. Default is + * <ALIGN_MIDDLE>. The label align defines the position of the label + * relative to the cell. <ALIGN_TOP> means the entire label bounds is + * placed completely just on the top of the vertex, <ALIGN_BOTTOM> means + * adjust on the bottom and <ALIGN_MIDDLE> means the label bounds are + * horizontally aligned with the bounds of the vertex. Note this value + * doesn't affect the positioning of label within the label bounds, to move + * the label vertically within the label bounds, use + * <STYLE_VERTICAL_ALIGN>. + */ + STYLE_VERTICAL_LABEL_POSITION: 'verticalLabelPosition', + + /** + * Variable: STYLE_IMAGE_ASPECT + * + * Defines the key for the image aspect style. Possible values are 0 (do + * not preserve aspect) or 1 (keep aspect). This is only used in + * <mxImageShape>. Default is 1. + */ + STYLE_IMAGE_ASPECT: 'imageAspect', + + /** + * Variable: STYLE_IMAGE_ALIGN + * + * Defines the key for the align style. Possible values are <ALIGN_LEFT>, + * <ALIGN_CENTER> and <ALIGN_RIGHT>. The value defines how any image in the + * vertex label is aligned horizontally within the label bounds of a + * <SHAPE_LABEL> shape. + */ + STYLE_IMAGE_ALIGN: 'imageAlign', + + /** + * Variable: STYLE_IMAGE_VERTICAL_ALIGN + * + * Defines the key for the verticalAlign style. Possible values are + * <ALIGN_TOP>, <ALIGN_MIDDLE> and <ALIGN_BOTTOM>. The value defines how + * any image in the vertex label is aligned vertically within the label + * bounds of a <SHAPE_LABEL> shape. + */ + STYLE_IMAGE_VERTICAL_ALIGN: 'imageVerticalAlign', + + /** + * Variable: STYLE_GLASS + * + * Defines the key for the glass style. Possible values are 0 (disabled) and + * 1(enabled). The default value is 0. This is used in <mxLabel>. + */ + STYLE_GLASS: 'glass', + + /** + * Variable: STYLE_IMAGE + * + * Defines the key for the image style. Possible values are any image URL, + * the type of the value is String. This is the path to the image to image + * that is to be displayed within the label of a vertex. Data URLs should + * use the following format: data:image/png,xyz where xyz is the base64 + * encoded data (without the "base64"-prefix). Note that Data URLs are only + * supported in modern browsers. + */ + STYLE_IMAGE: 'image', + + /** + * Variable: STYLE_IMAGE_WIDTH + * + * Defines the key for the imageWidth style. The type of this value is + * int, the value is the image width in pixels and must be greater than 0. + */ + STYLE_IMAGE_WIDTH: 'imageWidth', + + /** + * Variable: STYLE_IMAGE_HEIGHT + * + * Defines the key for the imageHeight style. The type of this value is + * int, the value is the image height in pixels and must be greater than 0. + */ + STYLE_IMAGE_HEIGHT: 'imageHeight', + + /** + * Variable: STYLE_IMAGE_BACKGROUND + * + * Defines the key for the image background color. This style is only used + * in <mxImageShape>. Possible values are all HTML color names or HEX + * codes. + */ + STYLE_IMAGE_BACKGROUND: 'imageBackground', + + /** + * Variable: STYLE_IMAGE_BORDER + * + * Defines the key for the image border color. This style is only used in + * <mxImageShape>. Possible values are all HTML color names or HEX codes. + */ + STYLE_IMAGE_BORDER: 'imageBorder', + + /** + * Variable: STYLE_IMAGE_FLIPH + * + * Defines the key for the horizontal image flip. This style is only used + * in <mxImageShape>. Possible values are 0 and 1. Default is 0. + */ + STYLE_IMAGE_FLIPH: 'imageFlipH', + + /** + * Variable: STYLE_IMAGE_FLIPV + * + * Defines the key for the vertical image flip. This style is only used + * in <mxImageShape>. Possible values are 0 and 1. Default is 0. + */ + STYLE_IMAGE_FLIPV: 'imageFlipV', + + /** + * Variable: STYLE_STENCIL_FLIPH + * + * Defines the key for the horizontal stencil flip. This style is only used + * for <mxStencilShape>. Possible values are 0 and 1. Default is 0. + */ + STYLE_STENCIL_FLIPH: 'stencilFlipH', + + /** + * Variable: STYLE_STENCIL_FLIPV + * + * Defines the key for the vertical stencil flip. This style is only used + * for <mxStencilShape>. Possible values are 0 and 1. Default is 0. + */ + STYLE_STENCIL_FLIPV: 'stencilFlipV', + + /** + * Variable: STYLE_NOLABEL + * + * Defines the key for the noLabel style. If this is + * true then no label is visible for a given cell. + * Possible values are true or false (1 or 0). + * Default is false. + */ + STYLE_NOLABEL: 'noLabel', + + /** + * Variable: STYLE_NOEDGESTYLE + * + * Defines the key for the noEdgeStyle style. If this is + * true then no edge style is applied for a given edge. + * Possible values are true or false (1 or 0). + * Default is false. + */ + STYLE_NOEDGESTYLE: 'noEdgeStyle', + + /** + * Variable: STYLE_LABEL_BACKGROUNDCOLOR + * + * Defines the key for the label background color. Possible values are all + * HTML color names or HEX codes. + */ + STYLE_LABEL_BACKGROUNDCOLOR: 'labelBackgroundColor', + + /** + * Variable: STYLE_LABEL_BORDERCOLOR + * + * Defines the key for the label border color. Possible values are all + * HTML color names or HEX codes. + */ + STYLE_LABEL_BORDERCOLOR: 'labelBorderColor', + + /** + * Variable: STYLE_LABEL_PADDING + * + * Defines the key for the label padding, ie. the space between the label + * border and the label. + */ + STYLE_LABEL_PADDING: 'labelPadding', + + /** + * Variable: STYLE_INDICATOR_SHAPE + * + * Defines the key for the indicator shape used within an <mxLabel>. + * Possible values are all SHAPE_* constants or the names of any new + * shapes. The indicatorShape has precedence over the indicatorImage. + */ + STYLE_INDICATOR_SHAPE: 'indicatorShape', + + /** + * Variable: STYLE_INDICATOR_IMAGE + * + * Defines the key for the indicator image used within an <mxLabel>. + * Possible values are all image URLs. The indicatorShape has + * precedence over the indicatorImage. + */ + STYLE_INDICATOR_IMAGE: 'indicatorImage', + + /** + * Variable: STYLE_INDICATOR_COLOR + * + * Defines the key for the indicatorColor style. Possible values are all + * HTML color names or HEX codes, as well as the special 'swimlane' keyword + * to refer to the color of the parent swimlane if one exists. + */ + STYLE_INDICATOR_COLOR: 'indicatorColor', + + /** + * Variable: STYLE_INDICATOR_STROKECOLOR + * + * Defines the key for the indicator stroke color in <mxLabel>. + * Possible values are all color codes. + */ + STYLE_INDICATOR_STROKECOLOR: 'indicatorStrokeColor', + + /** + * Variable: STYLE_INDICATOR_GRADIENTCOLOR + * + * Defines the key for the indicatorGradientColor style. Possible values + * are all HTML color names or HEX codes. This style is only supported in + * <SHAPE_LABEL> shapes. + */ + STYLE_INDICATOR_GRADIENTCOLOR: 'indicatorGradientColor', + + /** + * Variable: STYLE_INDICATOR_SPACING + * + * The defines the key for the spacing between the label and the + * indicator in <mxLabel>. Possible values are in pixels. + */ + STYLE_INDICATOR_SPACING: 'indicatorSpacing', + + /** + * Variable: STYLE_INDICATOR_WIDTH + * + * Defines the key for the indicator width. + * Possible values start at 0 (in pixels). + */ + STYLE_INDICATOR_WIDTH: 'indicatorWidth', + + /** + * Variable: STYLE_INDICATOR_HEIGHT + * + * Defines the key for the indicator height. + * Possible values start at 0 (in pixels). + */ + STYLE_INDICATOR_HEIGHT: 'indicatorHeight', + + /** + * Variable: STYLE_INDICATOR_DIRECTION + * + * Defines the key for the indicatorDirection style. The direction style is + * used to specify the direction of certain shapes (eg. <mxTriangle>). + * Possible values are <DIRECTION_EAST> (default), <DIRECTION_WEST>, + * <DIRECTION_NORTH> and <DIRECTION_SOUTH>. + */ + STYLE_INDICATOR_DIRECTION: 'indicatorDirection', + + /** + * Variable: STYLE_SHADOW + * + * Defines the key for the shadow style. The type of the value is Boolean. + */ + STYLE_SHADOW: 'shadow', + + /** + * Variable: STYLE_SEGMENT + * + * Defines the key for the segment style. The type of this value is + * float and the value represents the size of the horizontal + * segment of the entity relation style. Default is ENTITY_SEGMENT. + */ + STYLE_SEGMENT: 'segment', + + /** + * Variable: STYLE_ENDARROW + * + * Defines the key for the end arrow marker. + * Possible values are all constants with an ARROW-prefix. + * This is only used in <mxConnector>. + * + * Example: + * (code) + * style[mxConstants.STYLE_ENDARROW] = mxConstants.ARROW_CLASSIC; + * (end) + */ + STYLE_ENDARROW: 'endArrow', + + /** + * Variable: STYLE_STARTARROW + * + * Defines the key for the start arrow marker. + * Possible values are all constants with an ARROW-prefix. + * This is only used in <mxConnector>. + * See <STYLE_ENDARROW>. + */ + STYLE_STARTARROW: 'startArrow', + + /** + * Variable: STYLE_ENDSIZE + * + * Defines the key for the endSize style. The type of this value is numeric + * and the value represents the size of the end marker in pixels. + */ + STYLE_ENDSIZE: 'endSize', + + /** + * Variable: STYLE_STARTSIZE + * + * Defines the key for the startSize style. The type of this value is + * numeric and the value represents the size of the start marker or the + * size of the swimlane title region depending on the shape it is used for. + */ + STYLE_STARTSIZE: 'startSize', + + /** + * Variable: STYLE_ENDFILL + * + * Defines the key for the endFill style. Use 0 for no fill or 1 + * (default) for fill. (This style is only exported via <mxImageExport>.) + */ + STYLE_ENDFILL: 'endFill', + + /** + * Variable: STYLE_STARTFILL + * + * Defines the key for the startFill style. Use 0 for no fill or 1 + * (default) for fill. (This style is only exported via <mxImageExport>.) + */ + STYLE_STARTFILL: 'startFill', + + /** + * Variable: STYLE_DASHED + * + * Defines the key for the dashed style. Use 0 (default) for non-dashed or 1 + * for dashed. + */ + STYLE_DASHED: 'dashed', + + /** + * Defines the key for the dashed pattern style in SVG and image exports. + * The type of this value is a space separated list of numbers that specify + * a custom-defined dash pattern. Dash styles are defined in terms of the + * length of the dash (the drawn part of the stroke) and the length of the + * space between the dashes. The lengths are relative to the line width: a + * length of "1" is equal to the line width. VML ignores this style and + * uses dashStyle instead as defined in the VML specification. This style + * is only used in the <mxConnector> shape. + */ + STYLE_DASH_PATTERN: 'dashPattern', + + /** + * Variable: STYLE_ROUNDED + * + * Defines the key for the rounded style. The type of this value is + * Boolean. For edges this determines whether or not joins between edges + * segments are smoothed to a rounded finish. For vertices that have the + * rectangle shape, this determines whether or not the rectangle is + * rounded. + */ + STYLE_ROUNDED: 'rounded', + + /** + * Variable: STYLE_ARCSIZE + * + * Defines the rounding factor for a rounded rectangle in percent (without + * the percent sign). Possible values are between 0 and 100. If this value + * is not specified then RECTANGLE_ROUNDING_FACTOR * 100 is used. + * (This style is only exported via <mxImageExport>.) + */ + STYLE_ARCSIZE: 'arcSize', + + /** + * Variable: STYLE_SMOOTH + * + * An experimental style for edges. This style is currently not available + * in the backends and is implemented differently for VML and SVG. The use + * of this style is currently only recommended for VML. + */ + STYLE_SMOOTH: 'smooth', + + /** + * Variable: STYLE_SOURCE_PERIMETER_SPACING + * + * Defines the key for the source perimeter spacing. The type of this value + * is numeric. This is the distance between the source connection point of + * an edge and the perimeter of the source vertex in pixels. This style + * only applies to edges. + */ + STYLE_SOURCE_PERIMETER_SPACING: 'sourcePerimeterSpacing', + + /** + * Variable: STYLE_TARGET_PERIMETER_SPACING + * + * Defines the key for the target perimeter spacing. The type of this value + * is numeric. This is the distance between the target connection point of + * an edge and the perimeter of the target vertex in pixels. This style + * only applies to edges. + */ + STYLE_TARGET_PERIMETER_SPACING: 'targetPerimeterSpacing', + + /** + * Variable: STYLE_PERIMETER_SPACING + * + * Defines the key for the perimeter spacing. This is the distance between + * the connection point and the perimeter in pixels. When used in a vertex + * style, this applies to all incoming edges to floating ports (edges that + * terminate on the perimeter of the vertex). When used in an edge style, + * this spacing applies to the source and target separately, if they + * terminate in floating ports (on the perimeter of the vertex). + */ + STYLE_PERIMETER_SPACING: 'perimeterSpacing', + + /** + * Variable: STYLE_SPACING + * + * Defines the key for the spacing. The value represents the spacing, in + * pixels, added to each side of a label in a vertex (style applies to + * vertices only). + */ + STYLE_SPACING: 'spacing', + + /** + * Variable: STYLE_SPACING_TOP + * + * Defines the key for the spacingTop style. The value represents the + * spacing, in pixels, added to the top side of a label in a vertex (style + * applies to vertices only). + */ + STYLE_SPACING_TOP: 'spacingTop', + + /** + * Variable: STYLE_SPACING_LEFT + * + * Defines the key for the spacingLeft style. The value represents the + * spacing, in pixels, added to the left side of a label in a vertex (style + * applies to vertices only). + */ + STYLE_SPACING_LEFT: 'spacingLeft', + + /** + * Variable: STYLE_SPACING_BOTTOM + * + * Defines the key for the spacingBottom style The value represents the + * spacing, in pixels, added to the bottom side of a label in a vertex + * (style applies to vertices only). + */ + STYLE_SPACING_BOTTOM: 'spacingBottom', + + /** + * Variable: STYLE_SPACING_RIGHT + * + * Defines the key for the spacingRight style The value represents the + * spacing, in pixels, added to the right side of a label in a vertex (style + * applies to vertices only). + */ + STYLE_SPACING_RIGHT: 'spacingRight', + + /** + * Variable: STYLE_HORIZONTAL + * + * Defines the key for the horizontal style. Possible values are + * true or false. This value only applies to vertices. If the <STYLE_SHAPE> + * is <code>SHAPE_SWIMLANE</code> a value of false indicates that the + * swimlane should be drawn vertically, true indicates to draw it + * horizontally. If the shape style does not indicate that this vertex is a + * swimlane, this value affects only whether the label is drawn + * horizontally or vertically. + */ + STYLE_HORIZONTAL: 'horizontal', + + /** + * Variable: STYLE_DIRECTION + * + * Defines the key for the direction style. The direction style is used + * to specify the direction of certain shapes (eg. <mxTriangle>). + * Possible values are <DIRECTION_EAST> (default), <DIRECTION_WEST>, + * <DIRECTION_NORTH> and <DIRECTION_SOUTH>. + */ + STYLE_DIRECTION: 'direction', + + /** + * Variable: STYLE_ELBOW + * + * Defines the key for the elbow style. Possible values are + * <ELBOW_HORIZONTAL> and <ELBOW_VERTICAL>. Default is <ELBOW_HORIZONTAL>. + * This defines how the three segment orthogonal edge style leaves its + * terminal vertices. The vertical style leaves the terminal vertices at + * the top and bottom sides. + */ + STYLE_ELBOW: 'elbow', + + /** + * Variable: STYLE_FONTCOLOR + * + * Defines the key for the fontColor style. Possible values are all HTML + * color names or HEX codes. + */ + STYLE_FONTCOLOR: 'fontColor', + + /** + * Variable: STYLE_FONTFAMILY + * + * Defines the key for the fontFamily style. Possible values are names such + * as Arial; Dialog; Verdana; Times New Roman. The value is of type String. + */ + STYLE_FONTFAMILY: 'fontFamily', + + /** + * Variable: STYLE_FONTSIZE + * + * Defines the key for the fontSize style (in points). The type of the value + * is int. + */ + STYLE_FONTSIZE: 'fontSize', + + /** + * Variable: STYLE_FONTSTYLE + * + * Defines the key for the fontStyle style. Values may be any logical AND + * (sum) of <FONT_BOLD>, <FONT_ITALIC>, <FONT_UNDERLINE> and <FONT_SHADOW>. + * The type of the value is int. + */ + STYLE_FONTSTYLE: 'fontStyle', + + /** + * Variable: STYLE_AUTOSIZE + * + * Defines the key for the autosize style. This specifies if a cell should be + * resized automatically if the value has changed. Possible values are 0 or 1. + * Default is 0. See <mxGraph.isAutoSizeCell>. This is normally combined with + * <STYLE_RESIZABLE> to disable manual sizing. + */ + STYLE_AUTOSIZE: 'autosize', + + /** + * Variable: STYLE_FOLDABLE + * + * Defines the key for the foldable style. This specifies if a cell is foldable + * using a folding icon. Possible values are 0 or 1. Default is 1. See + * <mxGraph.isCellFoldable>. + */ + STYLE_FOLDABLE: 'foldable', + + /** + * Variable: STYLE_EDITABLE + * + * Defines the key for the editable style. This specifies if the value of + * a cell can be edited using the in-place editor. Possible values are 0 or + * 1. Default is 1. See <mxGraph.isCellEditable>. + */ + STYLE_EDITABLE: 'editable', + + /** + * Variable: STYLE_BENDABLE + * + * Defines the key for the bendable style. This specifies if the control + * points of an edge can be moved. Possible values are 0 or 1. Default is + * 1. See <mxGraph.isCellBendable>. + */ + STYLE_BENDABLE: 'bendable', + + /** + * Variable: STYLE_MOVABLE + * + * Defines the key for the movable style. This specifies if a cell can + * be moved. Possible values are 0 or 1. Default is 1. See + * <mxGraph.isCellMovable>. + */ + STYLE_MOVABLE: 'movable', + + /** + * Variable: STYLE_RESIZABLE + * + * Defines the key for the resizable style. This specifies if a cell can + * be resized. Possible values are 0 or 1. Default is 1. See + * <mxGraph.isCellResizable>. + */ + STYLE_RESIZABLE: 'resizable', + + /** + * Variable: STYLE_CLONEABLE + * + * Defines the key for the cloneable style. This specifies if a cell can + * be cloned. Possible values are 0 or 1. Default is 1. See + * <mxGraph.isCellCloneable>. + */ + STYLE_CLONEABLE: 'cloneable', + + /** + * Variable: STYLE_DELETABLE + * + * Defines the key for the deletable style. This specifies if a cell can be + * deleted. Possible values are 0 or 1. Default is 1. See + * <mxGraph.isCellDeletable>. + */ + STYLE_DELETABLE: 'deletable', + + /** + * Variable: STYLE_SHAPE + * + * Defines the key for the shape. Possible values are all constants + * with a SHAPE-prefix or any newly defined shape names. + */ + STYLE_SHAPE: 'shape', + + /** + * Variable: STYLE_EDGE + * + * Defines the key for the edge style. Possible values are the functions + * defined in <mxEdgeStyle>. + */ + STYLE_EDGE: 'edgeStyle', + + /** + * Variable: STYLE_LOOP + * + * Defines the key for the loop style. Possible values are the functions + * defined in <mxEdgeStyle>. + */ + STYLE_LOOP: 'loopStyle', + + /** + * Variable: STYLE_ROUTING_CENTER_X + * + * Defines the key for the horizontal routing center. Possible values are + * between -0.5 and 0.5. This is the relative offset from the center used + * for connecting edges. The type of this value is numeric. + */ + STYLE_ROUTING_CENTER_X: 'routingCenterX', + + /** + * Variable: STYLE_ROUTING_CENTER_Y + * + * Defines the key for the vertical routing center. Possible values are + * between -0.5 and 0.5. This is the relative offset from the center used + * for connecting edges. The type of this value is numeric. + */ + STYLE_ROUTING_CENTER_Y: 'routingCenterY', + + /** + * Variable: FONT_BOLD + * + * Constant for bold fonts. Default is 1. + */ + FONT_BOLD: 1, + + /** + * Variable: FONT_ITALIC + * + * Constant for italic fonts. Default is 2. + */ + FONT_ITALIC: 2, + + /** + * Variable: FONT_UNDERLINE + * + * Constant for underlined fonts. Default is 4. + */ + FONT_UNDERLINE: 4, + + /** + * Variable: FONT_SHADOW + * + * Constant for fonts with a shadow. Default is 8. + */ + FONT_SHADOW: 8, + + /** + * Variable: SHAPE_RECTANGLE + * + * Name under which <mxRectangleShape> is registered + * in <mxCellRenderer>. Default is rectangle. + */ + SHAPE_RECTANGLE: 'rectangle', + + /** + * Variable: SHAPE_ELLIPSE + * + * Name under which <mxEllipse> is registered + * in <mxCellRenderer>. Default is ellipse. + */ + SHAPE_ELLIPSE: 'ellipse', + + /** + * Variable: SHAPE_DOUBLE_ELLIPSE + * + * Name under which <mxDoubleEllipse> is registered + * in <mxCellRenderer>. Default is doubleEllipse. + */ + SHAPE_DOUBLE_ELLIPSE: 'doubleEllipse', + + /** + * Variable: SHAPE_RHOMBUS + * + * Name under which <mxRhombus> is registered + * in <mxCellRenderer>. Default is rhombus. + */ + SHAPE_RHOMBUS: 'rhombus', + + /** + * Variable: SHAPE_LINE + * + * Name under which <mxLine> is registered + * in <mxCellRenderer>. Default is line. + */ + SHAPE_LINE: 'line', + + /** + * Variable: SHAPE_IMAGE + * + * Name under which <mxImageShape> is registered + * in <mxCellRenderer>. Default is image. + */ + SHAPE_IMAGE: 'image', + + /** + * Variable: SHAPE_ARROW + * + * Name under which <mxArrow> is registered + * in <mxCellRenderer>. Default is arrow. + */ + SHAPE_ARROW: 'arrow', + + /** + * Variable: SHAPE_LABEL + * + * Name under which <mxLabel> is registered + * in <mxCellRenderer>. Default is label. + */ + SHAPE_LABEL: 'label', + + /** + * Variable: SHAPE_CYLINDER + * + * Name under which <mxCylinder> is registered + * in <mxCellRenderer>. Default is cylinder. + */ + SHAPE_CYLINDER: 'cylinder', + + /** + * Variable: SHAPE_SWIMLANE + * + * Name under which <mxSwimlane> is registered + * in <mxCellRenderer>. Default is swimlane. + */ + SHAPE_SWIMLANE: 'swimlane', + + /** + * Variable: SHAPE_CONNECTOR + * + * Name under which <mxConnector> is registered + * in <mxCellRenderer>. Default is connector. + */ + SHAPE_CONNECTOR: 'connector', + + /** + * Variable: SHAPE_ACTOR + * + * Name under which <mxActor> is registered + * in <mxCellRenderer>. Default is actor. + */ + SHAPE_ACTOR: 'actor', + + /** + * Variable: SHAPE_CLOUD + * + * Name under which <mxCloud> is registered + * in <mxCellRenderer>. Default is cloud. + */ + SHAPE_CLOUD: 'cloud', + + /** + * Variable: SHAPE_TRIANGLE + * + * Name under which <mxTriangle> is registered + * in <mxCellRenderer>. Default is triangle. + */ + SHAPE_TRIANGLE: 'triangle', + + /** + * Variable: SHAPE_HEXAGON + * + * Name under which <mxHexagon> is registered + * in <mxCellRenderer>. Default is hexagon. + */ + SHAPE_HEXAGON: 'hexagon', + + /** + * Variable: ARROW_CLASSIC + * + * Constant for classic arrow markers. + */ + ARROW_CLASSIC: 'classic', + + /** + * Variable: ARROW_BLOCK + * + * Constant for block arrow markers. + */ + ARROW_BLOCK: 'block', + + /** + * Variable: ARROW_OPEN + * + * Constant for open arrow markers. + */ + ARROW_OPEN: 'open', + + /** + * Variable: ARROW_OVAL + * + * Constant for oval arrow markers. + */ + ARROW_OVAL: 'oval', + + /** + * Variable: ARROW_DIAMOND + * + * Constant for diamond arrow markers. + */ + ARROW_DIAMOND: 'diamond', + + /** + * Variable: ARROW_DIAMOND + * + * Constant for diamond arrow markers. + */ + ARROW_DIAMOND_THIN: 'diamondThin', + + /** + * Variable: ALIGN_LEFT + * + * Constant for left horizontal alignment. Default is left. + */ + ALIGN_LEFT: 'left', + + /** + * Variable: ALIGN_CENTER + * + * Constant for center horizontal alignment. Default is center. + */ + ALIGN_CENTER: 'center', + + /** + * Variable: ALIGN_RIGHT + * + * Constant for right horizontal alignment. Default is right. + */ + ALIGN_RIGHT: 'right', + + /** + * Variable: ALIGN_TOP + * + * Constant for top vertical alignment. Default is top. + */ + ALIGN_TOP: 'top', + + /** + * Variable: ALIGN_MIDDLE + * + * Constant for middle vertical alignment. Default is middle. + */ + ALIGN_MIDDLE: 'middle', + + /** + * Variable: ALIGN_BOTTOM + * + * Constant for bottom vertical alignment. Default is bottom. + */ + ALIGN_BOTTOM: 'bottom', + + /** + * Variable: DIRECTION_NORTH + * + * Constant for direction north. Default is north. + */ + DIRECTION_NORTH: 'north', + + /** + * Variable: DIRECTION_SOUTH + * + * Constant for direction south. Default is south. + */ + DIRECTION_SOUTH: 'south', + + /** + * Variable: DIRECTION_EAST + * + * Constant for direction east. Default is east. + */ + DIRECTION_EAST: 'east', + + /** + * Variable: DIRECTION_WEST + * + * Constant for direction west. Default is west. + */ + DIRECTION_WEST: 'west', + + /** + * Variable: DIRECTION_MASK_NONE + * + * Constant for no direction. + */ + DIRECTION_MASK_NONE: 0, + + /** + * Variable: DIRECTION_MASK_WEST + * + * Bitwise mask for west direction. + */ + DIRECTION_MASK_WEST: 1, + + /** + * Variable: DIRECTION_MASK_NORTH + * + * Bitwise mask for north direction. + */ + DIRECTION_MASK_NORTH: 2, + + /** + * Variable: DIRECTION_MASK_SOUTH + * + * Bitwise mask for south direction. + */ + DIRECTION_MASK_SOUTH: 4, + + /** + * Variable: DIRECTION_MASK_EAST + * + * Bitwise mask for east direction. + */ + DIRECTION_MASK_EAST: 8, + + /** + * Variable: DIRECTION_MASK_ALL + * + * Bitwise mask for all directions. + */ + DIRECTION_MASK_ALL: 15, + + /** + * Variable: ELBOW_VERTICAL + * + * Constant for elbow vertical. Default is horizontal. + */ + ELBOW_VERTICAL: 'vertical', + + /** + * Variable: ELBOW_HORIZONTAL + * + * Constant for elbow horizontal. Default is horizontal. + */ + ELBOW_HORIZONTAL: 'horizontal', + + /** + * Variable: EDGESTYLE_ELBOW + * + * Name of the elbow edge style. Can be used as a string value + * for the STYLE_EDGE style. + */ + EDGESTYLE_ELBOW: 'elbowEdgeStyle', + + /** + * Variable: EDGESTYLE_ENTITY_RELATION + * + * Name of the entity relation edge style. Can be used as a string value + * for the STYLE_EDGE style. + */ + EDGESTYLE_ENTITY_RELATION: 'entityRelationEdgeStyle', + + /** + * Variable: EDGESTYLE_LOOP + * + * Name of the loop edge style. Can be used as a string value + * for the STYLE_EDGE style. + */ + EDGESTYLE_LOOP: 'loopEdgeStyle', + + /** + * Variable: EDGESTYLE_SIDETOSIDE + * + * Name of the side to side edge style. Can be used as a string value + * for the STYLE_EDGE style. + */ + EDGESTYLE_SIDETOSIDE: 'sideToSideEdgeStyle', + + /** + * Variable: EDGESTYLE_TOPTOBOTTOM + * + * Name of the top to bottom edge style. Can be used as a string value + * for the STYLE_EDGE style. + */ + EDGESTYLE_TOPTOBOTTOM: 'topToBottomEdgeStyle', + + /** + * Variable: EDGESTYLE_ORTHOGONAL + * + * Name of the generic orthogonal edge style. Can be used as a string value + * for the STYLE_EDGE style. + */ + EDGESTYLE_ORTHOGONAL: 'orthogonalEdgeStyle', + + /** + * Variable: EDGESTYLE_SEGMENT + * + * Name of the generic segment edge style. Can be used as a string value + * for the STYLE_EDGE style. + */ + EDGESTYLE_SEGMENT: 'segmentEdgeStyle', + + /** + * Variable: PERIMETER_ELLIPSE + * + * Name of the ellipse perimeter. Can be used as a string value + * for the STYLE_PERIMETER style. + */ + PERIMETER_ELLIPSE: 'ellipsePerimeter', + + /** + * Variable: PERIMETER_RECTANGLE + * + * Name of the rectangle perimeter. Can be used as a string value + * for the STYLE_PERIMETER style. + */ + PERIMETER_RECTANGLE: 'rectanglePerimeter', + + /** + * Variable: PERIMETER_RHOMBUS + * + * Name of the rhombus perimeter. Can be used as a string value + * for the STYLE_PERIMETER style. + */ + PERIMETER_RHOMBUS: 'rhombusPerimeter', + + /** + * Variable: PERIMETER_TRIANGLE + * + * Name of the triangle perimeter. Can be used as a string value + * for the STYLE_PERIMETER style. + */ + PERIMETER_TRIANGLE: 'trianglePerimeter' + +}; diff --git a/src/js/util/mxDictionary.js b/src/js/util/mxDictionary.js new file mode 100644 index 0000000..a2e503a --- /dev/null +++ b/src/js/util/mxDictionary.js @@ -0,0 +1,130 @@ +/** + * $Id: mxDictionary.js,v 1.12 2012-04-26 08:08:54 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxDictionary + * + * A wrapper class for an associative array with object keys. Note: This + * implementation uses <mxObjectIdentitiy> to turn object keys into strings. + * + * Constructor: mxEventSource + * + * Constructs a new dictionary which allows object to be used as keys. + */ +function mxDictionary() +{ + this.clear(); +}; + +/** + * Function: map + * + * Stores the (key, value) pairs in this dictionary. + */ +mxDictionary.prototype.map = null; + +/** + * Function: clear + * + * Clears the dictionary. + */ +mxDictionary.prototype.clear = function() +{ + this.map = {}; +}; + +/** + * Function: get + * + * Returns the value for the given key. + */ +mxDictionary.prototype.get = function(key) +{ + var id = mxObjectIdentity.get(key); + + return this.map[id]; +}; + +/** + * Function: put + * + * Stores the value under the given key and returns the previous + * value for that key. + */ +mxDictionary.prototype.put = function(key, value) +{ + var id = mxObjectIdentity.get(key); + var previous = this.map[id]; + this.map[id] = value; + + return previous; +}; + +/** + * Function: remove + * + * Removes the value for the given key and returns the value that + * has been removed. + */ +mxDictionary.prototype.remove = function(key) +{ + var id = mxObjectIdentity.get(key); + var previous = this.map[id]; + delete this.map[id]; + + return previous; +}; + +/** + * Function: getKeys + * + * Returns all keys as an array. + */ +mxDictionary.prototype.getKeys = function() +{ + var result = []; + + for (var key in this.map) + { + result.push(key); + } + + return result; +}; + +/** + * Function: getValues + * + * Returns all values as an array. + */ +mxDictionary.prototype.getValues = function() +{ + var result = []; + + for (var key in this.map) + { + result.push(this.map[key]); + } + + return result; +}; + +/** + * Function: visit + * + * Visits all entries in the dictionary using the given function with the + * following signature: function(key, value) where key is a string and + * value is an object. + * + * Parameters: + * + * visitor - A function that takes the key and value as arguments. + */ +mxDictionary.prototype.visit = function(visitor) +{ + for (var key in this.map) + { + visitor(key, this.map[key]); + } +}; diff --git a/src/js/util/mxDivResizer.js b/src/js/util/mxDivResizer.js new file mode 100644 index 0000000..2a2e4eb --- /dev/null +++ b/src/js/util/mxDivResizer.js @@ -0,0 +1,151 @@ +/** + * $Id: mxDivResizer.js,v 1.22 2010-01-02 09:45:14 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxDivResizer + * + * Maintains the size of a div element in Internet Explorer. This is a + * workaround for the right and bottom style being ignored in IE. + * + * If you need a div to cover the scrollwidth and -height of a document, + * then you can use this class as follows: + * + * (code) + * var resizer = new mxDivResizer(background); + * resizer.getDocumentHeight = function() + * { + * return document.body.scrollHeight; + * } + * resizer.getDocumentWidth = function() + * { + * return document.body.scrollWidth; + * } + * resizer.resize(); + * (end) + * + * Constructor: mxDivResizer + * + * Constructs an object that maintains the size of a div + * element when the window is being resized. This is only + * required for Internet Explorer as it ignores the respective + * stylesheet information for DIV elements. + * + * Parameters: + * + * div - Reference to the DOM node whose size should be maintained. + * container - Optional Container that contains the div. Default is the + * window. + */ +function mxDivResizer(div, container) +{ + if (div.nodeName.toLowerCase() == 'div') + { + if (container == null) + { + container = window; + } + + this.div = div; + var style = mxUtils.getCurrentStyle(div); + + if (style != null) + { + this.resizeWidth = style.width == 'auto'; + this.resizeHeight = style.height == 'auto'; + } + + mxEvent.addListener(container, 'resize', + mxUtils.bind(this, function(evt) + { + if (!this.handlingResize) + { + this.handlingResize = true; + this.resize(); + this.handlingResize = false; + } + }) + ); + + this.resize(); + } +}; + +/** + * Function: resizeWidth + * + * Boolean specifying if the width should be updated. + */ +mxDivResizer.prototype.resizeWidth = true; + +/** + * Function: resizeHeight + * + * Boolean specifying if the height should be updated. + */ +mxDivResizer.prototype.resizeHeight = true; + +/** + * Function: handlingResize + * + * Boolean specifying if the width should be updated. + */ +mxDivResizer.prototype.handlingResize = false; + +/** + * Function: resize + * + * Updates the style of the DIV after the window has been resized. + */ +mxDivResizer.prototype.resize = function() +{ + var w = this.getDocumentWidth(); + var h = this.getDocumentHeight(); + + var l = parseInt(this.div.style.left); + var r = parseInt(this.div.style.right); + var t = parseInt(this.div.style.top); + var b = parseInt(this.div.style.bottom); + + if (this.resizeWidth && + !isNaN(l) && + !isNaN(r) && + l >= 0 && + r >= 0 && + w - r - l > 0) + { + this.div.style.width = (w - r - l)+'px'; + } + + if (this.resizeHeight && + !isNaN(t) && + !isNaN(b) && + t >= 0 && + b >= 0 && + h - t - b > 0) + { + this.div.style.height = (h - t - b)+'px'; + } +}; + +/** + * Function: getDocumentWidth + * + * Hook for subclassers to return the width of the document (without + * scrollbars). + */ +mxDivResizer.prototype.getDocumentWidth = function() +{ + return document.body.clientWidth; +}; + +/** + * Function: getDocumentHeight + * + * Hook for subclassers to return the height of the document (without + * scrollbars). + */ +mxDivResizer.prototype.getDocumentHeight = function() +{ + return document.body.clientHeight; +}; diff --git a/src/js/util/mxDragSource.js b/src/js/util/mxDragSource.js new file mode 100644 index 0000000..d0cafdf --- /dev/null +++ b/src/js/util/mxDragSource.js @@ -0,0 +1,594 @@ +/** + * $Id: mxDragSource.js,v 1.14 2012-12-05 21:43:16 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxDragSource + * + * Wrapper to create a drag source from a DOM element so that the element can + * be dragged over a graph and dropped into the graph as a new cell. + * + * TODO: Problem is that in the dropHandler the current preview location is + * not available, so the preview and the dropHandler must match. + * + * Constructor: mxDragSource + * + * Constructs a new drag source for the given element. + */ +function mxDragSource(element, dropHandler) +{ + this.element = element; + this.dropHandler = dropHandler; + + // Handles a drag gesture on the element + var md = (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown'; + mxEvent.addListener(element, md, mxUtils.bind(this, this.mouseDown)); +}; + +/** + * Variable: element + * + * Reference to the DOM node which was made draggable. + */ +mxDragSource.prototype.element = null; + +/** + * Variable: dropHandler + * + * Holds the DOM node that is used to represent the drag preview. If this is + * null then the source element will be cloned and used for the drag preview. + */ +mxDragSource.prototype.dropHandler = null; + +/** + * Variable: dragOffset + * + * <mxPoint> that specifies the offset of the <dragElement>. Default is null. + */ +mxDragSource.prototype.dragOffset = null; + +/** + * Variable: dragElement + * + * Holds the DOM node that is used to represent the drag preview. If this is + * null then the source element will be cloned and used for the drag preview. + */ +mxDragSource.prototype.dragElement = null; + +/** + * Variable: previewElement + * + * Optional <mxRectangle> that specifies the unscaled size of the preview. + */ +mxDragSource.prototype.previewElement = null; + +/** + * Variable: enabled + * + * Specifies if this drag source is enabled. Default is true. + */ +mxDragSource.prototype.enabled = true; + +/** + * Variable: currentGraph + * + * Reference to the <mxGraph> that is the current drop target. + */ +mxDragSource.prototype.currentGraph = null; + +/** + * Variable: currentDropTarget + * + * Holds the current drop target under the mouse. + */ +mxDragSource.prototype.currentDropTarget = null; + +/** + * Variable: currentPoint + * + * Holds the current drop location. + */ +mxDragSource.prototype.currentPoint = null; + +/** + * Variable: currentGuide + * + * Holds an <mxGuide> for the <currentGraph> if <dragPreview> is not null. + */ +mxDragSource.prototype.currentGuide = null; + +/** + * Variable: currentGuide + * + * Holds an <mxGuide> for the <currentGraph> if <dragPreview> is not null. + */ +mxDragSource.prototype.currentHighlight = null; + +/** + * Variable: autoscroll + * + * Specifies if the graph should scroll automatically. Default is true. + */ +mxDragSource.prototype.autoscroll = true; + +/** + * Variable: guidesEnabled + * + * Specifies if <mxGuide> should be enabled. Default is true. + */ +mxDragSource.prototype.guidesEnabled = true; + +/** + * Variable: gridEnabled + * + * Specifies if the grid should be allowed. Default is true. + */ +mxDragSource.prototype.gridEnabled = true; + +/** + * Variable: highlightDropTargets + * + * Specifies if drop targets should be highlighted. Default is true. + */ +mxDragSource.prototype.highlightDropTargets = true; + +/** + * Variable: dragElementZIndex + * + * ZIndex for the drag element. Default is 100. + */ +mxDragSource.prototype.dragElementZIndex = 100; + +/** + * Variable: dragElementOpacity + * + * Opacity of the drag element in %. Default is 70. + */ +mxDragSource.prototype.dragElementOpacity = 70; + +/** + * Function: isEnabled + * + * Returns <enabled>. + */ +mxDragSource.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Sets <enabled>. + */ +mxDragSource.prototype.setEnabled = function(value) +{ + this.enabled = value; +}; + +/** + * Function: isGuidesEnabled + * + * Returns <guidesEnabled>. + */ +mxDragSource.prototype.isGuidesEnabled = function() +{ + return this.guidesEnabled; +}; + +/** + * Function: setGuidesEnabled + * + * Sets <guidesEnabled>. + */ +mxDragSource.prototype.setGuidesEnabled = function(value) +{ + this.guidesEnabled = value; +}; + +/** + * Function: isGridEnabled + * + * Returns <gridEnabled>. + */ +mxDragSource.prototype.isGridEnabled = function() +{ + return this.gridEnabled; +}; + +/** + * Function: setGridEnabled + * + * Sets <gridEnabled>. + */ +mxDragSource.prototype.setGridEnabled = function(value) +{ + this.gridEnabled = value; +}; + +/** + * Function: getGraphForEvent + * + * Returns the graph for the given mouse event. This implementation returns + * null. + */ +mxDragSource.prototype.getGraphForEvent = function(evt) +{ + return null; +}; + +/** + * Function: getDropTarget + * + * Returns the drop target for the given graph and coordinates. This + * implementation uses <mxGraph.getCellAt>. + */ +mxDragSource.prototype.getDropTarget = function(graph, x, y) +{ + return graph.getCellAt(x, y); +}; + +/** + * Function: createDragElement + * + * Creates and returns a clone of the <dragElementPrototype> or the <element> + * if the former is not defined. + */ +mxDragSource.prototype.createDragElement = function(evt) +{ + return this.element.cloneNode(true); +}; + +/** + * Function: createPreviewElement + * + * Creates and returns an element which can be used as a preview in the given + * graph. + */ +mxDragSource.prototype.createPreviewElement = function(graph) +{ + return null; +}; + +/** + * Function: mouseDown + * + * Returns the drop target for the given graph and coordinates. This + * implementation uses <mxGraph.getCellAt>. + */ +mxDragSource.prototype.mouseDown = function(evt) +{ + if (this.enabled && !mxEvent.isConsumed(evt) && this.mouseMoveHandler == null) + { + this.startDrag(evt); + + var mm = (mxClient.IS_TOUCH) ? 'touchmove' : 'mousemove'; + var mu = (mxClient.IS_TOUCH) ? 'touchend' : 'mouseup'; + + this.mouseMoveHandler = mxUtils.bind(this, this.mouseMove); + mxEvent.addListener(document, mm, this.mouseMoveHandler); + this.mouseUpHandler = mxUtils.bind(this, this.mouseUp); + mxEvent.addListener(document, mu, this.mouseUpHandler); + + // Prevents default action (native DnD for images in FF 10) + // but does not stop event propagation + mxEvent.consume(evt, true, false); + } +}; + +/** + * Function: startDrag + * + * Creates the <dragElement> using <createDragElement>. + */ +mxDragSource.prototype.startDrag = function(evt) +{ + this.dragElement = this.createDragElement(evt); + this.dragElement.style.position = 'absolute'; + this.dragElement.style.zIndex = this.dragElementZIndex; + mxUtils.setOpacity(this.dragElement, this.dragElementOpacity); +}; + + +/** + * Function: stopDrag + * + * Removes and destroys the <dragElement>. + */ +mxDragSource.prototype.stopDrag = function(evt) +{ + if (this.dragElement != null) + { + if (this.dragElement.parentNode != null) + { + this.dragElement.parentNode.removeChild(this.dragElement); + } + + this.dragElement = null; + } +}; + +/** + * Function: graphContainsEvent + * + * Returns true if the given graph contains the given event. + */ +mxDragSource.prototype.graphContainsEvent = function(graph, evt) +{ + var x = mxEvent.getClientX(evt); + var y = mxEvent.getClientY(evt); + var offset = mxUtils.getOffset(graph.container); + var origin = mxUtils.getScrollOrigin(); + + // Checks if event is inside the bounds of the graph container + return x >= offset.x - origin.x && y >= offset.y - origin.y && + x <= offset.x - origin.x + graph.container.offsetWidth && + y <= offset.y - origin.y + graph.container.offsetHeight; +}; + +/** + * Function: mouseMove + * + * Gets the graph for the given event using <getGraphForEvent>, updates the + * <currentGraph>, calling <dragEnter> and <dragExit> on the new and old graph, + * respectively, and invokes <dragOver> if <currentGraph> is not null. + */ +mxDragSource.prototype.mouseMove = function(evt) +{ + var graph = this.getGraphForEvent(evt); + + // Checks if event is inside the bounds of the graph container + if (graph != null && !this.graphContainsEvent(graph, evt)) + { + graph = null; + } + + if (graph != this.currentGraph) + { + if (this.currentGraph != null) + { + this.dragExit(this.currentGraph); + } + + this.currentGraph = graph; + + if (this.currentGraph != null) + { + this.dragEnter(this.currentGraph); + } + } + + if (this.currentGraph != null) + { + this.dragOver(this.currentGraph, evt); + } + + if (this.dragElement != null && (this.previewElement == null || this.previewElement.style.visibility != 'visible')) + { + var x = mxEvent.getClientX(evt); + var y = mxEvent.getClientY(evt); + + if (this.dragElement.parentNode == null) + { + document.body.appendChild(this.dragElement); + } + + this.dragElement.style.visibility = 'visible'; + + if (this.dragOffset != null) + { + x += this.dragOffset.x; + y += this.dragOffset.y; + } + + x += document.body.scrollLeft || document.documentElement.scrollLeft; + y += document.body.scrollTop || document.documentElement.scrollTop; + this.dragElement.style.left = x + 'px'; + this.dragElement.style.top = y + 'px'; + } + else if (this.dragElement != null) + { + this.dragElement.style.visibility = 'hidden'; + } + + mxEvent.consume(evt); +}; + +/** + * Function: mouseUp + * + * Processes the mouse up event and invokes <drop>, <dragExit> and <stopDrag> + * as required. + */ +mxDragSource.prototype.mouseUp = function(evt) +{ + if (this.currentGraph != null) + { + if (this.currentPoint != null && (this.previewElement == null || + this.previewElement.style.visibility != 'hidden')) + { + var scale = this.currentGraph.view.scale; + var tr = this.currentGraph.view.translate; + var x = this.currentPoint.x / scale - tr.x; + var y = this.currentPoint.y / scale - tr.y; + + this.drop(this.currentGraph, evt, this.currentDropTarget, x, y); + } + + this.dragExit(this.currentGraph); + } + + this.stopDrag(evt); + + this.currentGraph = null; + + if (this.mouseMoveHandler != null) + { + var mm = (mxClient.IS_TOUCH) ? 'touchmove' : 'mousemove'; + mxEvent.removeListener(document, mm, this.mouseMoveHandler); + this.mouseMoveHandler = null; + } + + if (this.mouseUpHandler != null) + { + var mu = (mxClient.IS_TOUCH) ? 'touchend' : 'mouseup'; + mxEvent.removeListener(document, mu, this.mouseUpHandler); + this.mouseUpHandler = null; + } + + mxEvent.consume(evt); +}; + +/** + * Function: dragEnter + * + * Actives the given graph as a drop target. + */ +mxDragSource.prototype.dragEnter = function(graph) +{ + graph.isMouseDown = true; + this.previewElement = this.createPreviewElement(graph); + + // Guide is only needed if preview element is used + if (this.isGuidesEnabled() && this.previewElement != null) + { + this.currentGuide = new mxGuide(graph, graph.graphHandler.getGuideStates()); + } + + if (this.highlightDropTargets) + { + this.currentHighlight = new mxCellHighlight(graph, mxConstants.DROP_TARGET_COLOR); + } +}; + +/** + * Function: dragExit + * + * Deactivates the given graph as a drop target. + */ +mxDragSource.prototype.dragExit = function(graph) +{ + this.currentDropTarget = null; + this.currentPoint = null; + graph.isMouseDown = false; + + if (this.previewElement != null) + { + if (this.previewElement.parentNode != null) + { + this.previewElement.parentNode.removeChild(this.previewElement); + } + + this.previewElement = null; + } + + if (this.currentGuide != null) + { + this.currentGuide.destroy(); + this.currentGuide = null; + } + + if (this.currentHighlight != null) + { + this.currentHighlight.destroy(); + this.currentHighlight = null; + } +}; + +/** + * Function: dragOver + * + * Implements autoscroll, updates the <currentPoint>, highlights any drop + * targets and updates the preview. + */ +mxDragSource.prototype.dragOver = function(graph, evt) +{ + var offset = mxUtils.getOffset(graph.container); + var origin = mxUtils.getScrollOrigin(graph.container); + var x = mxEvent.getClientX(evt) - offset.x + origin.x; + var y = mxEvent.getClientY(evt) - offset.y + origin.y; + + if (graph.autoScroll && (this.autoscroll == null || this.autoscroll)) + { + graph.scrollPointToVisible(x, y, graph.autoExtend); + } + + // Highlights the drop target under the mouse + if (this.currentHighlight != null && graph.isDropEnabled()) + { + this.currentDropTarget = this.getDropTarget(graph, x, y); + var state = graph.getView().getState(this.currentDropTarget); + this.currentHighlight.highlight(state); + } + + // Updates the location of the preview + if (this.previewElement != null) + { + if (this.previewElement.parentNode == null) + { + graph.container.appendChild(this.previewElement); + + this.previewElement.style.zIndex = '3'; + this.previewElement.style.position = 'absolute'; + } + + var gridEnabled = this.isGridEnabled() && graph.isGridEnabledEvent(evt); + var hideGuide = true; + + // Grid and guides + if (this.currentGuide != null && this.currentGuide.isEnabledForEvent(evt)) + { + // LATER: HTML preview appears smaller than SVG preview + var w = parseInt(this.previewElement.style.width); + var h = parseInt(this.previewElement.style.height); + var bounds = new mxRectangle(0, 0, w, h); + var delta = new mxPoint(x, y); + delta = this.currentGuide.move(bounds, delta, gridEnabled); + hideGuide = false; + x = delta.x; + y = delta.y; + } + else if (gridEnabled) + { + var scale = graph.view.scale; + var tr = graph.view.translate; + var off = graph.gridSize / 2; + x = (graph.snap(x / scale - tr.x - off) + tr.x) * scale; + y = (graph.snap(y / scale - tr.y - off) + tr.y) * scale; + } + + if (this.currentGuide != null && hideGuide) + { + this.currentGuide.hide(); + } + + if (this.previewOffset != null) + { + x += this.previewOffset.x; + y += this.previewOffset.y; + } + + this.previewElement.style.left = Math.round(x) + 'px'; + this.previewElement.style.top = Math.round(y) + 'px'; + this.previewElement.style.visibility = 'visible'; + } + + this.currentPoint = new mxPoint(x, y); +}; + +/** + * Function: drop + * + * Returns the drop target for the given graph and coordinates. This + * implementation uses <mxGraph.getCellAt>. + */ +mxDragSource.prototype.drop = function(graph, evt, dropTarget, x, y) +{ + this.dropHandler(graph, evt, dropTarget, x, y); + + // Had to move this to after the insert because it will + // affect the scrollbars of the window in IE to try and + // make the complete container visible. + // LATER: Should be made optional. + graph.container.focus(); +}; diff --git a/src/js/util/mxEffects.js b/src/js/util/mxEffects.js new file mode 100644 index 0000000..89d6a71 --- /dev/null +++ b/src/js/util/mxEffects.js @@ -0,0 +1,214 @@ +/** + * $Id: mxEffects.js,v 1.6 2012-01-04 10:01:16 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +var mxEffects = +{ + + /** + * Class: mxEffects + * + * Provides animation effects. + */ + + /** + * Function: animateChanges + * + * Asynchronous animated move operation. See also: <mxMorphing>. + * + * Example: + * + * (code) + * graph.model.addListener(mxEvent.CHANGE, function(sender, evt) + * { + * var changes = evt.getProperty('edit').changes; + * + * if (changes.length < 10) + * { + * mxEffects.animateChanges(graph, changes); + * } + * }); + * (end) + * + * Parameters: + * + * graph - <mxGraph> that received the changes. + * changes - Array of changes to be animated. + * done - Optional function argument that is invoked after the + * last step of the animation. + */ + animateChanges: function(graph, changes, done) + { + var maxStep = 10; + var step = 0; + + var animate = function() + { + var isRequired = false; + + for (var i = 0; i < changes.length; i++) + { + var change = changes[i]; + + if (change instanceof mxGeometryChange || + change instanceof mxTerminalChange || + change instanceof mxValueChange || + change instanceof mxChildChange || + change instanceof mxStyleChange) + { + var state = graph.getView().getState(change.cell || change.child, false); + + if (state != null) + { + isRequired = true; + + if (change.constructor != mxGeometryChange || graph.model.isEdge(change.cell)) + { + mxUtils.setOpacity(state.shape.node, 100 * step / maxStep); + } + else + { + var scale = graph.getView().scale; + + var dx = (change.geometry.x - change.previous.x) * scale; + var dy = (change.geometry.y - change.previous.y) * scale; + + var sx = (change.geometry.width - change.previous.width) * scale; + var sy = (change.geometry.height - change.previous.height) * scale; + + if (step == 0) + { + state.x -= dx; + state.y -= dy; + state.width -= sx; + state.height -= sy; + } + else + { + state.x += dx / maxStep; + state.y += dy / maxStep; + state.width += sx / maxStep; + state.height += sy / maxStep; + } + + graph.cellRenderer.redraw(state); + + // Fades all connected edges and children + mxEffects.cascadeOpacity(graph, change.cell, 100 * step / maxStep); + } + } + } + } + + // Workaround to force a repaint in AppleWebKit + mxUtils.repaintGraph(graph, new mxPoint(1, 1)); + + if (step < maxStep && isRequired) + { + step++; + window.setTimeout(animate, delay); + } + else if (done != null) + { + done(); + } + }; + + var delay = 30; + animate(); + }, + + /** + * Function: cascadeOpacity + * + * Sets the opacity on the given cell and its descendants. + * + * Parameters: + * + * graph - <mxGraph> that contains the cells. + * cell - <mxCell> to set the opacity for. + * opacity - New value for the opacity in %. + */ + cascadeOpacity: function(graph, cell, opacity) + { + // Fades all children + var childCount = graph.model.getChildCount(cell); + + for (var i=0; i<childCount; i++) + { + var child = graph.model.getChildAt(cell, i); + var childState = graph.getView().getState(child); + + if (childState != null) + { + mxUtils.setOpacity(childState.shape.node, opacity); + mxEffects.cascadeOpacity(graph, child, opacity); + } + } + + // Fades all connected edges + var edges = graph.model.getEdges(cell); + + if (edges != null) + { + for (var i=0; i<edges.length; i++) + { + var edgeState = graph.getView().getState(edges[i]); + + if (edgeState != null) + { + mxUtils.setOpacity(edgeState.shape.node, opacity); + } + } + } + }, + + /** + * Function: fadeOut + * + * Asynchronous fade-out operation. + */ + fadeOut: function(node, from, remove, step, delay, isEnabled) + { + step = step || 40; + delay = delay || 30; + + var opacity = from || 100; + + mxUtils.setOpacity(node, opacity); + + if (isEnabled || isEnabled == null) + { + var f = function() + { + opacity = Math.max(opacity-step, 0); + mxUtils.setOpacity(node, opacity); + + if (opacity > 0) + { + window.setTimeout(f, delay); + } + else + { + node.style.visibility = 'hidden'; + + if (remove && node.parentNode) + { + node.parentNode.removeChild(node); + } + } + }; + window.setTimeout(f, delay); + } + else + { + node.style.visibility = 'hidden'; + + if (remove && node.parentNode) + { + node.parentNode.removeChild(node); + } + } + } + +}; diff --git a/src/js/util/mxEvent.js b/src/js/util/mxEvent.js new file mode 100644 index 0000000..f831631 --- /dev/null +++ b/src/js/util/mxEvent.js @@ -0,0 +1,1175 @@ +/** + * $Id: mxEvent.js,v 1.76 2012-12-07 07:39:03 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +var mxEvent = +{ + + /** + * Class: mxEvent + * + * Cross-browser DOM event support. For internal event handling, + * <mxEventSource> and the graph event dispatch loop in <mxGraph> are used. + * + * Memory Leaks: + * + * Use this class for adding and removing listeners to/from DOM nodes. The + * <removeAllListeners> function is provided to remove all listeners that + * have been added using <addListener>. The function should be invoked when + * the last reference is removed in the JavaScript code, typically when the + * referenced DOM node is removed from the DOM, and helps to reduce memory + * leaks in IE6. + * + * Variable: objects + * + * Contains all objects where any listener was added using <addListener>. + * This is used to reduce memory leaks in IE, see <mxClient.dispose>. + */ + objects: [], + + /** + * Function: addListener + * + * Binds the function to the specified event on the given element. Use + * <mxUtils.bind> in order to bind the "this" keyword inside the function + * to a given execution scope. + */ + addListener: function() + { + var updateListenerList = function(element, eventName, funct) + { + if (element.mxListenerList == null) + { + element.mxListenerList = []; + mxEvent.objects.push(element); + } + + var entry = {name: eventName, f: funct}; + element.mxListenerList.push(entry); + }; + + if (window.addEventListener) + { + return function(element, eventName, funct) + { + element.addEventListener(eventName, funct, false); + updateListenerList(element, eventName, funct); + }; + } + else + { + return function(element, eventName, funct) + { + element.attachEvent('on' + eventName, funct); + updateListenerList(element, eventName, funct); + }; + } + }(), + + /** + * Function: removeListener + * + * Removes the specified listener from the given element. + */ + removeListener: function() + { + var updateListener = function(element, eventName, funct) + { + if (element.mxListenerList != null) + { + var listenerCount = element.mxListenerList.length; + + for (var i=0; i<listenerCount; i++) + { + var entry = element.mxListenerList[i]; + + if (entry.f == funct) + { + element.mxListenerList.splice(i, 1); + break; + } + } + + if (element.mxListenerList.length == 0) + { + element.mxListenerList = null; + } + } + }; + + if (window.removeEventListener) + { + return function(element, eventName, funct) + { + element.removeEventListener(eventName, funct, false); + updateListener(element, eventName, funct); + }; + } + else + { + return function(element, eventName, funct) + { + element.detachEvent('on' + eventName, funct); + updateListener(element, eventName, funct); + }; + } + }(), + + /** + * Function: removeAllListeners + * + * Removes all listeners from the given element. + */ + removeAllListeners: function(element) + { + var list = element.mxListenerList; + + if (list != null) + { + while (list.length > 0) + { + var entry = list[0]; + mxEvent.removeListener(element, entry.name, entry.f); + } + } + }, + + /** + * Function: redirectMouseEvents + * + * Redirects the mouse events from the given DOM node to the graph dispatch + * loop using the event and given state as event arguments. State can + * either be an instance of <mxCellState> or a function that returns an + * <mxCellState>. The down, move, up and dblClick arguments are optional + * functions that take the trigger event as arguments and replace the + * default behaviour. + */ + redirectMouseEvents: function(node, graph, state, down, move, up, dblClick) + { + var getState = function(evt) + { + return (typeof(state) == 'function') ? state(evt) : state; + }; + + var md = (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown'; + var mm = (mxClient.IS_TOUCH) ? 'touchmove' : 'mousemove'; + var mu = (mxClient.IS_TOUCH) ? 'touchend' : 'mouseup'; + + mxEvent.addListener(node, md, function (evt) + { + if (down != null) + { + down(evt); + } + else if (!mxEvent.isConsumed(evt)) + { + graph.fireMouseEvent(mxEvent.MOUSE_DOWN, + new mxMouseEvent(evt, getState(evt))); + } + }); + + mxEvent.addListener(node, mm, function (evt) + { + if (move != null) + { + move(evt); + } + else if (!mxEvent.isConsumed(evt)) + { + graph.fireMouseEvent(mxEvent.MOUSE_MOVE, + new mxMouseEvent(evt, getState(evt))); + } + }); + + mxEvent.addListener(node, mu, function (evt) + { + if (up != null) + { + up(evt); + } + else if (!mxEvent.isConsumed(evt)) + { + graph.fireMouseEvent(mxEvent.MOUSE_UP, + new mxMouseEvent(evt, getState(evt))); + } + }); + + mxEvent.addListener(node, 'dblclick', function (evt) + { + if (dblClick != null) + { + dblClick(evt); + } + else if (!mxEvent.isConsumed(evt)) + { + var tmp = getState(evt); + graph.dblClick(evt, (tmp != null) ? tmp.cell : null); + } + }); + }, + + /** + * Function: release + * + * Removes the known listeners from the given DOM node and its descendants. + * + * Parameters: + * + * element - DOM node to remove the listeners from. + */ + release: function(element) + { + if (element != null) + { + mxEvent.removeAllListeners(element); + + var children = element.childNodes; + + if (children != null) + { + var childCount = children.length; + + for (var i = 0; i < childCount; i += 1) + { + mxEvent.release(children[i]); + } + } + } + }, + + /** + * Function: addMouseWheelListener + * + * Installs the given function as a handler for mouse wheel events. The + * function has two arguments: the mouse event and a boolean that specifies + * if the wheel was moved up or down. + * + * This has been tested with IE 6 and 7, Firefox (all versions), Opera and + * Safari. It does currently not work on Safari for Mac. + * + * Example: + * + * (code) + * mxEvent.addMouseWheelListener(function (evt, up) + * { + * mxLog.show(); + * mxLog.debug('mouseWheel: up='+up); + * }); + *(end) + * + * Parameters: + * + * funct - Handler function that takes the event argument and a boolean up + * argument for the mousewheel direction. + */ + addMouseWheelListener: function(funct) + { + if (funct != null) + { + var wheelHandler = function(evt) + { + // IE does not give an event object but the + // global event object is the mousewheel event + // at this point in time. + if (evt == null) + { + evt = window.event; + } + + var delta = 0; + + if (mxClient.IS_NS && !mxClient.IS_SF && !mxClient.IS_GC) + { + delta = -evt.detail/2; + } + else + { + delta = evt.wheelDelta/120; + } + + // Handles the event using the given function + if (delta != 0) + { + funct(evt, delta > 0); + } + }; + + // Webkit has NS event API, but IE event name and details + if (mxClient.IS_NS) + { + var eventName = (mxClient.IS_SF || mxClient.IS_GC) ? + 'mousewheel' : 'DOMMouseScroll'; + mxEvent.addListener(window, eventName, wheelHandler); + } + else + { + // TODO: Does not work with Safari and Chrome but it should be + // working as tested in etc/markup/wheel.html + mxEvent.addListener(document, 'mousewheel', wheelHandler); + } + } + }, + + /** + * Function: disableContextMenu + * + * Disables the context menu for the given element. + */ + disableContextMenu: function() + { + if (mxClient.IS_IE && (typeof(document.documentMode) === 'undefined' || document.documentMode < 9)) + { + return function(element) + { + mxEvent.addListener(element, 'contextmenu', function() + { + return false; + }); + }; + } + else + { + return function(element) + { + element.setAttribute('oncontextmenu', 'return false;'); + }; + } + }(), + + /** + * Function: getSource + * + * Returns the event's target or srcElement depending on the browser. + */ + getSource: function(evt) + { + return (evt.srcElement != null) ? evt.srcElement : evt.target; + }, + + /** + * Function: isConsumed + * + * Returns true if the event has been consumed using <consume>. + */ + isConsumed: function(evt) + { + return evt.isConsumed != null && evt.isConsumed; + }, + + /** + * Function: isLeftMouseButton + * + * Returns true if the left mouse button is pressed for the given event. + * To check if a button is pressed during a mouseMove you should use the + * <mxGraph.isMouseDown> property. + */ + isLeftMouseButton: function(evt) + { + return evt.button == ((mxClient.IS_IE && (typeof(document.documentMode) === 'undefined' || document.documentMode < 9)) ? 1 : 0); + }, + + /** + * Function: isRightMouseButton + * + * Returns true if the right mouse button was pressed. Note that this + * button might not be available on some systems. For handling a popup + * trigger <isPopupTrigger> should be used. + */ + isRightMouseButton: function(evt) + { + return evt.button == 2; + }, + + /** + * Function: isPopupTrigger + * + * Returns true if the event is a popup trigger. This implementation + * returns true if the right mouse button or shift was pressed. + */ + isPopupTrigger: function(evt) + { + return mxEvent.isRightMouseButton(evt) || + (mxEvent.isShiftDown(evt) && + !mxEvent.isControlDown(evt)); + }, + + /** + * Function: isShiftDown + * + * Returns true if the shift key is pressed for the given event. + */ + isShiftDown: function(evt) + { + return (evt != null) ? evt.shiftKey : false; + }, + + /** + * Function: isAltDown + * + * Returns true if the alt key is pressed for the given event. + */ + isAltDown: function(evt) + { + return (evt != null) ? evt.altKey : false; + }, + + /** + * Function: isControlDown + * + * Returns true if the control key is pressed for the given event. + */ + isControlDown: function(evt) + { + return (evt != null) ? evt.ctrlKey : false; + }, + + /** + * Function: isMetaDown + * + * Returns true if the meta key is pressed for the given event. + */ + isMetaDown: function(evt) + { + return (evt != null) ? evt.metaKey : false; + }, + + /** + * Function: getMainEvent + * + * Returns the touch or mouse event that contains the mouse coordinates. + */ + getMainEvent: function(e) + { + if ((e.type == 'touchstart' || e.type == 'touchmove') && + e.touches != null && e.touches[0] != null) + { + e = e.touches[0]; + } + else if (e.type == 'touchend' && e.changedTouches != null && + e.changedTouches[0] != null) + { + e = e.changedTouches[0]; + } + + return e; + }, + + /** + * Function: getClientX + * + * Returns true if the meta key is pressed for the given event. + */ + getClientX: function(e) + { + return mxEvent.getMainEvent(e).clientX; + }, + + /** + * Function: getClientY + * + * Returns true if the meta key is pressed for the given event. + */ + getClientY: function(e) + { + return mxEvent.getMainEvent(e).clientY; + }, + + /** + * Function: consume + * + * Consumes the given event. + * + * Parameters: + * + * evt - Native event to be consumed. + * preventDefault - Optional boolean to prevent the default for the event. + * Default is true. + * stopPropagation - Option boolean to stop event propagation. Default is + * true. + */ + consume: function(evt, preventDefault, stopPropagation) + { + preventDefault = (preventDefault != null) ? preventDefault : true; + stopPropagation = (stopPropagation != null) ? stopPropagation : true; + + if (preventDefault) + { + if (evt.preventDefault) + { + if (stopPropagation) + { + evt.stopPropagation(); + } + + evt.preventDefault(); + } + else if (stopPropagation) + { + evt.cancelBubble = true; + } + } + + // Opera + evt.isConsumed = true; + + // Other browsers + evt.returnValue = false; + }, + + // + // Special handles in mouse events + // + + /** + * Variable: LABEL_HANDLE + * + * Index for the label handle in an mxMouseEvent. This should be a negative + * value that does not interfere with any possible handle indices. Default + * is -1. + */ + LABEL_HANDLE: -1, + + /** + * Variable: ROTATION_HANDLE + * + * Index for the rotation handle in an mxMouseEvent. This should be a + * negative value that does not interfere with any possible handle indices. + * Default is -2. + */ + ROTATION_HANDLE: -2, + + // + // Event names + // + + /** + * Variable: MOUSE_DOWN + * + * Specifies the event name for mouseDown. + */ + MOUSE_DOWN: 'mouseDown', + + /** + * Variable: MOUSE_MOVE + * + * Specifies the event name for mouseMove. + */ + MOUSE_MOVE: 'mouseMove', + + /** + * Variable: MOUSE_UP + * + * Specifies the event name for mouseUp. + */ + MOUSE_UP: 'mouseUp', + + /** + * Variable: ACTIVATE + * + * Specifies the event name for activate. + */ + ACTIVATE: 'activate', + + /** + * Variable: RESIZE_START + * + * Specifies the event name for resizeStart. + */ + RESIZE_START: 'resizeStart', + + /** + * Variable: RESIZE + * + * Specifies the event name for resize. + */ + RESIZE: 'resize', + + /** + * Variable: RESIZE_END + * + * Specifies the event name for resizeEnd. + */ + RESIZE_END: 'resizeEnd', + + /** + * Variable: MOVE_START + * + * Specifies the event name for moveStart. + */ + MOVE_START: 'moveStart', + + /** + * Variable: MOVE + * + * Specifies the event name for move. + */ + MOVE: 'move', + + /** + * Variable: MOVE_END + * + * Specifies the event name for moveEnd. + */ + MOVE_END: 'moveEnd', + + /** + * Variable: PAN_START + * + * Specifies the event name for panStart. + */ + PAN_START: 'panStart', + + /** + * Variable: PAN + * + * Specifies the event name for pan. + */ + PAN: 'pan', + + /** + * Variable: PAN_END + * + * Specifies the event name for panEnd. + */ + PAN_END: 'panEnd', + + /** + * Variable: MINIMIZE + * + * Specifies the event name for minimize. + */ + MINIMIZE: 'minimize', + + /** + * Variable: NORMALIZE + * + * Specifies the event name for normalize. + */ + NORMALIZE: 'normalize', + + /** + * Variable: MAXIMIZE + * + * Specifies the event name for maximize. + */ + MAXIMIZE: 'maximize', + + /** + * Variable: HIDE + * + * Specifies the event name for hide. + */ + HIDE: 'hide', + + /** + * Variable: SHOW + * + * Specifies the event name for show. + */ + SHOW: 'show', + + /** + * Variable: CLOSE + * + * Specifies the event name for close. + */ + CLOSE: 'close', + + /** + * Variable: DESTROY + * + * Specifies the event name for destroy. + */ + DESTROY: 'destroy', + + /** + * Variable: REFRESH + * + * Specifies the event name for refresh. + */ + REFRESH: 'refresh', + + /** + * Variable: SIZE + * + * Specifies the event name for size. + */ + SIZE: 'size', + + /** + * Variable: SELECT + * + * Specifies the event name for select. + */ + SELECT: 'select', + + /** + * Variable: FIRED + * + * Specifies the event name for fired. + */ + FIRED: 'fired', + + /** + * Variable: GET + * + * Specifies the event name for get. + */ + GET: 'get', + + /** + * Variable: RECEIVE + * + * Specifies the event name for receive. + */ + RECEIVE: 'receive', + + /** + * Variable: CONNECT + * + * Specifies the event name for connect. + */ + CONNECT: 'connect', + + /** + * Variable: DISCONNECT + * + * Specifies the event name for disconnect. + */ + DISCONNECT: 'disconnect', + + /** + * Variable: SUSPEND + * + * Specifies the event name for suspend. + */ + SUSPEND: 'suspend', + + /** + * Variable: RESUME + * + * Specifies the event name for suspend. + */ + RESUME: 'resume', + + /** + * Variable: MARK + * + * Specifies the event name for mark. + */ + MARK: 'mark', + + /** + * Variable: SESSION + * + * Specifies the event name for session. + */ + SESSION: 'session', + + /** + * Variable: ROOT + * + * Specifies the event name for root. + */ + ROOT: 'root', + + /** + * Variable: POST + * + * Specifies the event name for post. + */ + POST: 'post', + + /** + * Variable: OPEN + * + * Specifies the event name for open. + */ + OPEN: 'open', + + /** + * Variable: SAVE + * + * Specifies the event name for open. + */ + SAVE: 'save', + + /** + * Variable: BEFORE_ADD_VERTEX + * + * Specifies the event name for beforeAddVertex. + */ + BEFORE_ADD_VERTEX: 'beforeAddVertex', + + /** + * Variable: ADD_VERTEX + * + * Specifies the event name for addVertex. + */ + ADD_VERTEX: 'addVertex', + + /** + * Variable: AFTER_ADD_VERTEX + * + * Specifies the event name for afterAddVertex. + */ + AFTER_ADD_VERTEX: 'afterAddVertex', + + /** + * Variable: DONE + * + * Specifies the event name for done. + */ + DONE: 'done', + + /** + * Variable: EXECUTE + * + * Specifies the event name for execute. + */ + EXECUTE: 'execute', + + /** + * Variable: BEGIN_UPDATE + * + * Specifies the event name for beginUpdate. + */ + BEGIN_UPDATE: 'beginUpdate', + + /** + * Variable: END_UPDATE + * + * Specifies the event name for endUpdate. + */ + END_UPDATE: 'endUpdate', + + /** + * Variable: BEFORE_UNDO + * + * Specifies the event name for beforeUndo. + */ + BEFORE_UNDO: 'beforeUndo', + + /** + * Variable: UNDO + * + * Specifies the event name for undo. + */ + UNDO: 'undo', + + /** + * Variable: REDO + * + * Specifies the event name for redo. + */ + REDO: 'redo', + + /** + * Variable: CHANGE + * + * Specifies the event name for change. + */ + CHANGE: 'change', + + /** + * Variable: NOTIFY + * + * Specifies the event name for notify. + */ + NOTIFY: 'notify', + + /** + * Variable: LAYOUT_CELLS + * + * Specifies the event name for layoutCells. + */ + LAYOUT_CELLS: 'layoutCells', + + /** + * Variable: CLICK + * + * Specifies the event name for click. + */ + CLICK: 'click', + + /** + * Variable: SCALE + * + * Specifies the event name for scale. + */ + SCALE: 'scale', + + /** + * Variable: TRANSLATE + * + * Specifies the event name for translate. + */ + TRANSLATE: 'translate', + + /** + * Variable: SCALE_AND_TRANSLATE + * + * Specifies the event name for scaleAndTranslate. + */ + SCALE_AND_TRANSLATE: 'scaleAndTranslate', + + /** + * Variable: UP + * + * Specifies the event name for up. + */ + UP: 'up', + + /** + * Variable: DOWN + * + * Specifies the event name for down. + */ + DOWN: 'down', + + /** + * Variable: ADD + * + * Specifies the event name for add. + */ + ADD: 'add', + + /** + * Variable: REMOVE + * + * Specifies the event name for remove. + */ + REMOVE: 'remove', + + /** + * Variable: CLEAR + * + * Specifies the event name for clear. + */ + CLEAR: 'clear', + + /** + * Variable: ADD_CELLS + * + * Specifies the event name for addCells. + */ + ADD_CELLS: 'addCells', + + /** + * Variable: CELLS_ADDED + * + * Specifies the event name for cellsAdded. + */ + CELLS_ADDED: 'cellsAdded', + + /** + * Variable: MOVE_CELLS + * + * Specifies the event name for moveCells. + */ + MOVE_CELLS: 'moveCells', + + /** + * Variable: CELLS_MOVED + * + * Specifies the event name for cellsMoved. + */ + CELLS_MOVED: 'cellsMoved', + + /** + * Variable: RESIZE_CELLS + * + * Specifies the event name for resizeCells. + */ + RESIZE_CELLS: 'resizeCells', + + /** + * Variable: CELLS_RESIZED + * + * Specifies the event name for cellsResized. + */ + CELLS_RESIZED: 'cellsResized', + + /** + * Variable: TOGGLE_CELLS + * + * Specifies the event name for toggleCells. + */ + TOGGLE_CELLS: 'toggleCells', + + /** + * Variable: CELLS_TOGGLED + * + * Specifies the event name for cellsToggled. + */ + CELLS_TOGGLED: 'cellsToggled', + + /** + * Variable: ORDER_CELLS + * + * Specifies the event name for orderCells. + */ + ORDER_CELLS: 'orderCells', + + /** + * Variable: CELLS_ORDERED + * + * Specifies the event name for cellsOrdered. + */ + CELLS_ORDERED: 'cellsOrdered', + + /** + * Variable: REMOVE_CELLS + * + * Specifies the event name for removeCells. + */ + REMOVE_CELLS: 'removeCells', + + /** + * Variable: CELLS_REMOVED + * + * Specifies the event name for cellsRemoved. + */ + CELLS_REMOVED: 'cellsRemoved', + + /** + * Variable: GROUP_CELLS + * + * Specifies the event name for groupCells. + */ + GROUP_CELLS: 'groupCells', + + /** + * Variable: UNGROUP_CELLS + * + * Specifies the event name for ungroupCells. + */ + UNGROUP_CELLS: 'ungroupCells', + + /** + * Variable: REMOVE_CELLS_FROM_PARENT + * + * Specifies the event name for removeCellsFromParent. + */ + REMOVE_CELLS_FROM_PARENT: 'removeCellsFromParent', + + /** + * Variable: FOLD_CELLS + * + * Specifies the event name for foldCells. + */ + FOLD_CELLS: 'foldCells', + + /** + * Variable: CELLS_FOLDED + * + * Specifies the event name for cellsFolded. + */ + CELLS_FOLDED: 'cellsFolded', + + /** + * Variable: ALIGN_CELLS + * + * Specifies the event name for alignCells. + */ + ALIGN_CELLS: 'alignCells', + + /** + * Variable: LABEL_CHANGED + * + * Specifies the event name for labelChanged. + */ + LABEL_CHANGED: 'labelChanged', + + /** + * Variable: CONNECT_CELL + * + * Specifies the event name for connectCell. + */ + CONNECT_CELL: 'connectCell', + + /** + * Variable: CELL_CONNECTED + * + * Specifies the event name for cellConnected. + */ + CELL_CONNECTED: 'cellConnected', + + /** + * Variable: SPLIT_EDGE + * + * Specifies the event name for splitEdge. + */ + SPLIT_EDGE: 'splitEdge', + + /** + * Variable: FLIP_EDGE + * + * Specifies the event name for flipEdge. + */ + FLIP_EDGE: 'flipEdge', + + /** + * Variable: START_EDITING + * + * Specifies the event name for startEditing. + */ + START_EDITING: 'startEditing', + + /** + * Variable: ADD_OVERLAY + * + * Specifies the event name for addOverlay. + */ + ADD_OVERLAY: 'addOverlay', + + /** + * Variable: REMOVE_OVERLAY + * + * Specifies the event name for removeOverlay. + */ + REMOVE_OVERLAY: 'removeOverlay', + + /** + * Variable: UPDATE_CELL_SIZE + * + * Specifies the event name for updateCellSize. + */ + UPDATE_CELL_SIZE: 'updateCellSize', + + /** + * Variable: ESCAPE + * + * Specifies the event name for escape. + */ + ESCAPE: 'escape', + + /** + * Variable: CLICK + * + * Specifies the event name for click. + */ + CLICK: 'click', + + /** + * Variable: DOUBLE_CLICK + * + * Specifies the event name for doubleClick. + */ + DOUBLE_CLICK: 'doubleClick', + + /** + * Variable: START + * + * Specifies the event name for start. + */ + START: 'start', + + /** + * Variable: RESET + * + * Specifies the event name for reset. + */ + RESET: 'reset' + +}; diff --git a/src/js/util/mxEventObject.js b/src/js/util/mxEventObject.js new file mode 100644 index 0000000..cb08a55 --- /dev/null +++ b/src/js/util/mxEventObject.js @@ -0,0 +1,111 @@ +/** + * $Id: mxEventObject.js,v 1.11 2011-09-09 10:29:05 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxEventObject + * + * The mxEventObject is a wrapper for all properties of a single event. + * Additionally, it also offers functions to consume the event and check if it + * was consumed as follows: + * + * (code) + * evt.consume(); + * INV: evt.isConsumed() == true + * (end) + * + * Constructor: mxEventObject + * + * Constructs a new event object with the specified name. An optional + * sequence of key, value pairs can be appended to define properties. + * + * Example: + * + * (code) + * new mxEventObject("eventName", key1, val1, .., keyN, valN) + * (end) + */ +function mxEventObject(name) +{ + this.name = name; + this.properties = []; + + for (var i = 1; i < arguments.length; i += 2) + { + if (arguments[i + 1] != null) + { + this.properties[arguments[i]] = arguments[i + 1]; + } + } +}; + +/** + * Variable: name + * + * Holds the name. + */ +mxEventObject.prototype.name = null; + +/** + * Variable: properties + * + * Holds the properties as an associative array. + */ +mxEventObject.prototype.properties = null; + +/** + * Variable: consumed + * + * Holds the consumed state. Default is false. + */ +mxEventObject.prototype.consumed = false; + +/** + * Function: getName + * + * Returns <name>. + */ +mxEventObject.prototype.getName = function() +{ + return this.name; +}; + +/** + * Function: getProperties + * + * Returns <properties>. + */ +mxEventObject.prototype.getProperties = function() +{ + return this.properties; +}; + +/** + * Function: getProperty + * + * Returns the property for the given key. + */ +mxEventObject.prototype.getProperty = function(key) +{ + return this.properties[key]; +}; + +/** + * Function: isConsumed + * + * Returns true if the event has been consumed. + */ +mxEventObject.prototype.isConsumed = function() +{ + return this.consumed; +}; + +/** + * Function: consume + * + * Consumes the event. + */ +mxEventObject.prototype.consume = function() +{ + this.consumed = true; +}; diff --git a/src/js/util/mxEventSource.js b/src/js/util/mxEventSource.js new file mode 100644 index 0000000..595f560 --- /dev/null +++ b/src/js/util/mxEventSource.js @@ -0,0 +1,191 @@ +/** + * $Id: mxEventSource.js,v 1.25 2012-04-16 10:54:20 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxEventSource + * + * Base class for objects that dispatch named events. To create a subclass that + * inherits from mxEventSource, the following code is used. + * + * (code) + * function MyClass() { }; + * + * MyClass.prototype = new mxEventSource(); + * MyClass.prototype.constructor = MyClass; + * (end) + * + * Known Subclasses: + * + * <mxGraphModel>, <mxGraph>, <mxGraphView>, <mxEditor>, <mxCellOverlay>, + * <mxToolbar>, <mxWindow> + * + * Constructor: mxEventSource + * + * Constructs a new event source. + */ +function mxEventSource(eventSource) +{ + this.setEventSource(eventSource); +}; + +/** + * Variable: eventListeners + * + * Holds the event names and associated listeners in an array. The array + * contains the event name followed by the respective listener for each + * registered listener. + */ +mxEventSource.prototype.eventListeners = null; + +/** + * Variable: eventsEnabled + * + * Specifies if events can be fired. Default is true. + */ +mxEventSource.prototype.eventsEnabled = true; + +/** + * Variable: eventSource + * + * Optional source for events. Default is null. + */ +mxEventSource.prototype.eventSource = null; + +/** + * Function: isEventsEnabled + * + * Returns <eventsEnabled>. + */ +mxEventSource.prototype.isEventsEnabled = function() +{ + return this.eventsEnabled; +}; + +/** + * Function: setEventsEnabled + * + * Sets <eventsEnabled>. + */ +mxEventSource.prototype.setEventsEnabled = function(value) +{ + this.eventsEnabled = value; +}; + +/** + * Function: getEventSource + * + * Returns <eventSource>. + */ +mxEventSource.prototype.getEventSource = function() +{ + return this.eventSource; +}; + +/** + * Function: setEventSource + * + * Sets <eventSource>. + */ +mxEventSource.prototype.setEventSource = function(value) +{ + this.eventSource = value; +}; + +/** + * Function: addListener + * + * Binds the specified function to the given event name. If no event name + * is given, then the listener is registered for all events. + * + * The parameters of the listener are the sender and an <mxEventObject>. + */ +mxEventSource.prototype.addListener = function(name, funct) +{ + if (this.eventListeners == null) + { + this.eventListeners = []; + } + + this.eventListeners.push(name); + this.eventListeners.push(funct); +}; + +/** + * Function: removeListener + * + * Removes all occurrences of the given listener from <eventListeners>. + */ +mxEventSource.prototype.removeListener = function(funct) +{ + if (this.eventListeners != null) + { + var i = 0; + + while (i < this.eventListeners.length) + { + if (this.eventListeners[i+1] == funct) + { + this.eventListeners.splice(i, 2); + } + else + { + i += 2; + } + } + } +}; + +/** + * Function: fireEvent + * + * Dispatches the given event to the listeners which are registered for + * the event. The sender argument is optional. The current execution scope + * ("this") is used for the listener invocation (see <mxUtils.bind>). + * + * Example: + * + * (code) + * fireEvent(new mxEventObject("eventName", key1, val1, .., keyN, valN)) + * (end) + * + * Parameters: + * + * evt - <mxEventObject> that represents the event. + * sender - Optional sender to be passed to the listener. Default value is + * the return value of <getEventSource>. + */ +mxEventSource.prototype.fireEvent = function(evt, sender) +{ + if (this.eventListeners != null && + this.isEventsEnabled()) + { + if (evt == null) + { + evt = new mxEventObject(); + } + + if (sender == null) + { + sender = this.getEventSource(); + } + + if (sender == null) + { + sender = this; + } + + var args = [sender, evt]; + + for (var i = 0; i < this.eventListeners.length; i += 2) + { + var listen = this.eventListeners[i]; + + if (listen == null || + listen == evt.getName()) + { + this.eventListeners[i+1].apply(this, args); + } + } + } +}; diff --git a/src/js/util/mxForm.js b/src/js/util/mxForm.js new file mode 100644 index 0000000..bcee299 --- /dev/null +++ b/src/js/util/mxForm.js @@ -0,0 +1,202 @@ +/** + * $Id: mxForm.js,v 1.16 2010-10-08 04:21:45 david Exp $ + * Copyright (c) 2006-2010, Gaudenz Alder, David Benson + */ +/** + * Class: mxForm + * + * A simple class for creating HTML forms. + * + * Constructor: mxForm + * + * Creates a HTML table using the specified classname. + */ +function mxForm(className) +{ + this.table = document.createElement('table'); + this.table.className = className; + this.body = document.createElement('tbody'); + + this.table.appendChild(this.body); +}; + +/** + * Variable: table + * + * Holds the DOM node that represents the table. + */ +mxForm.prototype.table = null; + +/** + * Variable: body + * + * Holds the DOM node that represents the tbody (table body). New rows + * can be added to this object using DOM API. + */ +mxForm.prototype.body = false; + +/** + * Function: getTable + * + * Returns the table that contains this form. + */ +mxForm.prototype.getTable = function() +{ + return this.table; +}; + +/** + * Function: addButtons + * + * Helper method to add an OK and Cancel button using the respective + * functions. + */ +mxForm.prototype.addButtons = function(okFunct, cancelFunct) +{ + var tr = document.createElement('tr'); + var td = document.createElement('td'); + tr.appendChild(td); + td = document.createElement('td'); + + // Adds the ok button + var button = document.createElement('button'); + mxUtils.write(button, mxResources.get('ok') || 'OK'); + td.appendChild(button); + + mxEvent.addListener(button, 'click', function() + { + okFunct(); + }); + + // Adds the cancel button + button = document.createElement('button'); + mxUtils.write(button, mxResources.get('cancel') || 'Cancel'); + td.appendChild(button); + + mxEvent.addListener(button, 'click', function() + { + cancelFunct(); + }); + + tr.appendChild(td); + this.body.appendChild(tr); +}; + +/** + * Function: addText + * + * Adds a textfield for the given name and value and returns the textfield. + */ +mxForm.prototype.addText = function(name, value) +{ + var input = document.createElement('input'); + + input.setAttribute('type', 'text'); + input.value = value; + + return this.addField(name, input); +}; + +/** + * Function: addCheckbox + * + * Adds a checkbox for the given name and value and returns the textfield. + */ +mxForm.prototype.addCheckbox = function(name, value) +{ + var input = document.createElement('input'); + + input.setAttribute('type', 'checkbox'); + this.addField(name, input); + + // IE can only change the checked value if the input is inside the DOM + if (value) + { + input.checked = true; + } + + return input; +}; + +/** + * Function: addTextarea + * + * Adds a textarea for the given name and value and returns the textarea. + */ +mxForm.prototype.addTextarea = function(name, value, rows) +{ + var input = document.createElement('textarea'); + + if (mxClient.IS_NS) + { + rows--; + } + + input.setAttribute('rows', rows || 2); + input.value = value; + + return this.addField(name, input); +}; + +/** + * Function: addCombo + * + * Adds a combo for the given name and returns the combo. + */ +mxForm.prototype.addCombo = function(name, isMultiSelect, size) +{ + var select = document.createElement('select'); + + if (size != null) + { + select.setAttribute('size', size); + } + + if (isMultiSelect) + { + select.setAttribute('multiple', 'true'); + } + + return this.addField(name, select); +}; + +/** + * Function: addOption + * + * Adds an option for the given label to the specified combo. + */ +mxForm.prototype.addOption = function(combo, label, value, isSelected) +{ + var option = document.createElement('option'); + + mxUtils.writeln(option, label); + option.setAttribute('value', value); + + if (isSelected) + { + option.setAttribute('selected', isSelected); + } + + combo.appendChild(option); +}; + +/** + * Function: addField + * + * Adds a new row with the name and the input field in two columns and + * returns the given input. + */ +mxForm.prototype.addField = function(name, input) +{ + var tr = document.createElement('tr'); + var td = document.createElement('td'); + mxUtils.write(td, name); + tr.appendChild(td); + + td = document.createElement('td'); + td.appendChild(input); + tr.appendChild(td); + this.body.appendChild(tr); + + return input; +}; diff --git a/src/js/util/mxGuide.js b/src/js/util/mxGuide.js new file mode 100644 index 0000000..ab5c26d --- /dev/null +++ b/src/js/util/mxGuide.js @@ -0,0 +1,364 @@ +/** + * $Id: mxGuide.js,v 1.7 2012-04-13 12:53:30 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxGuide + * + * Implements the alignment of selection cells to other cells in the graph. + * + * Constructor: mxGuide + * + * Constructs a new guide object. + */ +function mxGuide(graph, states) +{ + this.graph = graph; + this.setStates(states); +}; + +/** + * Variable: graph + * + * Reference to the enclosing <mxGraph> instance. + */ +mxGuide.prototype.graph = null; + +/** + * Variable: states + * + * Contains the <mxCellStates> that are used for alignment. + */ +mxGuide.prototype.states = null; + +/** + * Variable: horizontal + * + * Specifies if horizontal guides are enabled. Default is true. + */ +mxGuide.prototype.horizontal = true; + +/** + * Variable: vertical + * + * Specifies if vertical guides are enabled. Default is true. + */ +mxGuide.prototype.vertical = true; + +/** + * Variable: vertical + * + * Holds the <mxShape> for the horizontal guide. + */ +mxGuide.prototype.guideX = null; + +/** + * Variable: vertical + * + * Holds the <mxShape> for the vertical guide. + */ +mxGuide.prototype.guideY = null; + +/** + * Variable: crisp + * + * Specifies if theguide should be rendered in crisp mode if applicable. + * Default is true. + */ +mxGuide.prototype.crisp = true; + +/** + * Function: setStates + * + * Sets the <mxCellStates> that should be used for alignment. + */ +mxGuide.prototype.setStates = function(states) +{ + this.states = states; +}; + +/** + * Function: isEnabledForEvent + * + * Returns true if the guide should be enabled for the given native event. This + * implementation always returns true. + */ +mxGuide.prototype.isEnabledForEvent = function(evt) +{ + return true; +}; + +/** + * Function: getGuideTolerance + * + * Returns the tolerance for the guides. Default value is + * gridSize * scale / 2. + */ +mxGuide.prototype.getGuideTolerance = function() +{ + return this.graph.gridSize * this.graph.view.scale / 2; +}; + +/** + * Function: createGuideShape + * + * Returns the mxShape to be used for painting the respective guide. This + * implementation returns a new, dashed and crisp <mxPolyline> using + * <mxConstants.GUIDE_COLOR> and <mxConstants.GUIDE_STROKEWIDTH> as the format. + * + * Parameters: + * + * horizontal - Boolean that specifies which guide should be created. + */ +mxGuide.prototype.createGuideShape = function(horizontal) +{ + var guide = new mxPolyline([], mxConstants.GUIDE_COLOR, mxConstants.GUIDE_STROKEWIDTH); + guide.crisp = this.crisp; + guide.isDashed = true; + + return guide; +}; + +/** + * Function: move + * + * Moves the <bounds> by the given <mxPoint> and returnt the snapped point. + */ +mxGuide.prototype.move = function(bounds, delta, gridEnabled) +{ + if (this.states != null && (this.horizontal || this.vertical) && bounds != null && delta != null) + { + var trx = this.graph.getView().translate; + var scale = this.graph.getView().scale; + var dx = delta.x; + var dy = delta.y; + + var overrideX = false; + var overrideY = false; + + var tt = this.getGuideTolerance(); + var ttX = tt; + var ttY = tt; + + var b = bounds.clone(); + b.x += delta.x; + b.y += delta.y; + + var left = b.x; + var right = b.x + b.width; + var center = b.getCenterX(); + var top = b.y; + var bottom = b.y + b.height; + var middle = b.getCenterY(); + + // Snaps the left, center and right to the given x-coordinate + function snapX(x) + { + x += this.graph.panDx; + var override = false; + + if (Math.abs(x - center) < ttX) + { + dx = x - bounds.getCenterX(); + ttX = Math.abs(x - center); + override = true; + } + else if (Math.abs(x - left) < ttX) + { + dx = x - bounds.x; + ttX = Math.abs(x - left); + override = true; + } + else if (Math.abs(x - right) < ttX) + { + dx = x - bounds.x - bounds.width; + ttX = Math.abs(x - right); + override = true; + } + + if (override) + { + if (this.guideX == null) + { + this.guideX = this.createGuideShape(true); + + // Makes sure to use either VML or SVG shapes in order to implement + // event-transparency on the background area of the rectangle since + // HTML shapes do not let mouseevents through even when transparent + this.guideX.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ? + mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG; + this.guideX.init(this.graph.getView().getOverlayPane()); + + if (this.graph.dialect == mxConstants.DIALECT_SVG) + { + this.guideX.node.setAttribute('pointer-events', 'none'); + this.guideX.pipe.setAttribute('pointer-events', 'none'); + } + } + + var c = this.graph.container; + x -= this.graph.panDx; + this.guideX.points = [new mxPoint(x, -this.graph.panDy), new mxPoint(x, c.scrollHeight - 3 - this.graph.panDy)]; + } + + overrideX = overrideX || override; + }; + + // Snaps the top, middle or bottom to the given y-coordinate + function snapY(y) + { + y += this.graph.panDy; + var override = false; + + if (Math.abs(y - middle) < ttY) + { + dy = y - bounds.getCenterY(); + ttY = Math.abs(y - middle); + override = true; + } + else if (Math.abs(y - top) < ttY) + { + dy = y - bounds.y; + ttY = Math.abs(y - top); + override = true; + } + else if (Math.abs(y - bottom) < ttY) + { + dy = y - bounds.y - bounds.height; + ttY = Math.abs(y - bottom); + override = true; + } + + if (override) + { + if (this.guideY == null) + { + this.guideY = this.createGuideShape(false); + + // Makes sure to use either VML or SVG shapes in order to implement + // event-transparency on the background area of the rectangle since + // HTML shapes do not let mouseevents through even when transparent + this.guideY.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ? + mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG; + this.guideY.init(this.graph.getView().getOverlayPane()); + + if (this.graph.dialect == mxConstants.DIALECT_SVG) + { + this.guideY.node.setAttribute('pointer-events', 'none'); + this.guideY.pipe.setAttribute('pointer-events', 'none'); + } + } + + var c = this.graph.container; + y -= this.graph.panDy; + this.guideY.points = [new mxPoint(-this.graph.panDx, y), new mxPoint(c.scrollWidth - 3 - this.graph.panDx, y)]; + } + + overrideY = overrideY || override; + }; + + for (var i = 0; i < this.states.length; i++) + { + var state = this.states[i]; + + if (state != null) + { + // Align x + if (this.horizontal) + { + snapX.call(this, state.getCenterX()); + snapX.call(this, state.x); + snapX.call(this, state.x + state.width); + } + + // Align y + if (this.vertical) + { + snapY.call(this, state.getCenterY()); + snapY.call(this, state.y); + snapY.call(this, state.y + state.height); + } + } + } + + if (!overrideX && this.guideX != null) + { + this.guideX.node.style.visibility = 'hidden'; + } + else if (this.guideX != null) + { + this.guideX.node.style.visibility = 'visible'; + this.guideX.redraw(); + } + + if (!overrideY && this.guideY != null) + { + this.guideY.node.style.visibility = 'hidden'; + } + else if (this.guideY != null) + { + this.guideY.node.style.visibility = 'visible'; + this.guideY.redraw(); + } + + // Moves cells that are off-grid back to the grid on move + if (gridEnabled) + { + if (!overrideX) + { + var tx = bounds.x - (this.graph.snap(bounds.x / + scale - trx.x) + trx.x) * scale; + dx = this.graph.snap(dx / scale) * scale - tx; + } + + if (!overrideY) + { + var ty = bounds.y - (this.graph.snap(bounds.y / + scale - trx.y) + trx.y) * scale; + dy = this.graph.snap(dy / scale) * scale - ty; + } + } + + delta = new mxPoint(dx, dy); + } + + return delta; +}; + +/** + * Function: hide + * + * Hides all current guides. + */ +mxGuide.prototype.hide = function() +{ + if (this.guideX != null) + { + this.guideX.node.style.visibility = 'hidden'; + } + + if (this.guideY != null) + { + this.guideY.node.style.visibility = 'hidden'; + } +}; + +/** + * Function: destroy + * + * Destroys all resources that this object uses. + */ +mxGuide.prototype.destroy = function() +{ + if (this.guideX != null) + { + this.guideX.destroy(); + this.guideX = null; + } + + if (this.guideY != null) + { + this.guideY.destroy(); + this.guideY = null; + } +}; diff --git a/src/js/util/mxImage.js b/src/js/util/mxImage.js new file mode 100644 index 0000000..39d1a09 --- /dev/null +++ b/src/js/util/mxImage.js @@ -0,0 +1,40 @@ +/** + * $Id: mxImage.js,v 1.7 2010-01-02 09:45:14 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxImage + * + * Encapsulates the URL, width and height of an image. + * + * Constructor: mxImage + * + * Constructs a new image. + */ +function mxImage(src, width, height) +{ + this.src = src; + this.width = width; + this.height = height; +}; + +/** + * Variable: src + * + * String that specifies the URL of the image. + */ +mxImage.prototype.src = null; + +/** + * Variable: width + * + * Integer that specifies the width of the image. + */ +mxImage.prototype.width = null; + +/** + * Variable: height + * + * Integer that specifies the height of the image. + */ +mxImage.prototype.height = null; diff --git a/src/js/util/mxImageBundle.js b/src/js/util/mxImageBundle.js new file mode 100644 index 0000000..dc4c2cf --- /dev/null +++ b/src/js/util/mxImageBundle.js @@ -0,0 +1,98 @@ +/** + * $Id: mxImageBundle.js,v 1.3 2011-01-20 19:08:11 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxImageBundle + * + * Maps from keys to base64 encoded images or file locations. All values must + * be URLs or use the format data:image/format followed by a comma and the base64 + * encoded image data, eg. "data:image/gif,XYZ", where XYZ is the base64 encoded + * image data. + * + * To add a new image bundle to an existing graph, the following code is used: + * + * (code) + * var bundle = new mxImageBundle(alt); + * bundle.putImage('myImage', 'data:image/gif,R0lGODlhEAAQAMIGAAAAAICAAICAgP' + + * '//AOzp2O3r2////////yH+FUNyZWF0ZWQgd2l0aCBUaGUgR0lNUAAh+QQBCgAHACwAAAAA' + + * 'EAAQAAADTXi63AowynnAMDfjPUDlnAAJhmeBFxAEloliKltWmiYCQvfVr6lBPB1ggxN1hi' + + * 'laSSASFQpIV5HJBDyHpqK2ejVRm2AAgZCdmCGO9CIBADs=', fallback); + * graph.addImageBundle(bundle); + * (end); + * + * Alt is an optional boolean (default is false) that specifies if the value + * or the fallback should be returned in <getImage>. + * + * The image can then be referenced in any cell style using image=myImage. + * If you are using mxOutline, you should use the same image bundles in the + * graph that renders the outline. + * + * The keys for images are resolved in <mxGraph.postProcessCellStyle> and + * turned into a data URI if the returned value has a short data URI format + * as specified above. + * + * A typical value for the fallback is a MTHML link as defined in RFC 2557. + * Note that this format requires a file to be dynamically created on the + * server-side, or the page that contains the graph to be modified to contain + * the resources, this can be done by adding a comment that contains the + * resource in the HEAD section of the page after the title tag. + * + * This type of fallback mechanism should be used in IE6 and IE7. IE8 does + * support data URIs, but the maximum size is limited to 32 KB, which means + * all data URIs should be limited to 32 KB. + */ +function mxImageBundle(alt) +{ + this.images = []; + this.alt = (alt != null) ? alt : false; +}; + +/** + * Variable: images + * + * Maps from keys to images. + */ +mxImageBundle.prototype.images = null; + +/** + * Variable: alt + * + * Specifies if the fallback representation should be returned. + */ +mxImageBundle.prototype.images = null; + +/** + * Function: putImage + * + * Adds the specified entry to the map. The entry is an object with a value and + * fallback property as specified in the arguments. + */ +mxImageBundle.prototype.putImage = function(key, value, fallback) +{ + this.images[key] = {value: value, fallback: fallback}; +}; + +/** + * Function: getImage + * + * Returns the value for the given key. This returns the value + * or fallback, depending on <alt>. The fallback is returned if + * <alt> is true, the value is returned otherwise. + */ +mxImageBundle.prototype.getImage = function(key) +{ + var result = null; + + if (key != null) + { + var img = this.images[key]; + + if (img != null) + { + result = (this.alt) ? img.fallback : img.value; + } + } + + return result; +}; diff --git a/src/js/util/mxImageExport.js b/src/js/util/mxImageExport.js new file mode 100644 index 0000000..dcbcf9a --- /dev/null +++ b/src/js/util/mxImageExport.js @@ -0,0 +1,1412 @@ +/** + * $Id: mxImageExport.js,v 1.47 2012-09-24 14:54:32 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxImageExport + * + * Creates a new image export instance to be used with an export canvas. Here + * is an example that uses this class to create an image via a backend using + * <mxXmlExportCanvas>. + * + * (code) + * var xmlDoc = mxUtils.createXmlDocument(); + * var root = xmlDoc.createElement('output'); + * xmlDoc.appendChild(root); + * + * var xmlCanvas = new mxXmlCanvas2D(root); + * var imgExport = new mxImageExport(); + * imgExport.drawState(graph.getView().getState(graph.model.root), xmlCanvas); + * + * var bounds = graph.getGraphBounds(); + * var w = Math.ceil(bounds.x + bounds.width); + * var h = Math.ceil(bounds.y + bounds.height); + * + * var xml = mxUtils.getXml(root); + * new mxXmlRequest('export', 'format=png&w=' + w + + * '&h=' + h + '&bg=#F9F7ED&xml=' + encodeURIComponent(xml)) + * .simulate(document, '_blank'); + * (end) + * + * In order to export images for a graph whose container is not visible or not + * part of the DOM, the following workaround can be used to compute the size of + * the labels. + * + * (code) + * mxText.prototype.getTableSize = function(table) + * { + * var oldParent = table.parentNode; + * + * document.body.appendChild(table); + * var size = new mxRectangle(0, 0, table.offsetWidth, table.offsetHeight); + * oldParent.appendChild(table); + * + * return size; + * }; + * (end) + * + * Constructor: mxImageExport + * + * Constructs a new image export. + */ +function mxImageExport() +{ + this.initShapes(); + this.initMarkers(); +}; + +/** + * Variable: includeOverlays + * + * Specifies if overlays should be included in the export. Default is false. + */ +mxImageExport.prototype.includeOverlays = false; + +/** + * Variable: glassSize + * + * Reference to the thread while the animation is running. + */ +mxImageExport.prototype.glassSize = 0.4; + +/** + * Variable: shapes + * + * Holds implementations for the built-in shapes. + */ +mxImageExport.prototype.shapes = null; + +/** + * Variable: markers + * + * Holds implementations for the built-in markers. + */ +mxImageExport.prototype.markers = null; + +/** + * Function: drawState + * + * Draws the given state and all its descendants to the given canvas. + */ +mxImageExport.prototype.drawState = function(state, canvas) +{ + if (state != null) + { + if (state.shape != null) + { + var shape = (state.shape.stencil != null) ? + state.shape.stencil : + this.shapes[state.style[mxConstants.STYLE_SHAPE]]; + + if (shape == null) + { + // Checks if there is a custom shape + if (typeof(state.shape.redrawPath) == 'function') + { + shape = this.createShape(state, canvas); + } + // Uses a rectangle for all vertices where no shape can be found + else if (state.view.graph.getModel().isVertex(state.cell)) + { + shape = this.shapes['rectangle']; + } + } + + if (shape != null) + { + this.drawShape(state, canvas, shape); + + if (this.includeOverlays) + { + this.drawOverlays(state, canvas); + } + } + } + + var graph = state.view.graph; + var childCount = graph.model.getChildCount(state.cell); + + for (var i = 0; i < childCount; i++) + { + var childState = graph.view.getState(graph.model.getChildAt(state.cell, i)); + this.drawState(childState, canvas); + } + } +}; + +/** + * Function: createShape + * + * Creates a shape wrapper for the custom shape in the given cell state and + * links its output to the given canvas. + */ +mxImageExport.prototype.createShape = function(state, canvas) +{ + return { + drawShape: function(canvas, state, bounds, background) + { + var path = + { + translate: new mxPoint(bounds.x, bounds.y), + moveTo: function(x, y) + { + canvas.moveTo(this.translate.x + x, this.translate.y + y); + }, + lineTo: function(x, y) + { + canvas.lineTo(this.translate.x + x, this.translate.y + y); + }, + quadTo: function(x1, y1, x, y) + { + canvas.quadTo(this.translate.x + x1, this.translate.y + y1, this.translate.x + x, this.translate.y + y); + }, + curveTo: function(x1, y1, x2, y2, x, y) + { + canvas.curveTo(this.translate.x + x1, this.translate.y + y1, this.translate.x + x2, this.translate.y + y2, this.translate.x + x, this.translate.y + y); + }, + end: function() + { + // do nothing + }, + close: function() + { + canvas.close(); + } + }; + + if (!background) + { + canvas.fillAndStroke(); + } + + // LATER: Remove empty path if shape does not implement foreground, add shadow/clipping + canvas.begin(); + state.shape.redrawPath.call(state.shape, path, bounds.x, bounds.y, bounds.width, bounds.height, !background); + + if (!background) + { + canvas.fillAndStroke(); + } + + return true; + } + }; +}; + +/** + * Function: drawOverlays + * + * Draws the overlays for the given state. This is called if <includeOverlays> + * is true. + */ +mxImageExport.prototype.drawOverlays = function(state, canvas) +{ + if (state.overlays != null) + { + state.overlays.visit(function(id, shape) + { + var bounds = shape.bounds; + + if (bounds != null) + { + canvas.image(bounds.x, bounds.y, bounds.width, bounds.height, shape.image); + } + }); + } +}; + +/** + * Function: drawShape + * + * Draws the given state to the given canvas. + */ +mxImageExport.prototype.drawShape = function(state, canvas, shape) +{ + var rotation = mxUtils.getNumber(state.style, mxConstants.STYLE_ROTATION, 0); + var direction = mxUtils.getValue(state.style, mxConstants.STYLE_DIRECTION, null); + + // New styles for shape flipping the stencil + var flipH = state.style[mxConstants.STYLE_STENCIL_FLIPH]; + var flipV = state.style[mxConstants.STYLE_STENCIL_FLIPV]; + + if (flipH ? !flipV : flipV) + { + rotation *= -1; + } + + // Default direction is east (ignored if rotation exists) + if (direction != null) + { + if (direction == 'north') + { + rotation += 270; + } + else if (direction == 'west') + { + rotation += 180; + } + else if (direction == 'south') + { + rotation += 90; + } + } + + if (flipH && flipV) + { + rotation += 180; + flipH = false; + flipV = false; + } + + // Saves the global state for each cell + canvas.save(); + + // Adds rotation and horizontal/vertical flipping + // FIXME: Rotation and stencil flip only supported for stencil shapes + rotation = rotation % 360; + + if (rotation != 0 || flipH || flipV) + { + canvas.rotate(rotation, flipH, flipV, state.getCenterX(), state.getCenterY()); + } + + // Note: Overwritten in mxStencil.paintShape (can depend on aspect) + var scale = state.view.scale; + var sw = mxUtils.getNumber(state.style, mxConstants.STYLE_STROKEWIDTH, 1) * scale; + canvas.setStrokeWidth(sw); + + var sw2 = sw / 2; + var bg = this.getBackgroundBounds(state); + + // Stencils will rotate the bounds as required + if (state.shape.stencil == null && (direction == 'south' || direction == 'north')) + { + var dx = (bg.width - bg.height) / 2; + bg.x += dx; + bg.y += -dx; + var tmp = bg.width; + bg.width = bg.height; + bg.height = tmp; + } + + var bb = new mxRectangle(bg.x - sw2, bg.y - sw2, bg.width + sw, bg.height + sw); + var alpha = mxUtils.getValue(state.style, mxConstants.STYLE_OPACITY, 100) / 100; + + var shp = state.style[mxConstants.STYLE_SHAPE]; + var imageShape = shp == mxConstants.SHAPE_IMAGE; + var gradientColor = (imageShape) ? null : mxUtils.getValue(state.style, mxConstants.STYLE_GRADIENTCOLOR); + + // Converts colors with special keyword none to null + if (gradientColor == mxConstants.NONE) + { + gradientColor = null; + } + + var fcKey = (imageShape) ? mxConstants.STYLE_IMAGE_BACKGROUND : mxConstants.STYLE_FILLCOLOR; + var fillColor = mxUtils.getValue(state.style, fcKey, null); + + if (fillColor == mxConstants.NONE) + { + fillColor = null; + } + + var scKey = (imageShape) ? mxConstants.STYLE_IMAGE_BORDER : mxConstants.STYLE_STROKECOLOR; + var strokeColor = mxUtils.getValue(state.style, scKey, null); + + if (strokeColor == mxConstants.NONE) + { + strokeColor = null; + } + + var glass = (fillColor != null && (shp == mxConstants.SHAPE_LABEL || shp == mxConstants.SHAPE_RECTANGLE)); + + // Draws the shadow if the fillColor is not transparent + if (mxUtils.getValue(state.style, mxConstants.STYLE_SHADOW, false)) + { + this.drawShadow(canvas, state, shape, rotation, flipH, flipV, bg, alpha, fillColor != null); + } + + canvas.setAlpha(alpha); + + // Sets the dashed state + if (mxUtils.getValue(state.style, mxConstants.STYLE_DASHED, '0') == '1') + { + canvas.setDashed(true); + + // Supports custom dash patterns + var dash = state.style['dashPattern']; + + if (dash != null) + { + canvas.setDashPattern(dash); + } + } + + // Draws background and foreground + if (strokeColor != null || fillColor != null) + { + if (strokeColor != null) + { + canvas.setStrokeColor(strokeColor); + } + + if (fillColor != null) + { + if (gradientColor != null && gradientColor != 'transparent') + { + canvas.setGradient(fillColor, gradientColor, bg.x, bg.y, bg.width, bg.height, direction); + } + else + { + canvas.setFillColor(fillColor); + } + } + + // Draws background and foreground of shape + glass = shape.drawShape(canvas, state, bg, true, false) && glass; + shape.drawShape(canvas, state, bg, false, false); + } + + // Draws the glass effect + // Requires background in generic shape for clipping + if (glass && mxUtils.getValue(state.style, mxConstants.STYLE_GLASS, 0) == 1) + { + this.drawGlass(state, canvas, bb, shape, this.glassSize); + } + + // Draws the image (currently disabled for everything but image and label shapes) + if (imageShape || shp == mxConstants.SHAPE_LABEL) + { + var src = state.view.graph.getImage(state); + + if (src != null) + { + var imgBounds = this.getImageBounds(state); + + if (imgBounds != null) + { + this.drawImage(state, canvas, imgBounds, src); + } + } + } + + // Restores canvas state + canvas.restore(); + + // Draws the label (label has separate rotation) + var txt = state.text; + + // Does not use mxCellRenderer.getLabelValue to avoid conversion of HTML entities for VML + var label = state.view.graph.getLabel(state.cell); + + if (txt != null && label != null && label.length > 0) + { + canvas.save(); + canvas.setAlpha(mxUtils.getValue(state.style, mxConstants.STYLE_TEXT_OPACITY, 100) / 100); + var bounds = new mxRectangle(txt.boundingBox.x, txt.boundingBox.y, txt.boundingBox.width, txt.boundingBox.height); + var vert = mxUtils.getValue(state.style, mxConstants.STYLE_HORIZONTAL, 1) == 0; + + // Vertical error offset + bounds.y += 2; + + if (vert) + { + if (txt.dialect != mxConstants.DIALECT_SVG) + { + var cx = bounds.x + bounds.width / 2; + var cy = bounds.y + bounds.height / 2; + var tmp = bounds.width; + bounds.width = bounds.height; + bounds.height = tmp; + bounds.x = cx - bounds.width / 2; + bounds.y = cy - bounds.height / 2; + } + else if (txt.dialect == mxConstants.DIALECT_SVG) + { + // Workarounds for different label bounding boxes (mostly ignoring rotation). + // LATER: Fix in mxText so that the bounding box is consistent and rotated. + // TODO: Check non-center/middle-aligned vertical labels in VML for IE8. + var b = state.y + state.height; + var cx = bounds.getCenterX() - state.x; + var cy = bounds.getCenterY() - state.y; + + var y = b - cx - bounds.height / 2; + bounds.x = state.x + cy - bounds.width / 2; + bounds.y = y; + //bounds.x -= state.height / 2 - state.width / 2; + //bounds.y -= state.width / 2 - state.height / 2; + } + } + + this.drawLabelBackground(state, canvas, bounds, vert); + this.drawLabel(state, canvas, bounds, vert, label); + canvas.restore(); + } +}; + +/** + * Function: drawGlass + * + * Draws the given state to the given canvas. + */ +mxImageExport.prototype.drawShadow = function(canvas, state, shape, rotation, flipH, flipV, bounds, alpha, filled) +{ + // Requires background in generic shape for shadow, looks like only one + // fillAndStroke is allowed per current path, try working around that + // Computes rotated shadow offset + var rad = rotation * Math.PI / 180; + var cos = Math.cos(-rad); + var sin = Math.sin(-rad); + var offset = mxUtils.getRotatedPoint(new mxPoint(mxConstants.SHADOW_OFFSET_X, mxConstants.SHADOW_OFFSET_Y), cos, sin); + + if (flipH) + { + offset.x *= -1; + } + + if (flipV) + { + offset.y *= -1; + } + + // TODO: Use save/restore instead of negative offset to restore (requires fix for HTML canvas) + canvas.translate(offset.x, offset.y); + + // Returns true if a shadow has been painted (path has been created) + if (shape.drawShape(canvas, state, bounds, true, true)) + { + canvas.setAlpha(mxConstants.SHADOW_OPACITY * alpha); + canvas.shadow(mxConstants.SHADOWCOLOR, filled); + } + + canvas.translate(-offset.x, -offset.y); +}; + +/** + * Function: drawGlass + * + * Draws the given state to the given canvas. + */ +mxImageExport.prototype.drawGlass = function(state, canvas, bounds, shape, size) +{ + // LATER: Clipping region should include stroke + if (shape.drawShape(canvas, state, bounds, true, false)) + { + canvas.save(); + canvas.clip(); + canvas.setGlassGradient(bounds.x, bounds.y, bounds.width, bounds.height); + + canvas.begin(); + canvas.moveTo(bounds.x, bounds.y); + canvas.lineTo(bounds.x, (bounds.y + bounds.height * size)); + canvas.quadTo((bounds.x + bounds.width * 0.5), + (bounds.y + bounds.height * 0.7), bounds.x + bounds.width, + (bounds.y + bounds.height * size)); + canvas.lineTo(bounds.x + bounds.width, bounds.y); + canvas.close(); + + canvas.fill(); + canvas.restore(); + } +}; + +/** + * Function: drawImage + * + * Draws the given state to the given canvas. + */ +mxImageExport.prototype.drawImage = function(state, canvas, bounds, image) +{ + var aspect = mxUtils.getValue(state.style, mxConstants.STYLE_IMAGE_ASPECT, 1) == 1; + var flipH = mxUtils.getValue(state.style, mxConstants.STYLE_IMAGE_FLIPH, 0) == 1; + var flipV = mxUtils.getValue(state.style, mxConstants.STYLE_IMAGE_FLIPV, 0) == 1; + + canvas.image(bounds.x, bounds.y, bounds.width, bounds.height, image, aspect, flipH, flipV); +}; + +/** + * Function: drawLabelBackground + * + * Draws background for the label of the given state to the given canvas. + */ +mxImageExport.prototype.drawLabelBackground = function(state, canvas, bounds, vert) +{ + var stroke = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_BORDERCOLOR); + var fill = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_BACKGROUNDCOLOR); + + if (stroke == mxConstants.NONE) + { + stroke = null; + } + + if (fill == mxConstants.NONE) + { + fill = null; + } + + if (stroke != null || fill != null) + { + var x = bounds.x; + var y = bounds.y - mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_PADDING, 0); + var w = bounds.width; + var h = bounds.height; + + if (vert) + { + x += (w - h) / 2; + y += (h - w) / 2; + var tmp = w; + w = h; + h = tmp; + } + + if (fill != null) + { + canvas.setFillColor(fill); + } + + if (stroke != null) + { + canvas.setStrokeColor(stroke); + canvas.setStrokeWidth(1); + canvas.setDashed(false); + } + + canvas.rect(x, y, w, h); + + if (fill != null && stroke != null) + { + canvas.fillAndStroke(); + } + else if (fill != null) + { + canvas.fill(); + } + else if (stroke != null) + { + canvas.stroke(); + } + } +}; + +/** + * Function: drawLabel + * + * Draws the given state to the given canvas. + */ +mxImageExport.prototype.drawLabel = function(state, canvas, bounds, vert, str) +{ + var scale = state.view.scale; + + // Applies color + canvas.setFontColor(mxUtils.getValue(state.style, mxConstants.STYLE_FONTCOLOR, '#000000')); + + // Applies font settings + canvas.setFontFamily(mxUtils.getValue(state.style, mxConstants.STYLE_FONTFAMILY, + mxConstants.DEFAULT_FONTFAMILY)); + canvas.setFontStyle(mxUtils.getValue(state.style, mxConstants.STYLE_FONTSTYLE, 0)); + canvas.setFontSize(mxUtils.getValue(state.style, mxConstants.STYLE_FONTSIZE, + mxConstants.DEFAULT_FONTSIZE) * scale); + + var align = mxUtils.getValue(state.style, mxConstants.STYLE_ALIGN, mxConstants.ALIGN_LEFT); + + // Uses null alignment for default values (valign default is 'top' which is fine) + if (align == 'left') + { + align = null; + } + + var y = bounds.y - mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_PADDING, 0); + var wrap = state.view.graph.isWrapping(state.cell); + var html = state.view.graph.isHtmlLabel(state.cell); + + // Replaces linefeeds in HTML markup to match the display output + if (html && mxText.prototype.replaceLinefeeds) + { + str = str.replace(/\n/g, '<br/>'); + } + + canvas.text(bounds.x, y, bounds.width, bounds.height, str, align, null, vert, wrap, (html) ? 'html' : ''); +}; + +/** + * Function: getBackgroundBounds + * + * Draws the given state to the given canvas. + */ +mxImageExport.prototype.getBackgroundBounds = function(state) +{ + if (state.style[mxConstants.STYLE_SHAPE] == mxConstants.SHAPE_SWIMLANE) + { + var scale = state.view.scale; + var start = mxUtils.getValue(state.style, mxConstants.STYLE_STARTSIZE, mxConstants.DEFAULT_STARTSIZE) * scale; + var w = state.width; + var h = state.height; + + if (mxUtils.getValue(state.style, mxConstants.STYLE_HORIZONTAL, true)) + { + h = start; + } + else + { + w = start; + } + + return new mxRectangle(state.x, state.y, Math.min(state.width, w), Math.min(state.height, h)); + } + else + { + return new mxRectangle(state.x, state.y, state.width, state.height); + } +}; + +/** + * Function: getImageBounds + * + * Draws the given state to the given canvas. + */ +mxImageExport.prototype.getImageBounds = function(state) +{ + var bounds = new mxRectangle(state.x, state.y, state.width, state.height); + var style = state.style; + + if (mxUtils.getValue(style, mxConstants.STYLE_SHAPE) != mxConstants.SHAPE_IMAGE) + { + var imgAlign = mxUtils.getValue(style, mxConstants.STYLE_IMAGE_ALIGN, mxConstants.ALIGN_LEFT); + var imgValign = mxUtils.getValue(style, mxConstants.STYLE_IMAGE_VERTICAL_ALIGN, mxConstants.ALIGN_MIDDLE); + var imgWidth = mxUtils.getValue(style, mxConstants.STYLE_IMAGE_WIDTH, mxConstants.DEFAULT_IMAGESIZE); + var imgHeight = mxUtils.getValue(style, mxConstants.STYLE_IMAGE_HEIGHT, mxConstants.DEFAULT_IMAGESIZE); + var spacing = mxUtils.getValue(style, mxConstants.STYLE_SPACING, 2); + + if (imgAlign == mxConstants.ALIGN_CENTER) + { + bounds.x += (bounds.width - imgWidth) / 2; + } + else if (imgAlign == mxConstants.ALIGN_RIGHT) + { + bounds.x += bounds.width - imgWidth - spacing - 2; + } + else + // LEFT + { + bounds.x += spacing + 4; + } + + if (imgValign == mxConstants.ALIGN_TOP) + { + bounds.y += spacing; + } + else if (imgValign == mxConstants.ALIGN_BOTTOM) + { + bounds.y += bounds.height - imgHeight - spacing; + } + else + // MIDDLE + { + bounds.y += (bounds.height - imgHeight) / 2; + } + + bounds.width = imgWidth; + bounds.height = imgHeight; + } + + return bounds; +}; + +/** + * Function: drawMarker + * + * Initializes the built-in shapes. + */ +mxImageExport.prototype.drawMarker = function(canvas, state, source) +{ + var offset = null; + + // Computes the norm and the inverse norm + var pts = state.absolutePoints; + var n = pts.length; + + var p0 = (source) ? pts[1] : pts[n - 2]; + var pe = (source) ? pts[0] : pts[n - 1]; + + var dx = pe.x - p0.x; + var dy = pe.y - p0.y; + + var dist = Math.max(1, Math.sqrt(dx * dx + dy * dy)); + + var unitX = dx / dist; + var unitY = dy / dist; + + var size = mxUtils.getValue(state.style, (source) ? + mxConstants.STYLE_STARTSIZE : + mxConstants.STYLE_ENDSIZE, + mxConstants.DEFAULT_MARKERSIZE); + + // Allow for stroke width in the end point used and the + // orthogonal vectors describing the direction of the marker + // TODO: Should get strokewidth from canvas (same for strokecolor) + var sw = mxUtils.getValue(state.style, mxConstants.STYLE_STROKEWIDTH, 1); + + pe = pe.clone(); + + var type = mxUtils.getValue(state.style, (source) ? + mxConstants.STYLE_STARTARROW : + mxConstants.STYLE_ENDARROW); + var f = this.markers[type]; + + if (f != null) + { + offset = f(canvas, state, type, pe, unitX, unitY, size, source, sw); + } + + return offset; +}; + +/** + * Function: initShapes + * + * Initializes the built-in shapes. + */ +mxImageExport.prototype.initShapes = function() +{ + this.shapes = []; + + // Implements the rectangle and rounded rectangle shape + this.shapes['rectangle'] = + { + drawShape: function(canvas, state, bounds, background) + { + if (background) + { + // Paints the shape + if (mxUtils.getValue(state.style, mxConstants.STYLE_ROUNDED, false)) + { + var f = mxUtils.getValue(state.style, mxConstants.STYLE_ARCSIZE, mxConstants.RECTANGLE_ROUNDING_FACTOR * 100) / 100; + var r = Math.min(bounds.width * f, bounds.height * f); + canvas.roundrect(bounds.x, bounds.y, bounds.width, bounds.height, r, r); + } + else + { + canvas.rect(bounds.x, bounds.y, bounds.width, bounds.height); + } + + return true; + } + else + { + canvas.fillAndStroke(); + } + } + }; + + // Implements the swimlane shape + this.shapes['swimlane'] = + { + drawShape: function(canvas, state, bounds, background) + { + if (background) + { + if (mxUtils.getValue(state.style, mxConstants.STYLE_ROUNDED, false)) + { + var r = Math.min(bounds.width * mxConstants.RECTANGLE_ROUNDING_FACTOR, + bounds.height * mxConstants.RECTANGLE_ROUNDING_FACTOR); + canvas.roundrect(bounds.x, bounds.y, bounds.width, bounds.height, r, r); + } + else + { + canvas.rect(bounds.x, bounds.y, bounds.width, bounds.height); + } + + return true; + } + else + { + canvas.fillAndStroke(); + canvas.begin(); + + var x = state.x; + var y = state.y; + var w = state.width; + var h = state.height; + + if (mxUtils.getValue(state.style, mxConstants.STYLE_HORIZONTAL, 1) == 0) + { + x += bounds.width; + w -= bounds.width; + + canvas.moveTo(x, y); + canvas.lineTo(x + w, y); + canvas.lineTo(x + w, y + h); + canvas.lineTo(x, y + h); + } + else + { + y += bounds.height; + h -= bounds.height; + + canvas.moveTo(x, y); + canvas.lineTo(x, y + h); + canvas.lineTo(x + w, y + h); + canvas.lineTo(x + w, y); + } + + canvas.stroke(); + } + } + }; + + this.shapes['image'] = this.shapes['rectangle']; + this.shapes['label'] = this.shapes['rectangle']; + + var imageExport = this; + + this.shapes['connector'] = + { + translatePoint: function(points, index, offset) + { + if (offset != null) + { + var pt = points[index].clone(); + pt.x += offset.x; + pt.y += offset.y; + points[index] = pt; + } + }, + + drawShape: function(canvas, state, bounds, background, shadow) + { + if (background) + { + var rounded = mxUtils.getValue(state.style, mxConstants.STYLE_ROUNDED, false); + var arcSize = mxConstants.LINE_ARCSIZE / 2; + + // Does not draw the markers in the shadow to match the display + canvas.setFillColor((shadow) ? mxConstants.NONE : mxUtils.getValue(state.style, mxConstants.STYLE_STROKECOLOR, "#000000")); + canvas.setDashed(false); + var pts = state.absolutePoints.slice(); + this.translatePoint(pts, 0, imageExport.drawMarker(canvas, state, true)); + this.translatePoint(pts, pts.length - 1, imageExport.drawMarker(canvas, state, false)); + canvas.setDashed(mxUtils.getValue(state.style, mxConstants.STYLE_DASHED, '0') == '1'); + + var pt = pts[0]; + var pe = pts[pts.length - 1]; + canvas.begin(); + canvas.moveTo(pt.x, pt.y); + + // Draws the line segments + for (var i = 1; i < pts.length - 1; i++) + { + var tmp = pts[i]; + var dx = pt.x - tmp.x; + var dy = pt.y - tmp.y; + + if ((rounded && i < pts.length - 1) && (dx != 0 || dy != 0)) + { + // Draws a line from the last point to the current + // point with a spacing of size off the current point + // into direction of the last point + var dist = Math.sqrt(dx * dx + dy * dy); + var nx1 = dx * Math.min(arcSize, dist / 2) / dist; + var ny1 = dy * Math.min(arcSize, dist / 2) / dist; + + var x1 = tmp.x + nx1; + var y1 = tmp.y + ny1; + canvas.lineTo(x1, y1); + + // Draws a curve from the last point to the current + // point with a spacing of size off the current point + // into direction of the next point + var next = pts[i + 1]; + dx = next.x - tmp.x; + dy = next.y - tmp.y; + + dist = Math.max(1, Math.sqrt(dx * dx + dy * dy)); + var nx2 = dx * Math.min(arcSize, dist / 2) / dist; + var ny2 = dy * Math.min(arcSize, dist / 2) / dist; + + var x2 = tmp.x + nx2; + var y2 = tmp.y + ny2; + + canvas.curveTo(tmp.x, tmp.y, tmp.x, tmp.y, x2, y2); + tmp = new mxPoint(x2, y2); + } + else + { + canvas.lineTo(tmp.x, tmp.y); + } + + pt = tmp; + } + + canvas.lineTo(pe.x, pe.y); + canvas.stroke(); + + return true; + } + else + { + // no foreground + } + } + }; + + this.shapes['arrow'] = + { + drawShape: function(canvas, state, bounds, background) + { + if (background) + { + // Geometry of arrow + var spacing = mxConstants.ARROW_SPACING; + var width = mxConstants.ARROW_WIDTH; + var arrow = mxConstants.ARROW_SIZE; + + // Base vector (between end points) + var pts = state.absolutePoints; + var p0 = pts[0]; + var pe = pts[pts.length - 1]; + var dx = pe.x - p0.x; + var dy = pe.y - p0.y; + var dist = Math.sqrt(dx * dx + dy * dy); + var length = dist - 2 * spacing - arrow; + + // Computes the norm and the inverse norm + var nx = dx / dist; + var ny = dy / dist; + var basex = length * nx; + var basey = length * ny; + var floorx = width * ny/3; + var floory = -width * nx/3; + + // Computes points + var p0x = p0.x - floorx / 2 + spacing * nx; + var p0y = p0.y - floory / 2 + spacing * ny; + var p1x = p0x + floorx; + var p1y = p0y + floory; + var p2x = p1x + basex; + var p2y = p1y + basey; + var p3x = p2x + floorx; + var p3y = p2y + floory; + // p4 not necessary + var p5x = p3x - 3 * floorx; + var p5y = p3y - 3 * floory; + + canvas.begin(); + canvas.moveTo(p0x, p0y); + canvas.lineTo(p1x, p1y); + canvas.lineTo(p2x, p2y); + canvas.lineTo(p3x, p3y); + canvas.lineTo(pe.x - spacing * nx, pe.y - spacing * ny); + canvas.lineTo(p5x, p5y); + canvas.lineTo(p5x + floorx, p5y + floory); + canvas.close(); + + return true; + } + else + { + canvas.fillAndStroke(); + } + } + }; + + this.shapes['cylinder'] = + { + drawShape: function(canvas, state, bounds, background) + { + if (background) + { + return false; + } + else + { + var x = bounds.x; + var y = bounds.y; + var w = bounds.width; + var h = bounds.height; + var dy = Math.min(mxCylinder.prototype.maxHeight, Math.floor(h / 5)); + + canvas.begin(); + canvas.moveTo(x, y + dy); + canvas.curveTo(x, y - dy / 3, x + w, y - dy / 3, x + w, y + dy); + canvas.lineTo(x + w, y + h - dy); + canvas.curveTo(x + w, y + h + dy / 3, x, y + h + dy / 3, x, y + h - dy); + canvas.close(); + canvas.fillAndStroke(); + + canvas.begin(); + canvas.moveTo(x, y + dy); + canvas.curveTo(x, y + 2 * dy, x + w, y + 2 * dy, x + w, y + dy); + canvas.stroke(); + } + } + }; + + this.shapes['line'] = + { + drawShape: function(canvas, state, bounds, background) + { + if (background) + { + return false; + } + else + { + canvas.begin(); + + var mid = state.getCenterY(); + canvas.moveTo(bounds.x, mid); + canvas.lineTo(bounds.x + bounds.width, mid); + + canvas.stroke(); + } + } + }; + + this.shapes['ellipse'] = + { + drawShape: function(canvas, state, bounds, background) + { + if (background) + { + canvas.ellipse(bounds.x, bounds.y, bounds.width, bounds.height); + + return true; + } + else + { + canvas.fillAndStroke(); + } + } + }; + + this.shapes['doubleEllipse'] = + { + drawShape: function(canvas, state, bounds, background) + { + var x = bounds.x; + var y = bounds.y; + var w = bounds.width; + var h = bounds.height; + + if (background) + { + canvas.ellipse(x, y, w, h); + + return true; + } + else + { + canvas.fillAndStroke(); + + var inset = Math.min(4, Math.min(w / 5, h / 5)); + x += inset; + y += inset; + w -= 2 * inset; + h -= 2 * inset; + + if (w > 0 && h > 0) + { + canvas.ellipse(x, y, w, h); + } + + canvas.stroke(); + } + } + }; + + this.shapes['triangle'] = + { + drawShape: function(canvas, state, bounds, background) + { + if (background) + { + var x = bounds.x; + var y = bounds.y; + var w = bounds.width; + var h = bounds.height; + canvas.begin(); + canvas.moveTo(x, y); + canvas.lineTo(x + w, y + h / 2); + canvas.lineTo(x, y + h); + canvas.close(); + + return true; + } + else + { + canvas.fillAndStroke(); + } + } + }; + + this.shapes['rhombus'] = + { + drawShape: function(canvas, state, bounds, background) + { + if (background) + { + var x = bounds.x; + var y = bounds.y; + var w = bounds.width; + var h = bounds.height; + var hw = w / 2; + var hh = h / 2; + + canvas.begin(); + canvas.moveTo(x + hw, y); + canvas.lineTo(x + w, y + hh); + canvas.lineTo(x + hw, y + h); + canvas.lineTo(x, y + hh); + canvas.close(); + + return true; + } + else + { + canvas.fillAndStroke(); + } + } + + }; + + this.shapes['hexagon'] = + { + drawShape: function(canvas, state, bounds, background) + { + if (background) + { + var x = bounds.x; + var y = bounds.y; + var w = bounds.width; + var h = bounds.height; + + canvas.begin(); + canvas.moveTo(x + 0.25 * w, y); + canvas.lineTo(x + 0.75 * w, y); + canvas.lineTo(x + w, y + 0.5 * h); + canvas.lineTo(x + 0.75 * w, y + h); + canvas.lineTo(x + 0.25 * w, y + h); + canvas.lineTo(x, y + 0.5 * h); + canvas.close(); + + return true; + } + else + { + canvas.fillAndStroke(); + } + } + }; + + this.shapes['actor'] = + { + drawShape: function(canvas, state, bounds, background) + { + if (background) + { + var x = bounds.x; + var y = bounds.y; + var w = bounds.width; + var h = bounds.height; + var width = w * 2 / 6; + + canvas.begin(); + canvas.moveTo(x, y + h); + canvas.curveTo(x, y + 3 * h / 5, x, y + 2 * h / 5, x + w / 2, y + 2 * h + / 5); + canvas.curveTo(x + w / 2 - width, y + 2 * h / 5, x + w / 2 - width, y, x + + w / 2, y); + canvas.curveTo(x + w / 2 + width, y, x + w / 2 + width, y + 2 * h / 5, x + + w / 2, y + 2 * h / 5); + canvas.curveTo(x + w, y + 2 * h / 5, x + w, y + 3 * h / 5, x + w, y + h); + canvas.close(); + + return true; + } + else + { + canvas.fillAndStroke(); + } + } + }; + + this.shapes['cloud'] = + { + drawShape: function(canvas, state, bounds, background) + { + if (background) + { + var x = bounds.x; + var y = bounds.y; + var w = bounds.width; + var h = bounds.height; + + canvas.begin(); + canvas.moveTo(x + 0.25 * w, y + 0.25 * h); + canvas.curveTo(x + 0.05 * w, y + 0.25 * h, x, + y + 0.5 * h, x + 0.16 * w, y + 0.55 * h); + canvas.curveTo(x, y + 0.66 * h, x + 0.18 * w, + y + 0.9 * h, x + 0.31 * w, y + 0.8 * h); + canvas.curveTo(x + 0.4 * w, y + h, x + 0.7 * w, + y + h, x + 0.8 * w, y + 0.8 * h); + canvas.curveTo(x + w, y + 0.8 * h, x + w, + y + 0.6 * h, x + 0.875 * w, y + 0.5 * h); + canvas.curveTo(x + w, y + 0.3 * h, x + 0.8 * w, + y + 0.1 * h, x + 0.625 * w, y + 0.2 * h); + canvas.curveTo(x + 0.5 * w, y + 0.05 * h, + x + 0.3 * w, y + 0.05 * h, + x + 0.25 * w, y + 0.25 * h); + canvas.close(); + + return true; + } + else + { + canvas.fillAndStroke(); + } + } + }; + +}; + +/** + * Function: initMarkers + * + * Initializes the built-in markers. + */ +mxImageExport.prototype.initMarkers = function() +{ + this.markers = []; + + var tmp = function(canvas, state, type, pe, unitX, unitY, size, source, sw) + { + // The angle of the forward facing arrow sides against the x axis is + // 26.565 degrees, 1/sin(26.565) = 2.236 / 2 = 1.118 ( / 2 allows for + // only half the strokewidth is processed ). + var endOffsetX = unitX * sw * 1.118; + var endOffsetY = unitY * sw * 1.118; + + pe.x -= endOffsetX; + pe.y -= endOffsetY; + + unitX = unitX * (size + sw); + unitY = unitY * (size + sw); + + canvas.begin(); + canvas.moveTo(pe.x, pe.y); + canvas.lineTo(pe.x - unitX - unitY / 2, pe.y - unitY + unitX / 2); + + if (type == mxConstants.ARROW_CLASSIC) + { + canvas.lineTo(pe.x - unitX * 3 / 4, pe.y - unitY * 3 / 4); + } + + canvas.lineTo(pe.x + unitY / 2 - unitX, pe.y - unitY - unitX / 2); + canvas.close(); + + var key = (source) ? mxConstants.STYLE_STARTFILL : mxConstants.STYLE_ENDFILL; + + if (state.style[key] == 0) + { + canvas.stroke(); + } + else + { + canvas.fillAndStroke(); + } + + var f = (type != mxConstants.ARROW_CLASSIC) ? 1 : 3 / 4; + return new mxPoint(-unitX * f - endOffsetX, -unitY * f - endOffsetY); + }; + + this.markers['classic'] = tmp; + this.markers['block'] = tmp; + + this.markers['open'] = function(canvas, state, type, pe, unitX, unitY, size, source, sw) + { + // The angle of the forward facing arrow sides against the x axis is + // 26.565 degrees, 1/sin(26.565) = 2.236 / 2 = 1.118 ( / 2 allows for + // only half the strokewidth is processed ). + var endOffsetX = unitX * sw * 1.118; + var endOffsetY = unitY * sw * 1.118; + + pe.x -= endOffsetX; + pe.y -= endOffsetY; + + unitX = unitX * (size + sw); + unitY = unitY * (size + sw); + + canvas.begin(); + canvas.moveTo(pe.x - unitX - unitY / 2, pe.y - unitY + unitX / 2); + canvas.lineTo(pe.x, pe.y); + canvas.lineTo(pe.x + unitY / 2 - unitX, pe.y - unitY - unitX / 2); + canvas.stroke(); + + return new mxPoint(-endOffsetX * 2, -endOffsetY * 2); + }; + + this.markers['oval'] = function(canvas, state, type, pe, unitX, unitY, size, source, sw) + { + var a = size / 2; + + canvas.ellipse(pe.x - a, pe.y - a, size, size); + + var key = (source) ? mxConstants.STYLE_STARTFILL : mxConstants.STYLE_ENDFILL; + + if (state.style[key] == 0) + { + canvas.stroke(); + } + else + { + canvas.fillAndStroke(); + } + + return new mxPoint(-unitX / 2, -unitY / 2); + }; + + var tmp_diamond = function(canvas, state, type, pe, unitX, unitY, size, source, sw) + { + // The angle of the forward facing arrow sides against the x axis is + // 45 degrees, 1/sin(45) = 1.4142 / 2 = 0.7071 ( / 2 allows for + // only half the strokewidth is processed ). Or 0.9862 for thin diamond. + // Note these values and the tk variable below are dependent, update + // both together (saves trig hard coding it). + var swFactor = (type == mxConstants.ARROW_DIAMOND) ? 0.7071 : 0.9862; + var endOffsetX = unitX * sw * swFactor; + var endOffsetY = unitY * sw * swFactor; + + unitX = unitX * (size + sw); + unitY = unitY * (size + sw); + + pe.x -= endOffsetX; + pe.y -= endOffsetY; + + // thickness factor for diamond + var tk = ((type == mxConstants.ARROW_DIAMOND) ? 2 : 3.4); + + canvas.begin(); + canvas.moveTo(pe.x, pe.y); + canvas.lineTo(pe.x - unitX / 2 - unitY / tk, pe.y + unitX / tk - unitY / 2); + canvas.lineTo(pe.x - unitX, pe.y - unitY); + canvas.lineTo(pe.x - unitX / 2 + unitY / tk, pe.y - unitY / 2 - unitX / tk); + canvas.close(); + + var key = (source) ? mxConstants.STYLE_STARTFILL : mxConstants.STYLE_ENDFILL; + + if (state.style[key] == 0) + { + canvas.stroke(); + } + else + { + canvas.fillAndStroke(); + } + + return new mxPoint(-endOffsetX - unitX, -endOffsetY - unitY); + }; + + this.markers['diamond'] = tmp_diamond; + this.markers['diamondThin'] = tmp_diamond; +}; diff --git a/src/js/util/mxLog.js b/src/js/util/mxLog.js new file mode 100644 index 0000000..c556e22 --- /dev/null +++ b/src/js/util/mxLog.js @@ -0,0 +1,410 @@ +/** + * $Id: mxLog.js,v 1.32 2012-11-12 09:40:59 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +var mxLog = +{ + /** + * Class: mxLog + * + * A singleton class that implements a simple console. + * + * Variable: consoleName + * + * Specifies the name of the console window. Default is 'Console'. + */ + consoleName: 'Console', + + /** + * Variable: TRACE + * + * Specified if the output for <enter> and <leave> should be visible in the + * console. Default is false. + */ + TRACE: false, + + /** + * Variable: DEBUG + * + * Specifies if the output for <debug> should be visible in the console. + * Default is true. + */ + DEBUG: true, + + /** + * Variable: WARN + * + * Specifies if the output for <warn> should be visible in the console. + * Default is true. + */ + WARN: true, + + /** + * Variable: buffer + * + * Buffer for pre-initialized content. + */ + buffer: '', + + /** + * Function: init + * + * Initializes the DOM node for the console. This requires document.body to + * point to a non-null value. This is called from within <setVisible> if the + * log has not yet been initialized. + */ + init: function() + { + if (mxLog.window == null && document.body != null) + { + var title = mxLog.consoleName + ' - mxGraph ' + mxClient.VERSION; + + // Creates a table that maintains the layout + var table = document.createElement('table'); + table.setAttribute('width', '100%'); + table.setAttribute('height', '100%'); + + var tbody = document.createElement('tbody'); + var tr = document.createElement('tr'); + var td = document.createElement('td'); + td.style.verticalAlign = 'top'; + + // Adds the actual console as a textarea + mxLog.textarea = document.createElement('textarea'); + mxLog.textarea.setAttribute('readOnly', 'true'); + mxLog.textarea.style.height = '100%'; + mxLog.textarea.style.resize = 'none'; + mxLog.textarea.value = mxLog.buffer; + + // Workaround for wrong width in standards mode + if (mxClient.IS_NS && document.compatMode != 'BackCompat') + { + mxLog.textarea.style.width = '99%'; + } + else + { + mxLog.textarea.style.width = '100%'; + } + + td.appendChild(mxLog.textarea); + tr.appendChild(td); + tbody.appendChild(tr); + + // Creates the container div + tr = document.createElement('tr'); + mxLog.td = document.createElement('td'); + mxLog.td.style.verticalAlign = 'top'; + mxLog.td.setAttribute('height', '30px'); + + tr.appendChild(mxLog.td); + tbody.appendChild(tr); + table.appendChild(tbody); + + // Adds various debugging buttons + mxLog.addButton('Info', function (evt) + { + mxLog.info(); + }); + + mxLog.addButton('DOM', function (evt) + { + var content = mxUtils.getInnerHtml(document.body); + mxLog.debug(content); + }); + + mxLog.addButton('Trace', function (evt) + { + mxLog.TRACE = !mxLog.TRACE; + + if (mxLog.TRACE) + { + mxLog.debug('Tracing enabled'); + } + else + { + mxLog.debug('Tracing disabled'); + } + }); + + mxLog.addButton('Copy', function (evt) + { + try + { + mxUtils.copy(mxLog.textarea.value); + } + catch (err) + { + mxUtils.alert(err); + } + }); + + mxLog.addButton('Show', function (evt) + { + try + { + mxUtils.popup(mxLog.textarea.value); + } + catch (err) + { + mxUtils.alert(err); + } + }); + + mxLog.addButton('Clear', function (evt) + { + mxLog.textarea.value = ''; + }); + + // Cross-browser code to get window size + var h = 0; + var w = 0; + + if (typeof(window.innerWidth) === 'number') + { + h = window.innerHeight; + w = window.innerWidth; + } + else + { + h = (document.documentElement.clientHeight || document.body.clientHeight); + w = document.body.clientWidth; + } + + mxLog.window = new mxWindow(title, table, Math.max(0, w-320), Math.max(0, h-210), 300, 160); + mxLog.window.setMaximizable(true); + mxLog.window.setScrollable(false); + mxLog.window.setResizable(true); + mxLog.window.setClosable(true); + mxLog.window.destroyOnClose = false; + + // Workaround for ignored textarea height in various setups + if ((mxClient.IS_NS || mxClient.IS_IE) && !mxClient.IS_GC && + !mxClient.IS_SF && document.compatMode != 'BackCompat') + { + var elt = mxLog.window.getElement(); + + var resizeHandler = function(sender, evt) + { + mxLog.textarea.style.height = Math.max(0, elt.offsetHeight - 70)+'px'; + }; + + mxLog.window.addListener(mxEvent.RESIZE_END, resizeHandler); + mxLog.window.addListener(mxEvent.MAXIMIZE, resizeHandler); + mxLog.window.addListener(mxEvent.NORMALIZE, resizeHandler); + + mxLog.textarea.style.height = '92px'; + } + } + }, + + /** + * Function: info + * + * Writes the current navigator information to the console. + */ + info: function() + { + mxLog.writeln(mxUtils.toString(navigator)); + }, + + /** + * Function: addButton + * + * Adds a button to the console using the given label and function. + */ + addButton: function(lab, funct) + { + var button = document.createElement('button'); + mxUtils.write(button, lab); + mxEvent.addListener(button, 'click', funct); + mxLog.td.appendChild(button); + }, + + /** + * Function: isVisible + * + * Returns true if the console is visible. + */ + isVisible: function() + { + if (mxLog.window != null) + { + return mxLog.window.isVisible(); + } + return false; + }, + + + /** + * Function: show + * + * Shows the console. + */ + show: function() + { + mxLog.setVisible(true); + }, + + /** + * Function: setVisible + * + * Shows or hides the console. + */ + setVisible: function(visible) + { + if (mxLog.window == null) + { + mxLog.init(); + } + + if (mxLog.window != null) + { + mxLog.window.setVisible(visible); + } + }, + + /** + * Function: enter + * + * Writes the specified string to the console + * if <TRACE> is true and returns the current + * time in milliseconds. + * + * Example: + * + * (code) + * mxLog.show(); + * var t0 = mxLog.enter('Hello'); + * // Do something + * mxLog.leave('World!', t0); + * (end) + */ + enter: function(string) + { + if (mxLog.TRACE) + { + mxLog.writeln('Entering '+string); + + return new Date().getTime(); + } + }, + + /** + * Function: leave + * + * Writes the specified string to the console + * if <TRACE> is true and computes the difference + * between the current time and t0 in milliseconds. + * See <enter> for an example. + */ + leave: function(string, t0) + { + if (mxLog.TRACE) + { + var dt = (t0 != 0) ? ' ('+(new Date().getTime() - t0)+' ms)' : ''; + mxLog.writeln('Leaving '+string+dt); + } + }, + + /** + * Function: debug + * + * Adds all arguments to the console if <DEBUG> is enabled. + * + * Example: + * + * (code) + * mxLog.show(); + * mxLog.debug('Hello, World!'); + * (end) + */ + debug: function() + { + if (mxLog.DEBUG) + { + mxLog.writeln.apply(this, arguments); + } + }, + + /** + * Function: warn + * + * Adds all arguments to the console if <WARN> is enabled. + * + * Example: + * + * (code) + * mxLog.show(); + * mxLog.warn('Hello, World!'); + * (end) + */ + warn: function() + { + if (mxLog.WARN) + { + mxLog.writeln.apply(this, arguments); + } + }, + + /** + * Function: write + * + * Adds the specified strings to the console. + */ + write: function() + { + var string = ''; + + for (var i = 0; i < arguments.length; i++) + { + string += arguments[i]; + + if (i < arguments.length - 1) + { + string += ' '; + } + } + + if (mxLog.textarea != null) + { + mxLog.textarea.value = mxLog.textarea.value + string; + + // Workaround for no update in Presto 2.5.22 (Opera 10.5) + if (navigator.userAgent.indexOf('Presto/2.5') >= 0) + { + mxLog.textarea.style.visibility = 'hidden'; + mxLog.textarea.style.visibility = 'visible'; + } + + mxLog.textarea.scrollTop = mxLog.textarea.scrollHeight; + } + else + { + mxLog.buffer += string; + } + }, + + /** + * Function: writeln + * + * Adds the specified strings to the console, appending a linefeed at the + * end of each string. + */ + writeln: function() + { + var string = ''; + + for (var i = 0; i < arguments.length; i++) + { + string += arguments[i]; + + if (i < arguments.length - 1) + { + string += ' '; + } + } + + mxLog.write(string + '\n'); + } + +}; diff --git a/src/js/util/mxMorphing.js b/src/js/util/mxMorphing.js new file mode 100644 index 0000000..442143d --- /dev/null +++ b/src/js/util/mxMorphing.js @@ -0,0 +1,239 @@ +/** + * $Id: mxMorphing.js,v 1.4 2010-06-03 13:37:07 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * + * Class: mxMorphing + * + * Implements animation for morphing cells. Here is an example of + * using this class for animating the result of a layout algorithm: + * + * (code) + * graph.getModel().beginUpdate(); + * try + * { + * var circleLayout = new mxCircleLayout(graph); + * circleLayout.execute(graph.getDefaultParent()); + * } + * finally + * { + * var morph = new mxMorphing(graph); + * morph.addListener(mxEvent.DONE, function() + * { + * graph.getModel().endUpdate(); + * }); + * + * morph.startAnimation(); + * } + * (end) + * + * Constructor: mxMorphing + * + * Constructs an animation. + * + * Parameters: + * + * graph - Reference to the enclosing <mxGraph>. + * steps - Optional number of steps in the morphing animation. Default is 6. + * ease - Optional easing constant for the animation. Default is 1.5. + * delay - Optional delay between the animation steps. Passed to <mxAnimation>. + */ +function mxMorphing(graph, steps, ease, delay) +{ + mxAnimation.call(this, delay); + this.graph = graph; + this.steps = (steps != null) ? steps : 6; + this.ease = (ease != null) ? ease : 1.5; +}; + +/** + * Extends mxEventSource. + */ +mxMorphing.prototype = new mxAnimation(); +mxMorphing.prototype.constructor = mxMorphing; + +/** + * Variable: graph + * + * Specifies the delay between the animation steps. Defaul is 30ms. + */ +mxMorphing.prototype.graph = null; + +/** + * Variable: steps + * + * Specifies the maximum number of steps for the morphing. + */ +mxMorphing.prototype.steps = null; + +/** + * Variable: step + * + * Contains the current step. + */ +mxMorphing.prototype.step = 0; + +/** + * Variable: ease + * + * Ease-off for movement towards the given vector. Larger values are + * slower and smoother. Default is 4. + */ +mxMorphing.prototype.ease = null; + +/** + * Variable: cells + * + * Optional array of cells to be animated. If this is not specified + * then all cells are checked and animated if they have been moved + * in the current transaction. + */ +mxMorphing.prototype.cells = null; + +/** + * Function: updateAnimation + * + * Animation step. + */ +mxMorphing.prototype.updateAnimation = function() +{ + var move = new mxCellStatePreview(this.graph); + + if (this.cells != null) + { + // Animates the given cells individually without recursion + for (var i = 0; i < this.cells.length; i++) + { + this.animateCell(cells[i], move, false); + } + } + else + { + // Animates all changed cells by using recursion to find + // the changed cells but not for the animation itself + this.animateCell(this.graph.getModel().getRoot(), move, true); + } + + this.show(move); + + if (move.isEmpty() || + this.step++ >= this.steps) + { + this.stopAnimation(); + } +}; + +/** + * Function: show + * + * Shows the changes in the given <mxCellStatePreview>. + */ +mxMorphing.prototype.show = function(move) +{ + move.show(); +}; + +/** + * Function: animateCell + * + * Animates the given cell state using <mxCellStatePreview.moveState>. + */ +mxMorphing.prototype.animateCell = function(cell, move, recurse) +{ + var state = this.graph.getView().getState(cell); + var delta = null; + + if (state != null) + { + // Moves the animated state from where it will be after the model + // change by subtracting the given delta vector from that location + delta = this.getDelta(state); + + if (this.graph.getModel().isVertex(cell) && + (delta.x != 0 || delta.y != 0)) + { + var translate = this.graph.view.getTranslate(); + var scale = this.graph.view.getScale(); + + delta.x += translate.x * scale; + delta.y += translate.y * scale; + + move.moveState(state, -delta.x / this.ease, -delta.y / this.ease); + } + } + + if (recurse && !this.stopRecursion(state, delta)) + { + var childCount = this.graph.getModel().getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + this.animateCell(this.graph.getModel().getChildAt(cell, i), move, recurse); + } + } +}; + +/** + * Function: stopRecursion + * + * Returns true if the animation should not recursively find more + * deltas for children if the given parent state has been animated. + */ +mxMorphing.prototype.stopRecursion = function(state, delta) +{ + return delta != null && (delta.x != 0 || delta.y != 0); +}; + +/** + * Function: getDelta + * + * Returns the vector between the current rendered state and the future + * location of the state after the display will be updated. + */ +mxMorphing.prototype.getDelta = function(state) +{ + var origin = this.getOriginForCell(state.cell); + var translate = this.graph.getView().getTranslate(); + var scale = this.graph.getView().getScale(); + var current = new mxPoint( + state.x / scale - translate.x, + state.y / scale - translate.y); + + return new mxPoint( + (origin.x - current.x) * scale, + (origin.y - current.y) * scale); +}; + +/** + * Function: getOriginForCell + * + * Returns the top, left corner of the given cell. TODO: Improve performance + * by using caching inside this method as the result per cell never changes + * during the lifecycle of this object. + */ +mxMorphing.prototype.getOriginForCell = function(cell) +{ + var result = null; + + if (cell != null) + { + result = this.getOriginForCell(this.graph.getModel().getParent(cell)); + var geo = this.graph.getCellGeometry(cell); + + // TODO: Handle offset, relative geometries etc + if (geo != null) + { + result.x += geo.x; + result.y += geo.y; + } + } + + if (result == null) + { + var t = this.graph.view.getTranslate(); + result = new mxPoint(-t.x, -t.y); + } + + return result; +}; diff --git a/src/js/util/mxMouseEvent.js b/src/js/util/mxMouseEvent.js new file mode 100644 index 0000000..e161d3a --- /dev/null +++ b/src/js/util/mxMouseEvent.js @@ -0,0 +1,241 @@ +/** + * $Id: mxMouseEvent.js,v 1.20 2011-03-02 17:24:39 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxMouseEvent + * + * Base class for all mouse events in mxGraph. A listener for this event should + * implement the following methods: + * + * (code) + * graph.addMouseListener( + * { + * mouseDown: function(sender, evt) + * { + * mxLog.debug('mouseDown'); + * }, + * mouseMove: function(sender, evt) + * { + * mxLog.debug('mouseMove'); + * }, + * mouseUp: function(sender, evt) + * { + * mxLog.debug('mouseUp'); + * } + * }); + * (end) + * + * Constructor: mxMouseEvent + * + * Constructs a new event object for the given arguments. + * + * Parameters: + * + * evt - Native mouse event. + * state - Optional <mxCellState> under the mouse. + * + */ +function mxMouseEvent(evt, state) +{ + this.evt = evt; + this.state = state; +}; + +/** + * Variable: consumed + * + * Holds the consumed state of this event. + */ +mxMouseEvent.prototype.consumed = false; + +/** + * Variable: evt + * + * Holds the inner event object. + */ +mxMouseEvent.prototype.evt = null; + +/** + * Variable: graphX + * + * Holds the x-coordinate of the event in the graph. This value is set in + * <mxGraph.fireMouseEvent>. + */ +mxMouseEvent.prototype.graphX = null; + +/** + * Variable: graphY + * + * Holds the y-coordinate of the event in the graph. This value is set in + * <mxGraph.fireMouseEvent>. + */ +mxMouseEvent.prototype.graphY = null; + +/** + * Variable: state + * + * Holds the optional <mxCellState> associated with this event. + */ +mxMouseEvent.prototype.state = null; + +/** + * Function: getEvent + * + * Returns <evt>. + */ +mxMouseEvent.prototype.getEvent = function() +{ + return this.evt; +}; + +/** + * Function: getSource + * + * Returns the target DOM element using <mxEvent.getSource> for <evt>. + */ +mxMouseEvent.prototype.getSource = function() +{ + return mxEvent.getSource(this.evt); +}; + +/** + * Function: isSource + * + * Returns true if the given <mxShape> is the source of <evt>. + */ +mxMouseEvent.prototype.isSource = function(shape) +{ + if (shape != null) + { + var source = this.getSource(); + + while (source != null) + { + if (source == shape.node) + { + return true; + } + + source = source.parentNode; + } + } + + return false; +}; + +/** + * Function: getX + * + * Returns <evt.clientX>. + */ +mxMouseEvent.prototype.getX = function() +{ + return mxEvent.getClientX(this.getEvent()); +}; + +/** + * Function: getY + * + * Returns <evt.clientY>. + */ +mxMouseEvent.prototype.getY = function() +{ + return mxEvent.getClientY(this.getEvent()); +}; + +/** + * Function: getGraphX + * + * Returns <graphX>. + */ +mxMouseEvent.prototype.getGraphX = function() +{ + return this.graphX; +}; + +/** + * Function: getGraphY + * + * Returns <graphY>. + */ +mxMouseEvent.prototype.getGraphY = function() +{ + return this.graphY; +}; + +/** + * Function: getState + * + * Returns <state>. + */ +mxMouseEvent.prototype.getState = function() +{ + return this.state; +}; + +/** + * Function: getCell + * + * Returns the <mxCell> in <state> is not null. + */ +mxMouseEvent.prototype.getCell = function() +{ + var state = this.getState(); + + if (state != null) + { + return state.cell; + } + + return null; +}; + +/** + * Function: isPopupTrigger + * + * Returns true if the event is a popup trigger. + */ +mxMouseEvent.prototype.isPopupTrigger = function() +{ + return mxEvent.isPopupTrigger(this.getEvent()); +}; + +/** + * Function: isConsumed + * + * Returns <consumed>. + */ +mxMouseEvent.prototype.isConsumed = function() +{ + return this.consumed; +}; + +/** + * Function: consume + * + * Sets <consumed> to true and invokes preventDefault on the native event + * if such a method is defined. This is used mainly to avoid the cursor from + * being changed to a text cursor in Webkit. You can use the preventDefault + * flag to disable this functionality. + * + * Parameters: + * + * preventDefault - Specifies if the native event should be canceled. Default + * is true. + */ +mxMouseEvent.prototype.consume = function(preventDefault) +{ + preventDefault = (preventDefault != null) ? preventDefault : true; + + if (preventDefault && this.evt.preventDefault) + { + this.evt.preventDefault(); + } + + // Workaround for images being dragged in IE + this.evt.returnValue = false; + + // Sets local consumed state + this.consumed = true; +}; diff --git a/src/js/util/mxObjectIdentity.js b/src/js/util/mxObjectIdentity.js new file mode 100644 index 0000000..778a4ea --- /dev/null +++ b/src/js/util/mxObjectIdentity.js @@ -0,0 +1,59 @@ +/** + * $Id: mxObjectIdentity.js,v 1.8 2010-01-02 09:45:14 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +var mxObjectIdentity = +{ + /** + * Class: mxObjectIdentity + * + * Identity for JavaScript objects. This is implemented using a simple + * incremeting counter which is stored in each object under <ID_NAME>. + * + * The identity for an object does not change during its lifecycle. + * + * Variable: FIELD_NAME + * + * Name of the field to be used to store the object ID. Default is + * '_mxObjectId'. + */ + FIELD_NAME: 'mxObjectId', + + /** + * Variable: counter + * + * Current counter for objects. + */ + counter: 0, + + /** + * Function: get + * + * Returns the object id for the given object. + */ + get: function(obj) + { + if (typeof(obj) == 'object' && + obj[mxObjectIdentity.FIELD_NAME] == null) + { + var ctor = mxUtils.getFunctionName(obj.constructor); + obj[mxObjectIdentity.FIELD_NAME] = ctor+'#'+mxObjectIdentity.counter++; + } + + return obj[mxObjectIdentity.FIELD_NAME]; + }, + + /** + * Function: clear + * + * Removes the object id from the given object. + */ + clear: function(obj) + { + if (typeof(obj) == 'object') + { + delete obj[mxObjectIdentity.FIELD_NAME]; + } + } + +}; diff --git a/src/js/util/mxPanningManager.js b/src/js/util/mxPanningManager.js new file mode 100644 index 0000000..9f9f349 --- /dev/null +++ b/src/js/util/mxPanningManager.js @@ -0,0 +1,262 @@ +/** + * $Id: mxPanningManager.js,v 1.7 2012-06-13 06:46:37 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxPanningManager + * + * Implements a handler for panning. + */ +function mxPanningManager(graph) +{ + this.thread = null; + this.active = false; + this.tdx = 0; + this.tdy = 0; + this.t0x = 0; + this.t0y = 0; + this.dx = 0; + this.dy = 0; + this.scrollbars = false; + this.scrollLeft = 0; + this.scrollTop = 0; + + this.mouseListener = + { + mouseDown: function(sender, me) { }, + mouseMove: function(sender, me) { }, + mouseUp: mxUtils.bind(this, function(sender, me) + { + if (this.active) + { + this.stop(); + } + }) + }; + + graph.addMouseListener(this.mouseListener); + + // Stops scrolling on every mouseup anywhere in the document + mxEvent.addListener(document, 'mouseup', mxUtils.bind(this, function() + { + if (this.active) + { + this.stop(); + } + })); + + var createThread = mxUtils.bind(this, function() + { + this.scrollbars = mxUtils.hasScrollbars(graph.container); + this.scrollLeft = graph.container.scrollLeft; + this.scrollTop = graph.container.scrollTop; + + return window.setInterval(mxUtils.bind(this, function() + { + this.tdx -= this.dx; + this.tdy -= this.dy; + + if (this.scrollbars) + { + var left = -graph.container.scrollLeft - Math.ceil(this.dx); + var top = -graph.container.scrollTop - Math.ceil(this.dy); + graph.panGraph(left, top); + graph.panDx = this.scrollLeft - graph.container.scrollLeft; + graph.panDy = this.scrollTop - graph.container.scrollTop; + graph.fireEvent(new mxEventObject(mxEvent.PAN)); + // TODO: Implement graph.autoExtend + } + else + { + graph.panGraph(this.getDx(), this.getDy()); + } + }), this.delay); + }); + + this.isActive = function() + { + return active; + }; + + this.getDx = function() + { + return Math.round(this.tdx); + }; + + this.getDy = function() + { + return Math.round(this.tdy); + }; + + this.start = function() + { + this.t0x = graph.view.translate.x; + this.t0y = graph.view.translate.y; + this.active = true; + }; + + this.panTo = function(x, y, w, h) + { + if (!this.active) + { + this.start(); + } + + this.scrollLeft = graph.container.scrollLeft; + this.scrollTop = graph.container.scrollTop; + + w = (w != null) ? w : 0; + h = (h != null) ? h : 0; + + var c = graph.container; + this.dx = x + w - c.scrollLeft - c.clientWidth; + + if (this.dx < 0 && Math.abs(this.dx) < this.border) + { + this.dx = this.border + this.dx; + } + else if (this.handleMouseOut) + { + this.dx = Math.max(this.dx, 0); + } + else + { + this.dx = 0; + } + + if (this.dx == 0) + { + this.dx = x - c.scrollLeft; + + if (this.dx > 0 && this.dx < this.border) + { + this.dx = this.dx - this.border; + } + else if (this.handleMouseOut) + { + this.dx = Math.min(0, this.dx); + } + else + { + this.dx = 0; + } + } + + this.dy = y + h - c.scrollTop - c.clientHeight; + + if (this.dy < 0 && Math.abs(this.dy) < this.border) + { + this.dy = this.border + this.dy; + } + else if (this.handleMouseOut) + { + this.dy = Math.max(this.dy, 0); + } + else + { + this.dy = 0; + } + + if (this.dy == 0) + { + this.dy = y - c.scrollTop; + + if (this.dy > 0 && this.dy < this.border) + { + this.dy = this.dy - this.border; + } + else if (this.handleMouseOut) + { + this.dy = Math.min(0, this.dy); + } + else + { + this.dy = 0; + } + } + + if (this.dx != 0 || this.dy != 0) + { + this.dx *= this.damper; + this.dy *= this.damper; + + if (this.thread == null) + { + this.thread = createThread(); + } + } + else if (this.thread != null) + { + window.clearInterval(this.thread); + this.thread = null; + } + }; + + this.stop = function() + { + if (this.active) + { + this.active = false; + + if (this.thread != null) + { + window.clearInterval(this.thread); + this.thread = null; + } + + this.tdx = 0; + this.tdy = 0; + + if (!this.scrollbars) + { + var px = graph.panDx; + var py = graph.panDy; + + if (px != 0 || py != 0) + { + graph.panGraph(0, 0); + graph.view.setTranslate(this.t0x + px / graph.view.scale, this.t0y + py / graph.view.scale); + } + } + else + { + graph.panDx = 0; + graph.panDy = 0; + graph.fireEvent(new mxEventObject(mxEvent.PAN)); + } + } + }; + + this.destroy = function() + { + graph.removeMouseListener(this.mouseListener); + }; +}; + +/** + * Variable: damper + * + * Damper value for the panning. Default is 1/6. + */ +mxPanningManager.prototype.damper = 1/6; + +/** + * Variable: delay + * + * Delay in milliseconds for the panning. Default is 10. + */ +mxPanningManager.prototype.delay = 10; + +/** + * Variable: handleMouseOut + * + * Specifies if mouse events outside of the component should be handled. Default is true. + */ +mxPanningManager.prototype.handleMouseOut = true; + +/** + * Variable: border + * + * Border to handle automatic panning inside the component. Default is 0 (disabled). + */ +mxPanningManager.prototype.border = 0; diff --git a/src/js/util/mxPath.js b/src/js/util/mxPath.js new file mode 100644 index 0000000..57efe74 --- /dev/null +++ b/src/js/util/mxPath.js @@ -0,0 +1,314 @@ +/** + * $Id: mxPath.js,v 1.24 2012-06-13 17:31:32 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxPath + * + * An abstraction for creating VML and SVG paths. See <mxActor> for using this + * object inside an <mxShape> for painting cells. + * + * Constructor: mxPath + * + * Constructs a path for the given format, which is one of svg or vml. + * + * Parameters: + * + * format - String specifying the <format>. May be one of vml or svg + * (default). + */ +function mxPath(format) +{ + this.format = format; + this.path = []; + this.translate = new mxPoint(0, 0); +}; + +/** + * Variable: format + * + * Defines the format for the output of this path. Possible values are + * svg and vml. + */ +mxPath.prototype.format = null; + +/** + * Variable: translate + * + * <mxPoint> that specifies the translation of the complete path. + */ +mxPath.prototype.translate = null; + +/** + * Variable: scale + * + * Number that specifies the translation of the path. + */ +mxPath.prototype.scale = 1; + +/** + * Variable: path + * + * Contains the textual representation of the path as an array. + */ +mxPath.prototype.path = null; + +/** + * Function: isVml + * + * Returns true if <format> is vml. + */ +mxPath.prototype.isVml = function() +{ + return this.format == 'vml'; +}; + +/** + * Function: getPath + * + * Returns string that represents the path in <format>. + */ +mxPath.prototype.getPath = function() +{ + return this.path.join(''); +}; + +/** + * Function: setTranslate + * + * Set the global translation of this path, that is, the origin of the + * coordinate system. + * + * Parameters: + * + * x - X-coordinate of the new origin. + * y - Y-coordinate of the new origin. + */ +mxPath.prototype.setTranslate = function(x, y) +{ + this.translate = new mxPoint(x, y); +}; + +/** + * Function: moveTo + * + * Moves the cursor to (x, y). + * + * Parameters: + * + * x - X-coordinate of the new cursor location. + * y - Y-coordinate of the new cursor location. + */ +mxPath.prototype.moveTo = function(x, y) +{ + x += this.translate.x; + y += this.translate.y; + + x *= this.scale; + y *= this.scale; + + if (this.isVml()) + { + this.path.push('m ', Math.round(x), ' ', Math.round(y), ' '); + } + else + { + this.path.push('M ', x, ' ', y, ' '); + } +}; + +/** + * Function: lineTo + * + * Draws a straight line from the current poin to (x, y). + * + * Parameters: + * + * x - X-coordinate of the endpoint. + * y - Y-coordinate of the endpoint. + */ +mxPath.prototype.lineTo = function(x, y) +{ + x += this.translate.x; + y += this.translate.y; + + x *= this.scale; + y *= this.scale; + + if (this.isVml()) + { + this.path.push('l ', Math.round(x), ' ', Math.round(y), ' '); + } + else + { + this.path.push('L ', x, ' ', y, ' '); + } +}; + +/** + * Function: quadTo + * + * Draws a quadratic Bézier curve from the current point to (x, y) using + * (x1, y1) as the control point. + * + * Parameters: + * + * x1 - X-coordinate of the control point. + * y1 - Y-coordinate of the control point. + * x - X-coordinate of the endpoint. + * y - Y-coordinate of the endpoint. + */ +mxPath.prototype.quadTo = function(x1, y1, x, y) +{ + x1 += this.translate.x; + y1 += this.translate.y; + + x1 *= this.scale; + y1 *= this.scale; + + x += this.translate.x; + y += this.translate.y; + + x *= this.scale; + y *= this.scale; + + if (this.isVml()) + { + this.path.push('c ', Math.round(x1), ' ', Math.round(y1), ' ', Math.round(x), ' ', + Math.round(y), ' ', Math.round(x), ' ', Math.round(y), ' '); + } + else + { + this.path.push('Q ', x1, ' ', y1, ' ', x, ' ', y, ' '); + } +}; + +/** + * Function: curveTo + * + * Draws a cubic Bézier curve from the current point to (x, y) using + * (x1, y1) as the control point at the beginning of the curve and (x2, y2) + * as the control point at the end of the curve. + * + * Parameters: + * + * x1 - X-coordinate of the first control point. + * y1 - Y-coordinate of the first control point. + * x2 - X-coordinate of the second control point. + * y2 - Y-coordinate of the second control point. + * x - X-coordinate of the endpoint. + * y - Y-coordinate of the endpoint. + */ +mxPath.prototype.curveTo = function(x1, y1, x2, y2, x, y) +{ + x1 += this.translate.x; + y1 += this.translate.y; + + x1 *= this.scale; + y1 *= this.scale; + + x2 += this.translate.x; + y2 += this.translate.y; + + x2 *= this.scale; + y2 *= this.scale; + + x += this.translate.x; + y += this.translate.y; + + x *= this.scale; + y *= this.scale; + + if (this.isVml()) + { + this.path.push('c ', Math.round(x1), ' ', Math.round(y1), ' ', Math.round(x2), + ' ', Math.round(y2), ' ', Math.round(x), ' ', Math.round(y), ' '); + } + else + { + this.path.push('C ', x1, ' ', y1, ' ', x2, + ' ', y2, ' ', x, ' ', y, ' '); + } +}; + +/** + * Function: ellipse + * + * Adds the given ellipse. Some implementations may require the path to be + * closed after this operation. + */ +mxPath.prototype.ellipse = function(x, y, w, h) +{ + x += this.translate.x; + y += this.translate.y; + x *= this.scale; + y *= this.scale; + + if (this.isVml()) + { + this.path.push('at ', Math.round(x), ' ', Math.round(y), ' ', Math.round(x + w), ' ', Math.round(y + h), ' ', + Math.round(x), ' ', Math.round(y + h / 2), ' ', Math.round(x), ' ', Math.round(y + h / 2)); + } + else + { + var startX = x; + var startY = y + h/2; + var endX = x + w; + var endY = y + h/2; + var r1 = w/2; + var r2 = h/2; + this.path.push('M ', startX, ' ', startY, ' '); + this.path.push('A ', r1, ' ', r2, ' 0 1 0 ', endX, ' ', endY, ' '); + this.path.push('A ', r1, ' ', r2, ' 0 1 0 ', startX, ' ', startY); + } +}; + +/** + * Function: addPath + * + * Adds the given path. + */ +mxPath.prototype.addPath = function(path) +{ + this.path = this.path.concat(path.path); +}; + +/** + * Function: write + * + * Writes directly into the path. This bypasses all conversions. + */ +mxPath.prototype.write = function(string) +{ + this.path.push(string, ' '); +}; + +/** + * Function: end + * + * Ends the path. + */ +mxPath.prototype.end = function() +{ + if (this.format == 'vml') + { + this.path.push('e'); + } +}; + +/** + * Function: close + * + * Closes the path. + */ +mxPath.prototype.close = function() +{ + if (this.format == 'vml') + { + this.path.push('x e'); + } + else + { + this.path.push('Z'); + } +}; diff --git a/src/js/util/mxPoint.js b/src/js/util/mxPoint.js new file mode 100644 index 0000000..e029a29 --- /dev/null +++ b/src/js/util/mxPoint.js @@ -0,0 +1,55 @@ +/** + * $Id: mxPoint.js,v 1.12 2010-01-02 09:45:14 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxPoint + * + * Implements a 2-dimensional vector with double precision coordinates. + * + * Constructor: mxPoint + * + * Constructs a new point for the optional x and y coordinates. If no + * coordinates are given, then the default values for <x> and <y> are used. + */ +function mxPoint(x, y) +{ + this.x = (x != null) ? x : 0; + this.y = (y != null) ? y : 0; +}; + +/** + * Variable: x + * + * Holds the x-coordinate of the point. Default is 0. + */ +mxPoint.prototype.x = null; + +/** + * Variable: y + * + * Holds the y-coordinate of the point. Default is 0. + */ +mxPoint.prototype.y = null; + +/** + * Function: equals + * + * Returns true if the given object equals this rectangle. + */ +mxPoint.prototype.equals = function(obj) +{ + return obj.x == this.x && + obj.y == this.y; +}; + +/** + * Function: clone + * + * Returns a clone of this <mxPoint>. + */ +mxPoint.prototype.clone = function() +{ + // Handles subclasses as well + return mxUtils.clone(this); +}; diff --git a/src/js/util/mxPopupMenu.js b/src/js/util/mxPopupMenu.js new file mode 100644 index 0000000..b188cb6 --- /dev/null +++ b/src/js/util/mxPopupMenu.js @@ -0,0 +1,574 @@ +/** + * $Id: mxPopupMenu.js,v 1.37 2012-04-22 10:16:23 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxPopupMenu + * + * Event handler that pans and creates popupmenus. To use the left + * mousebutton for panning without interfering with cell moving and + * resizing, use <isUseLeftButton> and <isIgnoreCell>. For grid size + * steps while panning, use <useGrid>. This handler is built-into + * <mxGraph.panningHandler> and enabled using <mxGraph.setPanning>. + * + * Constructor: mxPopupMenu + * + * Constructs an event handler that creates a popupmenu. The + * event handler is not installed anywhere in this ctor. + * + * Event: mxEvent.SHOW + * + * Fires after the menu has been shown in <popup>. + */ +function mxPopupMenu(factoryMethod) +{ + this.factoryMethod = factoryMethod; + + if (factoryMethod != null) + { + this.init(); + } +}; + +/** + * Extends mxEventSource. + */ +mxPopupMenu.prototype = new mxEventSource(); +mxPopupMenu.prototype.constructor = mxPopupMenu; + +/** + * Variable: submenuImage + * + * URL of the image to be used for the submenu icon. + */ +mxPopupMenu.prototype.submenuImage = mxClient.imageBasePath + '/submenu.gif'; + +/** + * Variable: zIndex + * + * Specifies the zIndex for the popupmenu and its shadow. Default is 1006. + */ +mxPopupMenu.prototype.zIndex = 10006; + +/** + * Variable: factoryMethod + * + * Function that is used to create the popup menu. The function takes the + * current panning handler, the <mxCell> under the mouse and the mouse + * event that triggered the call as arguments. + */ +mxPopupMenu.prototype.factoryMethod = null; + +/** + * Variable: useLeftButtonForPopup + * + * Specifies if popupmenus should be activated by clicking the left mouse + * button. Default is false. + */ +mxPopupMenu.prototype.useLeftButtonForPopup = false; + +/** + * Variable: enabled + * + * Specifies if events are handled. Default is true. + */ +mxPopupMenu.prototype.enabled = true; + +/** + * Variable: itemCount + * + * Contains the number of times <addItem> has been called for a new menu. + */ +mxPopupMenu.prototype.itemCount = 0; + +/** + * Variable: autoExpand + * + * Specifies if submenus should be expanded on mouseover. Default is false. + */ +mxPopupMenu.prototype.autoExpand = false; + +/** + * Variable: smartSeparators + * + * Specifies if separators should only be added if a menu item follows them. + * Default is false. + */ +mxPopupMenu.prototype.smartSeparators = false; + +/** + * Variable: labels + * + * Specifies if any labels should be visible. Default is true. + */ +mxPopupMenu.prototype.labels = true; + +/** + * Function: init + * + * Initializes the shapes required for this vertex handler. + */ +mxPopupMenu.prototype.init = function() +{ + // Adds the inner table + this.table = document.createElement('table'); + this.table.className = 'mxPopupMenu'; + + this.tbody = document.createElement('tbody'); + this.table.appendChild(this.tbody); + + // Adds the outer div + this.div = document.createElement('div'); + this.div.className = 'mxPopupMenu'; + this.div.style.display = 'inline'; + this.div.style.zIndex = this.zIndex; + this.div.appendChild(this.table); + + // Disables the context menu on the outer div + mxEvent.disableContextMenu(this.div); +}; + +/** + * Function: isEnabled + * + * Returns true if events are handled. This implementation + * returns <enabled>. + */ +mxPopupMenu.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Enables or disables event handling. This implementation + * updates <enabled>. + */ +mxPopupMenu.prototype.setEnabled = function(enabled) +{ + this.enabled = enabled; +}; + +/** + * Function: isPopupTrigger + * + * Returns true if the given event is a popupmenu trigger for the optional + * given cell. + * + * Parameters: + * + * me - <mxMouseEvent> that represents the mouse event. + */ +mxPopupMenu.prototype.isPopupTrigger = function(me) +{ + return me.isPopupTrigger() || (this.useLeftButtonForPopup && + mxEvent.isLeftMouseButton(me.getEvent())); +}; + +/** + * Function: addItem + * + * Adds the given item to the given parent item. If no parent item is specified + * then the item is added to the top-level menu. The return value may be used + * as the parent argument, ie. as a submenu item. The return value is the table + * row that represents the item. + * + * Paramters: + * + * title - String that represents the title of the menu item. + * image - Optional URL for the image icon. + * funct - Function associated that takes a mouseup or touchend event. + * parent - Optional item returned by <addItem>. + * iconCls - Optional string that represents the CSS class for the image icon. + * IconsCls is ignored if image is given. + * enabled - Optional boolean indicating if the item is enabled. Default is true. + */ +mxPopupMenu.prototype.addItem = function(title, image, funct, parent, iconCls, enabled) +{ + parent = parent || this; + this.itemCount++; + + // Smart separators only added if element contains items + if (parent.willAddSeparator) + { + if (parent.containsItems) + { + this.addSeparator(parent, true); + } + + parent.willAddSeparator = false; + } + + parent.containsItems = true; + var tr = document.createElement('tr'); + tr.className = 'mxPopupMenuItem'; + var col1 = document.createElement('td'); + col1.className = 'mxPopupMenuIcon'; + + // Adds the given image into the first column + if (image != null) + { + var img = document.createElement('img'); + img.src = image; + col1.appendChild(img); + } + else if (iconCls != null) + { + var div = document.createElement('div'); + div.className = iconCls; + col1.appendChild(div); + } + + tr.appendChild(col1); + + if (this.labels) + { + var col2 = document.createElement('td'); + col2.className = 'mxPopupMenuItem' + + ((enabled != null && !enabled) ? ' disabled' : ''); + mxUtils.write(col2, title); + col2.align = 'left'; + tr.appendChild(col2); + + var col3 = document.createElement('td'); + col3.className = 'mxPopupMenuItem' + + ((enabled != null && !enabled) ? ' disabled' : ''); + col3.style.paddingRight = '6px'; + col3.style.textAlign = 'right'; + + tr.appendChild(col3); + + if (parent.div == null) + { + this.createSubmenu(parent); + } + } + + parent.tbody.appendChild(tr); + + if (enabled == null || enabled) + { + var md = (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown'; + var mm = (mxClient.IS_TOUCH) ? 'touchmove' : 'mousemove'; + var mu = (mxClient.IS_TOUCH) ? 'touchend' : 'mouseup'; + + // Consumes the event on mouse down + mxEvent.addListener(tr, md, mxUtils.bind(this, function(evt) + { + this.eventReceiver = tr; + + if (parent.activeRow != tr && parent.activeRow != parent) + { + if (parent.activeRow != null && + parent.activeRow.div.parentNode != null) + { + this.hideSubmenu(parent); + } + + if (tr.div != null) + { + this.showSubmenu(parent, tr); + parent.activeRow = tr; + } + } + + mxEvent.consume(evt); + })); + + mxEvent.addListener(tr, mm, mxUtils.bind(this, function(evt) + { + if (parent.activeRow != tr && parent.activeRow != parent) + { + if (parent.activeRow != null && + parent.activeRow.div.parentNode != null) + { + this.hideSubmenu(parent); + } + + if (this.autoExpand && tr.div != null) + { + this.showSubmenu(parent, tr); + parent.activeRow = tr; + } + } + + // Sets hover style because TR in IE doesn't have hover + tr.className = 'mxPopupMenuItemHover'; + })); + + mxEvent.addListener(tr, mu, mxUtils.bind(this, function(evt) + { + // EventReceiver avoids clicks on a submenu item + // which has just been shown in the mousedown + if (this.eventReceiver == tr) + { + if (parent.activeRow != tr) + { + this.hideMenu(); + } + + if (funct != null) + { + funct(evt); + } + } + + this.eventReceiver = null; + mxEvent.consume(evt); + })); + + // Resets hover style because TR in IE doesn't have hover + mxEvent.addListener(tr, 'mouseout', + mxUtils.bind(this, function(evt) + { + tr.className = 'mxPopupMenuItem'; + }) + ); + } + + return tr; +}; + +/** + * Function: createSubmenu + * + * Creates the nodes required to add submenu items inside the given parent + * item. This is called in <addItem> if a parent item is used for the first + * time. This adds various DOM nodes and a <submenuImage> to the parent. + * + * Parameters: + * + * parent - An item returned by <addItem>. + */ +mxPopupMenu.prototype.createSubmenu = function(parent) +{ + parent.table = document.createElement('table'); + parent.table.className = 'mxPopupMenu'; + + parent.tbody = document.createElement('tbody'); + parent.table.appendChild(parent.tbody); + + parent.div = document.createElement('div'); + parent.div.className = 'mxPopupMenu'; + + parent.div.style.position = 'absolute'; + parent.div.style.display = 'inline'; + parent.div.style.zIndex = this.zIndex; + + parent.div.appendChild(parent.table); + + var img = document.createElement('img'); + img.setAttribute('src', this.submenuImage); + + // Last column of the submenu item in the parent menu + td = parent.firstChild.nextSibling.nextSibling; + td.appendChild(img); +}; + +/** + * Function: showSubmenu + * + * Shows the submenu inside the given parent row. + */ +mxPopupMenu.prototype.showSubmenu = function(parent, row) +{ + if (row.div != null) + { + row.div.style.left = (parent.div.offsetLeft + + row.offsetLeft+row.offsetWidth - 1) + 'px'; + row.div.style.top = (parent.div.offsetTop+row.offsetTop) + 'px'; + document.body.appendChild(row.div); + + // Moves the submenu to the left side if there is no space + var left = parseInt(row.div.offsetLeft); + var width = parseInt(row.div.offsetWidth); + + var b = document.body; + var d = document.documentElement; + + var right = (b.scrollLeft || d.scrollLeft) + (b.clientWidth || d.clientWidth); + + if (left + width > right) + { + row.div.style.left = (parent.div.offsetLeft - width + + ((mxClient.IS_IE) ? 6 : -6)) + 'px'; + } + + mxUtils.fit(row.div); + } +}; + +/** + * Function: addSeparator + * + * Adds a horizontal separator in the given parent item or the top-level menu + * if no parent is specified. + * + * Parameters: + * + * parent - Optional item returned by <addItem>. + * force - Optional boolean to ignore <smartSeparators>. Default is false. + */ +mxPopupMenu.prototype.addSeparator = function(parent, force) +{ + parent = parent || this; + + if (this.smartSeparators && !force) + { + parent.willAddSeparator = true; + } + else if (parent.tbody != null) + { + parent.willAddSeparator = false; + var tr = document.createElement('tr'); + + var col1 = document.createElement('td'); + col1.className = 'mxPopupMenuIcon'; + col1.style.padding = '0 0 0 0px'; + + tr.appendChild(col1); + + var col2 = document.createElement('td'); + col2.style.padding = '0 0 0 0px'; + col2.setAttribute('colSpan', '2'); + + var hr = document.createElement('hr'); + hr.setAttribute('size', '1'); + col2.appendChild(hr); + + tr.appendChild(col2); + + parent.tbody.appendChild(tr); + } +}; + +/** + * Function: popup + * + * Shows the popup menu for the given event and cell. + * + * Example: + * + * (code) + * graph.panningHandler.popup = function(x, y, cell, evt) + * { + * mxUtils.alert('Hello, World!'); + * } + * (end) + */ +mxPopupMenu.prototype.popup = function(x, y, cell, evt) +{ + if (this.div != null && this.tbody != null && this.factoryMethod != null) + { + this.div.style.left = x + 'px'; + this.div.style.top = y + 'px'; + + // Removes all child nodes from the existing menu + while (this.tbody.firstChild != null) + { + mxEvent.release(this.tbody.firstChild); + this.tbody.removeChild(this.tbody.firstChild); + } + + this.itemCount = 0; + this.factoryMethod(this, cell, evt); + + if (this.itemCount > 0) + { + this.showMenu(); + this.fireEvent(new mxEventObject(mxEvent.SHOW)); + } + } +}; + +/** + * Function: isMenuShowing + * + * Returns true if the menu is showing. + */ +mxPopupMenu.prototype.isMenuShowing = function() +{ + return this.div != null && this.div.parentNode == document.body; +}; + +/** + * Function: showMenu + * + * Shows the menu. + */ +mxPopupMenu.prototype.showMenu = function() +{ + // Disables filter-based shadow in IE9 standards mode + if (document.documentMode >= 9) + { + this.div.style.filter = 'none'; + } + + // Fits the div inside the viewport + document.body.appendChild(this.div); + mxUtils.fit(this.div); +}; + +/** + * Function: hideMenu + * + * Removes the menu and all submenus. + */ +mxPopupMenu.prototype.hideMenu = function() +{ + if (this.div != null) + { + if (this.div.parentNode != null) + { + this.div.parentNode.removeChild(this.div); + } + + this.hideSubmenu(this); + this.containsItems = false; + } +}; + +/** + * Function: hideSubmenu + * + * Removes all submenus inside the given parent. + * + * Parameters: + * + * parent - An item returned by <addItem>. + */ +mxPopupMenu.prototype.hideSubmenu = function(parent) +{ + if (parent.activeRow != null) + { + this.hideSubmenu(parent.activeRow); + + if (parent.activeRow.div.parentNode != null) + { + parent.activeRow.div.parentNode.removeChild(parent.activeRow.div); + } + + parent.activeRow = null; + } +}; + +/** + * Function: destroy + * + * Destroys the handler and all its resources and DOM nodes. + */ +mxPopupMenu.prototype.destroy = function() +{ + if (this.div != null) + { + mxEvent.release(this.div); + + if (this.div.parentNode != null) + { + this.div.parentNode.removeChild(this.div); + } + + this.div = null; + } +}; diff --git a/src/js/util/mxRectangle.js b/src/js/util/mxRectangle.js new file mode 100644 index 0000000..035abf5 --- /dev/null +++ b/src/js/util/mxRectangle.js @@ -0,0 +1,134 @@ +/** + * $Id: mxRectangle.js,v 1.17 2010-12-08 12:46:03 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxRectangle + * + * Extends <mxPoint> to implement a 2-dimensional rectangle with double + * precision coordinates. + * + * Constructor: mxRectangle + * + * Constructs a new rectangle for the optional parameters. If no parameters + * are given then the respective default values are used. + */ +function mxRectangle(x, y, width, height) +{ + mxPoint.call(this, x, y); + + this.width = (width != null) ? width : 0; + this.height = (height != null) ? height : 0; +}; + +/** + * Extends mxPoint. + */ +mxRectangle.prototype = new mxPoint(); +mxRectangle.prototype.constructor = mxRectangle; + +/** + * Variable: width + * + * Holds the width of the rectangle. Default is 0. + */ +mxRectangle.prototype.width = null; + +/** + * Variable: height + * + * Holds the height of the rectangle. Default is 0. + */ +mxRectangle.prototype.height = null; + +/** + * Function: setRect + * + * Sets this rectangle to the specified values + */ +mxRectangle.prototype.setRect = function(x, y, w, h) +{ + this.x = x; + this.y = y; + this.width = w; + this.height = h; +}; + +/** + * Function: getCenterX + * + * Returns the x-coordinate of the center point. + */ +mxRectangle.prototype.getCenterX = function () +{ + return this.x + this.width/2; +}; + +/** + * Function: getCenterY + * + * Returns the y-coordinate of the center point. + */ +mxRectangle.prototype.getCenterY = function () +{ + return this.y + this.height/2; +}; + +/** + * Function: add + * + * Adds the given rectangle to this rectangle. + */ +mxRectangle.prototype.add = function(rect) +{ + if (rect != null) + { + var minX = Math.min(this.x, rect.x); + var minY = Math.min(this.y, rect.y); + var maxX = Math.max(this.x + this.width, rect.x + rect.width); + var maxY = Math.max(this.y + this.height, rect.y + rect.height); + + this.x = minX; + this.y = minY; + this.width = maxX - minX; + this.height = maxY - minY; + } +}; + +/** + * Function: grow + * + * Grows the rectangle by the given amount, that is, this method subtracts + * the given amount from the x- and y-coordinates and adds twice the amount + * to the width and height. + */ +mxRectangle.prototype.grow = function(amount) +{ + this.x -= amount; + this.y -= amount; + this.width += 2 * amount; + this.height += 2 * amount; +}; + +/** + * Function: getPoint + * + * Returns the top, left corner as a new <mxPoint>. + */ +mxRectangle.prototype.getPoint = function() +{ + return new mxPoint(this.x, this.y); +}; + +/** + * Function: equals + * + * Returns true if the given object equals this rectangle. + */ +mxRectangle.prototype.equals = function(obj) +{ + return obj.x == this.x && + obj.y == this.y && + obj.width == this.width && + obj.height == this.height; +}; diff --git a/src/js/util/mxResources.js b/src/js/util/mxResources.js new file mode 100644 index 0000000..0969ebe --- /dev/null +++ b/src/js/util/mxResources.js @@ -0,0 +1,366 @@ +/** + * $Id: mxResources.js,v 1.32 2012-10-26 13:36:50 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +var mxResources = +{ + /** + * Class: mxResources + * + * Implements internationalization. You can provide any number of + * resource files on the server using the following format for the + * filename: name[-en].properties. The en stands for any lowercase + * 2-character language shortcut (eg. de for german, fr for french). + * + * If the optional language extension is omitted, then the file is used as a + * default resource which is loaded in all cases. If a properties file for a + * specific language exists, then it is used to override the settings in the + * default resource. All entries in the file are of the form key=value. The + * values may then be accessed in code via <get>. Lines without + * equal signs in the properties files are ignored. + * + * Resource files may either be added programmatically using + * <add> or via a resource tag in the UI section of the + * editor configuration file, eg: + * + * (code) + * <mxEditor> + * <ui> + * <resource basename="examples/resources/mxWorkflow"/> + * (end) + * + * The above element will load examples/resources/mxWorkflow.properties as well + * as the language specific file for the current language, if it exists. + * + * Values may contain placeholders of the form {1}...{n} where each placeholder + * is replaced with the value of the corresponding array element in the params + * argument passed to <mxResources.get>. The placeholder {1} maps to the first + * element in the array (at index 0). + * + * See <mxClient.language> for more information on specifying the default + * language or disabling all loading of resources. + * + * Lines that start with a # sign will be ignored. + * + * Special characters + * + * To use unicode characters, use the standard notation (eg. \u8fd1) or %u as a + * prefix (eg. %u20AC will display a Euro sign). For normal hex encoded strings, + * use % as a prefix, eg. %F6 will display a � (ö). + * + * See <resourcesEncoded> to disable this. If you disable this, make sure that + * your files are UTF-8 encoded. + * + * Variable: resources + * + * Associative array that maps from keys to values. + */ + resources: [], + + /** + * Variable: extension + * + * Specifies the extension used for language files. Default is '.properties'. + */ + extension: '.properties', + + /** + * Variable: resourcesEncoded + * + * Specifies whether or not values in resource files are encoded with \u or + * percentage. Default is true. + */ + resourcesEncoded: true, + + /** + * Variable: loadDefaultBundle + * + * Specifies if the default file for a given basename should be loaded. + * Default is true. + */ + loadDefaultBundle: true, + + /** + * Variable: loadDefaultBundle + * + * Specifies if the specific language file file for a given basename should + * be loaded. Default is true. + */ + loadSpecialBundle: true, + + /** + * Function: isBundleSupported + * + * Hook for subclassers to disable support for a given language. This + * implementation always returns true. + * + * Parameters: + * + * basename - The basename for which the file should be loaded. + * lan - The current language. + */ + isLanguageSupported: function(lan) + { + if (mxClient.languages != null) + { + return mxUtils.indexOf(mxClient.languages, lan) >= 0; + } + + return true; + }, + + /** + * Function: getDefaultBundle + * + * Hook for subclassers to return the URL for the special bundle. This + * implementation returns basename + <extension> or null if + * <loadDefaultBundle> is false. + * + * Parameters: + * + * basename - The basename for which the file should be loaded. + * lan - The current language. + */ + getDefaultBundle: function(basename, lan) + { + if (mxResources.loadDefaultBundle || !mxResources.isLanguageSupported(lan)) + { + return basename + mxResources.extension; + } + else + { + return null; + } + }, + + /** + * Function: getSpecialBundle + * + * Hook for subclassers to return the URL for the special bundle. This + * implementation returns basename + '_' + lan + <extension> or null if + * <loadSpecialBundle> is false or lan equals <mxClient.defaultLanguage>. + * + * If <mxResources.languages> is not null and <mxClient.language> contains + * a dash, then this method checks if <isLanguageSupported> returns true + * for the full language (including the dash). If that returns false the + * first part of the language (up to the dash) will be tried as an extension. + * + * If <mxResources.language> is null then the first part of the language is + * used to maintain backwards compatibility. + * + * Parameters: + * + * basename - The basename for which the file should be loaded. + * lan - The language for which the file should be loaded. + */ + getSpecialBundle: function(basename, lan) + { + if (mxClient.languages == null || !this.isLanguageSupported(lan)) + { + var dash = lan.indexOf('-'); + + if (dash > 0) + { + lan = lan.substring(0, dash); + } + } + + if (mxResources.loadSpecialBundle && mxResources.isLanguageSupported(lan) && lan != mxClient.defaultLanguage) + { + return basename + '_' + lan + mxResources.extension; + } + else + { + return null; + } + }, + + /** + * Function: add + * + * Adds the default and current language properties + * file for the specified basename. Existing keys + * are overridden as new files are added. + * + * Example: + * + * At application startup, additional resources may be + * added using the following code: + * + * (code) + * mxResources.add('resources/editor'); + * (end) + */ + add: function(basename, lan) + { + lan = (lan != null) ? lan : mxClient.language.toLowerCase(); + + if (lan != mxConstants.NONE) + { + // Loads the common language file (no extension) + var defaultBundle = mxResources.getDefaultBundle(basename, lan); + + if (defaultBundle != null) + { + try + { + var req = mxUtils.load(defaultBundle); + + if (req.isReady()) + { + mxResources.parse(req.getText()); + } + } + catch (e) + { + // ignore + } + } + + // Overlays the language specific file (_lan-extension) + var specialBundle = mxResources.getSpecialBundle(basename, lan); + + if (specialBundle != null) + { + try + { + var req = mxUtils.load(specialBundle); + + if (req.isReady()) + { + mxResources.parse(req.getText()); + } + } + catch (e) + { + // ignore + } + } + } + }, + + /** + * Function: parse + * + * Parses the key, value pairs in the specified + * text and stores them as local resources. + */ + parse: function(text) + { + if (text != null) + { + var lines = text.split('\n'); + + for (var i = 0; i < lines.length; i++) + { + if (lines[i].charAt(0) != '#') + { + var index = lines[i].indexOf('='); + + if (index > 0) + { + var key = lines[i].substring(0, index); + var idx = lines[i].length; + + if (lines[i].charCodeAt(idx - 1) == 13) + { + idx--; + } + + var value = lines[i].substring(index + 1, idx); + + if (this.resourcesEncoded) + { + value = value.replace(/\\(?=u[a-fA-F\d]{4})/g,"%"); + mxResources.resources[key] = unescape(value); + } + else + { + mxResources.resources[key] = value; + } + } + } + } + } + }, + + /** + * Function: get + * + * Returns the value for the specified resource key. + * + * Example: + * To read the value for 'welomeMessage', use the following: + * (code) + * var result = mxResources.get('welcomeMessage') || ''; + * (end) + * + * This would require an entry of the following form in + * one of the English language resource files: + * (code) + * welcomeMessage=Welcome to mxGraph! + * (end) + * + * The part behind the || is the string value to be used if the given + * resource is not available. + * + * Parameters: + * + * key - String that represents the key of the resource to be returned. + * params - Array of the values for the placeholders of the form {1}...{n} + * to be replaced with in the resulting string. + * defaultValue - Optional string that specifies the default return value. + */ + get: function(key, params, defaultValue) + { + var value = mxResources.resources[key]; + + // Applies the default value if no resource was found + if (value == null) + { + value = defaultValue; + } + + // Replaces the placeholders with the values in the array + if (value != null && + params != null) + { + var result = []; + var index = null; + + for (var i = 0; i < value.length; i++) + { + var c = value.charAt(i); + + if (c == '{') + { + index = ''; + } + else if (index != null && c == '}') + { + index = parseInt(index)-1; + + if (index >= 0 && index < params.length) + { + result.push(params[index]); + } + + index = null; + } + else if (index != null) + { + index += c; + } + else + { + result.push(c); + } + } + + value = result.join(''); + } + + return value; + } + +}; diff --git a/src/js/util/mxSession.js b/src/js/util/mxSession.js new file mode 100644 index 0000000..4c2a70c --- /dev/null +++ b/src/js/util/mxSession.js @@ -0,0 +1,674 @@ +/** + * $Id: mxSession.js,v 1.46 2012-08-22 15:30:49 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxSession + * + * Session for sharing an <mxGraphModel> with other parties + * via a backend that acts as a multicaster for all changes. + * + * Diagram Sharing: + * + * The diagram sharing is a mechanism where each atomic change of the model is + * encoded into XML using <mxCodec> and then transmitted to the server by the + * <mxSession> object. On the server, the XML data is dispatched to each + * listener on the same diagram (except the sender), and the XML is decoded + * back into atomic changes on the client side, which are then executed on the + * model and stored in the command history. + * + * The <mxSession.significantRemoteChanges> specifies how these changes are + * treated with respect to undo: The default value (true) will undo the last + * change regardless of whether it was a remote or a local change. If the + * switch is false, then an undo will go back until the last local change, + * silently undoing all remote changes up to that point. Note that these + * changes will be added as new remote changes to the history of the other + * clients. + * + * Event: mxEvent.CONNECT + * + * Fires after the session has been started, that is, after the response to the + * initial request was received and the session goes into polling mode. This + * event has no properties. + * + * Event: mxEvent.SUSPEND + * + * Fires after <suspend> was called an the session was not already in suspended + * state. This event has no properties. + * + * Event: mxEvent.RESUME + * + * Fires after the session was resumed in <resume>. This event has no + * properties. + * + * Event: mxEvent.DISCONNECT + * + * Fires after the session was stopped in <stop>. The <code>reason</code> + * property contains the optional exception that was passed to the stop method. + * + * Event: mxEvent.NOTIFY + * + * Fires after a notification was sent in <notify>. The <code>url</code> + * property contains the URL and the <code>xml</code> property contains the XML + * data of the request. + * + * Event: mxEvent.GET + * + * Fires after a response was received in <get>. The <code>url</code> property + * contains the URL and the <code>request</code> is the <mxXmlRequest> that + * contains the response. + * + * Event: mxEvent.FIRED + * + * Fires after an array of edits has been executed on the model. The + * <code>changes</code> property contains the array of changes. + * + * Event: mxEvent.RECEIVE + * + * Fires after an XML node was received in <receive>. The <code>node</code> + * property contains the node that was received. + * + * Constructor: mxSession + * + * Constructs a new session using the given <mxGraphModel> and URLs to + * communicate with the backend. + * + * Parameters: + * + * model - <mxGraphModel> that contains the data. + * urlInit - URL to be used for initializing the session. + * urlPoll - URL to be used for polling the backend. + * urlNotify - URL to be used for sending changes to the backend. + */ +function mxSession(model, urlInit, urlPoll, urlNotify) +{ + this.model = model; + this.urlInit = urlInit; + this.urlPoll = urlPoll; + this.urlNotify = urlNotify; + + // Resolves cells by id using the model + if (model != null) + { + this.codec = new mxCodec(); + + this.codec.lookup = function(id) + { + return model.getCell(id); + }; + } + + // Adds the listener for notifying the backend of any + // changes in the model + model.addListener(mxEvent.NOTIFY, mxUtils.bind(this, function(sender, evt) + { + var edit = evt.getProperty('edit'); + + if (edit != null && this.debug || (this.connected && !this.suspended)) + { + this.notify('<edit>'+this.encodeChanges(edit.changes, edit.undone)+'</edit>'); + } + })); +}; + +/** + * Extends mxEventSource. + */ +mxSession.prototype = new mxEventSource(); +mxSession.prototype.constructor = mxSession; + +/** + * Variable: model + * + * Reference to the enclosing <mxGraphModel>. + */ +mxSession.prototype.model = null; + +/** + * Variable: urlInit + * + * URL to initialize the session. + */ +mxSession.prototype.urlInit = null; + +/** + * Variable: urlPoll + * + * URL for polling the backend. + */ +mxSession.prototype.urlPoll = null; + +/** + * Variable: urlNotify + * + * URL to send changes to the backend. + */ +mxSession.prototype.urlNotify = null; + +/** + * Variable: codec + * + * Reference to the <mxCodec> used to encoding and decoding changes. + */ +mxSession.prototype.codec = null; + +/** + * Variable: linefeed + * + * Used for encoding linefeeds. Default is '
'. + */ +mxSession.prototype.linefeed = '
'; + +/** + * Variable: escapePostData + * + * Specifies if the data in the post request sent in <notify> + * should be converted using encodeURIComponent. Default is true. + */ +mxSession.prototype.escapePostData = true; + +/** + * Variable: significantRemoteChanges + * + * Whether remote changes should be significant in the + * local command history. Default is true. + */ +mxSession.prototype.significantRemoteChanges = true; + +/** + * Variable: sent + * + * Total number of sent bytes. + */ +mxSession.prototype.sent = 0; + +/** + * Variable: received + * + * Total number of received bytes. + */ +mxSession.prototype.received = 0; + +/** + * Variable: debug + * + * Specifies if the session should run in debug mode. In this mode, no + * connection is established. The data is written to the console instead. + * Default is false. + */ +mxSession.prototype.debug = false; + +/** + * Variable: connected + */ +mxSession.prototype.connected = false; + +/** + * Variable: send + */ +mxSession.prototype.suspended = false; + +/** + * Variable: polling + */ +mxSession.prototype.polling = false; + +/** + * Function: start + */ +mxSession.prototype.start = function() +{ + if (this.debug) + { + this.connected = true; + this.fireEvent(new mxEventObject(mxEvent.CONNECT)); + } + else if (!this.connected) + { + this.get(this.urlInit, mxUtils.bind(this, function(req) + { + this.connected = true; + this.fireEvent(new mxEventObject(mxEvent.CONNECT)); + this.poll(); + })); + } +}; + +/** + * Function: suspend + * + * Suspends the polling. Use <resume> to reactive the session. Fires a + * suspend event. + */ +mxSession.prototype.suspend = function() +{ + if (this.connected && !this.suspended) + { + this.suspended = true; + this.fireEvent(new mxEventObject(mxEvent.SUSPEND)); + } +}; + +/** + * Function: resume + * + * Resumes the session if it has been suspended. Fires a resume-event + * before starting the polling. + */ +mxSession.prototype.resume = function(type, attr, value) +{ + if (this.connected && + this.suspended) + { + this.suspended = false; + this.fireEvent(new mxEventObject(mxEvent.RESUME)); + + if (!this.polling) + { + this.poll(); + } + } +}; + +/** + * Function: stop + * + * Stops the session and fires a disconnect event. The given reason is + * passed to the disconnect event listener as the second argument. + */ +mxSession.prototype.stop = function(reason) +{ + if (this.connected) + { + this.connected = false; + } + + this.fireEvent(new mxEventObject(mxEvent.DISCONNECT, + 'reason', reason)); +}; + +/** + * Function: poll + * + * Sends an asynchronous GET request to <urlPoll>. + */ +mxSession.prototype.poll = function() +{ + if (this.connected && + !this.suspended && + this.urlPoll != null) + { + this.polling = true; + + this.get(this.urlPoll, mxUtils.bind(this, function() + { + this.poll(); + })); + } + else + { + this.polling = false; + } +}; + +/** + * Function: notify + * + * Sends out the specified XML to <urlNotify> and fires a <notify> event. + */ +mxSession.prototype.notify = function(xml, onLoad, onError) +{ + if (xml != null && + xml.length > 0) + { + if (this.urlNotify != null) + { + if (this.debug) + { + mxLog.show(); + mxLog.debug('mxSession.notify: '+this.urlNotify+' xml='+xml); + } + else + { + xml = '<message><delta>'+xml+'</delta></message>'; + + if (this.escapePostData) + { + xml = encodeURIComponent(xml); + } + + mxUtils.post(this.urlNotify, 'xml='+xml, onLoad, onError); + } + } + + this.sent += xml.length; + this.fireEvent(new mxEventObject(mxEvent.NOTIFY, + 'url', this.urlNotify, 'xml', xml)); + } +}; + +/** + * Function: get + * + * Sends an asynchronous get request to the given URL, fires a <get> event + * and invokes the given onLoad function when a response is received. + */ +mxSession.prototype.get = function(url, onLoad, onError) +{ + // Response after browser refresh has no global scope + // defined. This response is ignored and the session + // stops implicitely. + if (typeof(mxUtils) != 'undefined') + { + var onErrorWrapper = mxUtils.bind(this, function(ex) + { + if (onError != null) + { + onError(ex); + } + else + { + this.stop(ex); + } + }); + + // Handles a successful response for + // the above request. + mxUtils.get(url, mxUtils.bind(this, function(req) + { + if (typeof(mxUtils) != 'undefined') + { + if (req.isReady() && req.getStatus() != 404) + { + this.received += req.getText().length; + this.fireEvent(new mxEventObject(mxEvent.GET, 'url', url, 'request', req)); + + if (this.isValidResponse(req)) + { + if (req.getText().length > 0) + { + var node = req.getDocumentElement(); + + if (node == null) + { + onErrorWrapper('Invalid response: '+req.getText()); + } + else + { + this.receive(node); + } + } + + if (onLoad != null) + { + onLoad(req); + } + } + } + else + { + onErrorWrapper('Response not ready'); + } + } + }), + // Handles a transmission error for the + // above request + function(req) + { + onErrorWrapper('Transmission error'); + }); + } +}; + +/** + * Function: isValidResponse + * + * Returns true if the response data in the given <mxXmlRequest> is valid. + */ +mxSession.prototype.isValidResponse = function(req) +{ + // TODO: Find condition to check if response + // contains valid XML (not eg. the PHP code). + return req.getText().indexOf('<?php') < 0; +}; + +/** + * Function: encodeChanges + * + * Returns the XML representation for the given array of changes. + */ +mxSession.prototype.encodeChanges = function(changes, invert) +{ + // TODO: Use array for string concatenation + var xml = ''; + var step = (invert) ? -1 : 1; + var i0 = (invert) ? changes.length - 1 : 0; + + for (var i = i0; i >= 0 && i < changes.length; i += step) + { + // Newlines must be kept, they will be converted + // to 
 when the server sends data to the + // client + var node = this.codec.encode(changes[i]); + xml += mxUtils.getXml(node, this.linefeed); + } + + return xml; +}; + +/** + * Function: receive + * + * Processes the given node by applying the changes to the model. If the nodename + * is state, then the namespace is used as a prefix for creating Ids in the model, + * and the child nodes are visited recursively. If the nodename is delta, then the + * changes encoded in the child nodes are applied to the model. Each call to the + * receive function fires a <receive> event with the given node as the second argument + * after processing. If changes are processed, then the function additionally fires + * a <mxEvent.FIRED> event before the <mxEvent.RECEIVE> event. + */ +mxSession.prototype.receive = function(node) +{ + if (node != null && node.nodeType == mxConstants.NODETYPE_ELEMENT) + { + // Uses the namespace in the model + var ns = node.getAttribute('namespace'); + + if (ns != null) + { + this.model.prefix = ns + '-'; + } + + var child = node.firstChild; + + while (child != null) + { + var name = child.nodeName.toLowerCase(); + + if (name == 'state') + { + this.processState(child); + } + else if (name == 'delta') + { + this.processDelta(child); + } + + child = child.nextSibling; + } + + // Fires receive event + this.fireEvent(new mxEventObject(mxEvent.RECEIVE, 'node', node)); + } +}; + +/** + * Function: processState + * + * Processes the given state node which contains the current state of the + * remote model. + */ +mxSession.prototype.processState = function(node) +{ + var dec = new mxCodec(node.ownerDocument); + dec.decode(node.firstChild, this.model); +}; + +/** + * Function: processDelta + * + * Processes the given delta node which contains a sequence of edits which in + * turn map to one transaction on the remote model each. + */ +mxSession.prototype.processDelta = function(node) +{ + var edit = node.firstChild; + + while (edit != null) + { + if (edit.nodeName == 'edit') + { + this.processEdit(edit); + } + + edit = edit.nextSibling; + } +}; + +/** + * Function: processEdit + * + * Processes the given edit by executing its changes and firing the required + * events via the model. + */ +mxSession.prototype.processEdit = function(node) +{ + var changes = this.decodeChanges(node); + + if (changes.length > 0) + { + var edit = this.createUndoableEdit(changes); + + // No notify event here to avoid the edit from being encoded and transmitted + // LATER: Remove changes property (deprecated) + this.model.fireEvent(new mxEventObject(mxEvent.CHANGE, + 'edit', edit, 'changes', changes)); + this.model.fireEvent(new mxEventObject(mxEvent.UNDO, 'edit', edit)); + this.fireEvent(new mxEventObject(mxEvent.FIRED, 'edit', edit)); + } +}; + +/** + * Function: createUndoableEdit + * + * Creates a new <mxUndoableEdit> that implements the notify function to fire a + * <change> and <notify> event via the model. + */ +mxSession.prototype.createUndoableEdit = function(changes) +{ + var edit = new mxUndoableEdit(this.model, this.significantRemoteChanges); + edit.changes = changes; + + edit.notify = function() + { + // LATER: Remove changes property (deprecated) + edit.source.fireEvent(new mxEventObject(mxEvent.CHANGE, + 'edit', edit, 'changes', edit.changes)); + edit.source.fireEvent(new mxEventObject(mxEvent.NOTIFY, + 'edit', edit, 'changes', edit.changes)); + }; + + return edit; +}; + +/** + * Function: decodeChanges + * + * Decodes and executes the changes represented by the children in the + * given node. Returns an array that contains all changes. + */ +mxSession.prototype.decodeChanges = function(node) +{ + // Updates the document in the existing codec + this.codec.document = node.ownerDocument; + + // Parses and executes the changes on the model + var changes = []; + node = node.firstChild; + + while (node != null) + { + var change = this.decodeChange(node); + + if (change != null) + { + changes.push(change); + } + + node = node.nextSibling; + } + + return changes; +}; + +/** + * Function: decodeChange + * + * Decodes, executes and returns the change object represented by the given + * XML node. + */ +mxSession.prototype.decodeChange = function(node) +{ + var change = null; + + if (node.nodeType == mxConstants.NODETYPE_ELEMENT) + { + if (node.nodeName == 'mxRootChange') + { + // Handles the special case were no ids should be + // resolved in the existing model. This change will + // replace all registered ids and cells from the + // model and insert a new cell hierarchy instead. + var tmp = new mxCodec(node.ownerDocument); + change = tmp.decode(node); + } + else + { + change = this.codec.decode(node); + } + + if (change != null) + { + change.model = this.model; + change.execute(); + + // Workaround for references not being resolved if cells have + // been removed from the model prior to being referenced. This + // adds removed cells in the codec object lookup table. + if (node.nodeName == 'mxChildChange' && change.parent == null) + { + this.cellRemoved(change.child); + } + } + } + + return change; +}; + +/** + * Function: cellRemoved + * + * Adds removed cells to the codec object lookup for references to the removed + * cells after this point in time. + */ +mxSession.prototype.cellRemoved = function(cell, codec) +{ + this.codec.putObject(cell.getId(), cell); + + var childCount = this.model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + this.cellRemoved(this.model.getChildAt(cell, i)); + } +}; diff --git a/src/js/util/mxSvgCanvas2D.js b/src/js/util/mxSvgCanvas2D.js new file mode 100644 index 0000000..4af0642 --- /dev/null +++ b/src/js/util/mxSvgCanvas2D.js @@ -0,0 +1,1234 @@ +/** + * $Id: mxSvgCanvas2D.js,v 1.18 2012-11-23 15:13:19 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * + * Class: mxSvgCanvas2D + * + * Implements a canvas to be used with <mxImageExport>. This canvas writes all + * calls as SVG output to the given SVG root node. + * + * (code) + * var svgDoc = mxUtils.createXmlDocument(); + * var root = (svgDoc.createElementNS != null) ? + * svgDoc.createElementNS(mxConstants.NS_SVG, 'svg') : svgDoc.createElement('svg'); + * + * if (svgDoc.createElementNS == null) + * { + * root.setAttribute('xmlns', mxConstants.NS_SVG); + * } + * + * var bounds = graph.getGraphBounds(); + * root.setAttribute('width', (bounds.x + bounds.width + 4) + 'px'); + * root.setAttribute('height', (bounds.y + bounds.height + 4) + 'px'); + * root.setAttribute('version', '1.1'); + * + * svgDoc.appendChild(root); + * + * var svgCanvas = new mxSvgCanvas2D(root); + * (end) + * + * Constructor: mxSvgCanvas2D + * + * Constructs an SVG canvas. + * + * Parameters: + * + * root - SVG container for the output. + * styleEnabled - Optional boolean that specifies if a style section should be + * added. The style section sets the default font-size, font-family and + * stroke-miterlimit globally. Default is false. + */ +var mxSvgCanvas2D = function(root, styleEnabled) +{ + styleEnabled = (styleEnabled != null) ? styleEnabled : false; + + /** + * Variable: converter + * + * Holds the <mxUrlConverter> to convert image URLs. + */ + var converter = new mxUrlConverter(); + + /** + * Variable: autoAntiAlias + * + * Specifies if anti aliasing should be disabled for rectangles + * and orthogonal paths. Default is true. + */ + var autoAntiAlias = true; + + /** + * Variable: textEnabled + * + * Specifies if text output should be enabled. Default is true. + */ + var textEnabled = true; + + /** + * Variable: foEnabled + * + * Specifies if use of foreignObject for HTML markup is allowed. Default is true. + */ + var foEnabled = true; + + // Private helper function to create SVG elements + var create = function(tagName, namespace) + { + var doc = root.ownerDocument || document; + + if (doc.createElementNS != null) + { + return doc.createElementNS(namespace || mxConstants.NS_SVG, tagName); + } + else + { + var elt = doc.createElement(tagName); + + if (namespace != null) + { + elt.setAttribute('xmlns', namespace); + } + + return elt; + } + }; + + // Defs section contains optional style and gradients + var defs = create('defs'); + + // Creates defs section with optional global style + if (styleEnabled) + { + var style = create('style'); + style.setAttribute('type', 'text/css'); + mxUtils.write(style, 'svg{font-family:' + mxConstants.DEFAULT_FONTFAMILY + + ';font-size:' + mxConstants.DEFAULT_FONTSIZE + + ';fill:none;stroke-miterlimit:10}'); + + if (autoAntiAlias) + { + mxUtils.write(style, 'rect{shape-rendering:crispEdges}'); + } + + // Appends style to defs and defs to SVG container + defs.appendChild(style); + } + + root.appendChild(defs); + + // Defines the current state + var currentState = + { + dx: 0, + dy: 0, + scale: 1, + transform: '', + fill: null, + gradient: null, + stroke: null, + strokeWidth: 1, + dashed: false, + dashpattern: '3 3', + alpha: 1, + linecap: 'flat', + linejoin: 'miter', + miterlimit: 10, + fontColor: '#000000', + fontSize: mxConstants.DEFAULT_FONTSIZE, + fontFamily: mxConstants.DEFAULT_FONTFAMILY, + fontStyle: 0 + }; + + // Local variables + var currentPathIsOrthogonal = true; + var glassGradient = null; + var currentNode = null; + var currentPath = null; + var lastPoint = null; + var gradients = []; + var refCount = 0; + var stack = []; + + // Other private helper methods + var createGradientId = function(start, end, direction) + { + // Removes illegal characters from gradient ID + if (start.charAt(0) == '#') + { + start = start.substring(1); + } + + if (end.charAt(0) == '#') + { + end = end.substring(1); + } + + // Workaround for gradient IDs not working in Safari 5 / Chrome 6 + // if they contain uppercase characters + start = start.toLowerCase(); + end = end.toLowerCase(); + + // Wrong gradient directions possible? + var dir = null; + + if (direction == null || direction == mxConstants.DIRECTION_SOUTH) + { + dir = 's'; + } + else if (direction == mxConstants.DIRECTION_EAST) + { + dir = 'e'; + } + else + { + var tmp = start; + start = end; + end = tmp; + + if (direction == mxConstants.DIRECTION_NORTH) + { + dir = 's'; + } + else if (direction == mxConstants.DIRECTION_WEST) + { + dir = 'e'; + } + } + + return start+'-'+end+'-'+dir; + }; + + var createHtmlBody = function(str, align, valign) + { + var style = 'margin:0px;font-size:' + Math.floor(currentState.fontSize) + 'px;' + + 'font-family:' + currentState.fontFamily + ';color:' + currentState.fontColor+ ';'; + + if ((currentState.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD) + { + style += 'font-weight:bold;'; + } + + if ((currentState.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC) + { + style += 'font-style:italic;'; + } + + if ((currentState.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE) + { + style += 'font-decoration:underline;'; + } + + if (align == mxConstants.ALIGN_CENTER) + { + style += 'text-align:center;'; + } + else if (align == mxConstants.ALIGN_RIGHT) + { + style += 'text-align:right;'; + } + + // Converts HTML entities to unicode + var t = document.createElement('div'); + t.innerHTML = str; + str = t.innerHTML.replace(/ /g, ' '); + + // LATER: Add vertical align support via table, adds xmlns to workaround empty NS in IE9 standards + var node = mxUtils.parseXml('<div xmlns="http://www.w3.org/1999/xhtml" style="' + + style + '">' + str + '</div>').documentElement; + + return node; + }; + + var getSvgGradient = function(start, end, direction) + { + var id = createGradientId(start, end, direction); + var gradient = gradients[id]; + + if (gradient == null) + { + gradient = create('linearGradient'); + gradient.setAttribute('id', ++refCount); + gradient.setAttribute('x1', '0%'); + gradient.setAttribute('y1', '0%'); + gradient.setAttribute('x2', '0%'); + gradient.setAttribute('y2', '0%'); + + if (direction == null || direction == mxConstants.DIRECTION_SOUTH) + { + gradient.setAttribute('y2', '100%'); + } + else if (direction == mxConstants.DIRECTION_EAST) + { + gradient.setAttribute('x2', '100%'); + } + else if (direction == mxConstants.DIRECTION_NORTH) + { + gradient.setAttribute('y1', '100%'); + } + else if (direction == mxConstants.DIRECTION_WEST) + { + gradient.setAttribute('x1', '100%'); + } + + var stop = create('stop'); + stop.setAttribute('offset', '0%'); + stop.setAttribute('style', 'stop-color:'+start); + gradient.appendChild(stop); + + stop = create('stop'); + stop.setAttribute('offset', '100%'); + stop.setAttribute('style', 'stop-color:'+end); + gradient.appendChild(stop); + + defs.appendChild(gradient); + gradients[id] = gradient; + } + + return gradient.getAttribute('id'); + }; + + var appendNode = function(node, state, filled, stroked) + { + if (node != null) + { + if (state.clip != null) + { + node.setAttribute('clip-path', 'url(#' + state.clip + ')'); + state.clip = null; + } + + if (currentPath != null) + { + node.setAttribute('d', currentPath.join(' ')); + currentPath = null; + + if (autoAntiAlias && currentPathIsOrthogonal) + { + node.setAttribute('shape-rendering', 'crispEdges'); + state.strokeWidth = Math.max(1, state.strokeWidth); + } + } + + if (state.alpha < 1) + { + // LATER: Check if using fill/stroke-opacity here is faster + node.setAttribute('opacity', state.alpha); + //node.setAttribute('fill-opacity', state.alpha); + //node.setAttribute('stroke-opacity', state.alpha); + } + + if (filled && (state.fill != null || state.gradient != null)) + { + if (state.gradient != null) + { + node.setAttribute('fill', 'url(#' + state.gradient + ')'); + } + else + { + node.setAttribute('fill', state.fill.toLowerCase()); + } + } + else if (!styleEnabled) + { + node.setAttribute('fill', 'none'); + } + + if (stroked && state.stroke != null) + { + node.setAttribute('stroke', state.stroke.toLowerCase()); + + // Sets the stroke properties (1 is default is SVG) + if (state.strokeWidth != 1) + { + if (node.nodeName == 'rect' && autoAntiAlias) + { + state.strokeWidth = Math.max(1, state.strokeWidth); + } + + node.setAttribute('stroke-width', state.strokeWidth); + } + + if (node.nodeName == 'path') + { + // Linejoin miter is default in SVG + if (state.linejoin != null && state.linejoin != 'miter') + { + node.setAttribute('stroke-linejoin', state.linejoin); + } + + if (state.linecap != null) + { + // flat is called butt in SVG + var value = state.linecap; + + if (value == 'flat') + { + value = 'butt'; + } + + // Linecap butt is default in SVG + if (value != 'butt') + { + node.setAttribute('stroke-linecap', value); + } + } + + // Miterlimit 10 is default in our document + if (state.miterlimit != null && (!styleEnabled || state.miterlimit != 10)) + { + node.setAttribute('stroke-miterlimit', state.miterlimit); + } + } + + if (state.dashed) + { + var dash = state.dashpattern.split(' '); + + if (dash.length > 0) + { + var pat = []; + + for (var i = 0; i < dash.length; i++) + { + pat[i] = Number(dash[i]) * currentState.strokeWidth; + } + + + node.setAttribute('stroke-dasharray', pat.join(' ')); + } + } + } + + if (state.transform.length > 0) + { + node.setAttribute('transform', state.transform); + } + + root.appendChild(node); + } + }; + + // Private helper function to format a number + var f2 = function(x) + { + return Math.round(parseFloat(x) * 100) / 100; + }; + + // Returns public interface + return { + + /** + * Function: getConverter + * + * Returns <converter>. + */ + getConverter: function() + { + return converter; + }, + + /** + * Function: isAutoAntiAlias + * + * Returns <autoAntiAlias>. + */ + isAutoAntiAlias: function() + { + return autoAntiAlias; + }, + + /** + * Function: setAutoAntiAlias + * + * Sets <autoAntiAlias>. + */ + setAutoAntiAlias: function(value) + { + autoAntiAlias = value; + }, + + /** + * Function: isTextEnabled + * + * Returns <textEnabled>. + */ + isTextEnabled: function() + { + return textEnabled; + }, + + /** + * Function: setTextEnabled + * + * Sets <textEnabled>. + */ + setTextEnabled: function(value) + { + textEnabled = value; + }, + + /** + * Function: isFoEnabled + * + * Returns <foEnabled>. + */ + isFoEnabled: function() + { + return foEnabled; + }, + + /** + * Function: setFoEnabled + * + * Sets <foEnabled>. + */ + setFoEnabled: function(value) + { + foEnabled = value; + }, + + /** + * Function: save + * + * Saves the state of the graphics object. + */ + save: function() + { + stack.push(currentState); + currentState = mxUtils.clone(currentState); + }, + + /** + * Function: restore + * + * Restores the state of the graphics object. + */ + restore: function() + { + currentState = stack.pop(); + }, + + /** + * Function: scale + * + * Scales the current graphics object. + */ + scale: function(value) + { + currentState.scale *= value; + currentState.strokeWidth *= value; + }, + + /** + * Function: translate + * + * Translates the current graphics object. + */ + translate: function(dx, dy) + { + currentState.dx += dx; + currentState.dy += dy; + }, + + /** + * Function: rotate + * + * Rotates and/or flips the current graphics object. + */ + rotate: function(theta, flipH, flipV, cx, cy) + { + cx += currentState.dx; + cy += currentState.dy; + + cx *= currentState.scale; + cy *= currentState.scale; + + // This implementation uses custom scale/translate and built-in rotation + // Rotation state is part of the AffineTransform in state.transform + if (flipH ^ flipV) + { + var tx = (flipH) ? cx : 0; + var sx = (flipH) ? -1 : 1; + + var ty = (flipV) ? cy : 0; + var sy = (flipV) ? -1 : 1; + + currentState.transform += 'translate(' + f2(tx) + ',' + f2(ty) + ')'; + currentState.transform += 'scale(' + f2(sx) + ',' + f2(sy) + ')'; + currentState.transform += 'translate(' + f2(-tx) + ' ' + f2(-ty) + ')'; + } + + currentState.transform += 'rotate(' + f2(theta) + ',' + f2(cx) + ',' + f2(cy) + ')'; + }, + + /** + * Function: setStrokeWidth + * + * Sets the stroke width. + */ + setStrokeWidth: function(value) + { + currentState.strokeWidth = value * currentState.scale; + }, + + /** + * Function: setStrokeColor + * + * Sets the stroke color. + */ + setStrokeColor: function(value) + { + currentState.stroke = value; + }, + + /** + * Function: setDashed + * + * Sets the dashed state to true or false. + */ + setDashed: function(value) + { + currentState.dashed = value; + }, + + /** + * Function: setDashPattern + * + * Sets the dashed pattern to the given space separated list of numbers. + */ + setDashPattern: function(value) + { + currentState.dashpattern = value; + }, + + /** + * Function: setLineCap + * + * Sets the linecap. + */ + setLineCap: function(value) + { + currentState.linecap = value; + }, + + /** + * Function: setLineJoin + * + * Sets the linejoin. + */ + setLineJoin: function(value) + { + currentState.linejoin = value; + }, + + /** + * Function: setMiterLimit + * + * Sets the miterlimit. + */ + setMiterLimit: function(value) + { + currentState.miterlimit = value; + }, + + /** + * Function: setFontSize + * + * Sets the fontsize. + */ + setFontSize: function(value) + { + currentState.fontSize = value; + }, + + /** + * Function: setFontColor + * + * Sets the fontcolor. + */ + setFontColor: function(value) + { + currentState.fontColor = value; + }, + + /** + * Function: setFontFamily + * + * Sets the fontfamily. + */ + setFontFamily: function(value) + { + currentState.fontFamily = value; + }, + + /** + * Function: setFontStyle + * + * Sets the fontstyle. + */ + setFontStyle: function(value) + { + currentState.fontStyle = value; + }, + + /** + * Function: setAlpha + * + * Sets the current alpha. + */ + setAlpha: function(alpha) + { + currentState.alpha = alpha; + }, + + /** + * Function: setFillColor + * + * Sets the fillcolor. + */ + setFillColor: function(value) + { + currentState.fill = value; + currentState.gradient = null; + }, + + /** + * Function: setGradient + * + * Sets the gradient color. + */ + setGradient: function(color1, color2, x, y, w, h, direction) + { + if (color1 != null && color2 != null) + { + currentState.gradient = getSvgGradient(color1, color2, direction); + currentState.fill = color1; + } + }, + + /** + * Function: setGlassGradient + * + * Sets the glass gradient. + */ + setGlassGradient: function(x, y, w, h) + { + // Creates glass overlay gradient + if (glassGradient == null) + { + glassGradient = create('linearGradient'); + glassGradient.setAttribute('id', '0'); + glassGradient.setAttribute('x1', '0%'); + glassGradient.setAttribute('y1', '0%'); + glassGradient.setAttribute('x2', '0%'); + glassGradient.setAttribute('y2', '100%'); + + var stop1 = create('stop'); + stop1.setAttribute('offset', '0%'); + stop1.setAttribute('style', 'stop-color:#ffffff;stop-opacity:0.9'); + glassGradient.appendChild(stop1); + + var stop2 = create('stop'); + stop2.setAttribute('offset', '100%'); + stop2.setAttribute('style', 'stop-color:#ffffff;stop-opacity:0.1'); + glassGradient.appendChild(stop2); + + // Makes it the first entry of all gradients in defs + if (defs.firstChild.nextSibling != null) + { + defs.insertBefore(glassGradient, defs.firstChild.nextSibling); + } + else + { + defs.appendChild(glassGradient); + } + } + + // Glass gradient has hardcoded ID (see above) + currentState.gradient = '0'; + }, + + /** + * Function: rect + * + * Sets the current path to a rectangle. + */ + rect: function(x, y, w, h) + { + x += currentState.dx; + y += currentState.dy; + + currentNode = create('rect'); + currentNode.setAttribute('x', f2(x * currentState.scale)); + currentNode.setAttribute('y', f2(y * currentState.scale)); + currentNode.setAttribute('width', f2(w * currentState.scale)); + currentNode.setAttribute('height', f2(h * currentState.scale)); + + if (!styleEnabled && autoAntiAlias) + { + currentNode.setAttribute('shape-rendering', 'crispEdges'); + } + }, + + /** + * Function: roundrect + * + * Sets the current path to a rounded rectangle. + */ + roundrect: function(x, y, w, h, dx, dy) + { + x += currentState.dx; + y += currentState.dy; + + currentNode = create('rect'); + currentNode.setAttribute('x', f2(x * currentState.scale)); + currentNode.setAttribute('y', f2(y * currentState.scale)); + currentNode.setAttribute('width', f2(w * currentState.scale)); + currentNode.setAttribute('height', f2(h * currentState.scale)); + + if (dx > 0) + { + currentNode.setAttribute('rx', f2(dx * currentState.scale)); + } + + if (dy > 0) + { + currentNode.setAttribute('ry', f2(dy * currentState.scale)); + } + + if (!styleEnabled && autoAntiAlias) + { + currentNode.setAttribute('shape-rendering', 'crispEdges'); + } + }, + + /** + * Function: ellipse + * + * Sets the current path to an ellipse. + */ + ellipse: function(x, y, w, h) + { + x += currentState.dx; + y += currentState.dy; + + currentNode = create('ellipse'); + currentNode.setAttribute('cx', f2((x + w / 2) * currentState.scale)); + currentNode.setAttribute('cy', f2((y + h / 2) * currentState.scale)); + currentNode.setAttribute('rx', f2(w / 2 * currentState.scale)); + currentNode.setAttribute('ry', f2(h / 2 * currentState.scale)); + }, + + /** + * Function: image + * + * Paints an image. + */ + image: function(x, y, w, h, src, aspect, flipH, flipV) + { + src = converter.convert(src); + + // TODO: Add option for embedded images as base64. Current + // known issues are binary loading of cross-domain images. + aspect = (aspect != null) ? aspect : true; + flipH = (flipH != null) ? flipH : false; + flipV = (flipV != null) ? flipV : false; + x += currentState.dx; + y += currentState.dy; + + var node = create('image'); + node.setAttribute('x', f2(x * currentState.scale)); + node.setAttribute('y', f2(y * currentState.scale)); + node.setAttribute('width', f2(w * currentState.scale)); + node.setAttribute('height', f2(h * currentState.scale)); + + if (mxClient.IS_VML) + { + node.setAttribute('xlink:href', src); + } + else + { + node.setAttributeNS(mxConstants.NS_XLINK, 'xlink:href', src); + } + + if (!aspect) + { + node.setAttribute('preserveAspectRatio', 'none'); + } + + if (currentState.alpha < 1) + { + node.setAttribute('opacity', currentState.alpha); + } + + + var tr = currentState.transform; + + if (flipH || flipV) + { + var sx = 1; + var sy = 1; + var dx = 0; + var dy = 0; + + if (flipH) + { + sx = -1; + dx = -w - 2 * x; + } + + if (flipV) + { + sy = -1; + dy = -h - 2 * y; + } + + // Adds image tansformation to existing transforms + tr += 'scale(' + sx + ',' + sy + ')translate(' + dx + ',' + dy + ')'; + } + + if (tr.length > 0) + { + node.setAttribute('transform', tr); + } + + root.appendChild(node); + }, + + /** + * Function: text + * + * Paints the given text. Possible values for format are empty string for + * plain text and html for HTML markup. + */ + text: function(x, y, w, h, str, align, valign, vertical, wrap, format) + { + if (textEnabled) + { + x += currentState.dx; + y += currentState.dy; + + if (foEnabled && format == 'html') + { + var node = create('g'); + node.setAttribute('transform', currentState.transform + 'scale(' + currentState.scale + ',' + currentState.scale + ')'); + + if (currentState.alpha < 1) + { + node.setAttribute('opacity', currentState.alpha); + } + + var fo = create('foreignObject'); + fo.setAttribute('x', Math.round(x)); + fo.setAttribute('y', Math.round(y)); + fo.setAttribute('width', Math.round(w)); + fo.setAttribute('height', Math.round(h)); + fo.appendChild(createHtmlBody(str, align, valign)); + node.appendChild(fo); + root.appendChild(node); + } + else + { + var size = Math.floor(currentState.fontSize); + var node = create('g'); + var tr = currentState.transform; + + if (vertical) + { + var cx = x + w / 2; + var cy = y + h / 2; + tr += 'rotate(-90,' + f2(cx * currentState.scale) + ',' + f2(cy * currentState.scale) + ')'; + } + + if (tr.length > 0) + { + node.setAttribute('transform', tr); + } + + if (currentState.alpha < 1) + { + node.setAttribute('opacity', currentState.alpha); + } + + // Default is left + var anchor = (align == mxConstants.ALIGN_RIGHT) ? 'end' : + (align == mxConstants.ALIGN_CENTER) ? 'middle' : + 'start'; + + if (anchor == 'end') + { + x += Math.max(0, w - 2); + } + else if (anchor == 'middle') + { + x += w / 2; + } + else + { + x += (w > 0) ? 2 : 0; + } + + if ((currentState.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD) + { + node.setAttribute('font-weight', 'bold'); + } + + if ((currentState.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC) + { + node.setAttribute('font-style', 'italic'); + } + + if ((currentState.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE) + { + node.setAttribute('text-decoration', 'underline'); + } + + // Text-anchor start is default in SVG + if (anchor != 'start') + { + node.setAttribute('text-anchor', anchor); + } + + if (!styleEnabled || size != mxConstants.DEFAULT_FONTSIZE) + { + node.setAttribute('font-size', Math.floor(size * currentState.scale) + 'px'); + } + + if (!styleEnabled || currentState.fontFamily != mxConstants.DEFAULT_FONTFAMILY) + { + node.setAttribute('font-family', currentState.fontFamily); + } + + node.setAttribute('fill', currentState.fontColor); + + var lines = str.split('\n'); + + var lineHeight = size * 1.25; + var textHeight = (h > 0) ? size + (lines.length - 1) * lineHeight : lines.length * lineHeight - 1; + var dy = h - textHeight; + + // Top is default + if (valign == null || valign == mxConstants.ALIGN_TOP) + { + y = Math.max(y - 3 * currentState.scale, y + dy / 2 + ((h > 0) ? lineHeight / 2 - 8 : 0)); + } + else if (valign == mxConstants.ALIGN_MIDDLE) + { + y = y + dy / 2; + } + else if (valign == mxConstants.ALIGN_BOTTOM) + { + y = Math.min(y, y + dy + 2 * currentState.scale); + } + + y += size; + + for (var i = 0; i < lines.length; i++) + { + var text = create('text'); + text.setAttribute('x', f2(x * currentState.scale)); + text.setAttribute('y', f2(y * currentState.scale)); + + mxUtils.write(text, lines[i]); + node.appendChild(text); + y += size * 1.3; + } + + root.appendChild(node); + } + } + }, + + /** + * Function: begin + * + * Starts a new path. + */ + begin: function() + { + currentNode = create('path'); + currentPath = []; + lastPoint = null; + currentPathIsOrthogonal = true; + }, + + /** + * Function: moveTo + * + * Moves the current path the given coordinates. + */ + moveTo: function(x, y) + { + if (currentPath != null) + { + x += currentState.dx; + y += currentState.dy; + currentPath.push('M ' + f2(x * currentState.scale) + ' ' + f2(y * currentState.scale)); + + if (autoAntiAlias) + { + lastPoint = new mxPoint(x, y); + } + } + }, + + /** + * Function: lineTo + * + * Adds a line to the current path. + */ + lineTo: function(x, y) + { + if (currentPath != null) + { + x += currentState.dx; + y += currentState.dy; + currentPath.push('L ' + f2(x * currentState.scale) + ' ' + f2(y * currentState.scale)); + + if (autoAntiAlias) + { + if (lastPoint != null && currentPathIsOrthogonal && x != lastPoint.x && y != lastPoint.y) + { + currentPathIsOrthogonal = false; + } + + lastPoint = new mxPoint(x, y); + } + } + }, + + /** + * Function: quadTo + * + * Adds a quadratic curve to the current path. + */ + quadTo: function(x1, y1, x2, y2) + { + if (currentPath != null) + { + x1 += currentState.dx; + y1 += currentState.dy; + x2 += currentState.dx; + y2 += currentState.dy; + currentPath.push('Q ' + f2(x1 * currentState.scale) + ' ' + f2(y1 * currentState.scale) + + ' ' + f2(x2 * currentState.scale) + ' ' + f2(y2 * currentState.scale)); + currentPathIsOrthogonal = false; + } + }, + + /** + * Function: curveTo + * + * Adds a bezier curve to the current path. + */ + curveTo: function(x1, y1, x2, y2, x3, y3) + { + if (currentPath != null) + { + x1 += currentState.dx; + y1 += currentState.dy; + x2 += currentState.dx; + y2 += currentState.dy; + x3 += currentState.dx; + y3 += currentState.dy; + currentPath.push('C ' + f2(x1 * currentState.scale) + ' ' + f2(y1 * currentState.scale) + + ' ' + f2(x2 * currentState.scale) + ' ' + f2(y2 * currentState.scale) +' ' + + f2(x3 * currentState.scale) + ' ' + f2(y3 * currentState.scale)); + currentPathIsOrthogonal = false; + } + }, + + /** + * Function: close + * + * Closes the current path. + */ + close: function() + { + if (currentPath != null) + { + currentPath.push('Z'); + } + }, + + /** + * Function: stroke + * + * Paints the outline of the current path. + */ + stroke: function() + { + appendNode(currentNode, currentState, false, true); + }, + + /** + * Function: fill + * + * Fills the current path. + */ + fill: function() + { + appendNode(currentNode, currentState, true, false); + }, + + /** + * Function: fillstroke + * + * Fills and paints the outline of the current path. + */ + fillAndStroke: function() + { + appendNode(currentNode, currentState, true, true); + }, + + /** + * Function: shadow + * + * Paints the current path as a shadow of the given color. + */ + shadow: function(value, filled) + { + this.save(); + this.setStrokeColor(value); + + if (filled) + { + this.setFillColor(value); + this.fillAndStroke(); + } + else + { + this.stroke(); + } + + this.restore(); + }, + + /** + * Function: clip + * + * Uses the current path for clipping. + */ + clip: function() + { + if (currentNode != null) + { + if (currentPath != null) + { + currentNode.setAttribute('d', currentPath.join(' ')); + currentPath = null; + } + + var id = ++refCount; + var clip = create('clipPath'); + clip.setAttribute('id', id); + clip.appendChild(currentNode); + defs.appendChild(clip); + currentState.clip = id; + } + } + }; + +};
\ No newline at end of file diff --git a/src/js/util/mxToolbar.js b/src/js/util/mxToolbar.js new file mode 100644 index 0000000..754e6b3 --- /dev/null +++ b/src/js/util/mxToolbar.js @@ -0,0 +1,528 @@ +/** + * $Id: mxToolbar.js,v 1.36 2012-06-22 11:17:13 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxToolbar + * + * Creates a toolbar inside a given DOM node. The toolbar may contain icons, + * buttons and combo boxes. + * + * Event: mxEvent.SELECT + * + * Fires when an item was selected in the toolbar. The <code>function</code> + * property contains the function that was selected in <selectMode>. + * + * Constructor: mxToolbar + * + * Constructs a toolbar in the specified container. + * + * Parameters: + * + * container - DOM node that contains the toolbar. + */ +function mxToolbar(container) +{ + this.container = container; +}; + +/** + * Extends mxEventSource. + */ +mxToolbar.prototype = new mxEventSource(); +mxToolbar.prototype.constructor = mxToolbar; + +/** + * Variable: container + * + * Reference to the DOM nodes that contains the toolbar. + */ +mxToolbar.prototype.container = null; + +/** + * Variable: enabled + * + * Specifies if events are handled. Default is true. + */ +mxToolbar.prototype.enabled = true; + +/** + * Variable: noReset + * + * Specifies if <resetMode> requires a forced flag of true for resetting + * the current mode in the toolbar. Default is false. This is set to true + * if the toolbar item is double clicked to avoid a reset after a single + * use of the item. + */ +mxToolbar.prototype.noReset = false; + +/** + * Variable: updateDefaultMode + * + * Boolean indicating if the default mode should be the last selected + * switch mode or the first inserted switch mode. Default is true, that + * is the last selected switch mode is the default mode. The default mode + * is the mode to be selected after a reset of the toolbar. If this is + * false, then the default mode is the first inserted mode item regardless + * of what was last selected. Otherwise, the selected item after a reset is + * the previously selected item. + */ +mxToolbar.prototype.updateDefaultMode = true; + +/** + * Function: addItem + * + * Adds the given function as an image with the specified title and icon + * and returns the new image node. + * + * Parameters: + * + * title - Optional string that is used as the tooltip. + * icon - Optional URL of the image to be used. If no URL is given, then a + * button is created. + * funct - Function to execute on a mouse click. + * pressedIcon - Optional URL of the pressed image. Default is a gray + * background. + * style - Optional style classname. Default is mxToolbarItem. + * factoryMethod - Optional factory method for popup menu, eg. + * function(menu, evt, cell) { menu.addItem('Hello, World!'); } + */ +mxToolbar.prototype.addItem = function(title, icon, funct, pressedIcon, style, factoryMethod) +{ + var img = document.createElement((icon != null) ? 'img' : 'button'); + var initialClassName = style || ((factoryMethod != null) ? + 'mxToolbarMode' : 'mxToolbarItem'); + img.className = initialClassName; + img.setAttribute('src', icon); + + if (title != null) + { + if (icon != null) + { + img.setAttribute('title', title); + } + else + { + mxUtils.write(img, title); + } + } + + this.container.appendChild(img); + + // Invokes the function on a click on the toolbar item + if (funct != null) + { + mxEvent.addListener(img, (mxClient.IS_TOUCH) ? 'touchend' : 'click', funct); + } + + var md = (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown'; + var mu = (mxClient.IS_TOUCH) ? 'touchend' : 'mouseup'; + + // Highlights the toolbar item with a gray background + // while it is being clicked with the mouse + mxEvent.addListener(img, md, mxUtils.bind(this, function(evt) + { + if (pressedIcon != null) + { + img.setAttribute('src', pressedIcon); + } + else + { + img.style.backgroundColor = 'gray'; + } + + // Popup Menu + if (factoryMethod != null) + { + if (this.menu == null) + { + this.menu = new mxPopupMenu(); + this.menu.init(); + } + + var last = this.currentImg; + + if (this.menu.isMenuShowing()) + { + this.menu.hideMenu(); + } + + if (last != img) + { + // Redirects factory method to local factory method + this.currentImg = img; + this.menu.factoryMethod = factoryMethod; + + var point = new mxPoint( + img.offsetLeft, + img.offsetTop + img.offsetHeight); + this.menu.popup(point.x, point.y, null, evt); + + // Sets and overrides to restore classname + if (this.menu.isMenuShowing()) + { + img.className = initialClassName + 'Selected'; + + this.menu.hideMenu = function() + { + mxPopupMenu.prototype.hideMenu.apply(this); + img.className = initialClassName; + this.currentImg = null; + }; + } + } + } + })); + + var mouseHandler = mxUtils.bind(this, function(evt) + { + if (pressedIcon != null) + { + img.setAttribute('src', icon); + } + else + { + img.style.backgroundColor = ''; + } + }); + + mxEvent.addListener(img, mu, mouseHandler); + mxEvent.addListener(img, 'mouseout', mouseHandler); + + return img; +}; + +/** + * Function: addCombo + * + * Adds and returns a new SELECT element using the given style. The element + * is placed inside a DIV with the mxToolbarComboContainer style classname. + * + * Parameters: + * + * style - Optional style classname. Default is mxToolbarCombo. + */ +mxToolbar.prototype.addCombo = function(style) +{ + var div = document.createElement('div'); + div.style.display = 'inline'; + div.className = 'mxToolbarComboContainer'; + + var select = document.createElement('select'); + select.className = style || 'mxToolbarCombo'; + div.appendChild(select); + + this.container.appendChild(div); + + return select; +}; + +/** + * Function: addCombo + * + * Adds and returns a new SELECT element using the given title as the + * default element. The selection is reset to this element after each + * change. + * + * Parameters: + * + * title - String that specifies the title of the default element. + * style - Optional style classname. Default is mxToolbarCombo. + */ +mxToolbar.prototype.addActionCombo = function(title, style) +{ + var select = document.createElement('select'); + select.className = style || 'mxToolbarCombo'; + + this.addOption(select, title, null); + + mxEvent.addListener(select, 'change', function(evt) + { + var value = select.options[select.selectedIndex]; + select.selectedIndex = 0; + if (value.funct != null) + { + value.funct(evt); + } + }); + + this.container.appendChild(select); + + return select; +}; + +/** + * Function: addOption + * + * Adds and returns a new OPTION element inside the given SELECT element. + * If the given value is a function then it is stored in the option's funct + * field. + * + * Parameters: + * + * combo - SELECT element that will contain the new entry. + * title - String that specifies the title of the option. + * value - Specifies the value associated with this option. + */ +mxToolbar.prototype.addOption = function(combo, title, value) +{ + var option = document.createElement('option'); + mxUtils.writeln(option, title); + + if (typeof(value) == 'function') + { + option.funct = value; + } + else + { + option.setAttribute('value', value); + } + + combo.appendChild(option); + + return option; +}; + +/** + * Function: addSwitchMode + * + * Adds a new selectable item to the toolbar. Only one switch mode item may + * be selected at a time. The currently selected item is the default item + * after a reset of the toolbar. + */ +mxToolbar.prototype.addSwitchMode = function(title, icon, funct, pressedIcon, style) +{ + var img = document.createElement('img'); + img.initialClassName = style || 'mxToolbarMode'; + img.className = img.initialClassName; + img.setAttribute('src', icon); + img.altIcon = pressedIcon; + + if (title != null) + { + img.setAttribute('title', title); + } + + mxEvent.addListener(img, 'click', mxUtils.bind(this, function(evt) + { + var tmp = this.selectedMode.altIcon; + + if (tmp != null) + { + this.selectedMode.altIcon = this.selectedMode.getAttribute('src'); + this.selectedMode.setAttribute('src', tmp); + } + else + { + this.selectedMode.className = this.selectedMode.initialClassName; + } + + if (this.updateDefaultMode) + { + this.defaultMode = img; + } + + this.selectedMode = img; + + var tmp = img.altIcon; + + if (tmp != null) + { + img.altIcon = img.getAttribute('src'); + img.setAttribute('src', tmp); + } + else + { + img.className = img.initialClassName+'Selected'; + } + + this.fireEvent(new mxEventObject(mxEvent.SELECT)); + funct(); + })); + + this.container.appendChild(img); + + if (this.defaultMode == null) + { + this.defaultMode = img; + + // Function should fire only once so + // do not pass it with the select event + this.selectMode(img); + funct(); + } + + return img; +}; + +/** + * Function: addMode + * + * Adds a new item to the toolbar. The selection is typically reset after + * the item has been consumed, for example by adding a new vertex to the + * graph. The reset is not carried out if the item is double clicked. + * + * The function argument uses the following signature: funct(evt, cell) where + * evt is the native mouse event and cell is the cell under the mouse. + */ +mxToolbar.prototype.addMode = function(title, icon, funct, pressedIcon, style, toggle) +{ + toggle = (toggle != null) ? toggle : true; + var img = document.createElement((icon != null) ? 'img' : 'button'); + + img.initialClassName = style || 'mxToolbarMode'; + img.className = img.initialClassName; + img.setAttribute('src', icon); + img.altIcon = pressedIcon; + + if (title != null) + { + img.setAttribute('title', title); + } + + if (this.enabled && toggle) + { + mxEvent.addListener(img, 'click', mxUtils.bind(this, function(evt) + { + this.selectMode(img, funct); + this.noReset = false; + })); + mxEvent.addListener(img, 'dblclick', + mxUtils.bind(this, function(evt) + { + this.selectMode(img, funct); + this.noReset = true; + }) + ); + + if (this.defaultMode == null) + { + this.defaultMode = img; + this.defaultFunction = funct; + this.selectMode(img, funct); + } + } + + this.container.appendChild(img); + + return img; +}; + +/** + * Function: selectMode + * + * Resets the state of the previously selected mode and displays the given + * DOM node as selected. This function fires a select event with the given + * function as a parameter. + */ +mxToolbar.prototype.selectMode = function(domNode, funct) +{ + if (this.selectedMode != domNode) + { + if (this.selectedMode != null) + { + var tmp = this.selectedMode.altIcon; + + if (tmp != null) + { + this.selectedMode.altIcon = this.selectedMode.getAttribute('src'); + this.selectedMode.setAttribute('src', tmp); + } + else + { + this.selectedMode.className = this.selectedMode.initialClassName; + } + } + + this.selectedMode = domNode; + var tmp = this.selectedMode.altIcon; + + if (tmp != null) + { + this.selectedMode.altIcon = this.selectedMode.getAttribute('src'); + this.selectedMode.setAttribute('src', tmp); + } + else + { + this.selectedMode.className = this.selectedMode.initialClassName+'Selected'; + } + + this.fireEvent(new mxEventObject(mxEvent.SELECT, "function", funct)); + } +}; + +/** + * Function: resetMode + * + * Selects the default mode and resets the state of the previously selected + * mode. + */ +mxToolbar.prototype.resetMode = function(forced) +{ + if ((forced || !this.noReset) && + this.selectedMode != this.defaultMode) + { + // The last selected switch mode will be activated + // so the function was already executed and is + // no longer required here + this.selectMode(this.defaultMode, this.defaultFunction); + } +}; + +/** + * Function: addSeparator + * + * Adds the specifies image as a separator. + * + * Parameters: + * + * icon - URL of the separator icon. + */ +mxToolbar.prototype.addSeparator = function(icon) +{ + return this.addItem(null, icon, null); +}; + +/** + * Function: addBreak + * + * Adds a break to the container. + */ +mxToolbar.prototype.addBreak = function() +{ + mxUtils.br(this.container); +}; + +/** + * Function: addLine + * + * Adds a horizontal line to the container. + */ +mxToolbar.prototype.addLine = function() +{ + var hr = document.createElement('hr'); + + hr.style.marginRight = '6px'; + hr.setAttribute('size', '1'); + + this.container.appendChild(hr); +}; + +/** + * Function: destroy + * + * Removes the toolbar and all its associated resources. + */ +mxToolbar.prototype.destroy = function () +{ + mxEvent.release(this.container); + this.container = null; + this.defaultMode = null; + this.defaultFunction = null; + this.selectedMode = null; + + if (this.menu != null) + { + this.menu.destroy(); + } +}; diff --git a/src/js/util/mxUndoManager.js b/src/js/util/mxUndoManager.js new file mode 100644 index 0000000..2cb93cb --- /dev/null +++ b/src/js/util/mxUndoManager.js @@ -0,0 +1,229 @@ +/** + * $Id: mxUndoManager.js,v 1.30 2011-10-05 06:39:19 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxUndoManager + * + * Implements a command history. When changing the graph model, an + * <mxUndoableChange> object is created at the start of the transaction (when + * model.beginUpdate is called). All atomic changes are then added to this + * object until the last model.endUpdate call, at which point the + * <mxUndoableEdit> is dispatched in an event, and added to the history inside + * <mxUndoManager>. This is done by an event listener in + * <mxEditor.installUndoHandler>. + * + * Each atomic change of the model is represented by an object (eg. + * <mxRootChange>, <mxChildChange>, <mxTerminalChange> etc) which contains the + * complete undo information. The <mxUndoManager> also listens to the + * <mxGraphView> and stores it's changes to the current root as insignificant + * undoable changes, so that drilling (step into, step up) is undone. + * + * This means when you execute an atomic change on the model, then change the + * current root on the view and click undo, the change of the root will be + * undone together with the change of the model so that the display represents + * the state at which the model was changed. However, these changes are not + * transmitted for sharing as they do not represent a state change. + * + * Example: + * + * When adding an undo manager to a graph, make sure to add it + * to the model and the view as well to maintain a consistent + * display across multiple undo/redo steps. + * + * (code) + * var undoManager = new mxUndoManager(); + * var listener = function(sender, evt) + * { + * undoManager.undoableEditHappened(evt.getProperty('edit')); + * }; + * graph.getModel().addListener(mxEvent.UNDO, listener); + * graph.getView().addListener(mxEvent.UNDO, listener); + * (end) + * + * The code creates a function that informs the undoManager + * of an undoable edit and binds it to the undo event of + * <mxGraphModel> and <mxGraphView> using + * <mxEventSource.addListener>. + * + * Event: mxEvent.CLEAR + * + * Fires after <clear> was invoked. This event has no properties. + * + * Event: mxEvent.UNDO + * + * Fires afer a significant edit was undone in <undo>. The <code>edit</code> + * property contains the <mxUndoableEdit> that was undone. + * + * Event: mxEvent.REDO + * + * Fires afer a significant edit was redone in <redo>. The <code>edit</code> + * property contains the <mxUndoableEdit> that was redone. + * + * Event: mxEvent.ADD + * + * Fires after an undoable edit was added to the history. The <code>edit</code> + * property contains the <mxUndoableEdit> that was added. + * + * Constructor: mxUndoManager + * + * Constructs a new undo manager with the given history size. If no history + * size is given, then a default size of 100 steps is used. + */ +function mxUndoManager(size) +{ + this.size = (size != null) ? size : 100; + this.clear(); +}; + +/** + * Extends mxEventSource. + */ +mxUndoManager.prototype = new mxEventSource(); +mxUndoManager.prototype.constructor = mxUndoManager; + +/** + * Variable: size + * + * Maximum command history size. 0 means unlimited history. Default is + * 100. + */ +mxUndoManager.prototype.size = null; + +/** + * Variable: history + * + * Array that contains the steps of the command history. + */ +mxUndoManager.prototype.history = null; + +/** + * Variable: indexOfNextAdd + * + * Index of the element to be added next. + */ +mxUndoManager.prototype.indexOfNextAdd = 0; + +/** + * Function: isEmpty + * + * Returns true if the history is empty. + */ +mxUndoManager.prototype.isEmpty = function() +{ + return this.history.length == 0; +}; + +/** + * Function: clear + * + * Clears the command history. + */ +mxUndoManager.prototype.clear = function() +{ + this.history = []; + this.indexOfNextAdd = 0; + this.fireEvent(new mxEventObject(mxEvent.CLEAR)); +}; + +/** + * Function: canUndo + * + * Returns true if an undo is possible. + */ +mxUndoManager.prototype.canUndo = function() +{ + return this.indexOfNextAdd > 0; +}; + +/** + * Function: undo + * + * Undoes the last change. + */ +mxUndoManager.prototype.undo = function() +{ + while (this.indexOfNextAdd > 0) + { + var edit = this.history[--this.indexOfNextAdd]; + edit.undo(); + + if (edit.isSignificant()) + { + this.fireEvent(new mxEventObject(mxEvent.UNDO, 'edit', edit)); + break; + } + } +}; + +/** + * Function: canRedo + * + * Returns true if a redo is possible. + */ +mxUndoManager.prototype.canRedo = function() +{ + return this.indexOfNextAdd < this.history.length; +}; + +/** + * Function: redo + * + * Redoes the last change. + */ +mxUndoManager.prototype.redo = function() +{ + var n = this.history.length; + + while (this.indexOfNextAdd < n) + { + var edit = this.history[this.indexOfNextAdd++]; + edit.redo(); + + if (edit.isSignificant()) + { + this.fireEvent(new mxEventObject(mxEvent.REDO, 'edit', edit)); + break; + } + } +}; + +/** + * Function: undoableEditHappened + * + * Method to be called to add new undoable edits to the <history>. + */ +mxUndoManager.prototype.undoableEditHappened = function(undoableEdit) +{ + this.trim(); + + if (this.size > 0 && + this.size == this.history.length) + { + this.history.shift(); + } + + this.history.push(undoableEdit); + this.indexOfNextAdd = this.history.length; + this.fireEvent(new mxEventObject(mxEvent.ADD, 'edit', undoableEdit)); +}; + +/** + * Function: trim + * + * Removes all pending steps after <indexOfNextAdd> from the history, + * invoking die on each edit. This is called from <undoableEditHappened>. + */ +mxUndoManager.prototype.trim = function() +{ + if (this.history.length > this.indexOfNextAdd) + { + var edits = this.history.splice(this.indexOfNextAdd, + this.history.length - this.indexOfNextAdd); + + for (var i = 0; i < edits.length; i++) + { + edits[i].die(); + } + } +}; diff --git a/src/js/util/mxUndoableEdit.js b/src/js/util/mxUndoableEdit.js new file mode 100644 index 0000000..886c262 --- /dev/null +++ b/src/js/util/mxUndoableEdit.js @@ -0,0 +1,168 @@ +/** + * $Id: mxUndoableEdit.js,v 1.14 2010-09-15 16:58:51 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxUndoableEdit + * + * Implements a composite undoable edit. + * + * Constructor: mxUndoableEdit + * + * Constructs a new undoable edit for the given source. + */ +function mxUndoableEdit(source, significant) +{ + this.source = source; + this.changes = []; + this.significant = (significant != null) ? significant : true; +}; + +/** + * Variable: source + * + * Specifies the source of the edit. + */ +mxUndoableEdit.prototype.source = null; + +/** + * Variable: changes + * + * Array that contains the changes that make up this edit. The changes are + * expected to either have an undo and redo function, or an execute + * function. Default is an empty array. + */ +mxUndoableEdit.prototype.changes = null; + +/** + * Variable: significant + * + * Specifies if the undoable change is significant. + * Default is true. + */ +mxUndoableEdit.prototype.significant = null; + +/** + * Variable: undone + * + * Specifies if this edit has been undone. Default is false. + */ +mxUndoableEdit.prototype.undone = false; + +/** + * Variable: redone + * + * Specifies if this edit has been redone. Default is false. + */ +mxUndoableEdit.prototype.redone = false; + +/** + * Function: isEmpty + * + * Returns true if the this edit contains no changes. + */ +mxUndoableEdit.prototype.isEmpty = function() +{ + return this.changes.length == 0; +}; + +/** + * Function: isSignificant + * + * Returns <significant>. + */ +mxUndoableEdit.prototype.isSignificant = function() +{ + return this.significant; +}; + +/** + * Function: add + * + * Adds the specified change to this edit. The change is an object that is + * expected to either have an undo and redo, or an execute function. + */ +mxUndoableEdit.prototype.add = function(change) +{ + this.changes.push(change); +}; + +/** + * Function: notify + * + * Hook to notify any listeners of the changes after an <undo> or <redo> + * has been carried out. This implementation is empty. + */ +mxUndoableEdit.prototype.notify = function() { }; + +/** + * Function: die + * + * Hook to free resources after the edit has been removed from the command + * history. This implementation is empty. + */ +mxUndoableEdit.prototype.die = function() { }; + +/** + * Function: undo + * + * Undoes all changes in this edit. + */ +mxUndoableEdit.prototype.undo = function() +{ + if (!this.undone) + { + var count = this.changes.length; + + for (var i = count - 1; i >= 0; i--) + { + var change = this.changes[i]; + + if (change.execute != null) + { + change.execute(); + } + else if (change.undo != null) + { + change.undo(); + } + } + + this.undone = true; + this.redone = false; + } + + this.notify(); +}; + +/** + * Function: redo + * + * Redoes all changes in this edit. + */ +mxUndoableEdit.prototype.redo = function() +{ + if (!this.redone) + { + var count = this.changes.length; + + for (var i = 0; i < count; i++) + { + var change = this.changes[i]; + + if (change.execute != null) + { + change.execute(); + } + else if (change.redo != null) + { + change.redo(); + } + } + + this.undone = false; + this.redone = true; + } + + this.notify(); +}; diff --git a/src/js/util/mxUrlConverter.js b/src/js/util/mxUrlConverter.js new file mode 100644 index 0000000..764767f --- /dev/null +++ b/src/js/util/mxUrlConverter.js @@ -0,0 +1,141 @@ +/** + * $Id: mxUrlConverter.js,v 1.3 2012-08-24 17:10:41 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * + * Class: mxUrlConverter + * + * Converts relative and absolute URLs to absolute URLs with protocol and domain. + */ +var mxUrlConverter = function(root) +{ + /** + * Variable: enabled + * + * Specifies if the converter is enabled. Default is true. + */ + var enabled = true; + + /** + * Variable: baseUrl + * + * Specifies the base URL to be used as a prefix for relative URLs. + */ + var baseUrl = null; + + /** + * Variable: baseDomain + * + * Specifies the base domain to be used as a prefix for absolute URLs. + */ + var baseDomain = null; + + // Private helper function to update the base URL + var updateBaseUrl = function() + { + baseDomain = location.protocol + '//' + location.host; + baseUrl = baseDomain + location.pathname; + var tmp = baseUrl.lastIndexOf('/'); + + // Strips filename etc + if (tmp > 0) + { + baseUrl = baseUrl.substring(0, tmp + 1); + } + }; + + // Returns public interface + return { + + /** + * Function: isEnabled + * + * Returns <enabled>. + */ + isEnabled: function() + { + return enabled; + }, + + /** + * Function: setEnabled + * + * Sets <enabled>. + */ + setEnabled: function(value) + { + enabled = value; + }, + + /** + * Function: getBaseUrl + * + * Returns <baseUrl>. + */ + getBaseUrl: function() + { + return baseUrl; + }, + + /** + * Function: setBaseUrl + * + * Sets <baseUrl>. + */ + setBaseUrl: function(value) + { + baseUrl = value; + }, + + /** + * Function: getBaseDomain + * + * Returns <baseDomain>. + */ + getBaseDomain: function() + { + return baseUrl; + }, + + /** + * Function: setBaseDomain + * + * Sets <baseDomain>. + */ + setBaseDomain: function(value) + { + baseUrl = value; + }, + + /** + * Function: convert + * + * Converts the given URL to an absolute URL with protol and domain. + * Relative URLs are first converted to absolute URLs. + */ + convert: function(url) + { + if (enabled && url.indexOf('http://') != 0 && url.indexOf('https://') != 0 && url.indexOf('data:image') != 0) + { + if (baseUrl == null) + { + updateBaseUrl(); + } + + if (url.charAt(0) == '/') + { + url = baseDomain + url; + } + else + { + url = baseUrl + url; + } + } + + return url; + } + + }; + +};
\ No newline at end of file diff --git a/src/js/util/mxUtils.js b/src/js/util/mxUtils.js new file mode 100644 index 0000000..34c0318 --- /dev/null +++ b/src/js/util/mxUtils.js @@ -0,0 +1,3920 @@ +/** + * $Id: mxUtils.js,v 1.297 2012-12-07 19:47:29 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +var mxUtils = +{ + /** + * Class: mxUtils + * + * A singleton class that provides cross-browser helper methods. + * This is a global functionality. To access the functions in this + * class, use the global classname appended by the functionname. + * You may have to load chrome://global/content/contentAreaUtils.js + * to disable certain security restrictions in Mozilla for the <open>, + * <save>, <saveAs> and <copy> function. + * + * For example, the following code displays an error message: + * + * (code) + * mxUtils.error('Browser is not supported!', 200, false); + * (end) + * + * Variable: errorResource + * + * Specifies the resource key for the title of the error window. If the + * resource for this key does not exist then the value is used as + * the title. Default is 'error'. + */ + errorResource: (mxClient.language != 'none') ? 'error' : '', + + /** + * Variable: closeResource + * + * Specifies the resource key for the label of the close button. If the + * resource for this key does not exist then the value is used as + * the label. Default is 'close'. + */ + closeResource: (mxClient.language != 'none') ? 'close' : '', + + /** + * Variable: errorImage + * + * Defines the image used for error dialogs. + */ + errorImage: mxClient.imageBasePath + '/error.gif', + + /** + * Function: removeCursors + * + * Removes the cursors from the style of the given DOM node and its + * descendants. + * + * Parameters: + * + * element - DOM node to remove the cursor style from. + */ + removeCursors: function(element) + { + if (element.style != null) + { + element.style.cursor = ''; + } + + var children = element.childNodes; + + if (children != null) + { + var childCount = children.length; + + for (var i = 0; i < childCount; i += 1) + { + mxUtils.removeCursors(children[i]); + } + } + }, + + /** + * Function: repaintGraph + * + * Normally not required, this contains the code to workaround a repaint + * issue and force a repaint of the graph container in AppleWebKit. + * + * Parameters: + * + * graph - <mxGraph> to be repainted. + * pt - <mxPoint> where the dummy element should be placed. + */ + repaintGraph: function(graph, pt) + { + if (mxClient.IS_GC || mxClient.IS_SF || mxClient.IS_OP) + { + var c = graph.container; + + if (c != null && pt != null && (c.scrollLeft > 0 || c.scrollTop > 0)) + { + var dummy = document.createElement('div'); + dummy.style.position = 'absolute'; + dummy.style.left = pt.x + 'px'; + dummy.style.top = pt.y + 'px'; + dummy.style.width = '1px'; + dummy.style.height = '1px'; + + c.appendChild(dummy); + c.removeChild(dummy); + } + } + }, + + /** + * Function: getCurrentStyle + * + * Returns the current style of the specified element. + * + * Parameters: + * + * element - DOM node whose current style should be returned. + */ + getCurrentStyle: function() + { + if (mxClient.IS_IE) + { + return function(element) + { + return (element != null) ? element.currentStyle : null; + }; + } + else + { + return function(element) + { + return (element != null) ? + window.getComputedStyle(element, '') : + null; + }; + } + }(), + + /** + * Function: hasScrollbars + * + * Returns true if the overflow CSS property of the given node is either + * scroll or auto. + * + * Parameters: + * + * node - DOM node whose style should be checked for scrollbars. + */ + hasScrollbars: function(node) + { + var style = mxUtils.getCurrentStyle(node); + + return style != null && (style.overflow == 'scroll' || style.overflow == 'auto'); + }, + + /** + * Function: bind + * + * Returns a wrapper function that locks the execution scope of the given + * function to the specified scope. Inside funct, the "this" keyword + * becomes a reference to that scope. + */ + bind: function(scope, funct) + { + return function() + { + return funct.apply(scope, arguments); + }; + }, + + /** + * Function: eval + * + * Evaluates the given expression using eval and returns the JavaScript + * object that represents the expression result. Supports evaluation of + * expressions that define functions and returns the function object for + * these expressions. + * + * Parameters: + * + * expr - A string that represents a JavaScript expression. + */ + eval: function(expr) + { + var result = null; + + if (expr.indexOf('function') >= 0) + { + try + { + eval('var _mxJavaScriptExpression='+expr); + result = _mxJavaScriptExpression; + // TODO: Use delete here? + _mxJavaScriptExpression = null; + } + catch (e) + { + mxLog.warn(e.message + ' while evaluating ' + expr); + } + } + else + { + try + { + result = eval(expr); + } + catch (e) + { + mxLog.warn(e.message + ' while evaluating ' + expr); + } + } + + return result; + }, + + /** + * Function: findNode + * + * Returns the first node where attr equals value. + * This implementation does not use XPath. + */ + findNode: function(node, attr, value) + { + var tmp = node.getAttribute(attr); + + if (tmp != null && tmp == value) + { + return node; + } + + node = node.firstChild; + + while (node != null) + { + var result = mxUtils.findNode(node, attr, value); + + if (result != null) + { + return result; + } + + node = node.nextSibling; + } + + return null; + }, + + /** + * Function: findNodeByAttribute + * + * Returns the first node where the given attribute matches the given value. + * + * Parameters: + * + * node - Root node where the search should start. + * attr - Name of the attribute to be checked. + * value - Value of the attribute to match. + */ + findNodeByAttribute: function() + { + // Workaround for missing XPath support in IE9 + if (document.documentMode >= 9) + { + return function(node, attr, value) + { + var result = null; + + if (node != null) + { + if (node.nodeType == mxConstants.NODETYPE_ELEMENT && node.getAttribute(attr) == value) + { + result = node; + } + else + { + var child = node.firstChild; + + while (child != null && result == null) + { + result = mxUtils.findNodeByAttribute(child, attr, value); + child = child.nextSibling; + } + } + } + + return result; + }; + } + else if (mxClient.IS_IE) + { + return function(node, attr, value) + { + if (node == null) + { + return null; + } + else + { + var expr = '//*[@' + attr + '=\'' + value + '\']'; + + return node.ownerDocument.selectSingleNode(expr); + } + }; + } + else + { + return function(node, attr, value) + { + if (node == null) + { + return null; + } + else + { + var result = node.ownerDocument.evaluate( + '//*[@' + attr + '=\'' + value + '\']', + node.ownerDocument, null, + XPathResult.ANY_TYPE, null); + + return result.iterateNext(); + } + }; + } + }(), + + /** + * Function: getFunctionName + * + * Returns the name for the given function. + * + * Parameters: + * + * f - JavaScript object that represents a function. + */ + getFunctionName: function(f) + { + var str = null; + + if (f != null) + { + if (f.name != null) + { + str = f.name; + } + else + { + var tmp = f.toString(); + var idx1 = 9; + + while (tmp.charAt(idx1) == ' ') + { + idx1++; + } + + var idx2 = tmp.indexOf('(', idx1); + str = tmp.substring(idx1, idx2); + } + } + + return str; + }, + + /** + * Function: indexOf + * + * Returns the index of obj in array or -1 if the array does not contains + * the given object. + * + * Parameters: + * + * array - Array to check for the given obj. + * obj - Object to find in the given array. + */ + indexOf: function(array, obj) + { + if (array != null && obj != null) + { + for (var i = 0; i < array.length; i++) + { + if (array[i] == obj) + { + return i; + } + } + } + + return -1; + }, + + /** + * Function: remove + * + * Removes all occurrences of the given object in the given array or + * object. If there are multiple occurrences of the object, be they + * associative or as an array entry, all occurrences are removed from + * the array or deleted from the object. By removing the object from + * the array, all elements following the removed element are shifted + * by one step towards the beginning of the array. + * + * The length of arrays is not modified inside this function. + * + * Parameters: + * + * obj - Object to find in the given array. + * array - Array to check for the given obj. + */ + remove: function(obj, array) + { + var result = null; + + if (typeof(array) == 'object') + { + var index = mxUtils.indexOf(array, obj); + + while (index >= 0) + { + array.splice(index, 1); + result = obj; + index = mxUtils.indexOf(array, obj); + } + } + + for (var key in array) + { + if (array[key] == obj) + { + delete array[key]; + result = obj; + } + } + + return result; + }, + + /** + * Function: isNode + * + * Returns true if the given value is an XML node with the node name + * and if the optional attribute has the specified value. + * + * This implementation assumes that the given value is a DOM node if the + * nodeType property is numeric, that is, if isNaN returns false for + * value.nodeType. + * + * Parameters: + * + * value - Object that should be examined as a node. + * nodeName - String that specifies the node name. + * attributeName - Optional attribute name to check. + * attributeValue - Optional attribute value to check. + */ + isNode: function(value, nodeName, attributeName, attributeValue) + { + if (value != null && !isNaN(value.nodeType) && (nodeName == null || + value.nodeName.toLowerCase() == nodeName.toLowerCase())) + { + return attributeName == null || + value.getAttribute(attributeName) == attributeValue; + } + + return false; + }, + + /** + * Function: getChildNodes + * + * Returns an array of child nodes that are of the given node type. + * + * Parameters: + * + * node - Parent DOM node to return the children from. + * nodeType - Optional node type to return. Default is + * <mxConstants.NODETYPE_ELEMENT>. + */ + getChildNodes: function(node, nodeType) + { + nodeType = nodeType || mxConstants.NODETYPE_ELEMENT; + + var children = []; + var tmp = node.firstChild; + + while (tmp != null) + { + if (tmp.nodeType == nodeType) + { + children.push(tmp); + } + + tmp = tmp.nextSibling; + } + + return children; + }, + + /** + * Function: createXmlDocument + * + * Returns a new, empty XML document. + */ + createXmlDocument: function() + { + var doc = null; + + if (document.implementation && document.implementation.createDocument) + { + doc = document.implementation.createDocument('', '', null); + } + else if (window.ActiveXObject) + { + doc = new ActiveXObject('Microsoft.XMLDOM'); + } + + return doc; + }, + + /** + * Function: parseXml + * + * Parses the specified XML string into a new XML document and returns the + * new document. + * + * Example: + * + * (code) + * var doc = mxUtils.parseXml( + * '<mxGraphModel><root><MyDiagram id="0"><mxCell/></MyDiagram>'+ + * '<MyLayer id="1"><mxCell parent="0" /></MyLayer><MyObject id="2">'+ + * '<mxCell style="strokeColor=blue;fillColor=red" parent="1" vertex="1">'+ + * '<mxGeometry x="10" y="10" width="80" height="30" as="geometry"/>'+ + * '</mxCell></MyObject></root></mxGraphModel>'); + * (end) + * + * Parameters: + * + * xml - String that contains the XML data. + */ + parseXml: function() + { + if (mxClient.IS_IE && (typeof(document.documentMode) === 'undefined' || document.documentMode < 9)) + { + return function(xml) + { + var result = mxUtils.createXmlDocument(); + + result.async = 'false'; + result.loadXML(xml); + + return result; + }; + } + else + { + return function(xml) + { + var parser = new DOMParser(); + + return parser.parseFromString(xml, 'text/xml'); + }; + } + }(), + + /** + * Function: clearSelection + * + * Clears the current selection in the page. + */ + clearSelection: function() + { + if (document.selection) + { + return function() + { + document.selection.empty(); + }; + } + else if (window.getSelection) + { + return function() + { + window.getSelection().removeAllRanges(); + }; + } + }(), + + /** + * Function: getPrettyXML + * + * Returns a pretty printed string that represents the XML tree for the + * given node. This method should only be used to print XML for reading, + * use <getXml> instead to obtain a string for processing. + * + * Parameters: + * + * node - DOM node to return the XML for. + * tab - Optional string that specifies the indentation for one level. + * Default is two spaces. + * indent - Optional string that represents the current indentation. + * Default is an empty string. + */ + getPrettyXml: function(node, tab, indent) + { + var result = []; + + if (node != null) + { + tab = tab || ' '; + indent = indent || ''; + + if (node.nodeType == mxConstants.NODETYPE_TEXT) + { + result.push(node.nodeValue); + } + else + { + result.push(indent + '<'+node.nodeName); + + // Creates the string with the node attributes + // and converts all HTML entities in the values + var attrs = node.attributes; + + if (attrs != null) + { + for (var i = 0; i < attrs.length; i++) + { + var val = mxUtils.htmlEntities(attrs[i].nodeValue); + result.push(' ' + attrs[i].nodeName + + '="' + val + '"'); + } + } + + // Recursively creates the XML string for each + // child nodes and appends it here with an + // indentation + var tmp = node.firstChild; + + if (tmp != null) + { + result.push('>\n'); + + while (tmp != null) + { + result.push(mxUtils.getPrettyXml( + tmp, tab, indent + tab)); + tmp = tmp.nextSibling; + } + + result.push(indent + '</'+node.nodeName+'>\n'); + } + else + { + result.push('/>\n'); + } + } + } + + return result.join(''); + }, + + /** + * Function: removeWhitespace + * + * Removes the sibling text nodes for the given node that only consists + * of tabs, newlines and spaces. + * + * Parameters: + * + * node - DOM node whose siblings should be removed. + * before - Optional boolean that specifies the direction of the traversal. + */ + removeWhitespace: function(node, before) + { + var tmp = (before) ? node.previousSibling : node.nextSibling; + + while (tmp != null && tmp.nodeType == mxConstants.NODETYPE_TEXT) + { + var next = (before) ? tmp.previousSibling : tmp.nextSibling; + var text = mxUtils.getTextContent(tmp); + + if (mxUtils.trim(text).length == 0) + { + tmp.parentNode.removeChild(tmp); + } + + tmp = next; + } + }, + + /** + * Function: htmlEntities + * + * Replaces characters (less than, greater than, newlines and quotes) with + * their HTML entities in the given string and returns the result. + * + * Parameters: + * + * s - String that contains the characters to be converted. + * newline - If newlines should be replaced. Default is true. + */ + htmlEntities: function(s, newline) + { + s = s || ''; + + s = s.replace(/&/g,'&'); // 38 26 + s = s.replace(/"/g,'"'); // 34 22 + s = s.replace(/\'/g,'''); // 39 27 + s = s.replace(/</g,'<'); // 60 3C + s = s.replace(/>/g,'>'); // 62 3E + + if (newline == null || newline) + { + s = s.replace(/\n/g, '
'); + } + + return s; + }, + + /** + * Function: isVml + * + * Returns true if the given node is in the VML namespace. + * + * Parameters: + * + * node - DOM node whose tag urn should be checked. + */ + isVml: function(node) + { + return node != null && node.tagUrn == 'urn:schemas-microsoft-com:vml'; + }, + + /** + * Function: getXml + * + * Returns the XML content of the specified node. For Internet Explorer, + * all \r\n\t[\t]* are removed from the XML string and the remaining \r\n + * are replaced by \n. All \n are then replaced with linefeed, or 
 if + * no linefeed is defined. + * + * Parameters: + * + * node - DOM node to return the XML for. + * linefeed - Optional string that linefeeds are converted into. Default is + * 
 + */ + getXml: function(node, linefeed) + { + var xml = ''; + + if (node != null) + { + xml = node.xml; + + if (xml == null) + { + if (node.innerHTML) + { + xml = node.innerHTML; + } + else + { + var xmlSerializer = new XMLSerializer(); + xml = xmlSerializer.serializeToString(node); + } + } + else + { + xml = xml.replace(/\r\n\t[\t]*/g, ''). + replace(/>\r\n/g, '>'). + replace(/\r\n/g, '\n'); + } + } + + // Replaces linefeeds with HTML Entities. + linefeed = linefeed || '
'; + xml = xml.replace(/\n/g, linefeed); + + return xml; + }, + + /** + * Function: getTextContent + * + * Returns the text content of the specified node. + * + * Parameters: + * + * node - DOM node to return the text content for. + */ + getTextContent: function(node) + { + var result = ''; + + if (node != null) + { + if (node.firstChild != null) + { + node = node.firstChild; + } + + result = node.nodeValue || ''; + } + + return result; + }, + + /** + * Function: getInnerHtml + * + * Returns the inner HTML for the given node as a string or an empty string + * if no node was specified. The inner HTML is the text representing all + * children of the node, but not the node itself. + * + * Parameters: + * + * node - DOM node to return the inner HTML for. + */ + getInnerHtml: function() + { + if (mxClient.IS_IE) + { + return function(node) + { + if (node != null) + { + return node.innerHTML; + } + + return ''; + }; + } + else + { + return function(node) + { + if (node != null) + { + var serializer = new XMLSerializer(); + return serializer.serializeToString(node); + } + + return ''; + }; + } + }(), + + /** + * Function: getOuterHtml + * + * Returns the outer HTML for the given node as a string or an empty + * string if no node was specified. The outer HTML is the text representing + * all children of the node including the node itself. + * + * Parameters: + * + * node - DOM node to return the outer HTML for. + */ + getOuterHtml: function() + { + if (mxClient.IS_IE) + { + return function(node) + { + if (node != null) + { + if (node.outerHTML != null) + { + return node.outerHTML; + } + else + { + var tmp = []; + tmp.push('<'+node.nodeName); + + var attrs = node.attributes; + + if (attrs != null) + { + for (var i = 0; i < attrs.length; i++) + { + var value = attrs[i].nodeValue; + + if (value != null && value.length > 0) + { + tmp.push(' '); + tmp.push(attrs[i].nodeName); + tmp.push('="'); + tmp.push(value); + tmp.push('"'); + } + } + } + + if (node.innerHTML.length == 0) + { + tmp.push('/>'); + } + else + { + tmp.push('>'); + tmp.push(node.innerHTML); + tmp.push('</'+node.nodeName+'>'); + } + + return tmp.join(''); + } + } + + return ''; + }; + } + else + { + return function(node) + { + if (node != null) + { + var serializer = new XMLSerializer(); + return serializer.serializeToString(node); + } + + return ''; + }; + } + }(), + + /** + * Function: write + * + * Creates a text node for the given string and appends it to the given + * parent. Returns the text node. + * + * Parameters: + * + * parent - DOM node to append the text node to. + * text - String representing the text to be added. + */ + write: function(parent, text) + { + var doc = parent.ownerDocument; + var node = doc.createTextNode(text); + + if (parent != null) + { + parent.appendChild(node); + } + + return node; + }, + + /** + * Function: writeln + * + * Creates a text node for the given string and appends it to the given + * parent with an additional linefeed. Returns the text node. + * + * Parameters: + * + * parent - DOM node to append the text node to. + * text - String representing the text to be added. + */ + writeln: function(parent, text) + { + var doc = parent.ownerDocument; + var node = doc.createTextNode(text); + + if (parent != null) + { + parent.appendChild(node); + parent.appendChild(document.createElement('br')); + } + + return node; + }, + + /** + * Function: br + * + * Appends a linebreak to the given parent and returns the linebreak. + * + * Parameters: + * + * parent - DOM node to append the linebreak to. + */ + br: function(parent, count) + { + count = count || 1; + var br = null; + + for (var i = 0; i < count; i++) + { + if (parent != null) + { + br = parent.ownerDocument.createElement('br'); + parent.appendChild(br); + } + } + + return br; + }, + + /** + * Function: button + * + * Returns a new button with the given level and function as an onclick + * event handler. + * + * (code) + * document.body.appendChild(mxUtils.button('Test', function(evt) + * { + * alert('Hello, World!'); + * })); + * (end) + * + * Parameters: + * + * label - String that represents the label of the button. + * funct - Function to be called if the button is pressed. + * doc - Optional document to be used for creating the button. Default is the + * current document. + */ + button: function(label, funct, doc) + { + doc = (doc != null) ? doc : document; + + var button = doc.createElement('button'); + mxUtils.write(button, label); + + mxEvent.addListener(button, 'click', function(evt) + { + funct(evt); + }); + + return button; + }, + + /** + * Function: para + * + * Appends a new paragraph with the given text to the specified parent and + * returns the paragraph. + * + * Parameters: + * + * parent - DOM node to append the text node to. + * text - String representing the text for the new paragraph. + */ + para: function(parent, text) + { + var p = document.createElement('p'); + mxUtils.write(p, text); + + if (parent != null) + { + parent.appendChild(p); + } + + return p; + }, + + /** + * Function: linkAction + * + * Adds a hyperlink to the specified parent that invokes action on the + * specified editor. + * + * Parameters: + * + * parent - DOM node to contain the new link. + * text - String that is used as the link label. + * editor - <mxEditor> that will execute the action. + * action - String that defines the name of the action to be executed. + * pad - Optional left-padding for the link. Default is 0. + */ + linkAction: function(parent, text, editor, action, pad) + { + return mxUtils.link(parent, text, function() + { + editor.execute(action); + }, pad); + }, + + /** + * Function: linkInvoke + * + * Adds a hyperlink to the specified parent that invokes the specified + * function on the editor passing along the specified argument. The + * function name is the name of a function of the editor instance, + * not an action name. + * + * Parameters: + * + * parent - DOM node to contain the new link. + * text - String that is used as the link label. + * editor - <mxEditor> instance to execute the function on. + * functName - String that represents the name of the function. + * arg - Object that represents the argument to the function. + * pad - Optional left-padding for the link. Default is 0. + */ + linkInvoke: function(parent, text, editor, functName, arg, pad) + { + return mxUtils.link(parent, text, function() + { + editor[functName](arg); + }, pad); + }, + + /** + * Function: link + * + * Adds a hyperlink to the specified parent and invokes the given function + * when the link is clicked. + * + * Parameters: + * + * parent - DOM node to contain the new link. + * text - String that is used as the link label. + * funct - Function to execute when the link is clicked. + * pad - Optional left-padding for the link. Default is 0. + */ + link: function(parent, text, funct, pad) + { + var a = document.createElement('span'); + + a.style.color = 'blue'; + a.style.textDecoration = 'underline'; + a.style.cursor = 'pointer'; + + if (pad != null) + { + a.style.paddingLeft = pad+'px'; + } + + mxEvent.addListener(a, 'click', funct); + mxUtils.write(a, text); + + if (parent != null) + { + parent.appendChild(a); + } + + return a; + }, + + /** + * Function: fit + * + * Makes sure the given node is inside the visible area of the window. This + * is done by setting the left and top in the style. + */ + fit: function(node) + { + var left = parseInt(node.offsetLeft); + var width = parseInt(node.offsetWidth); + + var b = document.body; + var d = document.documentElement; + + var right = (b.scrollLeft || d.scrollLeft) + + (b.clientWidth || d.clientWidth); + + if (left + width > right) + { + node.style.left = Math.max((b.scrollLeft || d.scrollLeft), + right - width)+'px'; + } + + var top = parseInt(node.offsetTop); + var height = parseInt(node.offsetHeight); + + var bottom = (b.scrollTop || d.scrollTop) + + Math.max(b.clientHeight || 0, d.clientHeight); + + if (top + height > bottom) + { + node.style.top = Math.max((b.scrollTop || d.scrollTop), + bottom - height)+'px'; + } + }, + + /** + * Function: open + * + * Opens the specified file from the local filesystem and returns the + * contents of the file as a string. This implementation requires an + * ActiveX object in IE and special privileges in Firefox. Relative + * filenames are only supported in IE and will go onto the users' + * Desktop. You may have to load + * chrome://global/content/contentAreaUtils.js to disable certain + * security restrictions in Mozilla for this to work. + * + * See known-issues before using this function. + * + * Example: + * (code) + * var data = mxUtils.open('C:\\temp\\test.txt'); + * mxUtils.alert('Data: '+data); + * (end) + * + * Parameters: + * + * filename - String representing the local file name. + */ + open: function(filename) + { + // Requests required privileges in Firefox + if (mxClient.IS_NS) + { + try + { + netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect'); + } + catch (e) + { + mxUtils.alert('Permission to read file denied.'); + + return ''; + } + + var file = Components.classes['@mozilla.org/file/local;1'].createInstance(Components.interfaces.nsILocalFile); + file.initWithPath(filename); + + if (!file.exists()) + { + mxUtils.alert('File not found.'); + return ''; + } + + var is = Components.classes['@mozilla.org/network/file-input-stream;1'].createInstance(Components.interfaces.nsIFileInputStream); + is.init(file,0x01, 00004, null); + + var sis = Components.classes['@mozilla.org/scriptableinputstream;1'].createInstance(Components.interfaces.nsIScriptableInputStream); + sis.init(is); + + var output = sis.read(sis.available()); + + return output; + } + else + { + var activeXObject = new ActiveXObject('Scripting.FileSystemObject'); + + var newStream = activeXObject.OpenTextFile(filename, 1); + var text = newStream.readAll(); + newStream.close(); + + return text; + } + }, + + /** + * Function: save + * + * Saves the specified content in the given file on the local file system. + * This implementation requires an ActiveX object in IE and special + * privileges in Firefox. Relative filenames are only supported in IE and + * will be loaded from the users' Desktop. You may have to load + * chrome://global/content/contentAreaUtils.js to disable certain + * security restrictions in Mozilla for this to work. + * + * See known-issues before using this function. + * + * Example: + * + * (code) + * var data = 'Hello, World!'; + * mxUtils.save('C:\\test.txt', data); + * (end) + * + * Parameters: + * + * filename - String representing the local file name. + */ + save: function(filename, content) + { + if (mxClient.IS_NS) + { + try + { + netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect'); + } + catch (e) + { + mxUtils.alert('Permission to write file denied.'); + return; + } + + var file = Components.classes['@mozilla.org/file/local;1'].createInstance(Components.interfaces.nsILocalFile); + file.initWithPath(filename); + + if (!file.exists()) + { + file.create(0x00, 0644); + } + + var outputStream = Components.classes['@mozilla.org/network/file-output-stream;1'].createInstance(Components.interfaces.nsIFileOutputStream); + + outputStream.init(file, 0x20 | 0x02,00004, null); + outputStream.write(content, content.length); + outputStream.flush(); + outputStream.close(); + } + else + { + var fso = new ActiveXObject('Scripting.FileSystemObject'); + + var file = fso.CreateTextFile(filename, true); + file.Write(content); + file.Close(); + } + }, + + /** + * Function: saveAs + * + * Saves the specified content by displaying a dialog to save the content + * as a file on the local filesystem. This implementation does not use an + * ActiveX object in IE, however, it does require special privileges in + * Firefox. You may have to load + * chrome://global/content/contentAreaUtils.js to disable certain + * security restrictions in Mozilla for this to work. + * + * See known-issues before using this function. It is not recommended using + * this function in production environment as access to the filesystem + * cannot be guaranteed in Firefox. The following code is used in + * Firefox to try and enable saving to the filesystem. + * + * (code) + * netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect'); + * (end) + * + * Example: + * + * (code) + * mxUtils.saveAs('Hello, World!'); + * (end) + * + * Parameters: + * + * content - String representing the file's content. + */ + saveAs: function(content) + { + var iframe = document.createElement('iframe'); + iframe.setAttribute('src', ''); + iframe.style.visibility = 'hidden'; + document.body.appendChild(iframe); + + try + { + if (mxClient.IS_NS) + { + var doc = iframe.contentDocument; + + doc.open(); + doc.write(content); + doc.close(); + + try + { + netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect'); + // LATER: Remove existing HTML markup in file + iframe.focus(); + saveDocument(doc); + } + catch (e) + { + mxUtils.alert('Permission to save document denied.'); + } + } + else + { + var doc = iframe.contentWindow.document; + doc.write(content); + doc.execCommand('SaveAs', false, document.location); + } + } + finally + { + document.body.removeChild(iframe); + } + }, + + /** + * Function: copy + * + * Copies the specified content to the local clipboard. This implementation + * requires special privileges in Firefox. You may have to load + * chrome://global/content/contentAreaUtils.js to disable certain + * security restrictions in Mozilla for this to work. + * + * Parameters: + * + * content - String to be copied to the clipboard. + */ + copy: function(content) + { + if (window.clipboardData) + { + window.clipboardData.setData('Text', content); + } + else + { + netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect'); + + var clip = Components.classes['@mozilla.org/widget/clipboard;1'] + .createInstance(Components.interfaces.nsIClipboard); + + if (!clip) + { + return; + } + + var trans = Components.classes['@mozilla.org/widget/transferable;1'] + .createInstance(Components.interfaces.nsITransferable); + + if (!trans) + { + return; + } + + trans.addDataFlavor('text/unicode'); + var str = Components.classes['@mozilla.org/supports-string;1'] + .createInstance(Components.interfaces.nsISupportsString); + + var copytext=content; + str.data=copytext; + trans.setTransferData('text/unicode', str, copytext.length*2); + var clipid=Components.interfaces.nsIClipboard; + + clip.setData(trans,null,clipid.kGlobalClipboard); + } + }, + + /** + * Function: load + * + * Loads the specified URL *synchronously* and returns the <mxXmlRequest>. + * Throws an exception if the file cannot be loaded. See <mxUtils.get> for + * an asynchronous implementation. + * + * Example: + * + * (code) + * try + * { + * var req = mxUtils.load(filename); + * var root = req.getDocumentElement(); + * // Process XML DOM... + * } + * catch (ex) + * { + * mxUtils.alert('Cannot load '+filename+': '+ex); + * } + * (end) + * + * Parameters: + * + * url - URL to get the data from. + */ + load: function(url) + { + var req = new mxXmlRequest(url, null, 'GET', false); + req.send(); + + return req; + }, + + /** + * Function: get + * + * Loads the specified URL *asynchronously* and invokes the given functions + * depending on the request status. Returns the <mxXmlRequest> in use. Both + * functions take the <mxXmlRequest> as the only parameter. See + * <mxUtils.load> for a synchronous implementation. + * + * Example: + * + * (code) + * mxUtils.get(url, function(req) + * { + * var node = req.getDocumentElement(); + * // Process XML DOM... + * }); + * (end) + * + * So for example, to load a diagram into an existing graph model, the + * following code is used. + * + * (code) + * mxUtils.get(url, function(req) + * { + * var node = req.getDocumentElement(); + * var dec = new mxCodec(node.ownerDocument); + * dec.decode(node, graph.getModel()); + * }); + * (end) + * + * Parameters: + * + * url - URL to get the data from. + * onload - Optional function to execute for a successful response. + * onerror - Optional function to execute on error. + */ + get: function(url, onload, onerror) + { + return new mxXmlRequest(url, null, 'GET').send(onload, onerror); + }, + + /** + * Function: post + * + * Posts the specified params to the given URL *asynchronously* and invokes + * the given functions depending on the request status. Returns the + * <mxXmlRequest> in use. Both functions take the <mxXmlRequest> as the + * only parameter. Make sure to use encodeURIComponent for the parameter + * values. + * + * Example: + * + * (code) + * mxUtils.post(url, 'key=value', function(req) + * { + * mxUtils.alert('Ready: '+req.isReady()+' Status: '+req.getStatus()); + * // Process req.getDocumentElement() using DOM API if OK... + * }); + * (end) + * + * Parameters: + * + * url - URL to get the data from. + * params - Parameters for the post request. + * onload - Optional function to execute for a successful response. + * onerror - Optional function to execute on error. + */ + post: function(url, params, onload, onerror) + { + return new mxXmlRequest(url, params).send(onload, onerror); + }, + + /** + * Function: submit + * + * Submits the given parameters to the specified URL using + * <mxXmlRequest.simulate> and returns the <mxXmlRequest>. + * Make sure to use encodeURIComponent for the parameter + * values. + * + * Parameters: + * + * url - URL to get the data from. + * params - Parameters for the form. + * doc - Document to create the form in. + * target - Target to send the form result to. + */ + submit: function(url, params, doc, target) + { + return new mxXmlRequest(url, params).simulate(doc, target); + }, + + /** + * Function: loadInto + * + * Loads the specified URL *asynchronously* into the specified document, + * invoking onload after the document has been loaded. This implementation + * does not use <mxXmlRequest>, but the document.load method. + * + * Parameters: + * + * url - URL to get the data from. + * doc - The document to load the URL into. + * onload - Function to execute when the URL has been loaded. + */ + loadInto: function(url, doc, onload) + { + if (mxClient.IS_IE) + { + doc.onreadystatechange = function () + { + if (doc.readyState == 4) + { + onload(); + } + }; + } + else + { + doc.addEventListener('load', onload, false); + } + + doc.load(url); + }, + + /** + * Function: getValue + * + * Returns the value for the given key in the given associative array or + * the given default value if the value is null. + * + * Parameters: + * + * array - Associative array that contains the value for the key. + * key - Key whose value should be returned. + * defaultValue - Value to be returned if the value for the given + * key is null. + */ + getValue: function(array, key, defaultValue) + { + var value = (array != null) ? array[key] : null; + + if (value == null) + { + value = defaultValue; + } + + return value; + }, + + /** + * Function: getNumber + * + * Returns the numeric value for the given key in the given associative + * array or the given default value (or 0) if the value is null. The value + * is converted to a numeric value using the Number function. + * + * Parameters: + * + * array - Associative array that contains the value for the key. + * key - Key whose value should be returned. + * defaultValue - Value to be returned if the value for the given + * key is null. Default is 0. + */ + getNumber: function(array, key, defaultValue) + { + var value = (array != null) ? array[key] : null; + + if (value == null) + { + value = defaultValue || 0; + } + + return Number(value); + }, + + /** + * Function: getColor + * + * Returns the color value for the given key in the given associative + * array or the given default value if the value is null. If the value + * is <mxConstants.NONE> then null is returned. + * + * Parameters: + * + * array - Associative array that contains the value for the key. + * key - Key whose value should be returned. + * defaultValue - Value to be returned if the value for the given + * key is null. Default is null. + */ + getColor: function(array, key, defaultValue) + { + var value = (array != null) ? array[key] : null; + + if (value == null) + { + value = defaultValue; + } + else if (value == mxConstants.NONE) + { + value = null; + } + + return value; + }, + + /** + * Function: clone + * + * Recursively clones the specified object ignoring all fieldnames in the + * given array of transient fields. <mxObjectIdentity.FIELD_NAME> is always + * ignored by this function. + * + * Parameters: + * + * obj - Object to be cloned. + * transients - Optional array of strings representing the fieldname to be + * ignored. + * shallow - Optional boolean argument to specify if a shallow clone should + * be created, that is, one where all object references are not cloned or, + * in other words, one where only atomic (strings, numbers) values are + * cloned. Default is false. + */ + clone: function(obj, transients, shallow) + { + shallow = (shallow != null) ? shallow : false; + var clone = null; + + if (obj != null && typeof(obj.constructor) == 'function') + { + clone = new obj.constructor(); + + for (var i in obj) + { + if (i != mxObjectIdentity.FIELD_NAME && (transients == null || + mxUtils.indexOf(transients, i) < 0)) + { + if (!shallow && typeof(obj[i]) == 'object') + { + clone[i] = mxUtils.clone(obj[i]); + } + else + { + clone[i] = obj[i]; + } + } + } + } + + return clone; + }, + + /** + * Function: equalPoints + * + * Compares all mxPoints in the given lists. + * + * Parameters: + * + * a - Array of <mxPoints> to be compared. + * b - Array of <mxPoints> to be compared. + */ + equalPoints: function(a, b) + { + if ((a == null && b != null) || (a != null && b == null) || + (a != null && b != null && a.length != b.length)) + { + return false; + } + else if (a != null && b != null) + { + for (var i = 0; i < a.length; i++) + { + if (a[i] == b[i] || (a[i] != null && !a[i].equals(b[i]))) + { + return false; + } + } + } + + return true; + }, + + /** + * Function: equalEntries + * + * Compares all entries in the given dictionaries. + * + * Parameters: + * + * a - <mxRectangle> to be compared. + * b - <mxRectangle> to be compared. + */ + equalEntries: function(a, b) + { + if ((a == null && b != null) || (a != null && b == null) || + (a != null && b != null && a.length != b.length)) + { + return false; + } + else if (a != null && b != null) + { + for (var key in a) + { + if (a[key] != b[key]) + { + return false; + } + } + } + + return true; + }, + + /** + * Function: extend + * + * Assigns a copy of the superclass prototype to the subclass prototype. + * Note that this does not call the constructor of the superclass at this + * point, the superclass constructor should be called explicitely in the + * subclass constructor. Below is an example. + * + * (code) + * MyGraph = function(container, model, renderHint, stylesheet) + * { + * mxGraph.call(this, container, model, renderHint, stylesheet); + * } + * + * mxUtils.extend(MyGraph, mxGraph); + * (end) + * + * Parameters: + * + * ctor - Constructor of the subclass. + * superCtor - Constructor of the superclass. + */ + extend: function(ctor, superCtor) + { + var f = function() {}; + f.prototype = superCtor.prototype; + + ctor.prototype = new f(); + ctor.prototype.constructor = ctor; + }, + + /** + * Function: toString + * + * Returns a textual representation of the specified object. + * + * Parameters: + * + * obj - Object to return the string representation for. + */ + toString: function(obj) + { + var output = ''; + + for (var i in obj) + { + try + { + if (obj[i] == null) + { + output += i + ' = [null]\n'; + } + else if (typeof(obj[i]) == 'function') + { + output += i + ' => [Function]\n'; + } + else if (typeof(obj[i]) == 'object') + { + var ctor = mxUtils.getFunctionName(obj[i].constructor); + output += i + ' => [' + ctor + ']\n'; + } + else + { + output += i + ' = ' + obj[i] + '\n'; + } + } + catch (e) + { + output += i + '=' + e.message; + } + } + + return output; + }, + + /** + * Function: toRadians + * + * Converts the given degree to radians. + */ + toRadians: function(deg) + { + return Math.PI * deg / 180; + }, + + /** + * Function: arcToCurves + * + * Converts the given arc to a series of curves. + */ + arcToCurves: function(x0, y0, r1, r2, angle, largeArcFlag, sweepFlag, x, y) + { + x -= x0; + y -= y0; + + if (r1 === 0 || r2 === 0) + { + return result; + } + + var fS = sweepFlag; + var psai = angle; + r1 = Math.abs(r1); + r2 = Math.abs(r2); + var ctx = -x / 2; + var cty = -y / 2; + var cpsi = Math.cos(psai * Math.PI / 180); + var spsi = Math.sin(psai * Math.PI / 180); + var rxd = cpsi * ctx + spsi * cty; + var ryd = -1 * spsi * ctx + cpsi * cty; + var rxdd = rxd * rxd; + var rydd = ryd * ryd; + var r1x = r1 * r1; + var r2y = r2 * r2; + var lamda = rxdd / r1x + rydd / r2y; + var sds; + + if (lamda > 1) + { + r1 = Math.sqrt(lamda) * r1; + r2 = Math.sqrt(lamda) * r2; + sds = 0; + } + else + { + var seif = 1; + + if (largeArcFlag === fS) + { + seif = -1; + } + + sds = seif * Math.sqrt((r1x * r2y - r1x * rydd - r2y * rxdd) / (r1x * rydd + r2y * rxdd)); + } + + var txd = sds * r1 * ryd / r2; + var tyd = -1 * sds * r2 * rxd / r1; + var tx = cpsi * txd - spsi * tyd + x / 2; + var ty = spsi * txd + cpsi * tyd + y / 2; + var rad = Math.atan2((ryd - tyd) / r2, (rxd - txd) / r1) - Math.atan2(0, 1); + var s1 = (rad >= 0) ? rad : 2 * Math.PI + rad; + rad = Math.atan2((-ryd - tyd) / r2, (-rxd - txd) / r1) - Math.atan2((ryd - tyd) / r2, (rxd - txd) / r1); + var dr = (rad >= 0) ? rad : 2 * Math.PI + rad; + + if (fS == 0 && dr > 0) + { + dr -= 2 * Math.PI; + } + else if (fS != 0 && dr < 0) + { + dr += 2 * Math.PI; + } + + var sse = dr * 2 / Math.PI; + var seg = Math.ceil(sse < 0 ? -1 * sse : sse); + var segr = dr / seg; + var t = 8/3 * Math.sin(segr / 4) * Math.sin(segr / 4) / Math.sin(segr / 2); + var cpsir1 = cpsi * r1; + var cpsir2 = cpsi * r2; + var spsir1 = spsi * r1; + var spsir2 = spsi * r2; + var mc = Math.cos(s1); + var ms = Math.sin(s1); + var x2 = -t * (cpsir1 * ms + spsir2 * mc); + var y2 = -t * (spsir1 * ms - cpsir2 * mc); + var x3 = 0; + var y3 = 0; + + var result = []; + + for (var n = 0; n < seg; ++n) + { + s1 += segr; + mc = Math.cos(s1); + ms = Math.sin(s1); + + x3 = cpsir1 * mc - spsir2 * ms + tx; + y3 = spsir1 * mc + cpsir2 * ms + ty; + var dx = -t * (cpsir1 * ms + spsir2 * mc); + var dy = -t * (spsir1 * ms - cpsir2 * mc); + + // CurveTo updates x0, y0 so need to restore it + var index = n * 6; + result[index] = Number(x2 + x0); + result[index + 1] = Number(y2 + y0); + result[index + 2] = Number(x3 - dx + x0); + result[index + 3] = Number(y3 - dy + y0); + result[index + 4] = Number(x3 + x0); + result[index + 5] = Number(y3 + y0); + + x2 = x3 + dx; + y2 = y3 + dy; + } + + return result; + }, + + /** + * Function: getBoundingBox + * + * Returns the bounding box for the rotated rectangle. + */ + getBoundingBox: function(rect, rotation) + { + var result = null; + + if (rect != null && rotation != null && rotation != 0) + { + var rad = mxUtils.toRadians(rotation); + var cos = Math.cos(rad); + var sin = Math.sin(rad); + + var cx = new mxPoint( + rect.x + rect.width / 2, + rect.y + rect.height / 2); + + var p1 = new mxPoint(rect.x, rect.y); + var p2 = new mxPoint(rect.x + rect.width, rect.y); + var p3 = new mxPoint(p2.x, rect.y + rect.height); + var p4 = new mxPoint(rect.x, p3.y); + + p1 = mxUtils.getRotatedPoint(p1, cos, sin, cx); + p2 = mxUtils.getRotatedPoint(p2, cos, sin, cx); + p3 = mxUtils.getRotatedPoint(p3, cos, sin, cx); + p4 = mxUtils.getRotatedPoint(p4, cos, sin, cx); + + result = new mxRectangle(p1.x, p1.y, 0, 0); + result.add(new mxRectangle(p2.x, p2.y, 0, 0)); + result.add(new mxRectangle(p3.x, p3.y, 0, 0)); + result.add(new mxRectangle(p4.x, p4.y, 0, 0)); + } + + return result; + }, + + /** + * Function: getRotatedPoint + * + * Rotates the given point by the given cos and sin. + */ + getRotatedPoint: function(pt, cos, sin, c) + { + c = (c != null) ? c : new mxPoint(); + var x = pt.x - c.x; + var y = pt.y - c.y; + + var x1 = x * cos - y * sin; + var y1 = y * cos + x * sin; + + return new mxPoint(x1 + c.x, y1 + c.y); + }, + + /** + * Returns an integer mask of the port constraints of the given map + * @param dict the style map to determine the port constraints for + * @param defaultValue Default value to return if the key is undefined. + * @return the mask of port constraint directions + * + * Parameters: + * + * terminal - <mxCelState> that represents the terminal. + * edge - <mxCellState> that represents the edge. + * source - Boolean that specifies if the terminal is the source terminal. + * defaultValue - Default value to be returned. + */ + getPortConstraints: function(terminal, edge, source, defaultValue) + { + var value = mxUtils.getValue(terminal.style, mxConstants.STYLE_PORT_CONSTRAINT, null); + + if (value == null) + { + return defaultValue; + } + else + { + var directions = value.toString(); + var returnValue = mxConstants.DIRECTION_MASK_NONE; + + if (directions.indexOf(mxConstants.DIRECTION_NORTH) >= 0) + { + returnValue |= mxConstants.DIRECTION_MASK_NORTH; + } + if (directions.indexOf(mxConstants.DIRECTION_WEST) >= 0) + { + returnValue |= mxConstants.DIRECTION_MASK_WEST; + } + if (directions.indexOf(mxConstants.DIRECTION_SOUTH) >= 0) + { + returnValue |= mxConstants.DIRECTION_MASK_SOUTH; + } + if (directions.indexOf(mxConstants.DIRECTION_EAST) >= 0) + { + returnValue |= mxConstants.DIRECTION_MASK_EAST; + } + + return returnValue; + } + }, + + /** + * Function: reversePortConstraints + * + * Reverse the port constraint bitmask. For example, north | east + * becomes south | west + */ + reversePortConstraints: function(constraint) + { + var result = 0; + + result = (constraint & mxConstants.DIRECTION_MASK_WEST) << 3; + result |= (constraint & mxConstants.DIRECTION_MASK_NORTH) << 1; + result |= (constraint & mxConstants.DIRECTION_MASK_SOUTH) >> 1; + result |= (constraint & mxConstants.DIRECTION_MASK_EAST) >> 3; + + return result; + }, + + /** + * Function: findNearestSegment + * + * Finds the index of the nearest segment on the given cell state for + * the specified coordinate pair. + */ + findNearestSegment: function(state, x, y) + { + var index = -1; + + if (state.absolutePoints.length > 0) + { + var last = state.absolutePoints[0]; + var min = null; + + for (var i = 1; i < state.absolutePoints.length; i++) + { + var current = state.absolutePoints[i]; + var dist = mxUtils.ptSegDistSq(last.x, last.y, + current.x, current.y, x, y); + + if (min == null || dist < min) + { + min = dist; + index = i - 1; + } + + last = current; + } + } + + return index; + }, + + /** + * Function: rectangleIntersectsSegment + * + * Returns true if the given rectangle intersects the given segment. + * + * Parameters: + * + * bounds - <mxRectangle> that represents the rectangle. + * p1 - <mxPoint> that represents the first point of the segment. + * p2 - <mxPoint> that represents the second point of the segment. + */ + rectangleIntersectsSegment: function(bounds, p1, p2) + { + var top = bounds.y; + var left = bounds.x; + var bottom = top + bounds.height; + var right = left + bounds.width; + + // Find min and max X for the segment + var minX = p1.x; + var maxX = p2.x; + + if (p1.x > p2.x) + { + minX = p2.x; + maxX = p1.x; + } + + // Find the intersection of the segment's and rectangle's x-projections + if (maxX > right) + { + maxX = right; + } + + if (minX < left) + { + minX = left; + } + + if (minX > maxX) // If their projections do not intersect return false + { + return false; + } + + // Find corresponding min and max Y for min and max X we found before + var minY = p1.y; + var maxY = p2.y; + var dx = p2.x - p1.x; + + if (Math.abs(dx) > 0.0000001) + { + var a = (p2.y - p1.y) / dx; + var b = p1.y - a * p1.x; + minY = a * minX + b; + maxY = a * maxX + b; + } + + if (minY > maxY) + { + var tmp = maxY; + maxY = minY; + minY = tmp; + } + + // Find the intersection of the segment's and rectangle's y-projections + if (maxY > bottom) + { + maxY = bottom; + } + + if (minY < top) + { + minY = top; + } + + if (minY > maxY) // If Y-projections do not intersect return false + { + return false; + } + + return true; + }, + + /** + * Function: contains + * + * Returns true if the specified point (x, y) is contained in the given rectangle. + * + * Parameters: + * + * bounds - <mxRectangle> that represents the area. + * x - X-coordinate of the point. + * y - Y-coordinate of the point. + */ + contains: function(bounds, x, y) + { + return (bounds.x <= x && bounds.x + bounds.width >= x && + bounds.y <= y && bounds.y + bounds.height >= y); + }, + + /** + * Function: intersects + * + * Returns true if the two rectangles intersect. + * + * Parameters: + * + * a - <mxRectangle> to be checked for intersection. + * b - <mxRectangle> to be checked for intersection. + */ + intersects: function(a, b) + { + var tw = a.width; + var th = a.height; + var rw = b.width; + var rh = b.height; + + if (rw <= 0 || rh <= 0 || tw <= 0 || th <= 0) + { + return false; + } + + var tx = a.x; + var ty = a.y; + var rx = b.x; + var ry = b.y; + + rw += rx; + rh += ry; + tw += tx; + th += ty; + + return ((rw < rx || rw > tx) && + (rh < ry || rh > ty) && + (tw < tx || tw > rx) && + (th < ty || th > ry)); + }, + + /** + * Function: intersects + * + * Returns true if the two rectangles intersect. + * + * Parameters: + * + * a - <mxRectangle> to be checked for intersection. + * b - <mxRectangle> to be checked for intersection. + */ + intersectsHotspot: function(state, x, y, hotspot, min, max) + { + hotspot = (hotspot != null) ? hotspot : 1; + min = (min != null) ? min : 0; + max = (max != null) ? max : 0; + + if (hotspot > 0) + { + var cx = state.getCenterX(); + var cy = state.getCenterY(); + var w = state.width; + var h = state.height; + + var start = mxUtils.getValue(state.style, mxConstants.STYLE_STARTSIZE) * state.view.scale; + + if (start > 0) + { + if (mxUtils.getValue(state.style, + mxConstants.STYLE_HORIZONTAL, true)) + { + cy = state.y + start / 2; + h = start; + } + else + { + cx = state.x + start / 2; + w = start; + } + } + + w = Math.max(min, w * hotspot); + h = Math.max(min, h * hotspot); + + if (max > 0) + { + w = Math.min(w, max); + h = Math.min(h, max); + } + + var rect = new mxRectangle(cx - w / 2, cy - h / 2, w, h); + + return mxUtils.contains(rect, x, y); + } + + return true; + }, + + /** + * Function: getOffset + * + * Returns the offset for the specified container as an <mxPoint>. The + * offset is the distance from the top left corner of the container to the + * top left corner of the document. + * + * Parameters: + * + * container - DOM node to return the offset for. + * scollOffset - Optional boolean to add the scroll offset of the document. + * Default is false. + */ + getOffset: function(container, scrollOffset) + { + var offsetLeft = 0; + var offsetTop = 0; + + if (scrollOffset != null && scrollOffset) + { + var b = document.body; + var d = document.documentElement; + offsetLeft += (b.scrollLeft || d.scrollLeft); + offsetTop += (b.scrollTop || d.scrollTop); + } + + while (container.offsetParent) + { + offsetLeft += container.offsetLeft; + offsetTop += container.offsetTop; + + container = container.offsetParent; + } + + return new mxPoint(offsetLeft, offsetTop); + }, + + /** + * Function: getScrollOrigin + * + * Returns the top, left corner of the viewrect as an <mxPoint>. + */ + getScrollOrigin: function(node) + { + var b = document.body; + var d = document.documentElement; + var sl = (b.scrollLeft || d.scrollLeft); + var st = (b.scrollTop || d.scrollTop); + + var result = new mxPoint(sl, st); + + while (node != null && node != b && node != d) + { + if (!isNaN(node.scrollLeft) && !isNaN(node.scrollTop)) + { + result.x += node.scrollLeft; + result.y += node.scrollTop; + } + + node = node.parentNode; + } + + return result; + }, + + /** + * Function: convertPoint + * + * Converts the specified point (x, y) using the offset of the specified + * container and returns a new <mxPoint> with the result. + * + * Parameters: + * + * container - DOM node to use for the offset. + * x - X-coordinate of the point to be converted. + * y - Y-coordinate of the point to be converted. + */ + convertPoint: function(container, x, y) + { + var origin = mxUtils.getScrollOrigin(container); + var offset = mxUtils.getOffset(container); + + offset.x -= origin.x; + offset.y -= origin.y; + + return new mxPoint(x - offset.x, y - offset.y); + }, + + /** + * Function: ltrim + * + * Strips all whitespaces from the beginning of the string. + * Without the second parameter, Javascript function will trim these + * characters: + * + * - " " (ASCII 32 (0x20)), an ordinary space + * - "\t" (ASCII 9 (0x09)), a tab + * - "\n" (ASCII 10 (0x0A)), a new line (line feed) + * - "\r" (ASCII 13 (0x0D)), a carriage return + * - "\0" (ASCII 0 (0x00)), the NUL-byte + * - "\x0B" (ASCII 11 (0x0B)), a vertical tab + */ + ltrim: function(str, chars) + { + chars = chars || "\\s"; + + return str.replace(new RegExp("^[" + chars + "]+", "g"), ""); + }, + + /** + * Function: rtrim + * + * Strips all whitespaces from the end of the string. + * Without the second parameter, Javascript function will trim these + * characters: + * + * - " " (ASCII 32 (0x20)), an ordinary space + * - "\t" (ASCII 9 (0x09)), a tab + * - "\n" (ASCII 10 (0x0A)), a new line (line feed) + * - "\r" (ASCII 13 (0x0D)), a carriage return + * - "\0" (ASCII 0 (0x00)), the NUL-byte + * - "\x0B" (ASCII 11 (0x0B)), a vertical tab + */ + rtrim: function(str, chars) + { + chars = chars || "\\s"; + + return str.replace(new RegExp("[" + chars + "]+$", "g"), ""); + }, + + /** + * Function: trim + * + * Strips all whitespaces from both end of the string. + * Without the second parameter, Javascript function will trim these + * characters: + * + * - " " (ASCII 32 (0x20)), an ordinary space + * - "\t" (ASCII 9 (0x09)), a tab + * - "\n" (ASCII 10 (0x0A)), a new line (line feed) + * - "\r" (ASCII 13 (0x0D)), a carriage return + * - "\0" (ASCII 0 (0x00)), the NUL-byte + * - "\x0B" (ASCII 11 (0x0B)), a vertical tab + */ + trim: function(str, chars) + { + return mxUtils.ltrim(mxUtils.rtrim(str, chars), chars); + }, + + /** + * Function: isNumeric + * + * Returns true if the specified value is numeric, that is, if it is not + * null, not an empty string, not a HEX number and isNaN returns false. + * + * Parameters: + * + * str - String representing the possibly numeric value. + */ + isNumeric: function(str) + { + return str != null && (str.length == null || (str.length > 0 && + str.indexOf('0x') < 0) && str.indexOf('0X') < 0) && !isNaN(str); + }, + + /** + * Function: mod + * + * Returns the remainder of division of n by m. You should use this instead + * of the built-in operation as the built-in operation does not properly + * handle negative numbers. + */ + mod: function(n, m) + { + return ((n % m) + m) % m; + }, + + /** + * Function: intersection + * + * Returns the intersection of two lines as an <mxPoint>. + * + * Parameters: + * + * x0 - X-coordinate of the first line's startpoint. + * y0 - X-coordinate of the first line's startpoint. + * x1 - X-coordinate of the first line's endpoint. + * y1 - Y-coordinate of the first line's endpoint. + * x2 - X-coordinate of the second line's startpoint. + * y2 - Y-coordinate of the second line's startpoint. + * x3 - X-coordinate of the second line's endpoint. + * y3 - Y-coordinate of the second line's endpoint. + */ + intersection: function (x0, y0, x1, y1, x2, y2, x3, y3) + { + var denom = ((y3 - y2)*(x1 - x0)) - ((x3 - x2)*(y1 - y0)); + var nume_a = ((x3 - x2)*(y0 - y2)) - ((y3 - y2)*(x0 - x2)); + var nume_b = ((x1 - x0)*(y0 - y2)) - ((y1 - y0)*(x0 - x2)); + + var ua = nume_a / denom; + var ub = nume_b / denom; + + if(ua >= 0.0 && ua <= 1.0 && ub >= 0.0 && ub <= 1.0) + { + // Get the intersection point + var intersectionX = x0 + ua*(x1 - x0); + var intersectionY = y0 + ua*(y1 - y0); + + return new mxPoint(intersectionX, intersectionY); + } + + // No intersection + return null; + }, + + /** + * Function: ptSeqDistSq + * + * Returns the square distance between a segment and a point. + * + * Parameters: + * + * x1 - X-coordinate of the startpoint of the segment. + * y1 - Y-coordinate of the startpoint of the segment. + * x2 - X-coordinate of the endpoint of the segment. + * y2 - Y-coordinate of the endpoint of the segment. + * px - X-coordinate of the point. + * py - Y-coordinate of the point. + */ + ptSegDistSq: function(x1, y1, x2, y2, px, py) + { + x2 -= x1; + y2 -= y1; + + px -= x1; + py -= y1; + + var dotprod = px * x2 + py * y2; + var projlenSq; + + if (dotprod <= 0.0) + { + projlenSq = 0.0; + } + else + { + px = x2 - px; + py = y2 - py; + dotprod = px * x2 + py * y2; + + if (dotprod <= 0.0) + { + projlenSq = 0.0; + } + else + { + projlenSq = dotprod * dotprod / (x2 * x2 + y2 * y2); + } + } + + var lenSq = px * px + py * py - projlenSq; + + if (lenSq < 0) + { + lenSq = 0; + } + + return lenSq; + }, + + /** + * Function: relativeCcw + * + * Returns 1 if the given point on the right side of the segment, 0 if its + * on the segment, and -1 if the point is on the left side of the segment. + * + * Parameters: + * + * x1 - X-coordinate of the startpoint of the segment. + * y1 - Y-coordinate of the startpoint of the segment. + * x2 - X-coordinate of the endpoint of the segment. + * y2 - Y-coordinate of the endpoint of the segment. + * px - X-coordinate of the point. + * py - Y-coordinate of the point. + */ + relativeCcw: function(x1, y1, x2, y2, px, py) + { + x2 -= x1; + y2 -= y1; + px -= x1; + py -= y1; + var ccw = px * y2 - py * x2; + + if (ccw == 0.0) + { + ccw = px * x2 + py * y2; + + if (ccw > 0.0) + { + px -= x2; + py -= y2; + ccw = px * x2 + py * y2; + + if (ccw < 0.0) + { + ccw = 0.0; + } + } + } + + return (ccw < 0.0) ? -1 : ((ccw > 0.0) ? 1 : 0); + }, + + /** + * Function: animateChanges + * + * See <mxEffects.animateChanges>. This is for backwards compatibility and + * will be removed later. + */ + animateChanges: function(graph, changes) + { + // LATER: Deprecated, remove this function + mxEffects.animateChanges.apply(this, arguments); + }, + + /** + * Function: cascadeOpacity + * + * See <mxEffects.cascadeOpacity>. This is for backwards compatibility and + * will be removed later. + */ + cascadeOpacity: function(graph, cell, opacity) + { + mxEffects.cascadeOpacity.apply(this, arguments); + }, + + /** + * Function: fadeOut + * + * See <mxEffects.fadeOut>. This is for backwards compatibility and + * will be removed later. + */ + fadeOut: function(node, from, remove, step, delay, isEnabled) + { + mxEffects.fadeOut.apply(this, arguments); + }, + + /** + * Function: setOpacity + * + * Sets the opacity of the specified DOM node to the given value in %. + * + * Parameters: + * + * node - DOM node to set the opacity for. + * value - Opacity in %. Possible values are between 0 and 100. + */ + setOpacity: function(node, value) + { + if (mxUtils.isVml(node)) + { + if (value >= 100) + { + node.style.filter = null; + } + else + { + // TODO: Why is the division by 5 needed in VML? + node.style.filter = 'alpha(opacity=' + (value/5) + ')'; + } + } + else if (mxClient.IS_IE && (typeof(document.documentMode) === 'undefined' || document.documentMode < 9)) + { + if (value >= 100) + { + node.style.filter = null; + } + else + { + node.style.filter = 'alpha(opacity=' + value + ')'; + } + } + else + { + node.style.opacity = (value / 100); + } + }, + + /** + * Function: createImage + * + * Creates and returns an image (IMG node) or VML image (v:image) in IE6 in + * quirs mode. + * + * Parameters: + * + * src - URL that points to the image to be displayed. + */ + createImage: function(src) + { + var imageNode = null; + + if (mxClient.IS_IE6 && document.compatMode != 'CSS1Compat') + { + imageNode = document.createElement('v:image'); + imageNode.setAttribute('src', src); + imageNode.style.borderStyle = 'none'; + } + else + { + imageNode = document.createElement('img'); + imageNode.setAttribute('src', src); + imageNode.setAttribute('border', '0'); + } + + return imageNode; + }, + + /** + * Function: sortCells + * + * Sorts the given cells according to the order in the cell hierarchy. + * Ascending is optional and defaults to true. + */ + sortCells: function(cells, ascending) + { + ascending = (ascending != null) ? ascending : true; + var lookup = new mxDictionary(); + cells.sort(function(o1, o2) + { + var p1 = lookup.get(o1); + + if (p1 == null) + { + p1 = mxCellPath.create(o1).split(mxCellPath.PATH_SEPARATOR); + lookup.put(o1, p1); + } + + var p2 = lookup.get(o2); + + if (p2 == null) + { + p2 = mxCellPath.create(o2).split(mxCellPath.PATH_SEPARATOR); + lookup.put(o2, p2); + } + + var comp = mxCellPath.compare(p1, p2); + + return (comp == 0) ? 0 : (((comp > 0) == ascending) ? 1 : -1); + }); + + return cells; + }, + + /** + * Function: getStylename + * + * Returns the stylename in a style of the form [(stylename|key=value);] or + * an empty string if the given style does not contain a stylename. + * + * Parameters: + * + * style - String of the form [(stylename|key=value);]. + */ + getStylename: function(style) + { + if (style != null) + { + var pairs = style.split(';'); + var stylename = pairs[0]; + + if (stylename.indexOf('=') < 0) + { + return stylename; + } + } + + return ''; + }, + + /** + * Function: getStylenames + * + * Returns the stylenames in a style of the form [(stylename|key=value);] + * or an empty array if the given style does not contain any stylenames. + * + * Parameters: + * + * style - String of the form [(stylename|key=value);]. + */ + getStylenames: function(style) + { + var result = []; + + if (style != null) + { + var pairs = style.split(';'); + + for (var i = 0; i < pairs.length; i++) + { + if (pairs[i].indexOf('=') < 0) + { + result.push(pairs[i]); + } + } + } + + return result; + }, + + /** + * Function: indexOfStylename + * + * Returns the index of the given stylename in the given style. This + * returns -1 if the given stylename does not occur (as a stylename) in the + * given style, otherwise it returns the index of the first character. + */ + indexOfStylename: function(style, stylename) + { + if (style != null && stylename != null) + { + var tokens = style.split(';'); + var pos = 0; + + for (var i = 0; i < tokens.length; i++) + { + if (tokens[i] == stylename) + { + return pos; + } + + pos += tokens[i].length + 1; + } + } + + return -1; + }, + + /** + * Function: addStylename + * + * Adds the specified stylename to the given style if it does not already + * contain the stylename. + */ + addStylename: function(style, stylename) + { + if (mxUtils.indexOfStylename(style, stylename) < 0) + { + if (style == null) + { + style = ''; + } + else if (style.length > 0 && style.charAt(style.length - 1) != ';') + { + style += ';'; + } + + style += stylename; + } + + return style; + }, + + /** + * Function: removeStylename + * + * Removes all occurrences of the specified stylename in the given style + * and returns the updated style. Trailing semicolons are not preserved. + */ + removeStylename: function(style, stylename) + { + var result = []; + + if (style != null) + { + var tokens = style.split(';'); + + for (var i = 0; i < tokens.length; i++) + { + if (tokens[i] != stylename) + { + result.push(tokens[i]); + } + } + } + + return result.join(';'); + }, + + /** + * Function: removeAllStylenames + * + * Removes all stylenames from the given style and returns the updated + * style. + */ + removeAllStylenames: function(style) + { + var result = []; + + if (style != null) + { + var tokens = style.split(';'); + + for (var i = 0; i < tokens.length; i++) + { + // Keeps the key, value assignments + if (tokens[i].indexOf('=') >= 0) + { + result.push(tokens[i]); + } + } + } + + return result.join(';'); + }, + + /** + * Function: setCellStyles + * + * Assigns the value for the given key in the styles of the given cells, or + * removes the key from the styles if the value is null. + * + * Parameters: + * + * model - <mxGraphModel> to execute the transaction in. + * cells - Array of <mxCells> to be updated. + * key - Key of the style to be changed. + * value - New value for the given key. + */ + setCellStyles: function(model, cells, key, value) + { + if (cells != null && cells.length > 0) + { + model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + if (cells[i] != null) + { + var style = mxUtils.setStyle( + model.getStyle(cells[i]), + key, value); + model.setStyle(cells[i], style); + } + } + } + finally + { + model.endUpdate(); + } + } + }, + + /** + * Function: setStyle + * + * Adds or removes the given key, value pair to the style and returns the + * new style. If value is null or zero length then the key is removed from + * the style. This is for cell styles, not for CSS styles. + * + * Parameters: + * + * style - String of the form [(stylename|key=value);]. + * key - Key of the style to be changed. + * value - New value for the given key. + */ + setStyle: function(style, key, value) + { + var isValue = value != null && (typeof(value.length) == 'undefined' || value.length > 0); + + if (style == null || style.length == 0) + { + if (isValue) + { + style = key+'='+value; + } + } + else + { + var index = style.indexOf(key+'='); + + if (index < 0) + { + if (isValue) + { + var sep = (style.charAt(style.length-1) == ';') ? '' : ';'; + style = style + sep + key+'='+value; + } + } + else + { + var tmp = (isValue) ? (key + '=' + value) : ''; + var cont = style.indexOf(';', index); + + if (!isValue) + { + cont++; + } + + style = style.substring(0, index) + tmp + + ((cont > index) ? style.substring(cont) : ''); + } + } + + return style; + }, + + /** + * Function: setCellStyleFlags + * + * Sets or toggles the flag bit for the given key in the cell's styles. + * If value is null then the flag is toggled. + * + * Example: + * + * (code) + * var cells = graph.getSelectionCells(); + * mxUtils.setCellStyleFlags(graph.model, + * cells, + * mxConstants.STYLE_FONTSTYLE, + * mxConstants.FONT_BOLD); + * (end) + * + * Toggles the bold font style. + * + * Parameters: + * + * model - <mxGraphModel> that contains the cells. + * cells - Array of <mxCells> to change the style for. + * key - Key of the style to be changed. + * flag - Integer for the bit to be changed. + * value - Optional boolean value for the flag. + */ + setCellStyleFlags: function(model, cells, key, flag, value) + { + if (cells != null && cells.length > 0) + { + model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + if (cells[i] != null) + { + var style = mxUtils.setStyleFlag( + model.getStyle(cells[i]), + key, flag, value); + model.setStyle(cells[i], style); + } + } + } + finally + { + model.endUpdate(); + } + } + }, + + /** + * Function: setStyleFlag + * + * Sets or removes the given key from the specified style and returns the + * new style. If value is null then the flag is toggled. + * + * Parameters: + * + * style - String of the form [(stylename|key=value);]. + * key - Key of the style to be changed. + * flag - Integer for the bit to be changed. + * value - Optional boolean value for the given flag. + */ + setStyleFlag: function(style, key, flag, value) + { + if (style == null || style.length == 0) + { + if (value || value == null) + { + style = key+'='+flag; + } + else + { + style = key+'=0'; + } + } + else + { + var index = style.indexOf(key+'='); + + if (index < 0) + { + var sep = (style.charAt(style.length-1) == ';') ? '' : ';'; + + if (value || value == null) + { + style = style + sep + key + '=' + flag; + } + else + { + style = style + sep + key + '=0'; + } + } + else + { + var cont = style.indexOf(';', index); + var tmp = ''; + + if (cont < 0) + { + tmp = style.substring(index+key.length+1); + } + else + { + tmp = style.substring(index+key.length+1, cont); + } + + if (value == null) + { + tmp = parseInt(tmp) ^ flag; + } + else if (value) + { + tmp = parseInt(tmp) | flag; + } + else + { + tmp = parseInt(tmp) & ~flag; + } + + style = style.substring(0, index) + key + '=' + tmp + + ((cont >= 0) ? style.substring(cont) : ''); + } + } + + return style; + }, + + /** + * Function: getSizeForString + * + * Returns an <mxRectangle> with the size (width and height in pixels) of + * the given string. The string may contain HTML markup. Newlines should be + * converted to <br> before calling this method. + * + * Example: + * + * (code) + * var label = graph.getLabel(cell).replace(/\n/g, "<br>"); + * var size = graph.getSizeForString(label); + * (end) + * + * Parameters: + * + * text - String whose size should be returned. + * fontSize - Integer that specifies the font size in pixels. Default is + * <mxConstants.DEFAULT_FONTSIZE>. + * fontFamily - String that specifies the name of the font family. Default + * is <mxConstants.DEFAULT_FONTFAMILY>. + */ + getSizeForString: function(text, fontSize, fontFamily) + { + var div = document.createElement('div'); + + // Sets the font size and family if non-default + div.style.fontSize = (fontSize || mxConstants.DEFAULT_FONTSIZE) + 'px'; + div.style.fontFamily = fontFamily || mxConstants.DEFAULT_FONTFAMILY; + + // Disables block layout and outside wrapping and hides the div + div.style.position = 'absolute'; + div.style.display = 'inline'; + div.style.visibility = 'hidden'; + + // Adds the text and inserts into DOM for updating of size + div.innerHTML = text; + document.body.appendChild(div); + + // Gets the size and removes from DOM + var size = new mxRectangle(0, 0, div.offsetWidth, div.offsetHeight); + document.body.removeChild(div); + + return size; + }, + + /** + * Function: getViewXml + */ + getViewXml: function(graph, scale, cells, x0, y0) + { + x0 = (x0 != null) ? x0 : 0; + y0 = (y0 != null) ? y0 : 0; + scale = (scale != null) ? scale : 1; + + if (cells == null) + { + var model = graph.getModel(); + cells = [model.getRoot()]; + } + + var view = graph.getView(); + var result = null; + + // Disables events on the view + var eventsEnabled = view.isEventsEnabled(); + view.setEventsEnabled(false); + + // Workaround for label bounds not taken into account for image export. + // Creates a temporary draw pane which is used for rendering the text. + // Text rendering is required for finding the bounds of the labels. + var drawPane = view.drawPane; + var overlayPane = view.overlayPane; + + if (graph.dialect == mxConstants.DIALECT_SVG) + { + view.drawPane = document.createElementNS(mxConstants.NS_SVG, 'g'); + view.canvas.appendChild(view.drawPane); + + // Redirects cell overlays into temporary container + view.overlayPane = document.createElementNS(mxConstants.NS_SVG, 'g'); + view.canvas.appendChild(view.overlayPane); + } + else + { + view.drawPane = view.drawPane.cloneNode(false); + view.canvas.appendChild(view.drawPane); + + // Redirects cell overlays into temporary container + view.overlayPane = view.overlayPane.cloneNode(false); + view.canvas.appendChild(view.overlayPane); + } + + // Resets the translation + var translate = view.getTranslate(); + view.translate = new mxPoint(x0, y0); + + // Creates the temporary cell states in the view + var temp = new mxTemporaryCellStates(graph.getView(), scale, cells); + + try + { + var enc = new mxCodec(); + result = enc.encode(graph.getView()); + } + finally + { + temp.destroy(); + view.translate = translate; + view.canvas.removeChild(view.drawPane); + view.canvas.removeChild(view.overlayPane); + view.drawPane = drawPane; + view.overlayPane = overlayPane; + view.setEventsEnabled(eventsEnabled); + } + + return result; + }, + + /** + * Function: getScaleForPageCount + * + * Returns the scale to be used for printing the graph with the given + * bounds across the specifies number of pages with the given format. The + * scale is always computed such that it given the given amount or fewer + * pages in the print output. See <mxPrintPreview> for an example. + * + * Parameters: + * + * pageCount - Specifies the number of pages in the print output. + * graph - <mxGraph> that should be printed. + * pageFormat - Optional <mxRectangle> that specifies the page format. + * Default is <mxConstants.PAGE_FORMAT_A4_PORTRAIT>. + * border - The border along each side of every page. + */ + getScaleForPageCount: function(pageCount, graph, pageFormat, border) + { + if (pageCount < 1) + { + // We can't work with less than 1 page, return no scale + // change + return 1; + } + + pageFormat = (pageFormat != null) ? pageFormat : mxConstants.PAGE_FORMAT_A4_PORTRAIT; + border = (border != null) ? border : 0; + + var availablePageWidth = pageFormat.width - (border * 2); + var availablePageHeight = pageFormat.height - (border * 2); + + // Work out the number of pages required if the + // graph is not scaled. + var graphBounds = graph.getGraphBounds().clone(); + var sc = graph.getView().getScale(); + graphBounds.width /= sc; + graphBounds.height /= sc; + var graphWidth = graphBounds.width; + var graphHeight = graphBounds.height; + + var scale = 1; + + // The ratio of the width/height for each printer page + var pageFormatAspectRatio = availablePageWidth / availablePageHeight; + // The ratio of the width/height for the graph to be printer + var graphAspectRatio = graphWidth / graphHeight; + + // The ratio of horizontal pages / vertical pages for this + // graph to maintain its aspect ratio on this page format + var pagesAspectRatio = graphAspectRatio / pageFormatAspectRatio; + + // Factor the square root of the page count up and down + // by the pages aspect ratio to obtain a horizontal and + // vertical page count that adds up to the page count + // and has the correct aspect ratio + var pageRoot = Math.sqrt(pageCount); + var pagesAspectRatioSqrt = Math.sqrt(pagesAspectRatio); + var numRowPages = pageRoot * pagesAspectRatioSqrt; + var numColumnPages = pageRoot / pagesAspectRatioSqrt; + + // These value are rarely more than 2 rounding downs away from + // a total that meets the page count. In cases of one being less + // than 1 page, the other value can be too high and take more iterations + // In this case, just change that value to be the page count, since + // we know the other value is 1 + if (numRowPages < 1 && numColumnPages > pageCount) + { + var scaleChange = numColumnPages / pageCount; + numColumnPages = pageCount; + numRowPages /= scaleChange; + } + + if (numColumnPages < 1 && numRowPages > pageCount) + { + var scaleChange = numRowPages / pageCount; + numRowPages = pageCount; + numColumnPages /= scaleChange; + } + + var currentTotalPages = Math.ceil(numRowPages) * Math.ceil(numColumnPages); + + var numLoops = 0; + + // Iterate through while the rounded up number of pages comes to + // a total greater than the required number + while (currentTotalPages > pageCount) + { + // Round down the page count (rows or columns) that is + // closest to its next integer down in percentage terms. + // i.e. Reduce the page total by reducing the total + // page area by the least possible amount + + var roundRowDownProportion = Math.floor(numRowPages) / numRowPages; + var roundColumnDownProportion = Math.floor(numColumnPages) / numColumnPages; + + // If the round down proportion is, work out the proportion to + // round down to 1 page less + if (roundRowDownProportion == 1) + { + roundRowDownProportion = Math.floor(numRowPages-1) / numRowPages; + } + if (roundColumnDownProportion == 1) + { + roundColumnDownProportion = Math.floor(numColumnPages-1) / numColumnPages; + } + + // Check which rounding down is smaller, but in the case of very small roundings + // try the other dimension instead + var scaleChange = 1; + + // Use the higher of the two values + if (roundRowDownProportion > roundColumnDownProportion) + { + scaleChange = roundRowDownProportion; + } + else + { + scaleChange = roundColumnDownProportion; + } + + numRowPages = numRowPages * scaleChange; + numColumnPages = numColumnPages * scaleChange; + currentTotalPages = Math.ceil(numRowPages) * Math.ceil(numColumnPages); + + numLoops++; + + if (numLoops > 10) + { + break; + } + } + + // Work out the scale from the number of row pages required + // The column pages will give the same value + var posterWidth = availablePageWidth * numRowPages; + scale = posterWidth / graphWidth; + + // Allow for rounding errors + return scale * 0.99999; + }, + + /** + * Function: show + * + * Copies the styles and the markup from the graph's container into the + * given document and removes all cursor styles. The document is returned. + * + * This function should be called from within the document with the graph. + * If you experience problems with missing stylesheets in IE then try adding + * the domain to the trusted sites. + * + * Parameters: + * + * graph - <mxGraph> to be copied. + * doc - Document where the new graph is created. + * x0 - X-coordinate of the graph view origin. Default is 0. + * y0 - Y-coordinate of the graph view origin. Default is 0. + */ + show: function(graph, doc, x0, y0) + { + x0 = (x0 != null) ? x0 : 0; + y0 = (y0 != null) ? y0 : 0; + + if (doc == null) + { + var wnd = window.open(); + doc = wnd.document; + } + else + { + doc.open(); + } + + var bounds = graph.getGraphBounds(); + var dx = -bounds.x + x0; + var dy = -bounds.y + y0; + + // Needs a special way of creating the page so that no click is required + // to refresh the contents after the external CSS styles have been loaded. + // To avoid a click or programmatic refresh, the styleSheets[].cssText + // property is copied over from the original document. + if (mxClient.IS_IE) + { + var html = '<html>'; + html += '<head>'; + + var base = document.getElementsByTagName('base'); + + for (var i = 0; i < base.length; i++) + { + html += base[i].outerHTML; + } + + html += '<style>'; + + // Copies the stylesheets without having to load them again + for (var i = 0; i < document.styleSheets.length; i++) + { + try + { + html += document.styleSheets(i).cssText; + } + catch (e) + { + // ignore security exception + } + } + + html += '</style>'; + + html += '</head>'; + html += '<body>'; + + // Copies the contents of the graph container + html += graph.container.innerHTML; + + html += '</body>'; + html += '<html>'; + + doc.writeln(html); + doc.close(); + + // Makes sure the inner container is on the top, left + var node = doc.body.getElementsByTagName('DIV')[0]; + + if (node != null) + { + node.style.position = 'absolute'; + node.style.left = dx + 'px'; + node.style.top = dy + 'px'; + } + } + else + { + doc.writeln('<html'); + doc.writeln('<head>'); + + var base = document.getElementsByTagName('base'); + + for (var i=0; i<base.length; i++) + { + doc.writeln(mxUtils.getOuterHtml(base[i])); + } + + var links = document.getElementsByTagName('link'); + + for (var i=0; i<links.length; i++) + { + doc.writeln(mxUtils.getOuterHtml(links[i])); + } + + var styles = document.getElementsByTagName('style'); + + for (var i=0; i<styles.length; i++) + { + doc.writeln(mxUtils.getOuterHtml(styles[i])); + } + + doc.writeln('</head>'); + doc.writeln('</html>'); + doc.close(); + + // Workaround for FF2 which has no body element in a document where + // the body has been added using document.write. + if (doc.body == null) + { + doc.documentElement.appendChild(doc.createElement('body')); + } + + // Workaround for missing scrollbars in FF + doc.body.style.overflow = 'auto'; + + var node = graph.container.firstChild; + + while (node != null) + { + var clone = node.cloneNode(true); + doc.body.appendChild(clone); + node = node.nextSibling; + } + + // Shifts negative coordinates into visible space + var node = doc.getElementsByTagName('g')[0]; + + if (node != null) + { + node.setAttribute('transform', 'translate(' + dx + ',' + dy + ')'); + + // Updates the size of the SVG container + var root = node.ownerSVGElement; + root.setAttribute('width', bounds.width + Math.max(bounds.x, 0) + 3); + root.setAttribute('height', bounds.height + Math.max(bounds.y, 0) + 3); + } + } + + mxUtils.removeCursors(doc.body); + + return doc; + }, + + /** + * Function: printScreen + * + * Prints the specified graph using a new window and the built-in print + * dialog. + * + * This function should be called from within the document with the graph. + * + * Parameters: + * + * graph - <mxGraph> to be printed. + */ + printScreen: function(graph) + { + var wnd = window.open(); + mxUtils.show(graph, wnd.document); + + var print = function() + { + wnd.focus(); + wnd.print(); + wnd.close(); + }; + + // Workaround for Google Chrome which needs a bit of a + // delay in order to render the SVG contents + if (mxClient.IS_GC) + { + wnd.setTimeout(print, 500); + } + else + { + print(); + } + }, + + /** + * Function: popup + * + * Shows the specified text content in a new <mxWindow> or a new browser + * window if isInternalWindow is false. + * + * Parameters: + * + * content - String that specifies the text to be displayed. + * isInternalWindow - Optional boolean indicating if an mxWindow should be + * used instead of a new browser window. Default is false. + */ + popup: function(content, isInternalWindow) + { + if (isInternalWindow) + { + var div = document.createElement('div'); + + div.style.overflow = 'scroll'; + div.style.width = '636px'; + div.style.height = '460px'; + + var pre = document.createElement('pre'); + pre.innerHTML = mxUtils.htmlEntities(content, false). + replace(/\n/g,'<br>').replace(/ /g, ' '); + + div.appendChild(pre); + + var w = document.body.clientWidth; + var h = (document.body.clientHeight || document.documentElement.clientHeight); + var wnd = new mxWindow('Popup Window', div, + w/2-320, h/2-240, 640, 480, false, true); + + wnd.setClosable(true); + wnd.setVisible(true); + } + else + { + // Wraps up the XML content in a textarea + if (mxClient.IS_NS) + { + var wnd = window.open(); + wnd.document.writeln('<pre>'+mxUtils.htmlEntities(content)+'</pre'); + wnd.document.close(); + } + else + { + var wnd = window.open(); + var pre = wnd.document.createElement('pre'); + pre.innerHTML = mxUtils.htmlEntities(content, false). + replace(/\n/g,'<br>').replace(/ /g, ' '); + wnd.document.body.appendChild(pre); + } + } + }, + + /** + * Function: alert + * + * Displayss the given alert in a new dialog. This implementation uses the + * built-in alert function. This is used to display validation errors when + * connections cannot be changed or created. + * + * Parameters: + * + * message - String specifying the message to be displayed. + */ + alert: function(message) + { + alert(message); + }, + + /** + * Function: prompt + * + * Displays the given message in a prompt dialog. This implementation uses + * the built-in prompt function. + * + * Parameters: + * + * message - String specifying the message to be displayed. + * defaultValue - Optional string specifying the default value. + */ + prompt: function(message, defaultValue) + { + return prompt(message, defaultValue); + }, + + /** + * Function: confirm + * + * Displays the given message in a confirm dialog. This implementation uses + * the built-in confirm function. + * + * Parameters: + * + * message - String specifying the message to be displayed. + */ + confirm: function(message) + { + return confirm(message); + }, + + /** + * Function: error + * + * Displays the given error message in a new <mxWindow> of the given width. + * If close is true then an additional close button is added to the window. + * The optional icon specifies the icon to be used for the window. Default + * is <mxUtils.errorImage>. + * + * Parameters: + * + * message - String specifying the message to be displayed. + * width - Integer specifying the width of the window. + * close - Optional boolean indicating whether to add a close button. + * icon - Optional icon for the window decoration. + */ + error: function(message, width, close, icon) + { + var div = document.createElement('div'); + div.style.padding = '20px'; + + var img = document.createElement('img'); + img.setAttribute('src', icon || mxUtils.errorImage); + img.setAttribute('valign', 'bottom'); + img.style.verticalAlign = 'middle'; + div.appendChild(img); + + div.appendChild(document.createTextNode('\u00a0')); // + div.appendChild(document.createTextNode('\u00a0')); // + div.appendChild(document.createTextNode('\u00a0')); // + mxUtils.write(div, message); + + var w = document.body.clientWidth; + var h = (document.body.clientHeight || document.documentElement.clientHeight); + var warn = new mxWindow(mxResources.get(mxUtils.errorResource) || + mxUtils.errorResource, div, (w-width)/2, h/4, width, null, + false, true); + + if (close) + { + mxUtils.br(div); + + var tmp = document.createElement('p'); + var button = document.createElement('button'); + + if (mxClient.IS_IE) + { + button.style.cssText = 'float:right'; + } + else + { + button.setAttribute('style', 'float:right'); + } + + mxEvent.addListener(button, 'click', function(evt) + { + warn.destroy(); + }); + + mxUtils.write(button, mxResources.get(mxUtils.closeResource) || + mxUtils.closeResource); + + tmp.appendChild(button); + div.appendChild(tmp); + + mxUtils.br(div); + + warn.setClosable(true); + } + + warn.setVisible(true); + + return warn; + }, + + /** + * Function: makeDraggable + * + * Configures the given DOM element to act as a drag source for the + * specified graph. Returns a a new <mxDragSource>. If + * <mxDragSource.guideEnabled> is enabled then the x and y arguments must + * be used in funct to match the preview location. + * + * Example: + * + * (code) + * var funct = function(graph, evt, cell, x, y) + * { + * if (graph.canImportCell(cell)) + * { + * var parent = graph.getDefaultParent(); + * var vertex = null; + * + * graph.getModel().beginUpdate(); + * try + * { + * vertex = graph.insertVertex(parent, null, 'Hello', x, y, 80, 30); + * } + * finally + * { + * graph.getModel().endUpdate(); + * } + * + * graph.setSelectionCell(vertex); + * } + * } + * + * var img = document.createElement('img'); + * img.setAttribute('src', 'editors/images/rectangle.gif'); + * img.style.position = 'absolute'; + * img.style.left = '0px'; + * img.style.top = '0px'; + * img.style.width = '16px'; + * img.style.height = '16px'; + * + * var dragImage = img.cloneNode(true); + * dragImage.style.width = '32px'; + * dragImage.style.height = '32px'; + * mxUtils.makeDraggable(img, graph, funct, dragImage); + * document.body.appendChild(img); + * (end) + * + * Parameters: + * + * element - DOM element to make draggable. + * graphF - <mxGraph> that acts as the drop target or a function that takes a + * mouse event and returns the current <mxGraph>. + * funct - Function to execute on a successful drop. + * dragElement - Optional DOM node to be used for the drag preview. + * dx - Optional horizontal offset between the cursor and the drag + * preview. + * dy - Optional vertical offset between the cursor and the drag + * preview. + * autoscroll - Optional boolean that specifies if autoscroll should be + * used. Default is mxGraph.autoscroll. + * scalePreview - Optional boolean that specifies if the preview element + * should be scaled according to the graph scale. If this is true, then + * the offsets will also be scaled. Default is false. + * highlightDropTargets - Optional boolean that specifies if dropTargets + * should be highlighted. Default is true. + * getDropTarget - Optional function to return the drop target for a given + * location (x, y). Default is mxGraph.getCellAt. + */ + makeDraggable: function(element, graphF, funct, dragElement, dx, dy, autoscroll, + scalePreview, highlightDropTargets, getDropTarget) + { + var dragSource = new mxDragSource(element, funct); + dragSource.dragOffset = new mxPoint((dx != null) ? dx : 0, + (dy != null) ? dy : mxConstants.TOOLTIP_VERTICAL_OFFSET); + dragSource.autoscroll = autoscroll; + + // Cannot enable this by default. This needs to be enabled in the caller + // if the funct argument uses the new x- and y-arguments. + dragSource.setGuidesEnabled(false); + + if (highlightDropTargets != null) + { + dragSource.highlightDropTargets = highlightDropTargets; + } + + // Overrides function to find drop target cell + if (getDropTarget != null) + { + dragSource.getDropTarget = getDropTarget; + } + + // Overrides function to get current graph + dragSource.getGraphForEvent = function(evt) + { + return (typeof(graphF) == 'function') ? graphF(evt) : graphF; + }; + + // Translates switches into dragSource customizations + if (dragElement != null) + { + dragSource.createDragElement = function() + { + return dragElement.cloneNode(true); + }; + + if (scalePreview) + { + dragSource.createPreviewElement = function(graph) + { + var elt = dragElement.cloneNode(true); + + var w = parseInt(elt.style.width); + var h = parseInt(elt.style.height); + elt.style.width = Math.round(w * graph.view.scale) + 'px'; + elt.style.height = Math.round(h * graph.view.scale) + 'px'; + + return elt; + }; + } + } + + return dragSource; + } + +}; diff --git a/src/js/util/mxWindow.js b/src/js/util/mxWindow.js new file mode 100644 index 0000000..e4cbcfc --- /dev/null +++ b/src/js/util/mxWindow.js @@ -0,0 +1,1065 @@ +/** + * $Id: mxWindow.js,v 1.67 2012-10-11 17:18:51 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxWindow + * + * Basic window inside a document. + * + * Examples: + * + * Creating a simple window. + * + * (code) + * var tb = document.createElement('div'); + * var wnd = new mxWindow('Title', tb, 100, 100, 200, 200, true, true); + * wnd.setVisible(true); + * (end) + * + * Creating a window that contains an iframe. + * + * (code) + * var frame = document.createElement('iframe'); + * frame.setAttribute('width', '192px'); + * frame.setAttribute('height', '172px'); + * frame.setAttribute('src', 'http://www.example.com/'); + * frame.style.backgroundColor = 'white'; + * + * var w = document.body.clientWidth; + * var h = (document.body.clientHeight || document.documentElement.clientHeight); + * var wnd = new mxWindow('Title', frame, (w-200)/2, (h-200)/3, 200, 200); + * wnd.setVisible(true); + * (end) + * + * To limit the movement of a window, eg. to keep it from being moved beyond + * the top, left corner the following method can be overridden (recommended): + * + * (code) + * wnd.setLocation = function(x, y) + * { + * x = Math.max(0, x); + * y = Math.max(0, y); + * mxWindow.prototype.setLocation.apply(this, arguments); + * }; + * (end) + * + * Or the following event handler can be used: + * + * (code) + * wnd.addListener(mxEvent.MOVE, function(e) + * { + * wnd.setLocation(Math.max(0, wnd.getX()), Math.max(0, wnd.getY())); + * }); + * (end) + * + * Event: mxEvent.MOVE_START + * + * Fires before the window is moved. The <code>event</code> property contains + * the corresponding mouse event. + * + * Event: mxEvent.MOVE + * + * Fires while the window is being moved. The <code>event</code> property + * contains the corresponding mouse event. + * + * Event: mxEvent.MOVE_END + * + * Fires after the window is moved. The <code>event</code> property contains + * the corresponding mouse event. + * + * Event: mxEvent.RESIZE_START + * + * Fires before the window is resized. The <code>event</code> property contains + * the corresponding mouse event. + * + * Event: mxEvent.RESIZE + * + * Fires while the window is being resized. The <code>event</code> property + * contains the corresponding mouse event. + * + * Event: mxEvent.RESIZE_END + * + * Fires after the window is resized. The <code>event</code> property contains + * the corresponding mouse event. + * + * Event: mxEvent.MAXIMIZE + * + * Fires after the window is maximized. The <code>event</code> property + * contains the corresponding mouse event. + * + * Event: mxEvent.MINIMIZE + * + * Fires after the window is minimized. The <code>event</code> property + * contains the corresponding mouse event. + * + * Event: mxEvent.NORMALIZE + * + * Fires after the window is normalized, that is, it returned from + * maximized or minimized state. The <code>event</code> property contains the + * corresponding mouse event. + * + * Event: mxEvent.ACTIVATE + * + * Fires after a window is activated. The <code>previousWindow</code> property + * contains the previous window. The event sender is the active window. + * + * Event: mxEvent.SHOW + * + * Fires after the window is shown. This event has no properties. + * + * Event: mxEvent.HIDE + * + * Fires after the window is hidden. This event has no properties. + * + * Event: mxEvent.CLOSE + * + * Fires before the window is closed. The <code>event</code> property contains + * the corresponding mouse event. + * + * Event: mxEvent.DESTROY + * + * Fires before the window is destroyed. This event has no properties. + * + * Constructor: mxWindow + * + * Constructs a new window with the given dimension and title to display + * the specified content. The window elements use the given style as a + * prefix for the classnames of the respective window elements, namely, + * the window title and window pane. The respective postfixes are appended + * to the given stylename as follows: + * + * style - Base style for the window. + * style+Title - Style for the window title. + * style+Pane - Style for the window pane. + * + * The default value for style is mxWindow, resulting in the following + * classnames for the window elements: mxWindow, mxWindowTitle and + * mxWindowPane. + * + * If replaceNode is given then the window replaces the given DOM node in + * the document. + * + * Parameters: + * + * title - String that represents the title of the new window. + * content - DOM node that is used as the window content. + * x - X-coordinate of the window location. + * y - Y-coordinate of the window location. + * width - Width of the window. + * height - Optional height of the window. Default is to match the height + * of the content at the specified width. + * minimizable - Optional boolean indicating if the window is minimizable. + * Default is true. + * movable - Optional boolean indicating if the window is movable. Default + * is true. + * replaceNode - Optional DOM node that the window should replace. + * style - Optional base classname for the window elements. Default is + * mxWindow. + */ +function mxWindow(title, content, x, y, width, height, minimizable, movable, replaceNode, style) +{ + if (content != null) + { + minimizable = (minimizable != null) ? minimizable : true; + this.content = content; + this.init(x, y, width, height, style); + + this.installMaximizeHandler(); + this.installMinimizeHandler(); + this.installCloseHandler(); + this.setMinimizable(minimizable); + this.setTitle(title); + + if (movable == null || movable) + { + this.installMoveHandler(); + } + + if (replaceNode != null && replaceNode.parentNode != null) + { + replaceNode.parentNode.replaceChild(this.div, replaceNode); + } + else + { + document.body.appendChild(this.div); + } + } +}; + +/** + * Extends mxEventSource. + */ +mxWindow.prototype = new mxEventSource(); +mxWindow.prototype.constructor = mxWindow; + +/** + * Variable: closeImage + * + * URL of the image to be used for the close icon in the titlebar. + */ +mxWindow.prototype.closeImage = mxClient.imageBasePath + '/close.gif'; + +/** + * Variable: minimizeImage + * + * URL of the image to be used for the minimize icon in the titlebar. + */ +mxWindow.prototype.minimizeImage = mxClient.imageBasePath + '/minimize.gif'; + +/** + * Variable: normalizeImage + * + * URL of the image to be used for the normalize icon in the titlebar. + */ +mxWindow.prototype.normalizeImage = mxClient.imageBasePath + '/normalize.gif'; + +/** + * Variable: maximizeImage + * + * URL of the image to be used for the maximize icon in the titlebar. + */ +mxWindow.prototype.maximizeImage = mxClient.imageBasePath + '/maximize.gif'; + +/** + * Variable: normalizeImage + * + * URL of the image to be used for the resize icon. + */ +mxWindow.prototype.resizeImage = mxClient.imageBasePath + '/resize.gif'; + +/** + * Variable: visible + * + * Boolean flag that represents the visible state of the window. + */ +mxWindow.prototype.visible = false; + +/** + * Variable: content + * + * Reference to the DOM node that represents the window content. + */ +mxWindow.prototype.content = false; + +/** + * Variable: minimumSize + * + * <mxRectangle> that specifies the minimum width and height of the window. + * Default is (50, 40). + */ +mxWindow.prototype.minimumSize = new mxRectangle(0, 0, 50, 40); + +/** + * Variable: title + * + * Reference to the DOM node (TD) that contains the title. + */ +mxWindow.prototype.title = false; + +/** + * Variable: content + * + * Reference to the DOM node that represents the window content. + */ +mxWindow.prototype.content = false; + +/** + * Variable: destroyOnClose + * + * Specifies if the window should be destroyed when it is closed. If this + * is false then the window is hidden using <setVisible>. Default is true. + */ +mxWindow.prototype.destroyOnClose = true; + +/** + * Function: init + * + * Initializes the DOM tree that represents the window. + */ +mxWindow.prototype.init = function(x, y, width, height, style) +{ + style = (style != null) ? style : 'mxWindow'; + + this.div = document.createElement('div'); + this.div.className = style; + this.div.style.left = x+'px'; + this.div.style.top = y+'px'; + this.table = document.createElement('table'); + this.table.className = style; + + // Workaround for table size problems in FF + if (width != null) + { + if (!mxClient.IS_IE) + { + this.div.style.width = width+'px'; + } + + this.table.style.width = width+'px'; + } + + if (height != null) + { + if (!mxClient.IS_IE) + { + this.div.style.height = height+'px'; + } + + this.table.style.height = height+'px'; + } + + // Creates title row + var tbody = document.createElement('tbody'); + var tr = document.createElement('tr'); + + this.title = document.createElement('td'); + this.title.className = style+'Title'; + tr.appendChild(this.title); + tbody.appendChild(tr); + + // Creates content row and table cell + tr = document.createElement('tr'); + this.td = document.createElement('td'); + this.td.className = style+'Pane'; + + this.contentWrapper = document.createElement('div'); + this.contentWrapper.className = style+'Pane'; + this.contentWrapper.style.width = '100%'; + this.contentWrapper.appendChild(this.content); + + // Workaround for div around div restricts height + // of inner div if outerdiv has hidden overflow + if (mxClient.IS_IE || this.content.nodeName.toUpperCase() != 'DIV') + { + this.contentWrapper.style.height = '100%'; + } + + // Puts all content into the DOM + this.td.appendChild(this.contentWrapper); + tr.appendChild(this.td); + tbody.appendChild(tr); + this.table.appendChild(tbody); + this.div.appendChild(this.table); + + // Puts the window on top of other windows when clicked + var activator = mxUtils.bind(this, function(evt) + { + this.activate(); + }); + + var md = (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown'; + mxEvent.addListener(this.title, md, activator); + mxEvent.addListener(this.table, md, activator); + + this.hide(); +}; + +/** + * Function: setTitle + * + * Sets the window title to the given string. HTML markup inside the title + * will be escaped. + */ +mxWindow.prototype.setTitle = function(title) +{ + // Removes all text content nodes (normally just one) + var child = this.title.firstChild; + + while (child != null) + { + var next = child.nextSibling; + + if (child.nodeType == mxConstants.NODETYPE_TEXT) + { + child.parentNode.removeChild(child); + } + + child = next; + } + + mxUtils.write(this.title, title || ''); +}; + +/** + * Function: setScrollable + * + * Sets if the window contents should be scrollable. + */ +mxWindow.prototype.setScrollable = function(scrollable) +{ + // Workaround for hang in Presto 2.5.22 (Opera 10.5) + if (navigator.userAgent.indexOf('Presto/2.5') < 0) + { + if (scrollable) + { + this.contentWrapper.style.overflow = 'auto'; + } + else + { + this.contentWrapper.style.overflow = 'hidden'; + } + } +}; + +/** + * Function: activate + * + * Puts the window on top of all other windows. + */ +mxWindow.prototype.activate = function() +{ + if (mxWindow.activeWindow != this) + { + var style = mxUtils.getCurrentStyle(this.getElement()); + var index = (style != null) ? style.zIndex : 3; + + if (mxWindow.activeWindow) + { + var elt = mxWindow.activeWindow.getElement(); + + if (elt != null && elt.style != null) + { + elt.style.zIndex = index; + } + } + + var previousWindow = mxWindow.activeWindow; + this.getElement().style.zIndex = parseInt(index) + 1; + mxWindow.activeWindow = this; + + this.fireEvent(new mxEventObject(mxEvent.ACTIVATE, 'previousWindow', previousWindow)); + } +}; + +/** + * Function: getElement + * + * Returuns the outermost DOM node that makes up the window. + */ +mxWindow.prototype.getElement = function() +{ + return this.div; +}; + +/** + * Function: fit + * + * Makes sure the window is inside the client area of the window. + */ +mxWindow.prototype.fit = function() +{ + mxUtils.fit(this.div); +}; + +/** + * Function: isResizable + * + * Returns true if the window is resizable. + */ +mxWindow.prototype.isResizable = function() +{ + if (this.resize != null) + { + return this.resize.style.display != 'none'; + } + + return false; +}; + +/** + * Function: setResizable + * + * Sets if the window should be resizable. + */ +mxWindow.prototype.setResizable = function(resizable) +{ + if (resizable) + { + if (this.resize == null) + { + this.resize = document.createElement('img'); + this.resize.style.position = 'absolute'; + this.resize.style.bottom = '2px'; + this.resize.style.right = '2px'; + + this.resize.setAttribute('src', mxClient.imageBasePath + '/resize.gif'); + this.resize.style.cursor = 'nw-resize'; + + var md = (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown'; + var mm = (mxClient.IS_TOUCH) ? 'touchmove' : 'mousemove'; + var mu = (mxClient.IS_TOUCH) ? 'touchend' : 'mouseup'; + + mxEvent.addListener(this.resize, md, mxUtils.bind(this, function(evt) + { + this.activate(); + var startX = mxEvent.getClientX(evt); + var startY = mxEvent.getClientY(evt); + var width = this.div.offsetWidth; + var height = this.div.offsetHeight; + + // Adds a temporary pair of listeners to intercept + // the gesture event in the document + var dragHandler = mxUtils.bind(this, function(evt) + { + var dx = mxEvent.getClientX(evt) - startX; + var dy = mxEvent.getClientY(evt) - startY; + + this.setSize(width + dx, height + dy); + + this.fireEvent(new mxEventObject(mxEvent.RESIZE, 'event', evt)); + mxEvent.consume(evt); + }); + + var dropHandler = mxUtils.bind(this, function(evt) + { + mxEvent.removeListener(document, mm, dragHandler); + mxEvent.removeListener(document, mu, dropHandler); + + this.fireEvent(new mxEventObject(mxEvent.RESIZE_END, 'event', evt)); + mxEvent.consume(evt); + }); + + mxEvent.addListener(document, mm, dragHandler); + mxEvent.addListener(document, mu, dropHandler); + + this.fireEvent(new mxEventObject(mxEvent.RESIZE_START, 'event', evt)); + mxEvent.consume(evt); + })); + + this.div.appendChild(this.resize); + } + else + { + this.resize.style.display = 'inline'; + } + } + else if (this.resize != null) + { + this.resize.style.display = 'none'; + } +}; + +/** + * Function: setSize + * + * Sets the size of the window. + */ +mxWindow.prototype.setSize = function(width, height) +{ + width = Math.max(this.minimumSize.width, width); + height = Math.max(this.minimumSize.height, height); + + // Workaround for table size problems in FF + if (!mxClient.IS_IE) + { + this.div.style.width = width + 'px'; + this.div.style.height = height + 'px'; + } + + this.table.style.width = width + 'px'; + this.table.style.height = height + 'px'; + + if (!mxClient.IS_IE) + { + this.contentWrapper.style.height = + (this.div.offsetHeight - this.title.offsetHeight - 2)+'px'; + } +}; + +/** + * Function: setMinimizable + * + * Sets if the window is minimizable. + */ +mxWindow.prototype.setMinimizable = function(minimizable) +{ + this.minimize.style.display = (minimizable) ? '' : 'none'; +}; + +/** + * Function: getMinimumSize + * + * Returns an <mxRectangle> that specifies the size for the minimized window. + * A width or height of 0 means keep the existing width or height. This + * implementation returns the height of the window title and keeps the width. + */ +mxWindow.prototype.getMinimumSize = function() +{ + return new mxRectangle(0, 0, 0, this.title.offsetHeight); +}; + +/** + * Function: installMinimizeHandler + * + * Installs the event listeners required for minimizing the window. + */ +mxWindow.prototype.installMinimizeHandler = function() +{ + this.minimize = document.createElement('img'); + + this.minimize.setAttribute('src', this.minimizeImage); + this.minimize.setAttribute('align', 'right'); + this.minimize.setAttribute('title', 'Minimize'); + this.minimize.style.cursor = 'pointer'; + this.minimize.style.marginRight = '1px'; + this.minimize.style.display = 'none'; + + this.title.appendChild(this.minimize); + + var minimized = false; + var maxDisplay = null; + var height = null; + + var funct = mxUtils.bind(this, function(evt) + { + this.activate(); + + if (!minimized) + { + minimized = true; + + this.minimize.setAttribute('src', this.normalizeImage); + this.minimize.setAttribute('title', 'Normalize'); + this.contentWrapper.style.display = 'none'; + maxDisplay = this.maximize.style.display; + + this.maximize.style.display = 'none'; + height = this.table.style.height; + + var minSize = this.getMinimumSize(); + + if (minSize.height > 0) + { + if (!mxClient.IS_IE) + { + this.div.style.height = minSize.height + 'px'; + } + + this.table.style.height = minSize.height + 'px'; + } + + if (minSize.width > 0) + { + if (!mxClient.IS_IE) + { + this.div.style.width = minSize.width + 'px'; + } + + this.table.style.width = minSize.width + 'px'; + } + + if (this.resize != null) + { + this.resize.style.visibility = 'hidden'; + } + + this.fireEvent(new mxEventObject(mxEvent.MINIMIZE, 'event', evt)); + } + else + { + minimized = false; + + this.minimize.setAttribute('src', this.minimizeImage); + this.minimize.setAttribute('title', 'Minimize'); + this.contentWrapper.style.display = ''; // default + this.maximize.style.display = maxDisplay; + + if (!mxClient.IS_IE) + { + this.div.style.height = height; + } + + this.table.style.height = height; + + if (this.resize != null) + { + this.resize.style.visibility = ''; + } + + this.fireEvent(new mxEventObject(mxEvent.NORMALIZE, 'event', evt)); + } + + mxEvent.consume(evt); + }); + + var md = (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown'; + mxEvent.addListener(this.minimize, md, funct); +}; + +/** + * Function: setMaximizable + * + * Sets if the window is maximizable. + */ +mxWindow.prototype.setMaximizable = function(maximizable) +{ + this.maximize.style.display = (maximizable) ? '' : 'none'; +}; + +/** + * Function: installMaximizeHandler + * + * Installs the event listeners required for maximizing the window. + */ +mxWindow.prototype.installMaximizeHandler = function() +{ + this.maximize = document.createElement('img'); + + this.maximize.setAttribute('src', this.maximizeImage); + this.maximize.setAttribute('align', 'right'); + this.maximize.setAttribute('title', 'Maximize'); + this.maximize.style.cursor = 'default'; + this.maximize.style.marginLeft = '1px'; + this.maximize.style.cursor = 'pointer'; + this.maximize.style.display = 'none'; + + this.title.appendChild(this.maximize); + + var maximized = false; + var x = null; + var y = null; + var height = null; + var width = null; + + var funct = mxUtils.bind(this, function(evt) + { + this.activate(); + + if (this.maximize.style.display != 'none') + { + if (!maximized) + { + maximized = true; + + this.maximize.setAttribute('src', this.normalizeImage); + this.maximize.setAttribute('title', 'Normalize'); + this.contentWrapper.style.display = ''; + this.minimize.style.visibility = 'hidden'; + + // Saves window state + x = parseInt(this.div.style.left); + y = parseInt(this.div.style.top); + height = this.table.style.height; + width = this.table.style.width; + + this.div.style.left = '0px'; + this.div.style.top = '0px'; + + if (!mxClient.IS_IE) + { + this.div.style.height = (document.body.clientHeight-2)+'px'; + this.div.style.width = (document.body.clientWidth-2)+'px'; + } + + this.table.style.width = (document.body.clientWidth-2)+'px'; + this.table.style.height = (document.body.clientHeight-2)+'px'; + + if (this.resize != null) + { + this.resize.style.visibility = 'hidden'; + } + + if (!mxClient.IS_IE) + { + var style = mxUtils.getCurrentStyle(this.contentWrapper); + + if (style.overflow == 'auto' || this.resize != null) + { + this.contentWrapper.style.height = + (this.div.offsetHeight - this.title.offsetHeight - 2)+'px'; + } + } + + this.fireEvent(new mxEventObject(mxEvent.MAXIMIZE, 'event', evt)); + } + else + { + maximized = false; + + this.maximize.setAttribute('src', this.maximizeImage); + this.maximize.setAttribute('title', 'Maximize'); + this.contentWrapper.style.display = ''; + this.minimize.style.visibility = ''; + + // Restores window state + this.div.style.left = x+'px'; + this.div.style.top = y+'px'; + + if (!mxClient.IS_IE) + { + this.div.style.height = height; + this.div.style.width = width; + + var style = mxUtils.getCurrentStyle(this.contentWrapper); + + if (style.overflow == 'auto' || this.resize != null) + { + this.contentWrapper.style.height = + (this.div.offsetHeight - this.title.offsetHeight - 2)+'px'; + } + } + + this.table.style.height = height; + this.table.style.width = width; + + if (this.resize != null) + { + this.resize.style.visibility = ''; + } + + this.fireEvent(new mxEventObject(mxEvent.NORMALIZE, 'event', evt)); + } + + mxEvent.consume(evt); + } + }); + + var md = (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown'; + mxEvent.addListener(this.maximize, md, funct); + mxEvent.addListener(this.title, 'dblclick', funct); +}; + +/** + * Function: installMoveHandler + * + * Installs the event listeners required for moving the window. + */ +mxWindow.prototype.installMoveHandler = function() +{ + this.title.style.cursor = 'move'; + + var md = (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown'; + var mm = (mxClient.IS_TOUCH) ? 'touchmove' : 'mousemove'; + var mu = (mxClient.IS_TOUCH) ? 'touchend' : 'mouseup'; + + mxEvent.addListener(this.title, md, mxUtils.bind(this, function(evt) + { + var startX = mxEvent.getClientX(evt); + var startY = mxEvent.getClientY(evt); + var x = this.getX(); + var y = this.getY(); + + // Adds a temporary pair of listeners to intercept + // the gesture event in the document + var dragHandler = mxUtils.bind(this, function(evt) + { + var dx = mxEvent.getClientX(evt) - startX; + var dy = mxEvent.getClientY(evt) - startY; + this.setLocation(x + dx, y + dy); + this.fireEvent(new mxEventObject(mxEvent.MOVE, 'event', evt)); + mxEvent.consume(evt); + }); + + var dropHandler = mxUtils.bind(this, function(evt) + { + mxEvent.removeListener(document, mm, dragHandler); + mxEvent.removeListener(document, mu, dropHandler); + + this.fireEvent(new mxEventObject(mxEvent.MOVE_END, 'event', evt)); + mxEvent.consume(evt); + }); + + mxEvent.addListener(document, mm, dragHandler); + mxEvent.addListener(document, mu, dropHandler); + + this.fireEvent(new mxEventObject(mxEvent.MOVE_START, 'event', evt)); + mxEvent.consume(evt); + })); +}; + +/** + * Function: setLocation + * + * Sets the upper, left corner of the window. + */ + mxWindow.prototype.setLocation = function(x, y) + { + this.div.style.left = x + 'px'; + this.div.style.top = y + 'px'; + }; + +/** + * Function: getX + * + * Returns the current position on the x-axis. + */ +mxWindow.prototype.getX = function() +{ + return parseInt(this.div.style.left); +}; + +/** + * Function: getY + * + * Returns the current position on the y-axis. + */ +mxWindow.prototype.getY = function() +{ + return parseInt(this.div.style.top); +}; + +/** + * Function: installCloseHandler + * + * Adds the <closeImage> as a new image node in <closeImg> and installs the + * <close> event. + */ +mxWindow.prototype.installCloseHandler = function() +{ + this.closeImg = document.createElement('img'); + + this.closeImg.setAttribute('src', this.closeImage); + this.closeImg.setAttribute('align', 'right'); + this.closeImg.setAttribute('title', 'Close'); + this.closeImg.style.marginLeft = '2px'; + this.closeImg.style.cursor = 'pointer'; + this.closeImg.style.display = 'none'; + + this.title.insertBefore(this.closeImg, this.title.firstChild); + + var md = (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown'; + mxEvent.addListener(this.closeImg, md, mxUtils.bind(this, function(evt) + { + this.fireEvent(new mxEventObject(mxEvent.CLOSE, 'event', evt)); + + if (this.destroyOnClose) + { + this.destroy(); + } + else + { + this.setVisible(false); + } + + mxEvent.consume(evt); + })); +}; + +/** + * Function: setImage + * + * Sets the image associated with the window. + * + * Parameters: + * + * image - URL of the image to be used. + */ +mxWindow.prototype.setImage = function(image) +{ + this.image = document.createElement('img'); + this.image.setAttribute('src', image); + this.image.setAttribute('align', 'left'); + this.image.style.marginRight = '4px'; + this.image.style.marginLeft = '0px'; + this.image.style.marginTop = '-2px'; + + this.title.insertBefore(this.image, this.title.firstChild); +}; + +/** + * Function: setClosable + * + * Sets the image associated with the window. + * + * Parameters: + * + * closable - Boolean specifying if the window should be closable. + */ +mxWindow.prototype.setClosable = function(closable) +{ + this.closeImg.style.display = (closable) ? '' : 'none'; +}; + +/** + * Function: isVisible + * + * Returns true if the window is visible. + */ +mxWindow.prototype.isVisible = function() +{ + if (this.div != null) + { + return this.div.style.visibility != 'hidden'; + } + + return false; +}; + +/** + * Function: setVisible + * + * Shows or hides the window depending on the given flag. + * + * Parameters: + * + * visible - Boolean indicating if the window should be made visible. + */ +mxWindow.prototype.setVisible = function(visible) +{ + if (this.div != null && this.isVisible() != visible) + { + if (visible) + { + this.show(); + } + else + { + this.hide(); + } + } +}; + +/** + * Function: show + * + * Shows the window. + */ +mxWindow.prototype.show = function() +{ + this.div.style.visibility = ''; + this.activate(); + + var style = mxUtils.getCurrentStyle(this.contentWrapper); + + if (!mxClient.IS_IE && (style.overflow == 'auto' || this.resize != null)) + { + this.contentWrapper.style.height = + (this.div.offsetHeight - this.title.offsetHeight - 2)+'px'; + } + + this.fireEvent(new mxEventObject(mxEvent.SHOW)); +}; + +/** + * Function: hide + * + * Hides the window. + */ +mxWindow.prototype.hide = function() +{ + this.div.style.visibility = 'hidden'; + this.fireEvent(new mxEventObject(mxEvent.HIDE)); +}; + +/** + * Function: destroy + * + * Destroys the window and removes all associated resources. Fires a + * <destroy> event prior to destroying the window. + */ +mxWindow.prototype.destroy = function() +{ + this.fireEvent(new mxEventObject(mxEvent.DESTROY)); + + if (this.div != null) + { + mxEvent.release(this.div); + this.div.parentNode.removeChild(this.div); + this.div = null; + } + + this.title = null; + this.content = null; + this.contentWrapper = null; +}; diff --git a/src/js/util/mxXmlCanvas2D.js b/src/js/util/mxXmlCanvas2D.js new file mode 100644 index 0000000..499c71a --- /dev/null +++ b/src/js/util/mxXmlCanvas2D.js @@ -0,0 +1,715 @@ +/** + * $Id: mxXmlCanvas2D.js,v 1.9 2012-04-24 13:56:56 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * + * Class: mxXmlCanvas2D + * + * Implements a canvas to be used with <mxImageExport>. This canvas writes all + * calls as child nodes to the given root XML node. + * + * (code) + * var xmlDoc = mxUtils.createXmlDocument(); + * var root = xmlDoc.createElement('output'); + * xmlDoc.appendChild(root); + * var xmlCanvas = new mxXmlCanvas2D(root); + * (end) + * + * Constructor: mxXmlCanvas2D + * + * Constructs a XML canvas. + * + * Parameters: + * + * root - XML node for adding child nodes. + */ +var mxXmlCanvas2D = function(root) +{ + /** + * Variable: converter + * + * Holds the <mxUrlConverter> to convert image URLs. + */ + var converter = new mxUrlConverter(); + + /** + * Variable: compressed + * + * Specifies if the output should be compressed by removing redundant calls. + * Default is true. + */ + var compressed = true; + + /** + * Variable: textEnabled + * + * Specifies if text output should be enabled. Default is true. + */ + var textEnabled = true; + + // Private reference to the owner document + var doc = root.ownerDocument; + + // Implements stack for save/restore + var stack = []; + + // Implements state for redundancy checks + var state = + { + alpha: 1, + dashed: false, + strokewidth: 1, + fontsize: mxConstants.DEFAULT_FONTSIZE, + fontfamily: mxConstants.DEFAULT_FONTFAMILY, + fontcolor: '#000000' + }; + + // Private helper function set set precision to 2 + var f2 = function(x) + { + return Math.round(parseFloat(x) * 100) / 100; + }; + + // Returns public interface + return { + + /** + * Function: getConverter + * + * Returns <converter>. + */ + getConverter: function() + { + return converter; + }, + + /** + * Function: isCompressed + * + * Returns <compressed>. + */ + isCompressed: function() + { + return compressed; + }, + + /** + * Function: setCompressed + * + * Sets <compressed>. + */ + setCompressed: function(value) + { + compressed = value; + }, + + /** + * Function: isTextEnabled + * + * Returns <textEnabled>. + */ + isTextEnabled: function() + { + return textEnabled; + }, + + /** + * Function: setTextEnabled + * + * Sets <textEnabled>. + */ + setTextEnabled: function(value) + { + textEnabled = value; + }, + + /** + * Function: getDocument + * + * Returns the owner document of the root element. + */ + getDocument: function() + { + return doc; + }, + + /** + * Function: save + * + * Saves the state of the graphics object. + */ + save: function() + { + if (compressed) + { + stack.push(state); + state = mxUtils.clone(state); + } + + root.appendChild(doc.createElement('save')); + }, + + /** + * Function: restore + * + * Restores the state of the graphics object. + */ + restore: function() + { + if (compressed) + { + state = stack.pop(); + } + + root.appendChild(doc.createElement('restore')); + }, + + /** + * Function: scale + * + * Scales the current graphics object. + */ + scale: function(value) + { + var elem = doc.createElement('scale'); + elem.setAttribute('scale', value); + root.appendChild(elem); + }, + + /** + * Function: translate + * + * Translates the current graphics object. + */ + translate: function(dx, dy) + { + var elem = doc.createElement('translate'); + elem.setAttribute('dx', f2(dx)); + elem.setAttribute('dy', f2(dy)); + root.appendChild(elem); + }, + + /** + * Function: rotate + * + * Rotates and/or flips the current graphics object. + */ + rotate: function(theta, flipH, flipV, cx, cy) + { + var elem = doc.createElement('rotate'); + elem.setAttribute('theta', f2(theta)); + elem.setAttribute('flipH', (flipH) ? '1' : '0'); + elem.setAttribute('flipV', (flipV) ? '1' : '0'); + elem.setAttribute('cx', f2(cx)); + elem.setAttribute('cy', f2(cy)); + root.appendChild(elem); + }, + + /** + * Function: setStrokeWidth + * + * Sets the stroke width. + */ + setStrokeWidth: function(value) + { + if (compressed) + { + if (state.strokewidth == value) + { + return; + } + + state.strokewidth = value; + } + + var elem = doc.createElement('strokewidth'); + elem.setAttribute('width', f2(value)); + root.appendChild(elem); + }, + + /** + * Function: setStrokeColor + * + * Sets the stroke color. + */ + setStrokeColor: function(value) + { + var elem = doc.createElement('strokecolor'); + elem.setAttribute('color', value); + root.appendChild(elem); + }, + + /** + * Function: setDashed + * + * Sets the dashed state to true or false. + */ + setDashed: function(value) + { + if (compressed) + { + if (state.dashed == value) + { + return; + } + + state.dashed = value; + } + + var elem = doc.createElement('dashed'); + elem.setAttribute('dashed', (value) ? '1' : '0'); + root.appendChild(elem); + }, + + /** + * Function: setDashPattern + * + * Sets the dashed pattern to the given space separated list of numbers. + */ + setDashPattern: function(value) + { + var elem = doc.createElement('dashpattern'); + elem.setAttribute('pattern', value); + root.appendChild(elem); + }, + + /** + * Function: setLineCap + * + * Sets the linecap. + */ + setLineCap: function(value) + { + var elem = doc.createElement('linecap'); + elem.setAttribute('cap', value); + root.appendChild(elem); + }, + + /** + * Function: setLineJoin + * + * Sets the linejoin. + */ + setLineJoin: function(value) + { + var elem = doc.createElement('linejoin'); + elem.setAttribute('join', value); + root.appendChild(elem); + }, + + /** + * Function: setMiterLimit + * + * Sets the miterlimit. + */ + setMiterLimit: function(value) + { + var elem = doc.createElement('miterlimit'); + elem.setAttribute('limit', value); + root.appendChild(elem); + }, + + /** + * Function: setFontSize + * + * Sets the fontsize. + */ + setFontSize: function(value) + { + if (textEnabled) + { + if (compressed) + { + if (state.fontsize == value) + { + return; + } + + state.fontsize = value; + } + + var elem = doc.createElement('fontsize'); + elem.setAttribute('size', value); + root.appendChild(elem); + } + }, + + /** + * Function: setFontColor + * + * Sets the fontcolor. + */ + setFontColor: function(value) + { + if (textEnabled) + { + if (compressed) + { + if (state.fontcolor == value) + { + return; + } + + state.fontcolor = value; + } + + var elem = doc.createElement('fontcolor'); + elem.setAttribute('color', value); + root.appendChild(elem); + } + }, + + /** + * Function: setFontFamily + * + * Sets the fontfamily. + */ + setFontFamily: function(value) + { + if (textEnabled) + { + if (compressed) + { + if (state.fontfamily == value) + { + return; + } + + state.fontfamily = value; + } + + var elem = doc.createElement('fontfamily'); + elem.setAttribute('family', value); + root.appendChild(elem); + } + }, + + /** + * Function: setFontStyle + * + * Sets the fontstyle. + */ + setFontStyle: function(value) + { + if (textEnabled) + { + var elem = doc.createElement('fontstyle'); + elem.setAttribute('style', value); + root.appendChild(elem); + } + }, + + /** + * Function: setAlpha + * + * Sets the current alpha. + */ + setAlpha: function(alpha) + { + if (compressed) + { + if (state.alpha == alpha) + { + return; + } + + state.alpha = alpha; + } + + var elem = doc.createElement('alpha'); + elem.setAttribute('alpha', f2(alpha)); + root.appendChild(elem); + }, + + /** + * Function: setFillColor + * + * Sets the fillcolor. + */ + setFillColor: function(value) + { + var elem = doc.createElement('fillcolor'); + elem.setAttribute('color', value); + root.appendChild(elem); + }, + + /** + * Function: setGradient + * + * Sets the gradient color. + */ + setGradient: function(color1, color2, x, y, w, h, direction) + { + var elem = doc.createElement('gradient'); + elem.setAttribute('c1', color1); + elem.setAttribute('c2', color2); + elem.setAttribute('x', f2(x)); + elem.setAttribute('y', f2(y)); + elem.setAttribute('w', f2(w)); + elem.setAttribute('h', f2(h)); + + // Default direction is south + if (direction != null) + { + elem.setAttribute('direction', direction); + } + + root.appendChild(elem); + }, + + /** + * Function: setGlassGradient + * + * Sets the glass gradient. + */ + setGlassGradient: function(x, y, w, h) + { + var elem = doc.createElement('glass'); + elem.setAttribute('x', f2(x)); + elem.setAttribute('y', f2(y)); + elem.setAttribute('w', f2(w)); + elem.setAttribute('h', f2(h)); + root.appendChild(elem); + }, + + /** + * Function: rect + * + * Sets the current path to a rectangle. + */ + rect: function(x, y, w, h) + { + var elem = doc.createElement('rect'); + elem.setAttribute('x', f2(x)); + elem.setAttribute('y', f2(y)); + elem.setAttribute('w', f2(w)); + elem.setAttribute('h', f2(h)); + root.appendChild(elem); + }, + + /** + * Function: roundrect + * + * Sets the current path to a rounded rectangle. + */ + roundrect: function(x, y, w, h, dx, dy) + { + var elem = doc.createElement('roundrect'); + elem.setAttribute('x', f2(x)); + elem.setAttribute('y', f2(y)); + elem.setAttribute('w', f2(w)); + elem.setAttribute('h', f2(h)); + elem.setAttribute('dx', f2(dx)); + elem.setAttribute('dy', f2(dy)); + root.appendChild(elem); + }, + + /** + * Function: ellipse + * + * Sets the current path to an ellipse. + */ + ellipse: function(x, y, w, h) + { + var elem = doc.createElement('ellipse'); + elem.setAttribute('x', f2(x)); + elem.setAttribute('y', f2(y)); + elem.setAttribute('w', f2(w)); + elem.setAttribute('h', f2(h)); + root.appendChild(elem); + }, + + /** + * Function: image + * + * Paints an image. + */ + image: function(x, y, w, h, src, aspect, flipH, flipV) + { + src = converter.convert(src); + + // TODO: Add option for embedding images as base64 + var elem = doc.createElement('image'); + elem.setAttribute('x', f2(x)); + elem.setAttribute('y', f2(y)); + elem.setAttribute('w', f2(w)); + elem.setAttribute('h', f2(h)); + elem.setAttribute('src', src); + elem.setAttribute('aspect', (aspect) ? '1' : '0'); + elem.setAttribute('flipH', (flipH) ? '1' : '0'); + elem.setAttribute('flipV', (flipV) ? '1' : '0'); + root.appendChild(elem); + }, + + /** + * Function: text + * + * Paints the given text. + */ + text: function(x, y, w, h, str, align, valign, vertical, wrap, format) + { + if (textEnabled) + { + var elem = doc.createElement('text'); + elem.setAttribute('x', f2(x)); + elem.setAttribute('y', f2(y)); + elem.setAttribute('w', f2(w)); + elem.setAttribute('h', f2(h)); + elem.setAttribute('str', str); + + if (align != null) + { + elem.setAttribute('align', align); + } + + if (valign != null) + { + elem.setAttribute('valign', valign); + } + + elem.setAttribute('vertical', (vertical) ? '1' : '0'); + elem.setAttribute('wrap', (wrap) ? '1' : '0'); + elem.setAttribute('format', format); + root.appendChild(elem); + } + }, + + /** + * Function: begin + * + * Starts a new path. + */ + begin: function() + { + root.appendChild(doc.createElement('begin')); + }, + + /** + * Function: moveTo + * + * Moves the current path the given coordinates. + */ + moveTo: function(x, y) + { + var elem = doc.createElement('move'); + elem.setAttribute('x', f2(x)); + elem.setAttribute('y', f2(y)); + root.appendChild(elem); + }, + + /** + * Function: lineTo + * + * Adds a line to the current path. + */ + lineTo: function(x, y) + { + var elem = doc.createElement('line'); + elem.setAttribute('x', f2(x)); + elem.setAttribute('y', f2(y)); + root.appendChild(elem); + }, + + /** + * Function: quadTo + * + * Adds a quadratic curve to the current path. + */ + quadTo: function(x1, y1, x2, y2) + { + var elem = doc.createElement('quad'); + elem.setAttribute('x1', f2(x1)); + elem.setAttribute('y1', f2(y1)); + elem.setAttribute('x2', f2(x2)); + elem.setAttribute('y2', f2(y2)); + root.appendChild(elem); + }, + + /** + * Function: curveTo + * + * Adds a bezier curve to the current path. + */ + curveTo: function(x1, y1, x2, y2, x3, y3) + { + var elem = doc.createElement('curve'); + elem.setAttribute('x1', f2(x1)); + elem.setAttribute('y1', f2(y1)); + elem.setAttribute('x2', f2(x2)); + elem.setAttribute('y2', f2(y2)); + elem.setAttribute('x3', f2(x3)); + elem.setAttribute('y3', f2(y3)); + root.appendChild(elem); + }, + + /** + * Function: close + * + * Closes the current path. + */ + close: function() + { + root.appendChild(doc.createElement('close')); + }, + + /** + * Function: stroke + * + * Paints the outline of the current path. + */ + stroke: function() + { + root.appendChild(doc.createElement('stroke')); + }, + + /** + * Function: fill + * + * Fills the current path. + */ + fill: function() + { + root.appendChild(doc.createElement('fill')); + }, + + /** + * Function: fillstroke + * + * Fills and paints the outline of the current path. + */ + fillAndStroke: function() + { + root.appendChild(doc.createElement('fillstroke')); + }, + + /** + * Function: shadow + * + * Paints the current path as a shadow of the given color. + */ + shadow: function(value, filled) + { + var elem = doc.createElement('shadow'); + elem.setAttribute('value', value); + + if (filled != null) + { + elem.setAttribute('filled', (filled) ? '1' : '0'); + } + + root.appendChild(elem); + }, + + /** + * Function: clip + * + * Uses the current path for clipping. + */ + clip: function() + { + root.appendChild(doc.createElement('clip')); + } + }; + +};
\ No newline at end of file diff --git a/src/js/util/mxXmlRequest.js b/src/js/util/mxXmlRequest.js new file mode 100644 index 0000000..0ac55ed --- /dev/null +++ b/src/js/util/mxXmlRequest.js @@ -0,0 +1,425 @@ +/** + * $Id: mxXmlRequest.js,v 1.38 2012-04-22 10:16:23 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxXmlRequest + * + * XML HTTP request wrapper. See also: <mxUtils.get>, <mxUtils.post> and + * <mxUtils.load>. This class provides a cross-browser abstraction for Ajax + * requests. + * + * Encoding: + * + * For encoding parameter values, the built-in encodeURIComponent JavaScript + * method must be used. For automatic encoding of post data in <mxEditor> the + * <mxEditor.escapePostData> switch can be set to true (default). The encoding + * will be carried out using the conte type of the page. That is, the page + * containting the editor should contain a meta tag in the header, eg. + * <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> + * + * Example: + * + * (code) + * var onload = function(req) + * { + * mxUtils.alert(req.getDocumentElement()); + * } + * + * var onerror = function(req) + * { + * mxUtils.alert(req.getStatus()); + * } + * new mxXmlRequest(url, 'key=value').send(onload, onerror); + * (end) + * + * Sends an asynchronous POST request to the specified URL. + * + * Example: + * + * (code) + * var req = new mxXmlRequest(url, 'key=value', 'POST', false); + * req.send(); + * mxUtils.alert(req.getDocumentElement()); + * (end) + * + * Sends a synchronous POST request to the specified URL. + * + * Example: + * + * (code) + * var encoder = new mxCodec(); + * var result = encoder.encode(graph.getModel()); + * var xml = encodeURIComponent(mxUtils.getXml(result)); + * new mxXmlRequest(url, 'xml='+xml).send(); + * (end) + * + * Sends an encoded graph model to the specified URL using xml as the + * parameter name. The parameter can then be retrieved in C# as follows: + * + * (code) + * string xml = HttpUtility.UrlDecode(context.Request.Params["xml"]); + * (end) + * + * Or in Java as follows: + * + * (code) + * String xml = URLDecoder.decode(request.getParameter("xml"), "UTF-8").replace("\n", "
"); + * (end) + * + * Note that the linefeeds should only be replaced if the XML is + * processed in Java, for example when creating an image. + * + * Constructor: mxXmlRequest + * + * Constructs an XML HTTP request. + * + * Parameters: + * + * url - Target URL of the request. + * params - Form encoded parameters to send with a POST request. + * method - String that specifies the request method. Possible values are + * POST and GET. Default is POST. + * async - Boolean specifying if an asynchronous request should be used. + * Default is true. + * username - String specifying the username to be used for the request. + * password - String specifying the password to be used for the request. + */ +function mxXmlRequest(url, params, method, async, username, password) +{ + this.url = url; + this.params = params; + this.method = method || 'POST'; + this.async = (async != null) ? async : true; + this.username = username; + this.password = password; +}; + +/** + * Variable: url + * + * Holds the target URL of the request. + */ +mxXmlRequest.prototype.url = null; + +/** + * Variable: params + * + * Holds the form encoded data for the POST request. + */ +mxXmlRequest.prototype.params = null; + +/** + * Variable: method + * + * Specifies the request method. Possible values are POST and GET. Default + * is POST. + */ +mxXmlRequest.prototype.method = null; + +/** + * Variable: async + * + * Boolean indicating if the request is asynchronous. + */ +mxXmlRequest.prototype.async = null; + +/** + * Variable: binary + * + * Boolean indicating if the request is binary. This option is ignored in IE. + * In all other browsers the requested mime type is set to + * text/plain; charset=x-user-defined. Default is false. + */ +mxXmlRequest.prototype.binary = false; + +/** + * Variable: username + * + * Specifies the username to be used for authentication. + */ +mxXmlRequest.prototype.username = null; + +/** + * Variable: password + * + * Specifies the password to be used for authentication. + */ +mxXmlRequest.prototype.password = null; + +/** + * Variable: request + * + * Holds the inner, browser-specific request object. + */ +mxXmlRequest.prototype.request = null; + +/** + * Function: isBinary + * + * Returns <binary>. + */ +mxXmlRequest.prototype.isBinary = function() +{ + return this.binary; +}; + +/** + * Function: setBinary + * + * Sets <binary>. + */ +mxXmlRequest.prototype.setBinary = function(value) +{ + this.binary = value; +}; + +/** + * Function: getText + * + * Returns the response as a string. + */ +mxXmlRequest.prototype.getText = function() +{ + return this.request.responseText; +}; + +/** + * Function: isReady + * + * Returns true if the response is ready. + */ +mxXmlRequest.prototype.isReady = function() +{ + return this.request.readyState == 4; +}; + +/** + * Function: getDocumentElement + * + * Returns the document element of the response XML document. + */ +mxXmlRequest.prototype.getDocumentElement = function() +{ + var doc = this.getXml(); + + if (doc != null) + { + return doc.documentElement; + } + + return null; +}; + +/** + * Function: getXml + * + * Returns the response as an XML document. Use <getDocumentElement> to get + * the document element of the XML document. + */ +mxXmlRequest.prototype.getXml = function() +{ + var xml = this.request.responseXML; + + // Handles missing response headers in IE, the first condition handles + // the case where responseXML is there, but using its nodes leads to + // type errors in the mxCellCodec when putting the nodes into a new + // document. This happens in IE9 standards mode and with XML user + // objects only, as they are used directly as values in cells. + if (document.documentMode >= 9 || xml == null || xml.documentElement == null) + { + xml = mxUtils.parseXml(this.request.responseText); + } + + return xml; +}; + +/** + * Function: getText + * + * Returns the response as a string. + */ +mxXmlRequest.prototype.getText = function() +{ + return this.request.responseText; +}; + +/** + * Function: getStatus + * + * Returns the status as a number, eg. 404 for "Not found" or 200 for "OK". + * Note: The NS_ERROR_NOT_AVAILABLE for invalid responses cannot be cought. + */ +mxXmlRequest.prototype.getStatus = function() +{ + return this.request.status; +}; + +/** + * Function: create + * + * Creates and returns the inner <request> object. + */ +mxXmlRequest.prototype.create = function() +{ + if (window.XMLHttpRequest) + { + return function() + { + var req = new XMLHttpRequest(); + + // TODO: Check for overrideMimeType required here? + if (this.isBinary() && req.overrideMimeType) + { + req.overrideMimeType('text/plain; charset=x-user-defined'); + } + + return req; + }; + } + else if (typeof(ActiveXObject) != "undefined") + { + return function() + { + // TODO: Implement binary option + return new ActiveXObject("Microsoft.XMLHTTP"); + }; + } +}(); + +/** + * Function: send + * + * Send the <request> to the target URL using the specified functions to + * process the response asychronously. + * + * Parameters: + * + * onload - Function to be invoked if a successful response was received. + * onerror - Function to be called on any error. + */ +mxXmlRequest.prototype.send = function(onload, onerror) +{ + this.request = this.create(); + + if (this.request != null) + { + if (onload != null) + { + this.request.onreadystatechange = mxUtils.bind(this, function() + { + if (this.isReady()) + { + onload(this); + this.onreadystatechaange = null; + } + }); + } + + this.request.open(this.method, this.url, this.async, + this.username, this.password); + this.setRequestHeaders(this.request, this.params); + this.request.send(this.params); + } +}; + +/** + * Function: setRequestHeaders + * + * Sets the headers for the given request and parameters. This sets the + * content-type to application/x-www-form-urlencoded if any params exist. + * + * Example: + * + * (code) + * request.setRequestHeaders = function(request, params) + * { + * if (params != null) + * { + * request.setRequestHeader('Content-Type', + * 'multipart/form-data'); + * request.setRequestHeader('Content-Length', + * params.length); + * } + * }; + * (end) + * + * Use the code above before calling <send> if you require a + * multipart/form-data request. + */ +mxXmlRequest.prototype.setRequestHeaders = function(request, params) +{ + if (params != null) + { + request.setRequestHeader('Content-Type', + 'application/x-www-form-urlencoded'); + } +}; + +/** + * Function: simulate + * + * Creates and posts a request to the given target URL using a dynamically + * created form inside the given document. + * + * Parameters: + * + * docs - Document that contains the form element. + * target - Target to send the form result to. + */ +mxXmlRequest.prototype.simulate = function(doc, target) +{ + doc = doc || document; + var old = null; + + if (doc == document) + { + old = window.onbeforeunload; + window.onbeforeunload = null; + } + + var form = doc.createElement('form'); + form.setAttribute('method', this.method); + form.setAttribute('action', this.url); + + if (target != null) + { + form.setAttribute('target', target); + } + + form.style.display = 'none'; + form.style.visibility = 'hidden'; + + var pars = (this.params.indexOf('&') > 0) ? + this.params.split('&') : + this.params.split(); + + // Adds the parameters as textareas to the form + for (var i=0; i<pars.length; i++) + { + var pos = pars[i].indexOf('='); + + if (pos > 0) + { + var name = pars[i].substring(0, pos); + var value = pars[i].substring(pos+1); + + var textarea = doc.createElement('textarea'); + textarea.setAttribute('name', name); + value = value.replace(/\n/g, '
'); + + var content = doc.createTextNode(value); + textarea.appendChild(content); + form.appendChild(textarea); + } + } + + doc.body.appendChild(form); + form.submit(); + doc.body.removeChild(form); + + if (old != null) + { + window.onbeforeunload = old; + } +}; diff --git a/src/js/view/mxCellEditor.js b/src/js/view/mxCellEditor.js new file mode 100644 index 0000000..2086cca --- /dev/null +++ b/src/js/view/mxCellEditor.js @@ -0,0 +1,522 @@ +/** + * $Id: mxCellEditor.js,v 1.62 2012-12-11 16:59:31 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxCellEditor + * + * In-place editor for the graph. To control this editor, use + * <mxGraph.invokesStopCellEditing>, <mxGraph.enterStopsCellEditing> and + * <mxGraph.escapeEnabled>. If <mxGraph.enterStopsCellEditing> is true then + * ctrl-enter or shift-enter can be used to create a linefeed. The F2 and + * escape keys can always be used to stop editing. To customize the location + * of the textbox in the graph, override <getEditorBounds> as follows: + * + * (code) + * graph.cellEditor.getEditorBounds = function(state) + * { + * var result = mxCellEditor.prototype.getEditorBounds.apply(this, arguments); + * + * if (this.graph.getModel().isEdge(state.cell)) + * { + * result.x = state.getCenterX() - result.width / 2; + * result.y = state.getCenterY() - result.height / 2; + * } + * + * return result; + * }; + * (end) + * + * The textarea uses the mxCellEditor CSS class. You can modify this class in + * your custom CSS. Note: You should modify the CSS after loading the client + * in the page. + * + * Example: + * + * To only allow numeric input in the in-place editor, use the following code. + * + * (code) + * var text = graph.cellEditor.textarea; + * + * mxEvent.addListener(text, 'keydown', function (evt) + * { + * if (!(evt.keyCode >= 48 && evt.keyCode <= 57) && + * !(evt.keyCode >= 96 && evt.keyCode <= 105)) + * { + * mxEvent.consume(evt); + * } + * }); + * (end) + * + * Initial values: + * + * To implement an initial value for cells without a label, use the + * <emptyLabelText> variable. + * + * Resize in Chrome: + * + * Resize of the textarea is disabled by default. If you want to enable + * this feature extend <init> and set this.textarea.style.resize = ''. + * + * Constructor: mxCellEditor + * + * Constructs a new in-place editor for the specified graph. + * + * Parameters: + * + * graph - Reference to the enclosing <mxGraph>. + */ +function mxCellEditor(graph) +{ + this.graph = graph; +}; + +/** + * Variable: graph + * + * Reference to the enclosing <mxGraph>. + */ +mxCellEditor.prototype.graph = null; + +/** + * Variable: textarea + * + * Holds the input textarea. Note that this may be null before the first + * edit. Instantiated in <init>. + */ +mxCellEditor.prototype.textarea = null; + +/** + * Variable: editingCell + * + * Reference to the <mxCell> that is currently being edited. + */ +mxCellEditor.prototype.editingCell = null; + +/** + * Variable: trigger + * + * Reference to the event that was used to start editing. + */ +mxCellEditor.prototype.trigger = null; + +/** + * Variable: modified + * + * Specifies if the label has been modified. + */ +mxCellEditor.prototype.modified = false; + +/** + * Variable: emptyLabelText + * + * Text to be displayed for empty labels. Default is ''. This can be set + * to eg. "[Type Here]" to easier visualize editing of empty labels. The + * value is only displayed before the first keystroke and is never used + * as the actual editin value. + */ +mxCellEditor.prototype.emptyLabelText = ''; + +/** + * Variable: textNode + * + * Reference to the label DOM node that has been hidden. + */ +mxCellEditor.prototype.textNode = ''; + +/** + * Function: init + * + * Creates the <textarea> and installs the event listeners. The key handler + * updates the <modified> state. + */ +mxCellEditor.prototype.init = function () +{ + this.textarea = document.createElement('textarea'); + + this.textarea.className = 'mxCellEditor'; + this.textarea.style.position = 'absolute'; + this.textarea.style.overflow = 'visible'; + + this.textarea.setAttribute('cols', '20'); + this.textarea.setAttribute('rows', '4'); + + if (mxClient.IS_GC) + { + this.textarea.style.resize = 'none'; + } + + mxEvent.addListener(this.textarea, 'blur', mxUtils.bind(this, function(evt) + { + this.focusLost(); + })); + + mxEvent.addListener(this.textarea, 'keydown', mxUtils.bind(this, function(evt) + { + if (!mxEvent.isConsumed(evt)) + { + if (evt.keyCode == 113 /* F2 */ || (this.graph.isEnterStopsCellEditing() && + evt.keyCode == 13 /* Enter */ && !mxEvent.isControlDown(evt) && + !mxEvent.isShiftDown(evt))) + { + this.graph.stopEditing(false); + mxEvent.consume(evt); + } + else if (evt.keyCode == 27 /* Escape */) + { + this.graph.stopEditing(true); + mxEvent.consume(evt); + } + else + { + // Clears the initial empty label on the first keystroke + if (this.clearOnChange) + { + this.clearOnChange = false; + this.textarea.value = ''; + } + + // Updates the modified flag for storing the value + this.setModified(true); + } + } + })); +}; + +/** + * Function: isModified + * + * Returns <modified>. + */ +mxCellEditor.prototype.isModified = function() +{ + return this.modified; +}; + +/** + * Function: setModified + * + * Sets <modified> to the specified boolean value. + */ +mxCellEditor.prototype.setModified = function(value) +{ + this.modified = value; +}; + +/** + * Function: focusLost + * + * Called if the textarea has lost focus. + */ +mxCellEditor.prototype.focusLost = function() +{ + this.stopEditing(!this.graph.isInvokesStopCellEditing()); +}; + +/** + * Function: startEditing + * + * Starts the editor for the given cell. + * + * Parameters: + * + * cell - <mxCell> to start editing. + * trigger - Optional mouse event that triggered the editor. + */ +mxCellEditor.prototype.startEditing = function(cell, trigger) +{ + // Lazy instantiates textarea to save memory in IE + if (this.textarea == null) + { + this.init(); + } + + this.stopEditing(true); + var state = this.graph.getView().getState(cell); + + if (state != null) + { + this.editingCell = cell; + this.trigger = trigger; + this.textNode = null; + + if (state.text != null && this.isHideLabel(state)) + { + this.textNode = state.text.node; + this.textNode.style.visibility = 'hidden'; + } + + // Configures the style of the in-place editor + var scale = this.graph.getView().scale; + var size = mxUtils.getValue(state.style, mxConstants.STYLE_FONTSIZE, mxConstants.DEFAULT_FONTSIZE) * scale; + var family = mxUtils.getValue(state.style, mxConstants.STYLE_FONTFAMILY, mxConstants.DEFAULT_FONTFAMILY); + var color = mxUtils.getValue(state.style, mxConstants.STYLE_FONTCOLOR, 'black'); + var align = (this.graph.model.isEdge(state.cell)) ? mxConstants.ALIGN_LEFT : + mxUtils.getValue(state.style, mxConstants.STYLE_ALIGN, mxConstants.ALIGN_LEFT); + var bold = (mxUtils.getValue(state.style, mxConstants.STYLE_FONTSTYLE, 0) & + mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD; + var italic = (mxUtils.getValue(state.style, mxConstants.STYLE_FONTSTYLE, 0) & + mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC; + var uline = (mxUtils.getValue(state.style, mxConstants.STYLE_FONTSTYLE, 0) & + mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE; + + this.textarea.style.fontSize = size + 'px'; + this.textarea.style.fontFamily = family; + this.textarea.style.textAlign = align; + this.textarea.style.color = color; + this.textarea.style.fontWeight = (bold) ? 'bold' : 'normal'; + this.textarea.style.fontStyle = (italic) ? 'italic' : ''; + this.textarea.style.textDecoration = (uline) ? 'underline' : ''; + + // Specifies the bounds of the editor box + var bounds = this.getEditorBounds(state); + + this.textarea.style.left = bounds.x + 'px'; + this.textarea.style.top = bounds.y + 'px'; + this.textarea.style.width = bounds.width + 'px'; + this.textarea.style.height = bounds.height + 'px'; + this.textarea.style.zIndex = 5; + + var value = this.getInitialValue(state, trigger); + + // Uses an optional text value for empty labels which is cleared + // when the first keystroke appears. This makes it easier to see + // that a label is being edited even if the label is empty. + if (value == null || value.length == 0) + { + value = this.getEmptyLabelText(); + this.clearOnChange = true; + } + else + { + this.clearOnChange = false; + } + + this.setModified(false); + this.textarea.value = value; + this.graph.container.appendChild(this.textarea); + + if (this.textarea.style.display != 'none') + { + // FIXME: Doesn't bring up the virtual keyboard on iPad + this.textarea.focus(); + this.textarea.select(); + } + } +}; + +/** + * Function: stopEditing + * + * Stops the editor and applies the value if cancel is false. + */ +mxCellEditor.prototype.stopEditing = function(cancel) +{ + cancel = cancel || false; + + if (this.editingCell != null) + { + if (this.textNode != null) + { + this.textNode.style.visibility = 'visible'; + this.textNode = null; + } + + if (!cancel && this.isModified()) + { + this.graph.labelChanged(this.editingCell, this.getCurrentValue(), this.trigger); + } + + this.editingCell = null; + this.trigger = null; + this.textarea.blur(); + this.textarea.parentNode.removeChild(this.textarea); + } +}; + +/** + * Function: getInitialValue + * + * Gets the initial editing value for the given cell. + */ +mxCellEditor.prototype.getInitialValue = function(state, trigger) +{ + return this.graph.getEditingValue(state.cell, trigger); +}; + +/** + * Function: getCurrentValue + * + * Returns the current editing value. + */ +mxCellEditor.prototype.getCurrentValue = function() +{ + return this.textarea.value.replace(/\r/g, ''); +}; + +/** + * Function: isHideLabel + * + * Returns true if the label should be hidden while the cell is being + * edited. + */ +mxCellEditor.prototype.isHideLabel = function(state) +{ + return true; +}; + +/** + * Function: getMinimumSize + * + * Returns the minimum width and height for editing the given state. + */ +mxCellEditor.prototype.getMinimumSize = function(state) +{ + var scale = this.graph.getView().scale; + + return new mxRectangle(0, 0, (state.text == null) ? 30 : state.text.size * scale + 20, + (this.textarea.style.textAlign == 'left') ? 120 : 40); +}; + +/** + * Function: getEditorBounds + * + * Returns the <mxRectangle> that defines the bounds of the editor. + */ +mxCellEditor.prototype.getEditorBounds = function(state) +{ + var isEdge = this.graph.getModel().isEdge(state.cell); + var scale = this.graph.getView().scale; + var minSize = this.getMinimumSize(state); + var minWidth = minSize.width; + var minHeight = minSize.height; + + var spacing = parseInt(state.style[mxConstants.STYLE_SPACING] || 2) * scale; + var spacingTop = (parseInt(state.style[mxConstants.STYLE_SPACING_TOP] || 0)) * scale + spacing; + var spacingRight = (parseInt(state.style[mxConstants.STYLE_SPACING_RIGHT] || 0)) * scale + spacing; + var spacingBottom = (parseInt(state.style[mxConstants.STYLE_SPACING_BOTTOM] || 0)) * scale + spacing; + var spacingLeft = (parseInt(state.style[mxConstants.STYLE_SPACING_LEFT] || 0)) * scale + spacing; + + var result = new mxRectangle(state.x, state.y, + Math.max(minWidth, state.width - spacingLeft - spacingRight), + Math.max(minHeight, state.height - spacingTop - spacingBottom)); + + if (isEdge) + { + result.x = state.absoluteOffset.x; + result.y = state.absoluteOffset.y; + + if (state.text != null && state.text.boundingBox != null) + { + // Workaround for label containing just spaces in which case + // the bounding box location contains negative numbers + if (state.text.boundingBox.x > 0) + { + result.x = state.text.boundingBox.x; + } + + if (state.text.boundingBox.y > 0) + { + result.y = state.text.boundingBox.y; + } + } + } + else if (state.text != null && state.text.boundingBox != null) + { + result.x = Math.min(result.x, state.text.boundingBox.x); + result.y = Math.min(result.y, state.text.boundingBox.y); + } + + result.x += spacingLeft; + result.y += spacingTop; + + if (state.text != null && state.text.boundingBox != null) + { + if (!isEdge) + { + result.width = Math.max(result.width, state.text.boundingBox.width); + result.height = Math.max(result.height, state.text.boundingBox.height); + } + else + { + result.width = Math.max(minWidth, state.text.boundingBox.width); + result.height = Math.max(minHeight, state.text.boundingBox.height); + } + } + + // Applies the horizontal and vertical label positions + if (this.graph.getModel().isVertex(state.cell)) + { + var horizontal = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_POSITION, mxConstants.ALIGN_CENTER); + + if (horizontal == mxConstants.ALIGN_LEFT) + { + result.x -= state.width; + } + else if (horizontal == mxConstants.ALIGN_RIGHT) + { + result.x += state.width; + } + + var vertical = mxUtils.getValue(state.style, mxConstants.STYLE_VERTICAL_LABEL_POSITION, mxConstants.ALIGN_MIDDLE); + + if (vertical == mxConstants.ALIGN_TOP) + { + result.y -= state.height; + } + else if (vertical == mxConstants.ALIGN_BOTTOM) + { + result.y += state.height; + } + } + + return result; +}; + +/** + * Function: getEmptyLabelText + * + * Returns the initial label value to be used of the label of the given + * cell is empty. This label is displayed and cleared on the first keystroke. + * This implementation returns <emptyLabelText>. + * + * Parameters: + * + * cell - <mxCell> for which a text for an empty editing box should be + * returned. + */ +mxCellEditor.prototype.getEmptyLabelText = function (cell) +{ + return this.emptyLabelText; +}; + +/** + * Function: getEditingCell + * + * Returns the cell that is currently being edited or null if no cell is + * being edited. + */ +mxCellEditor.prototype.getEditingCell = function () +{ + return this.editingCell; +}; + +/** + * Function: destroy + * + * Destroys the editor and removes all associated resources. + */ +mxCellEditor.prototype.destroy = function () +{ + if (this.textarea != null) + { + mxEvent.release(this.textarea); + + if (this.textarea.parentNode != null) + { + this.textarea.parentNode.removeChild(this.textarea); + } + + this.textarea = null; + } +}; diff --git a/src/js/view/mxCellOverlay.js b/src/js/view/mxCellOverlay.js new file mode 100644 index 0000000..316e2c4 --- /dev/null +++ b/src/js/view/mxCellOverlay.js @@ -0,0 +1,233 @@ +/** + * $Id: mxCellOverlay.js,v 1.18 2012-12-06 15:58:44 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxCellOverlay + * + * Extends <mxEventSource> to implement a graph overlay, represented by an icon + * and a tooltip. Overlays can handle and fire <click> events and are added to + * the graph using <mxGraph.addCellOverlay>, and removed using + * <mxGraph.removeCellOverlay>, or <mxGraph.removeCellOverlays> to remove all overlays. + * The <mxGraph.getCellOverlays> function returns the array of overlays for a given + * cell in a graph. If multiple overlays exist for the same cell, then + * <getBounds> should be overridden in at least one of the overlays. + * + * Overlays appear on top of all cells in a special layer. If this is not + * desirable, then the image must be rendered as part of the shape or label of + * the cell instead. + * + * Example: + * + * The following adds a new overlays for a given vertex and selects the cell + * if the overlay is clicked. + * + * (code) + * var overlay = new mxCellOverlay(img, html); + * graph.addCellOverlay(vertex, overlay); + * overlay.addListener(mxEvent.CLICK, function(sender, evt) + * { + * var cell = evt.getProperty('cell'); + * graph.setSelectionCell(cell); + * }); + * (end) + * + * For cell overlays to be printed use <mxPrintPreview.printOverlays>. + * + * Event: mxEvent.CLICK + * + * Fires when the user clicks on the overlay. The <code>event</code> property + * contains the corresponding mouse event and the <code>cell</code> property + * contains the cell. For touch devices this is fired if the element receives + * a touchend event. + * + * Constructor: mxCellOverlay + * + * Constructs a new overlay using the given image and tooltip. + * + * Parameters: + * + * image - <mxImage> that represents the icon to be displayed. + * tooltip - Optional string that specifies the tooltip. + * align - Optional horizontal alignment for the overlay. Possible + * values are <ALIGN_LEFT>, <ALIGN_CENTER> and <ALIGN_RIGHT> + * (default). + * verticalAlign - Vertical alignment for the overlay. Possible + * values are <ALIGN_TOP>, <ALIGN_MIDDLE> and <ALIGN_BOTTOM> + * (default). + */ +function mxCellOverlay(image, tooltip, align, verticalAlign, offset, cursor) +{ + this.image = image; + this.tooltip = tooltip; + this.align = (align != null) ? align : this.align; + this.verticalAlign = (verticalAlign != null) ? verticalAlign : this.verticalAlign; + this.offset = (offset != null) ? offset : new mxPoint(); + this.cursor = (cursor != null) ? cursor : 'help'; +}; + +/** + * Extends mxEventSource. + */ +mxCellOverlay.prototype = new mxEventSource(); +mxCellOverlay.prototype.constructor = mxCellOverlay; + +/** + * Variable: image + * + * Holds the <mxImage> to be used as the icon. + */ +mxCellOverlay.prototype.image = null; + +/** + * Variable: tooltip + * + * Holds the optional string to be used as the tooltip. + */ +mxCellOverlay.prototype.tooltip = null; + +/** + * Variable: align + * + * Holds the horizontal alignment for the overlay. Default is + * <mxConstants.ALIGN_RIGHT>. For edges, the overlay always appears in the + * center of the edge. + */ +mxCellOverlay.prototype.align = mxConstants.ALIGN_RIGHT; + +/** + * Variable: verticalAlign + * + * Holds the vertical alignment for the overlay. Default is + * <mxConstants.ALIGN_BOTTOM>. For edges, the overlay always appears in the + * center of the edge. + */ +mxCellOverlay.prototype.verticalAlign = mxConstants.ALIGN_BOTTOM; + +/** + * Variable: offset + * + * Holds the offset as an <mxPoint>. The offset will be scaled according to the + * current scale. + */ +mxCellOverlay.prototype.offset = null; + +/** + * Variable: cursor + * + * Holds the cursor for the overlay. Default is 'help'. + */ +mxCellOverlay.prototype.cursor = null; + +/** + * Variable: defaultOverlap + * + * Defines the overlapping for the overlay, that is, the proportional distance + * from the origin to the point defined by the alignment. Default is 0.5. + */ +mxCellOverlay.prototype.defaultOverlap = 0.5; + +/** + * Function: getBounds + * + * Returns the bounds of the overlay for the given <mxCellState> as an + * <mxRectangle>. This should be overridden when using multiple overlays + * per cell so that the overlays do not overlap. + * + * The following example will place the overlay along an edge (where + * x=[-1..1] from the start to the end of the edge and y is the + * orthogonal offset in px). + * + * (code) + * overlay.getBounds = function(state) + * { + * var bounds = mxCellOverlay.prototype.getBounds.apply(this, arguments); + * + * if (state.view.graph.getModel().isEdge(state.cell)) + * { + * var pt = state.view.getPoint(state, {x: 0, y: 0, relative: true}); + * + * bounds.x = pt.x - bounds.width / 2; + * bounds.y = pt.y - bounds.height / 2; + * } + * + * return bounds; + * }; + * (end) + * + * Parameters: + * + * state - <mxCellState> that represents the current state of the + * associated cell. + */ +mxCellOverlay.prototype.getBounds = function(state) +{ + var isEdge = state.view.graph.getModel().isEdge(state.cell); + var s = state.view.scale; + var pt = null; + + var w = this.image.width; + var h = this.image.height; + + if (isEdge) + { + var pts = state.absolutePoints; + + if (pts.length % 2 == 1) + { + pt = pts[Math.floor(pts.length / 2)]; + } + else + { + var idx = pts.length / 2; + var p0 = pts[idx-1]; + var p1 = pts[idx]; + pt = new mxPoint(p0.x + (p1.x - p0.x) / 2, + p0.y + (p1.y - p0.y) / 2); + } + } + else + { + pt = new mxPoint(); + + if (this.align == mxConstants.ALIGN_LEFT) + { + pt.x = state.x; + } + else if (this.align == mxConstants.ALIGN_CENTER) + { + pt.x = state.x + state.width / 2; + } + else + { + pt.x = state.x + state.width; + } + + if (this.verticalAlign == mxConstants.ALIGN_TOP) + { + pt.y = state.y; + } + else if (this.verticalAlign == mxConstants.ALIGN_MIDDLE) + { + pt.y = state.y + state.height / 2; + } + else + { + pt.y = state.y + state.height; + } + } + + return new mxRectangle(pt.x - (w * this.defaultOverlap - this.offset.x) * s, + pt.y - (h * this.defaultOverlap - this.offset.y) * s, w * s, h * s); +}; + +/** + * Function: toString + * + * Returns the textual representation of the overlay to be used as the + * tooltip. This implementation returns <tooltip>. + */ +mxCellOverlay.prototype.toString = function() +{ + return this.tooltip; +}; diff --git a/src/js/view/mxCellRenderer.js b/src/js/view/mxCellRenderer.js new file mode 100644 index 0000000..6b506ad --- /dev/null +++ b/src/js/view/mxCellRenderer.js @@ -0,0 +1,1480 @@ +/** + * $Id: mxCellRenderer.js,v 1.189 2012-11-20 09:06:07 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxCellRenderer + * + * Renders cells into a document object model. The <defaultShapes> is a global + * map of shapename, constructor pairs that is used in all instances. You can + * get a list of all available shape names using the following code. + * + * In general the cell renderer is in charge of creating, redrawing and + * destroying the shape and label associated with a cell state, as well as + * some other graphical objects, namely controls and overlays. The shape + * hieararchy in the display (ie. the hierarchy in which the DOM nodes + * appear in the document) does not reflect the cell hierarchy. The shapes + * are a (flat) sequence of shapes and labels inside the draw pane of the + * graph view, with some exceptions, namely the HTML labels being placed + * directly inside the graph container for certain browsers. + * + * (code) + * mxLog.show(); + * for (var i in mxCellRenderer.prototype.defaultShapes) + * { + * mxLog.debug(i); + * } + * (end) + * + * Constructor: mxCellRenderer + * + * Constructs a new cell renderer with the following built-in shapes: + * arrow, rectangle, ellipse, rhombus, image, line, label, cylinder, + * swimlane, connector, actor and cloud. + */ +function mxCellRenderer() +{ + this.shapes = mxUtils.clone(this.defaultShapes); +}; + +/** + * Variable: shapes + * + * Array that maps from shape names to shape constructors. All entries + * in <defaultShapes> are added to this array. + */ +mxCellRenderer.prototype.shapes = null; + +/** + * Variable: defaultEdgeShape + * + * Defines the default shape for edges. Default is <mxConnector>. + */ +mxCellRenderer.prototype.defaultEdgeShape = mxConnector; + +/** + * Variable: defaultVertexShape + * + * Defines the default shape for vertices. Default is <mxRectangleShape>. + */ +mxCellRenderer.prototype.defaultVertexShape = mxRectangleShape; + +/** + * Variable: defaultShapes + * + * Static array that contains the globally registered shapes which are + * known to all instances of this class. For adding instance-specific + * shapes you should use <registerShape> on the instance. For adding + * a shape to this array you can use the following code: + * + * (code) + * mxCellRenderer.prototype.defaultShapes['myshape'] = myShape; + * (end) + * + * Where 'myshape' is the key under which the shape is to be registered + * and myShape is the name of the constructor function. + */ +mxCellRenderer.prototype.defaultShapes = new Object(); + +// Adds default shapes into the default shapes array +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_ARROW] = mxArrow; +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_RECTANGLE] = mxRectangleShape; +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_ELLIPSE] = mxEllipse; +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_DOUBLE_ELLIPSE] = mxDoubleEllipse; +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_RHOMBUS] = mxRhombus; +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_IMAGE] = mxImageShape; +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_LINE] = mxLine; +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_LABEL] = mxLabel; +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_CYLINDER] = mxCylinder; +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_SWIMLANE] = mxSwimlane; +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_CONNECTOR] = mxConnector; +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_ACTOR] = mxActor; +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_CLOUD] = mxCloud; +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_TRIANGLE] = mxTriangle; +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_HEXAGON] = mxHexagon; + +/** + * Function: registerShape + * + * Registers the given constructor under the specified key in this instance + * of the renderer. + * + * Example: + * + * (code) + * this.registerShape(mxConstants.SHAPE_RECTANGLE, mxRectangleShape); + * (end) + * + * Parameters: + * + * key - String representing the shape name. + * shape - Constructor of the <mxShape> subclass. + */ +mxCellRenderer.prototype.registerShape = function(key, shape) +{ + this.shapes[key] = shape; +}; + +/** + * Function: initialize + * + * Initializes the display for the given cell state. This is required once + * after the cell state has been created. This is invoked in + * mxGraphView.createState. + * + * Parameters: + * + * state - <mxCellState> for which the display should be initialized. + * rendering - Optional boolean that specifies if the cell should actually + * be initialized for any given DOM node. If this is false then init + * will not be called on the shape. + */ +mxCellRenderer.prototype.initialize = function(state, rendering) +{ + var model = state.view.graph.getModel(); + + if (state.view.graph.container != null && state.shape == null && + state.cell != state.view.currentRoot && + (model.isVertex(state.cell) || model.isEdge(state.cell))) + { + this.createShape(state); + + if (state.shape != null && (rendering == null || rendering)) + { + this.initializeShape(state); + + // Maintains the model order in the DOM + if (state.view.graph.ordered || model.isEdge(state.cell)) + { + //state.orderChanged = true; + state.invalidOrder = true; + } + else if (state.view.graph.keepEdgesInForeground && this.firstEdge != null) + { + if (this.firstEdge.parentNode == state.shape.node.parentNode) + { + this.insertState(state, this.firstEdge); + } + else + { + this.firstEdge = null; + } + } + + state.shape.scale = state.view.scale; + + this.createCellOverlays(state); + this.installListeners(state); + } + } +}; + +/** + * Function: initializeShape + * + * Initializes the shape in the given state by calling its init method with + * the correct container. + * + * Parameters: + * + * state - <mxCellState> for which the shape should be initialized. + */ +mxCellRenderer.prototype.initializeShape = function(state) +{ + state.shape.init(state.view.getDrawPane()); +}; + +/** + * Returns the previous state that has a shape inside the given parent. + */ +mxCellRenderer.prototype.getPreviousStateInContainer = function(state, container) +{ + var result = null; + var graph = state.view.graph; + var model = graph.getModel(); + var child = state.cell; + var p = model.getParent(child); + + while (p != null && result == null) + { + result = this.findPreviousStateInContainer(graph, p, child, container); + child = p; + p = model.getParent(child); + } + + return result; +}; + +/** + * Returns the previous state that has a shape inside the given parent. + */ +mxCellRenderer.prototype.findPreviousStateInContainer = function(graph, cell, stop, container) +{ + // Recurse first + var result = null; + var model = graph.getModel(); + + if (stop != null) + { + var start = cell.getIndex(stop); + + for (var i = start - 1; i >= 0 && result == null; i--) + { + result = this.findPreviousStateInContainer(graph, model.getChildAt(cell, i), null, container); + } + } + else + { + var childCount = model.getChildCount(cell); + + for (var i = childCount - 1; i >= 0 && result == null; i--) + { + result = this.findPreviousStateInContainer(graph, model.getChildAt(cell, i), null, container); + } + } + + if (result == null) + { + result = graph.view.getState(cell); + + if (result != null && (result.shape == null || result.shape.node == null || + result.shape.node.parentNode != container)) + { + result = null; + } + } + + return result; +}; + +/** + * Function: order + * + * Orders the DOM node of the shape for the given state according to the + * position of the corresponding cell in the graph model. + * + * Parameters: + * + * state - <mxCellState> whose shape's DOM node should be ordered. + */ +mxCellRenderer.prototype.order = function(state) +{ + var container = state.shape.node.parentNode; + var previous = this.getPreviousStateInContainer(state, container); + var nextNode = container.firstChild; + + if (previous != null) + { + nextNode = previous.shape.node; + + if (previous.text != null && previous.text.node != null && + previous.text.node.parentNode == container) + { + nextNode = previous.text.node; + } + + nextNode = nextNode.nextSibling; + } + + this.insertState(state, nextNode); +}; + +/** + * Function: orderEdge + * + * Orders the DOM node of the shape for the given edge's state according to + * the <mxGraph.keepEdgesInBackground> and <mxGraph.keepEdgesInBackground> + * rules. + * + * Parameters: + * + * state - <mxCellState> whose shape's DOM node should be ordered. + */ +mxCellRenderer.prototype.orderEdge = function(state) +{ + var view = state.view; + var model = view.graph.getModel(); + + // Moves edges to the foreground/background + if (view.graph.keepEdgesInForeground) + { + if (this.firstEdge == null || this.firstEdge.parentNode == null || + this.firstEdge.parentNode != state.shape.node.parentNode) + { + this.firstEdge = state.shape.node; + } + } + else if (view.graph.keepEdgesInBackground) + { + var node = state.shape.node; + var parent = node.parentNode; + + // Keeps the DOM node in front of its parent + var pcell = model.getParent(state.cell); + var pstate = view.getState(pcell); + + if (pstate != null && pstate.shape != null && pstate.shape.node != null) + { + var child = pstate.shape.node.nextSibling; + + if (child != null && child != node) + { + this.insertState(state, child); + } + } + else + { + var child = parent.firstChild; + + if (child != null && child != node) + { + this.insertState(state, child); + } + } + } +}; + +/** + * Function: insertState + * + * Inserts the given state before the given node into its parent. + * + * Parameters: + * + * state - <mxCellState> for which the shape should be created. + */ +mxCellRenderer.prototype.insertState = function(state, nextNode) +{ + state.shape.node.parentNode.insertBefore(state.shape.node, nextNode); + + if (state.text != null && state.text.node != null && + state.text.node.parentNode == state.shape.node.parentNode) + { + state.shape.node.parentNode.insertBefore(state.text.node, state.shape.node.nextSibling); + } +}; + +/** + * Function: createShape + * + * Creates the shape for the given cell state. The shape is configured + * using <configureShape>. + * + * Parameters: + * + * state - <mxCellState> for which the shape should be created. + */ +mxCellRenderer.prototype.createShape = function(state) +{ + if (state.style != null) + { + // Checks if there is a stencil for the name and creates + // a shape instance for the stencil if one exists + var key = state.style[mxConstants.STYLE_SHAPE]; + var stencil = mxStencilRegistry.getStencil(key); + + if (stencil != null) + { + state.shape = new mxStencilShape(stencil); + } + else + { + var ctor = this.getShapeConstructor(state); + state.shape = new ctor(); + } + + // Sets the initial bounds and points (will be updated in redraw) + state.shape.points = state.absolutePoints; + state.shape.bounds = new mxRectangle( + state.x, state.y, state.width, state.height); + state.shape.dialect = state.view.graph.dialect; + + this.configureShape(state); + } +}; + +/** + * Function: getShapeConstructor + * + * Returns the constructor to be used for creating the shape. + */ +mxCellRenderer.prototype.getShapeConstructor = function(state) +{ + var key = state.style[mxConstants.STYLE_SHAPE]; + var ctor = (key != null) ? this.shapes[key] : null; + + if (ctor == null) + { + ctor = (state.view.graph.getModel().isEdge(state.cell)) ? + this.defaultEdgeShape : this.defaultVertexShape; + } + + return ctor; +}; + +/** + * Function: configureShape + * + * Configures the shape for the given cell state. + * + * Parameters: + * + * state - <mxCellState> for which the shape should be configured. + */ +mxCellRenderer.prototype.configureShape = function(state) +{ + state.shape.apply(state); + var image = state.view.graph.getImage(state); + + if (image != null) + { + state.shape.image = image; + } + + var indicator = state.view.graph.getIndicatorColor(state); + var key = state.view.graph.getIndicatorShape(state); + var ctor = (key != null) ? this.shapes[key] : null; + + // Configures the indicator shape or image + if (indicator != null) + { + state.shape.indicatorShape = ctor; + state.shape.indicatorColor = indicator; + state.shape.indicatorGradientColor = + state.view.graph.getIndicatorGradientColor(state); + state.shape.indicatorDirection = + state.style[mxConstants.STYLE_INDICATOR_DIRECTION]; + } + else + { + var indicator = state.view.graph.getIndicatorImage(state); + + if (indicator != null) + { + state.shape.indicatorImage = indicator; + } + } + + this.postConfigureShape(state); +}; + +/** + * Function: postConfigureShape + * + * Replaces any reserved words used for attributes, eg. inherit, + * indicated or swimlane for colors in the shape for the given state. + * This implementation resolves these keywords on the fill, stroke + * and gradient color keys. + */ +mxCellRenderer.prototype.postConfigureShape = function(state) +{ + if (state.shape != null) + { + this.resolveColor(state, 'indicatorColor', mxConstants.STYLE_FILLCOLOR); + this.resolveColor(state, 'indicatorGradientColor', mxConstants.STYLE_GRADIENTCOLOR); + this.resolveColor(state, 'fill', mxConstants.STYLE_FILLCOLOR); + this.resolveColor(state, 'stroke', mxConstants.STYLE_STROKECOLOR); + this.resolveColor(state, 'gradient', mxConstants.STYLE_GRADIENTCOLOR); + } +}; + +/** + * Function: resolveColor + * + * Resolves special keywords 'inherit', 'indicated' and 'swimlane' and sets + * the respective color on the shape. + */ +mxCellRenderer.prototype.resolveColor = function(state, field, key) +{ + var value = state.shape[field]; + var graph = state.view.graph; + var referenced = null; + + if (value == 'inherit') + { + referenced = graph.model.getParent(state.cell); + } + else if (value == 'swimlane') + { + if (graph.model.getTerminal(state.cell, false) != null) + { + referenced = graph.model.getTerminal(state.cell, false); + } + else + { + referenced = state.cell; + } + + referenced = graph.getSwimlane(referenced); + key = graph.swimlaneIndicatorColorAttribute; + } + else if (value == 'indicated') + { + state.shape[field] = state.shape.indicatorColor; + } + + if (referenced != null) + { + var rstate = graph.getView().getState(referenced); + state.shape[field] = null; + + if (rstate != null) + { + if (rstate.shape != null && field != 'indicatorColor') + { + state.shape[field] = rstate.shape[field]; + } + else + { + state.shape[field] = rstate.style[key]; + } + } + } +}; + +/** + * Function: getLabelValue + * + * Returns the value to be used for the label. + * + * Parameters: + * + * state - <mxCellState> for which the label should be created. + */ +mxCellRenderer.prototype.getLabelValue = function(state) +{ + var graph = state.view.graph; + var value = graph.getLabel(state.cell); + + if (!graph.isHtmlLabel(state.cell) && !mxUtils.isNode(value) && + graph.dialect != mxConstants.DIALECT_SVG && value != null) + { + value = mxUtils.htmlEntities(value, false); + } + + return value; +}; + +/** + * Function: createLabel + * + * Creates the label for the given cell state. + * + * Parameters: + * + * state - <mxCellState> for which the label should be created. + */ +mxCellRenderer.prototype.createLabel = function(state, value) +{ + var graph = state.view.graph; + var isEdge = graph.getModel().isEdge(state.cell); + + if (state.style[mxConstants.STYLE_FONTSIZE] > 0 || + state.style[mxConstants.STYLE_FONTSIZE] == null) + { + // Avoids using DOM node for empty labels + var isForceHtml = (graph.isHtmlLabel(state.cell) || + (value != null && mxUtils.isNode(value))) && + graph.dialect == mxConstants.DIALECT_SVG; + + state.text = new mxText(value, new mxRectangle(), + (state.style[mxConstants.STYLE_ALIGN] || + mxConstants.ALIGN_CENTER), + graph.getVerticalAlign(state), + state.style[mxConstants.STYLE_FONTCOLOR], + state.style[mxConstants.STYLE_FONTFAMILY], + state.style[mxConstants.STYLE_FONTSIZE], + state.style[mxConstants.STYLE_FONTSTYLE], + state.style[mxConstants.STYLE_SPACING], + state.style[mxConstants.STYLE_SPACING_TOP], + state.style[mxConstants.STYLE_SPACING_RIGHT], + state.style[mxConstants.STYLE_SPACING_BOTTOM], + state.style[mxConstants.STYLE_SPACING_LEFT], + state.style[mxConstants.STYLE_HORIZONTAL], + state.style[mxConstants.STYLE_LABEL_BACKGROUNDCOLOR], + state.style[mxConstants.STYLE_LABEL_BORDERCOLOR], + graph.isWrapping(state.cell) && graph.isHtmlLabel(state.cell), + graph.isLabelClipped(state.cell), + state.style[mxConstants.STYLE_OVERFLOW], + state.style[mxConstants.STYLE_LABEL_PADDING]); + state.text.opacity = state.style[mxConstants.STYLE_TEXT_OPACITY]; + + state.text.dialect = (isForceHtml) ? + mxConstants.DIALECT_STRICTHTML : + state.view.graph.dialect; + this.initializeLabel(state); + + // Workaround for touch devices routing all events for a mouse + // gesture (down, move, up) via the initial DOM node. IE is even + // worse in that it redirects the event via the initial DOM node + // but the event source is the node under the mouse, so we need + // to check if this is the case and force getCellAt for the + // subsequent mouseMoves and the final mouseUp. + var forceGetCell = false; + + var getState = function(evt) + { + var result = state; + + if (mxClient.IS_TOUCH || forceGetCell) + { + var x = mxEvent.getClientX(evt); + var y = mxEvent.getClientY(evt); + + // Dispatches the drop event to the graph which + // consumes and executes the source function + var pt = mxUtils.convertPoint(graph.container, x, y); + result = graph.view.getState(graph.getCellAt(pt.x, pt.y)); + } + + return result; + }; + + // TODO: Add handling for gestures + var md = (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown'; + var mm = (mxClient.IS_TOUCH) ? 'touchmove' : 'mousemove'; + var mu = (mxClient.IS_TOUCH) ? 'touchend' : 'mouseup'; + + mxEvent.addListener(state.text.node, md, + mxUtils.bind(this, function(evt) + { + if (this.isLabelEvent(state, evt)) + { + graph.fireMouseEvent(mxEvent.MOUSE_DOWN, + new mxMouseEvent(evt, state)); + forceGetCell = graph.dialect != mxConstants.DIALECT_SVG && mxEvent.getSource(evt).nodeName == 'IMG'; + } + }) + ); + + mxEvent.addListener(state.text.node, mm, + mxUtils.bind(this, function(evt) + { + if (this.isLabelEvent(state, evt)) + { + graph.fireMouseEvent(mxEvent.MOUSE_MOVE, + new mxMouseEvent(evt, getState(evt))); + } + }) + ); + + mxEvent.addListener(state.text.node, mu, + mxUtils.bind(this, function(evt) + { + if (this.isLabelEvent(state, evt)) + { + graph.fireMouseEvent(mxEvent.MOUSE_UP, + new mxMouseEvent(evt, getState(evt))); + forceGetCell = false; + } + }) + ); + + mxEvent.addListener(state.text.node, 'dblclick', + mxUtils.bind(this, function(evt) + { + if (this.isLabelEvent(state, evt)) + { + graph.dblClick(evt, state.cell); + mxEvent.consume(evt); + } + }) + ); + } +}; + +/** + * Function: initializeLabel + * + * Initiailzes the label with a suitable container. + * + * Parameters: + * + * state - <mxCellState> whose label should be initialized. + */ +mxCellRenderer.prototype.initializeLabel = function(state) +{ + var graph = state.view.graph; + + if (state.text.dialect != mxConstants.DIALECT_SVG) + { + // Adds the text to the container if the dialect is not SVG and we + // have an SVG-based browser which doesn't support foreignObjects + if (mxClient.IS_SVG && mxClient.NO_FO) + { + state.text.init(graph.container); + } + else if (mxUtils.isVml(state.view.getDrawPane())) + { + if (state.shape.label != null) + { + state.text.init(state.shape.label); + } + else + { + state.text.init(state.shape.node); + } + } + } + + if (state.text.node == null) + { + state.text.init(state.view.getDrawPane()); + + if (state.shape != null && state.text != null) + { + state.shape.node.parentNode.insertBefore( + state.text.node, state.shape.node.nextSibling); + } + } +}; + +/** + * Function: createCellOverlays + * + * Creates the actual shape for showing the overlay for the given cell state. + * + * Parameters: + * + * state - <mxCellState> for which the overlay should be created. + */ +mxCellRenderer.prototype.createCellOverlays = function(state) +{ + var graph = state.view.graph; + var overlays = graph.getCellOverlays(state.cell); + var dict = null; + + if (overlays != null) + { + dict = new mxDictionary(); + + for (var i = 0; i < overlays.length; i++) + { + var shape = (state.overlays != null) ? state.overlays.remove(overlays[i]) : null; + + if (shape == null) + { + var tmp = new mxImageShape(new mxRectangle(), + overlays[i].image.src); + tmp.dialect = state.view.graph.dialect; + tmp.preserveImageAspect = false; + tmp.overlay = overlays[i]; + this.initializeOverlay(state, tmp); + this.installCellOverlayListeners(state, overlays[i], tmp); + + if (overlays[i].cursor != null) + { + tmp.node.style.cursor = overlays[i].cursor; + } + + dict.put(overlays[i], tmp); + } + else + { + dict.put(overlays[i], shape); + } + } + } + + // Removes unused + if (state.overlays != null) + { + state.overlays.visit(function(id, shape) + { + shape.destroy(); + }); + } + + state.overlays = dict; +}; + +/** + * Function: initializeOverlay + * + * Initializes the given overlay. + * + * Parameters: + * + * state - <mxCellState> for which the overlay should be created. + * overlay - <mxImageShape> that represents the overlay. + */ +mxCellRenderer.prototype.initializeOverlay = function(state, overlay) +{ + overlay.init(state.view.getOverlayPane()); +}; + +/** + * Function: installOverlayListeners + * + * Installs the listeners for the given <mxCellState>, <mxCellOverlay> and + * <mxShape> that represents the overlay. + */ +mxCellRenderer.prototype.installCellOverlayListeners = function(state, overlay, shape) +{ + var graph = state.view.graph; + + mxEvent.addListener(shape.node, 'click', function (evt) + { + if (graph.isEditing()) + { + graph.stopEditing(!graph.isInvokesStopCellEditing()); + } + + overlay.fireEvent(new mxEventObject(mxEvent.CLICK, + 'event', evt, 'cell', state.cell)); + }); + + var md = (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown'; + var mm = (mxClient.IS_TOUCH) ? 'touchmove' : 'mousemove'; + + mxEvent.addListener(shape.node, md, function (evt) + { + mxEvent.consume(evt); + }); + + mxEvent.addListener(shape.node, mm, function (evt) + { + graph.fireMouseEvent(mxEvent.MOUSE_MOVE, + new mxMouseEvent(evt, state)); + }); + + if (mxClient.IS_TOUCH) + { + mxEvent.addListener(shape.node, 'touchend', function (evt) + { + overlay.fireEvent(new mxEventObject(mxEvent.CLICK, + 'event', evt, 'cell', state.cell)); + }); + } +}; + +/** + * Function: createControl + * + * Creates the control for the given cell state. + * + * Parameters: + * + * state - <mxCellState> for which the control should be created. + */ +mxCellRenderer.prototype.createControl = function(state) +{ + var graph = state.view.graph; + var image = graph.getFoldingImage(state); + + if (graph.foldingEnabled && image != null) + { + if (state.control == null) + { + var b = new mxRectangle(0, 0, image.width, image.height); + state.control = new mxImageShape(b, image.src); + state.control.dialect = graph.dialect; + state.control.preserveImageAspect = false; + + this.initControl(state, state.control, true, function (evt) + { + if (graph.isEnabled()) + { + var collapse = !graph.isCellCollapsed(state.cell); + graph.foldCells(collapse, false, [state.cell]); + mxEvent.consume(evt); + } + }); + } + } + else if (state.control != null) + { + state.control.destroy(); + state.control = null; + } +}; + +/** + * Function: initControl + * + * Initializes the given control and returns the corresponding DOM node. + * + * Parameters: + * + * state - <mxCellState> for which the control should be initialized. + * control - <mxShape> to be initialized. + * handleEvents - Boolean indicating if mousedown and mousemove should fire events via the graph. + * clickHandler - Optional function to implement clicks on the control. + */ +mxCellRenderer.prototype.initControl = function(state, control, handleEvents, clickHandler) +{ + var graph = state.view.graph; + + // In the special case where the label is in HTML and the display is SVG the image + // should go into the graph container directly in order to be clickable. Otherwise + // it is obscured by the HTML label that overlaps the cell. + var isForceHtml = graph.isHtmlLabel(state.cell) && + mxClient.NO_FO && + graph.dialect == mxConstants.DIALECT_SVG; + + if (isForceHtml) + { + control.dialect = mxConstants.DIALECT_PREFERHTML; + control.init(graph.container); + control.node.style.zIndex = 1; + } + else + { + control.init(state.view.getOverlayPane()); + } + + var node = control.innerNode || control.node; + + if (clickHandler) + { + if (graph.isEnabled()) + { + node.style.cursor = 'pointer'; + } + + mxEvent.addListener(node, 'click', clickHandler); + } + + if (handleEvents) + { + var md = (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown'; + var mm = (mxClient.IS_TOUCH) ? 'touchmove' : 'mousemove'; + + mxEvent.addListener(node, md, function (evt) + { + graph.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt, state)); + mxEvent.consume(evt); + }); + + mxEvent.addListener(node, mm, function (evt) + { + graph.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt, state)); + }); + } + + return node; +}; + +/** + * Function: isShapeEvent + * + * Returns true if the event is for the shape of the given state. This + * implementation always returns true. + * + * Parameters: + * + * state - <mxCellState> whose shape fired the event. + * evt - Mouse event which was fired. + */ +mxCellRenderer.prototype.isShapeEvent = function(state, evt) +{ + return true; +}; + +/** + * Function: isLabelEvent + * + * Returns true if the event is for the label of the given state. This + * implementation always returns true. + * + * Parameters: + * + * state - <mxCellState> whose label fired the event. + * evt - Mouse event which was fired. + */ +mxCellRenderer.prototype.isLabelEvent = function(state, evt) +{ + return true; +}; + +/** + * Function: installListeners + * + * Installs the event listeners for the given cell state. + * + * Parameters: + * + * state - <mxCellState> for which the event listeners should be isntalled. + */ +mxCellRenderer.prototype.installListeners = function(state) +{ + var graph = state.view.graph; + + // Receives events from transparent backgrounds + if (graph.dialect == mxConstants.DIALECT_SVG) + { + var events = 'all'; + + // Disabled fill-events on non-filled edges + if (graph.getModel().isEdge(state.cell) && state.shape.stroke != null && + (state.shape.fill == null || state.shape.fill == mxConstants.NONE)) + { + events = 'visibleStroke'; + } + + // Specifies the event-processing on the shape + if (state.shape.innerNode != null) + { + state.shape.innerNode.setAttribute('pointer-events', events); + } + else + { + state.shape.node.setAttribute('pointer-events', events); + } + } + + // Workaround for touch devices routing all events for a mouse + // gesture (down, move, up) via the initial DOM node. Same for + // HTML images in all IE versions (VML images are working). + var getState = function(evt) + { + var result = state; + + if ((graph.dialect != mxConstants.DIALECT_SVG && mxEvent.getSource(evt).nodeName == 'IMG') || mxClient.IS_TOUCH) + { + var x = mxEvent.getClientX(evt); + var y = mxEvent.getClientY(evt); + + // Dispatches the drop event to the graph which + // consumes and executes the source function + var pt = mxUtils.convertPoint(graph.container, x, y); + result = graph.view.getState(graph.getCellAt(pt.x, pt.y)); + } + + return result; + }; + + // Experimental support for two-finger pinch to resize cells + var gestureInProgress = false; + + mxEvent.addListener(state.shape.node, 'gesturestart', + mxUtils.bind(this, function(evt) + { + // FIXME: Breaks encapsulation to reset the double + // tap event handling when gestures take place + graph.lastTouchTime = 0; + + gestureInProgress = true; + mxEvent.consume(evt); + }) + ); + + var md = (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown'; + var mm = (mxClient.IS_TOUCH) ? 'touchmove' : 'mousemove'; + var mu = (mxClient.IS_TOUCH) ? 'touchend' : 'mouseup'; + + mxEvent.addListener(state.shape.node, md, + mxUtils.bind(this, function(evt) + { + if (this.isShapeEvent(state, evt) && !gestureInProgress) + { + // Redirects events from the "event-transparent" region of + // a swimlane to the graph. This is only required in HTML, + // SVG and VML do not fire mouse events on transparent + // backgrounds. + graph.fireMouseEvent(mxEvent.MOUSE_DOWN, + new mxMouseEvent(evt, (state.shape != null && + mxEvent.getSource(evt) == state.shape.content) ? + null : state)); + } + else if (gestureInProgress) + { + mxEvent.consume(evt); + } + }) + ); + + mxEvent.addListener(state.shape.node, mm, + mxUtils.bind(this, function(evt) + { + if (this.isShapeEvent(state, evt) && !gestureInProgress) + { + graph.fireMouseEvent(mxEvent.MOUSE_MOVE, + new mxMouseEvent(evt, (state.shape != null && + mxEvent.getSource(evt) == state.shape.content) ? + null : getState(evt))); + } + else if (gestureInProgress) + { + mxEvent.consume(evt); + } + }) + ); + + mxEvent.addListener(state.shape.node, mu, + mxUtils.bind(this, function(evt) + { + if (this.isShapeEvent(state, evt) && !gestureInProgress) + { + graph.fireMouseEvent(mxEvent.MOUSE_UP, + new mxMouseEvent(evt, (state.shape != null && + mxEvent.getSource(evt) == state.shape.content) ? + null : getState(evt))); + } + else if (gestureInProgress) + { + mxEvent.consume(evt); + } + }) + ); + + // Experimental handling for gestures. Double-tap handling is implemented + // in mxGraph.fireMouseEvent. + var dc = (mxClient.IS_TOUCH) ? 'gestureend' : 'dblclick'; + + mxEvent.addListener(state.shape.node, dc, + mxUtils.bind(this, function(evt) + { + gestureInProgress = false; + + if (dc == 'gestureend') + { + // FIXME: Breaks encapsulation to reset the double + // tap event handling when gestures take place + graph.lastTouchTime = 0; + + if (graph.gestureEnabled) + { + graph.handleGesture(state, evt); + mxEvent.consume(evt); + } + } + else if (this.isShapeEvent(state, evt)) + { + graph.dblClick(evt, (state.shape != null && + mxEvent.getSource(evt) == state.shape.content) ? + null : state.cell); + mxEvent.consume(evt); + } + }) + ); +}; + +/** + * Function: redrawLabel + * + * Redraws the label for the given cell state. + * + * Parameters: + * + * state - <mxCellState> whose label should be redrawn. + */ +mxCellRenderer.prototype.redrawLabel = function(state) +{ + var value = this.getLabelValue(state); + + // FIXME: Add label always if HTML label and NO_FO + if (state.text == null && value != null && (mxUtils.isNode(value) || value.length > 0)) + { + this.createLabel(state, value); + } + else if (state.text != null && (value == null || value.length == 0)) + { + state.text.destroy(); + state.text = null; + } + + if (state.text != null) + { + var graph = state.view.graph; + var wrapping = graph.isWrapping(state.cell); + var clipping = graph.isLabelClipped(state.cell); + var bounds = this.getLabelBounds(state); + + if (state.text.value != value || state.text.isWrapping != wrapping || + state.text.isClipping != clipping || state.text.scale != state.view.scale || + !state.text.bounds.equals(bounds)) + { + state.text.value = value; + state.text.bounds = bounds; + state.text.scale = this.getTextScale(state); + state.text.isWrapping = wrapping; + state.text.isClipping = clipping; + + state.text.redraw(); + } + } +}; + +/** + * Function: getTextScale + * + * Returns the scaling used for the label of the given state + * + * Parameters: + * + * state - <mxCellState> whose label scale should be returned. + */ +mxCellRenderer.prototype.getTextScale = function(state) +{ + return state.view.scale; +}; + +/** + * Function: getLabelBounds + * + * Returns the bounds to be used to draw the label of the given state. + * + * Parameters: + * + * state - <mxCellState> whose label bounds should be returned. + */ +mxCellRenderer.prototype.getLabelBounds = function(state) +{ + var graph = state.view.graph; + var isEdge = graph.getModel().isEdge(state.cell); + var bounds = new mxRectangle(state.absoluteOffset.x, state.absoluteOffset.y); + + if (!isEdge) + { + bounds.x += state.x; + bounds.y += state.y; + + // Minimum of 1 fixes alignment bug in HTML labels + bounds.width = Math.max(1, state.width); + bounds.height = Math.max(1, state.height); + + if (graph.isSwimlane(state.cell)) + { + var scale = graph.view.scale; + var size = graph.getStartSize(state.cell); + + if (size.width > 0) + { + bounds.width = size.width * scale; + } + else if (size.height > 0) + { + bounds.height = size.height * scale; + } + } + } + + return bounds; +}; + +/** + * Function: redrawCellOverlays + * + * Redraws the overlays for the given cell state. + * + * Parameters: + * + * state - <mxCellState> whose overlays should be redrawn. + */ +mxCellRenderer.prototype.redrawCellOverlays = function(state) +{ + this.createCellOverlays(state); + + if (state.overlays != null) + { + state.overlays.visit(function(id, shape) + { + var bounds = shape.overlay.getBounds(state); + + if (shape.bounds == null || shape.scale != state.view.scale || + !shape.bounds.equals(bounds)) + { + shape.bounds = bounds; + shape.scale = state.view.scale; + shape.redraw(); + } + }); + } +}; + +/** + * Function: redrawControl + * + * Redraws the control for the given cell state. + * + * Parameters: + * + * state - <mxCellState> whose control should be redrawn. + */ +mxCellRenderer.prototype.redrawControl = function(state) +{ + if (state.control != null) + { + var bounds = this.getControlBounds(state); + var s = state.view.scale; + + if (state.control.scale != s || !state.control.bounds.equals(bounds)) + { + state.control.bounds = bounds; + state.control.scale = s; + state.control.redraw(); + } + } +}; + +/** + * Function: getControlBounds + * + * Returns the bounds to be used to draw the control (folding icon) of the + * given state. + */ +mxCellRenderer.prototype.getControlBounds = function(state) +{ + if (state.control != null) + { + var oldScale = state.control.scale; + var w = state.control.bounds.width / oldScale; + var h = state.control.bounds.height / oldScale; + var s = state.view.scale; + + return (state.view.graph.getModel().isEdge(state.cell)) ? + new mxRectangle(state.x + state.width / 2 - w / 2 * s, + state.y + state.height / 2 - h / 2 * s, w * s, h * s) + : new mxRectangle(state.x + w / 2 * s, + state.y + h / 2 * s, w * s, h * s); + } + + return null; +}; + +/** + * Function: redraw + * + * Updates the bounds or points and scale of the shapes for the given cell + * state. This is called in mxGraphView.validatePoints as the last step of + * updating all cells. + * + * Parameters: + * + * state - <mxCellState> for which the shapes should be updated. + * force - Optional boolean that specifies if the cell should be reconfiured + * and redrawn without any additional checks. + * rendering - Optional boolean that specifies if the cell should actually + * be drawn into the DOM. If this is false then redraw and/or reconfigure + * will not be called on the shape. + */ +mxCellRenderer.prototype.redraw = function(state, force, rendering) +{ + if (state.shape != null) + { + var model = state.view.graph.getModel(); + var isEdge = model.isEdge(state.cell); + reconfigure = (force != null) ? force : false; + + // Handles changes of the collapse icon + this.createControl(state); + + // Handles changes to the order in the DOM + if (state.orderChanged || state.invalidOrder) + { + if (state.view.graph.ordered) + { + this.order(state); + } + else + { + // Assert state.cell is edge + this.orderEdge(state); + } + + // Required to update inherited styles + reconfigure = state.orderChanged; + } + + delete state.invalidOrder; + delete state.orderChanged; + + // Checks if the style in the state is different from the style + // in the shape and re-applies the style if required + if (!reconfigure && !mxUtils.equalEntries(state.shape.style, state.style)) + { + reconfigure = true; + } + + // Reconfiures the shape after an order or style change + if (reconfigure) + { + this.configureShape(state); + state.shape.reconfigure(); + } + + // Redraws the cell if required + if (force || state.shape.bounds == null || state.shape.scale != state.view.scale || + !state.shape.bounds.equals(state) || + !mxUtils.equalPoints(state.shape.points, state.absolutePoints)) + { + // FIXME: Move indicator color update into shape.redraw +// var indicator = state.view.graph.getIndicatorColor(state); +// if (indicator != null) +// { +// state.shape.indicatorColor = indicator; +// } + + if (state.absolutePoints != null) + { + state.shape.points = state.absolutePoints.slice(); + } + else + { + state.shape.points = null; + } + + state.shape.bounds = new mxRectangle( + state.x, state.y, state.width, state.height); + state.shape.scale = state.view.scale; + + if (rendering == null || rendering) + { + state.shape.redraw(); + } + else + { + state.shape.updateBoundingBox(); + } + } + + // Updates the text label, overlays and control + if (rendering == null || rendering) + { + this.redrawLabel(state); + this.redrawCellOverlays(state); + this.redrawControl(state); + } + } +}; + +/** + * Function: destroy + * + * Destroys the shapes associated with the given cell state. + * + * Parameters: + * + * state - <mxCellState> for which the shapes should be destroyed. + */ +mxCellRenderer.prototype.destroy = function(state) +{ + if (state.shape != null) + { + if (state.text != null) + { + state.text.destroy(); + state.text = null; + } + + if (state.overlays != null) + { + state.overlays.visit(function(id, shape) + { + shape.destroy(); + }); + + state.overlays = null; + } + + if (state.control != null) + { + state.control.destroy(); + state.control = null; + } + + state.shape.destroy(); + state.shape = null; + } +}; diff --git a/src/js/view/mxCellState.js b/src/js/view/mxCellState.js new file mode 100644 index 0000000..7e7a3b0 --- /dev/null +++ b/src/js/view/mxCellState.js @@ -0,0 +1,375 @@ +/** + * $Id: mxCellState.js,v 1.42 2012-03-19 10:47:08 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxCellState + * + * Represents the current state of a cell in a given <mxGraphView>. + * + * For edges, the edge label position is stored in <absoluteOffset>. + * + * The size for oversize labels can be retrieved using the boundingBox property + * of the <text> field as shown below. + * + * (code) + * var bbox = (state.text != null) ? state.text.boundingBox : null; + * (end) + * + * Constructor: mxCellState + * + * Constructs a new object that represents the current state of the given + * cell in the specified view. + * + * Parameters: + * + * view - <mxGraphView> that contains the state. + * cell - <mxCell> that this state represents. + * style - Array of key, value pairs that constitute the style. + */ +function mxCellState(view, cell, style) +{ + this.view = view; + this.cell = cell; + this.style = style; + + this.origin = new mxPoint(); + this.absoluteOffset = new mxPoint(); +}; + +/** + * Extends mxRectangle. + */ +mxCellState.prototype = new mxRectangle(); +mxCellState.prototype.constructor = mxCellState; + +/** + * Variable: view + * + * Reference to the enclosing <mxGraphView>. + */ +mxCellState.prototype.view = null; + +/** + * Variable: cell + * + * Reference to the <mxCell> that is represented by this state. + */ +mxCellState.prototype.cell = null; + +/** + * Variable: style + * + * Contains an array of key, value pairs that represent the style of the + * cell. + */ +mxCellState.prototype.style = null; + +/** + * Variable: invalid + * + * Specifies if the state is invalid. Default is true. + */ +mxCellState.prototype.invalid = true; + +/** + * Variable: invalidOrder + * + * Specifies if the cell has an invalid order. For internal use. Default is + * false. + */ +mxCellState.prototype.invalidOrder = false; + +/** + * Variable: orderChanged + * + * Specifies if the cell has changed order and the display needs to be + * updated. + */ +mxCellState.prototype.orderChanged = false; + +/** + * Variable: origin + * + * <mxPoint> that holds the origin for all child cells. Default is a new + * empty <mxPoint>. + */ +mxCellState.prototype.origin = null; + +/** + * Variable: absolutePoints + * + * Holds an array of <mxPoints> that represent the absolute points of an + * edge. + */ +mxCellState.prototype.absolutePoints = null; + +/** + * Variable: absoluteOffset + * + * <mxPoint> that holds the absolute offset. For edges, this is the + * absolute coordinates of the label position. For vertices, this is the + * offset of the label relative to the top, left corner of the vertex. + */ +mxCellState.prototype.absoluteOffset = null; + +/** + * Variable: visibleSourceState + * + * Caches the visible source terminal state. + */ +mxCellState.prototype.visibleSourceState = null; + +/** + * Variable: visibleTargetState + * + * Caches the visible target terminal state. + */ +mxCellState.prototype.visibleTargetState = null; + +/** + * Variable: terminalDistance + * + * Caches the distance between the end points for an edge. + */ +mxCellState.prototype.terminalDistance = 0; + +/** + * Variable: length + * + * Caches the length of an edge. + */ +mxCellState.prototype.length = 0; + +/** + * Variable: segments + * + * Array of numbers that represent the cached length of each segment of the + * edge. + */ +mxCellState.prototype.segments = null; + +/** + * Variable: shape + * + * Holds the <mxShape> that represents the cell graphically. + */ +mxCellState.prototype.shape = null; + +/** + * Variable: text + * + * Holds the <mxText> that represents the label of the cell. Thi smay be + * null if the cell has no label. + */ +mxCellState.prototype.text = null; + +/** + * Function: getPerimeterBounds + * + * Returns the <mxRectangle> that should be used as the perimeter of the + * cell. + * + * Parameters: + * + * border - Optional border to be added around the perimeter bounds. + * bounds - Optional <mxRectangle> to be used as the initial bounds. + */ +mxCellState.prototype.getPerimeterBounds = function (border, bounds) +{ + border = border || 0; + bounds = (bounds != null) ? bounds : new mxRectangle(this.x, this.y, this.width, this.height); + + if (this.shape != null && this.shape.stencil != null) + { + var aspect = this.shape.stencil.computeAspect(this, bounds, null); + + bounds.x = aspect.x; + bounds.y = aspect.y; + bounds.width = this.shape.stencil.w0 * aspect.width; + bounds.height = this.shape.stencil.h0 * aspect.height; + } + + if (border != 0) + { + bounds.grow(border); + } + + return bounds; +}; + +/** + * Function: setAbsoluteTerminalPoint + * + * Sets the first or last point in <absolutePoints> depending on isSource. + * + * Parameters: + * + * point - <mxPoint> that represents the terminal point. + * isSource - Boolean that specifies if the first or last point should + * be assigned. + */ +mxCellState.prototype.setAbsoluteTerminalPoint = function (point, isSource) +{ + if (isSource) + { + if (this.absolutePoints == null) + { + this.absolutePoints = []; + } + + if (this.absolutePoints.length == 0) + { + this.absolutePoints.push(point); + } + else + { + this.absolutePoints[0] = point; + } + } + else + { + if (this.absolutePoints == null) + { + this.absolutePoints = []; + this.absolutePoints.push(null); + this.absolutePoints.push(point); + } + else if (this.absolutePoints.length == 1) + { + this.absolutePoints.push(point); + } + else + { + this.absolutePoints[this.absolutePoints.length - 1] = point; + } + } +}; + +/** + * Function: setCursor + * + * Sets the given cursor on the shape and text shape. + */ +mxCellState.prototype.setCursor = function (cursor) +{ + if (this.shape != null) + { + this.shape.setCursor(cursor); + } + + if (this.text != null) + { + this.text.setCursor(cursor); + } +}; + +/** + * Function: getVisibleTerminal + * + * Returns the visible source or target terminal cell. + * + * Parameters: + * + * source - Boolean that specifies if the source or target cell should be + * returned. + */ +mxCellState.prototype.getVisibleTerminal = function (source) +{ + var tmp = this.getVisibleTerminalState(source); + + return (tmp != null) ? tmp.cell : null; +}; + +/** + * Function: getVisibleTerminalState + * + * Returns the visible source or target terminal state. + * + * Parameters: + * + * source - Boolean that specifies if the source or target state should be + * returned. + */ +mxCellState.prototype.getVisibleTerminalState = function (source) +{ + return (source) ? this.visibleSourceState : this.visibleTargetState; +}; + +/** + * Function: setVisibleTerminalState + * + * Sets the visible source or target terminal state. + * + * Parameters: + * + * terminalState - <mxCellState> that represents the terminal. + * source - Boolean that specifies if the source or target state should be set. + */ +mxCellState.prototype.setVisibleTerminalState = function (terminalState, source) +{ + if (source) + { + this.visibleSourceState = terminalState; + } + else + { + this.visibleTargetState = terminalState; + } +}; + +/** + * Destructor: destroy + * + * Destroys the state and all associated resources. + */ +mxCellState.prototype.destroy = function () +{ + this.view.graph.cellRenderer.destroy(this); +}; + +/** + * Function: clone + * + * Returns a clone of this <mxPoint>. + */ +mxCellState.prototype.clone = function() +{ + var clone = new mxCellState(this.view, this.cell, this.style); + + // Clones the absolute points + if (this.absolutePoints != null) + { + clone.absolutePoints = []; + + for (var i = 0; i < this.absolutePoints.length; i++) + { + clone.absolutePoints[i] = this.absolutePoints[i].clone(); + } + } + + if (this.origin != null) + { + clone.origin = this.origin.clone(); + } + + if (this.absoluteOffset != null) + { + clone.absoluteOffset = this.absoluteOffset.clone(); + } + + if (this.boundingBox != null) + { + clone.boundingBox = this.boundingBox.clone(); + } + + clone.terminalDistance = this.terminalDistance; + clone.segments = this.segments; + clone.length = this.length; + clone.x = this.x; + clone.y = this.y; + clone.width = this.width; + clone.height = this.height; + + return clone; +}; diff --git a/src/js/view/mxCellStatePreview.js b/src/js/view/mxCellStatePreview.js new file mode 100644 index 0000000..b853748 --- /dev/null +++ b/src/js/view/mxCellStatePreview.js @@ -0,0 +1,223 @@ +/** + * $Id: mxCellStatePreview.js,v 1.6 2012-10-26 07:19:11 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * + * Class: mxCellStatePreview + * + * Implements a live preview for moving cells. + * + * Constructor: mxCellStatePreview + * + * Constructs a move preview for the given graph. + * + * Parameters: + * + * graph - Reference to the enclosing <mxGraph>. + */ +function mxCellStatePreview(graph) +{ + this.graph = graph; + this.deltas = new Object(); +}; + +/** + * Variable: graph + * + * Reference to the enclosing <mxGraph>. + */ +mxCellStatePreview.prototype.graph = null; + +/** + * Variable: deltas + * + * Reference to the enclosing <mxGraph>. + */ +mxCellStatePreview.prototype.deltas = null; + +/** + * Variable: count + * + * Contains the number of entries in the map. + */ +mxCellStatePreview.prototype.count = 0; + +/** + * Function: isEmpty + * + * Returns true if this contains no entries. + */ +mxCellStatePreview.prototype.isEmpty = function() +{ + return this.count == 0; +}; + +/** + * Function: moveState + */ +mxCellStatePreview.prototype.moveState = function(state, dx, dy, add, includeEdges) +{ + add = (add != null) ? add : true; + includeEdges = (includeEdges != null) ? includeEdges : true; + var id = mxCellPath.create(state.cell); + var delta = this.deltas[id]; + + if (delta == null) + { + delta = new mxPoint(dx, dy); + this.deltas[id] = delta; + this.count++; + } + else + { + if (add) + { + delta.X += dx; + delta.Y += dy; + } + else + { + delta.X = dx; + delta.Y = dy; + } + } + + if (includeEdges) + { + this.addEdges(state); + } + + return delta; +}; + +/** + * Function: show + */ +mxCellStatePreview.prototype.show = function(visitor) +{ + var model = this.graph.getModel(); + var root = model.getRoot(); + + // Translates the states in step + for (var id in this.deltas) + { + var cell = mxCellPath.resolve(root, id); + var state = this.graph.view.getState(cell); + var delta = this.deltas[id]; + var parentState = this.graph.view.getState( + model.getParent(cell)); + this.translateState(parentState, state, delta.x, delta.y); + } + + // Revalidates the states in step + for (var id in this.deltas) + { + var cell = mxCellPath.resolve(root, id); + var state = this.graph.view.getState(cell); + var delta = this.deltas[id]; + var parentState = this.graph.view.getState( + model.getParent(cell)); + this.revalidateState(parentState, state, delta.x, delta.y, visitor); + } +}; + +/** + * Function: translateState + */ +mxCellStatePreview.prototype.translateState = function(parentState, state, dx, dy) +{ + if (state != null) + { + var model = this.graph.getModel(); + + if (model.isVertex(state.cell)) + { + // LATER: Use hashtable to store initial state bounds + state.invalid = true; + this.graph.view.validateBounds(parentState, state.cell); + var geo = model.getGeometry(state.cell); + var id = mxCellPath.create(state.cell); + + // Moves selection cells and non-relative vertices in + // the first phase so that edge terminal points will + // be updated in the second phase + if ((dx != 0 || dy != 0) && geo != null && + (!geo.relative || this.deltas[id] != null)) + { + state.x += dx; + state.y += dy; + } + } + + var childCount = model.getChildCount(state.cell); + + for (var i = 0; i < childCount; i++) + { + this.translateState(state, this.graph.view.getState( + model.getChildAt(state.cell, i)), dx, dy); + } + } +}; + +/** + * Function: revalidateState + */ +mxCellStatePreview.prototype.revalidateState = function(parentState, state, dx, dy, visitor) +{ + if (state != null) + { + // Updates the edge terminal points and restores the + // (relative) positions of any (relative) children + state.invalid = true; + this.graph.view.validatePoints(parentState, state.cell); + + // Moves selection vertices which are relative + var id = mxCellPath.create(state.cell); + var model = this.graph.getModel(); + var geo = this.graph.getCellGeometry(state.cell); + + if ((dx != 0 || dy != 0) && geo != null && geo.relative && + model.isVertex(state.cell) && (parentState == null || + model.isVertex(parentState.cell) || this.deltas[id] != null)) + { + state.x += dx; + state.y += dy; + + this.graph.cellRenderer.redraw(state); + } + + // Invokes the visitor on the given state + if (visitor != null) + { + visitor(state); + } + + var childCount = model.getChildCount(state.cell); + + for (var i = 0; i < childCount; i++) + { + this.revalidateState(state, this.graph.view.getState(model.getChildAt( + state.cell, i)), dx, dy, visitor); + } + } +}; + +/** + * Function: addEdges + */ +mxCellStatePreview.prototype.addEdges = function(state) +{ + var model = this.graph.getModel(); + var edgeCount = model.getEdgeCount(state.cell); + + for (var i = 0; i < edgeCount; i++) + { + var s = this.graph.view.getState(model.getEdgeAt(state.cell, i)); + + if (s != null) + { + this.moveState(s, 0, 0); + } + } +}; diff --git a/src/js/view/mxConnectionConstraint.js b/src/js/view/mxConnectionConstraint.js new file mode 100644 index 0000000..70f457f --- /dev/null +++ b/src/js/view/mxConnectionConstraint.js @@ -0,0 +1,42 @@ +/** + * $Id: mxConnectionConstraint.js,v 1.2 2010-04-29 09:33:52 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxConnectionConstraint + * + * Defines an object that contains the constraints about how to connect one + * side of an edge to its terminal. + * + * Constructor: mxConnectionConstraint + * + * Constructs a new connection constraint for the given point and boolean + * arguments. + * + * Parameters: + * + * point - Optional <mxPoint> that specifies the fixed location of the point + * in relative coordinates. Default is null. + * perimeter - Optional boolean that specifies if the fixed point should be + * projected onto the perimeter of the terminal. Default is true. + */ +function mxConnectionConstraint(point, perimeter) +{ + this.point = point; + this.perimeter = (perimeter != null) ? perimeter : true; +}; + +/** + * Variable: point + * + * <mxPoint> that specifies the fixed location of the connection point. + */ +mxConnectionConstraint.prototype.point = null; + +/** + * Variable: perimeter + * + * Boolean that specifies if the point should be projected onto the perimeter + * of the terminal. + */ +mxConnectionConstraint.prototype.perimeter = null; diff --git a/src/js/view/mxEdgeStyle.js b/src/js/view/mxEdgeStyle.js new file mode 100644 index 0000000..41493d6 --- /dev/null +++ b/src/js/view/mxEdgeStyle.js @@ -0,0 +1,1302 @@ +/** + * $Id: mxEdgeStyle.js,v 1.68 2012-11-20 09:06:07 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +var mxEdgeStyle = +{ + /** + * Class: mxEdgeStyle + * + * Provides various edge styles to be used as the values for + * <mxConstants.STYLE_EDGE> in a cell style. + * + * Example: + * + * (code) + * var style = stylesheet.getDefaultEdgeStyle(); + * style[mxConstants.STYLE_EDGE] = mxEdgeStyle.ElbowConnector; + * (end) + * + * Sets the default edge style to <ElbowConnector>. + * + * Custom edge style: + * + * To write a custom edge style, a function must be added to the mxEdgeStyle + * object as follows: + * + * (code) + * mxEdgeStyle.MyStyle = function(state, source, target, points, result) + * { + * if (source != null && target != null) + * { + * var pt = new mxPoint(target.getCenterX(), source.getCenterY()); + * + * if (mxUtils.contains(source, pt.x, pt.y)) + * { + * pt.y = source.y + source.height; + * } + * + * result.push(pt); + * } + * }; + * (end) + * + * In the above example, a right angle is created using a point on the + * horizontal center of the target vertex and the vertical center of the source + * vertex. The code checks if that point intersects the source vertex and makes + * the edge straight if it does. The point is then added into the result array, + * which acts as the return value of the function. + * + * The new edge style should then be registered in the <mxStyleRegistry> as follows: + * (code) + * mxStyleRegistry.putValue('myEdgeStyle', mxEdgeStyle.MyStyle); + * (end) + * + * The custom edge style above can now be used in a specific edge as follows: + * + * (code) + * model.setStyle(edge, 'edgeStyle=myEdgeStyle'); + * (end) + * + * Note that the key of the <mxStyleRegistry> entry for the function should + * be used in string values, unless <mxGraphView.allowEval> is true, in + * which case you can also use mxEdgeStyle.MyStyle for the value in the + * cell style above. + * + * Or it can be used for all edges in the graph as follows: + * + * (code) + * var style = graph.getStylesheet().getDefaultEdgeStyle(); + * style[mxConstants.STYLE_EDGE] = mxEdgeStyle.MyStyle; + * (end) + * + * Note that the object can be used directly when programmatically setting + * the value, but the key in the <mxStyleRegistry> should be used when + * setting the value via a key, value pair in a cell style. + * + * Function: EntityRelation + * + * Implements an entity relation style for edges (as used in database + * schema diagrams). At the time the function is called, the result + * array contains a placeholder (null) for the first absolute point, + * that is, the point where the edge and source terminal are connected. + * The implementation of the style then adds all intermediate waypoints + * except for the last point, that is, the connection point between the + * edge and the target terminal. The first ant the last point in the + * result array are then replaced with mxPoints that take into account + * the terminal's perimeter and next point on the edge. + * + * Parameters: + * + * state - <mxCellState> that represents the edge to be updated. + * source - <mxCellState> that represents the source terminal. + * target - <mxCellState> that represents the target terminal. + * points - List of relative control points. + * result - Array of <mxPoints> that represent the actual points of the + * edge. + */ + EntityRelation: function (state, source, target, points, result) + { + var view = state.view; + var graph = view.graph; + var segment = mxUtils.getValue(state.style, + mxConstants.STYLE_SEGMENT, + mxConstants.ENTITY_SEGMENT) * view.scale; + + var pts = state.absolutePoints; + var p0 = pts[0]; + var pe = pts[pts.length-1]; + + var isSourceLeft = false; + + if (p0 != null) + { + source = new mxCellState(); + source.x = p0.x; + source.y = p0.y; + } + else if (source != null) + { + var constraint = mxUtils.getPortConstraints(source, state, true, mxConstants.DIRECTION_MASK_NONE); + + if (constraint != mxConstants.DIRECTION_MASK_NONE) + { + isSourceLeft = constraint == mxConstants.DIRECTION_MASK_WEST; + } + else + { + var sourceGeometry = graph.getCellGeometry(source.cell); + + if (sourceGeometry.relative) + { + isSourceLeft = sourceGeometry.x <= 0.5; + } + else if (target != null) + { + isSourceLeft = target.x + target.width < source.x; + } + } + } + else + { + return; + } + + var isTargetLeft = true; + + if (pe != null) + { + target = new mxCellState(); + target.x = pe.x; + target.y = pe.y; + } + else if (target != null) + { + var constraint = mxUtils.getPortConstraints(target, state, false, mxConstants.DIRECTION_MASK_NONE); + + if (constraint != mxConstants.DIRECTION_MASK_NONE) + { + isTargetLeft = constraint == mxConstants.DIRECTION_MASK_WEST; + } + else + { + var targetGeometry = graph.getCellGeometry(target.cell); + + if (targetGeometry.relative) + { + isTargetLeft = targetGeometry.x <= 0.5; + } + else if (source != null) + { + isTargetLeft = source.x + source.width < target.x; + } + } + } + + if (source != null && target != null) + { + var x0 = (isSourceLeft) ? source.x : source.x + source.width; + var y0 = view.getRoutingCenterY(source); + + var xe = (isTargetLeft) ? target.x : target.x + target.width; + var ye = view.getRoutingCenterY(target); + + var seg = segment; + + var dx = (isSourceLeft) ? -seg : seg; + var dep = new mxPoint(x0 + dx, y0); + + dx = (isTargetLeft) ? -seg : seg; + var arr = new mxPoint(xe + dx, ye); + + // Adds intermediate points if both go out on same side + if (isSourceLeft == isTargetLeft) + { + var x = (isSourceLeft) ? + Math.min(x0, xe)-segment : + Math.max(x0, xe)+segment; + + result.push(new mxPoint(x, y0)); + result.push(new mxPoint(x, ye)); + } + else if ((dep.x < arr.x) == isSourceLeft) + { + var midY = y0 + (ye - y0) / 2; + + result.push(dep); + result.push(new mxPoint(dep.x, midY)); + result.push(new mxPoint(arr.x, midY)); + result.push(arr); + } + else + { + result.push(dep); + result.push(arr); + } + } + }, + + /** + * Function: Loop + * + * Implements a self-reference, aka. loop. + */ + Loop: function (state, source, target, points, result) + { + if (source != null) + { + var view = state.view; + var graph = view.graph; + var pt = (points != null && points.length > 0) ? points[0] : null; + + if (pt != null) + { + pt = view.transformControlPoint(state, pt); + + if (mxUtils.contains(source, pt.x, pt.y)) + { + pt = null; + } + } + + var x = 0; + var dx = 0; + var y = 0; + var dy = 0; + + var seg = mxUtils.getValue(state.style, mxConstants.STYLE_SEGMENT, + graph.gridSize) * view.scale; + var dir = mxUtils.getValue(state.style, mxConstants.STYLE_DIRECTION, + mxConstants.DIRECTION_WEST); + + if (dir == mxConstants.DIRECTION_NORTH || + dir == mxConstants.DIRECTION_SOUTH) + { + x = view.getRoutingCenterX(source); + dx = seg; + } + else + { + y = view.getRoutingCenterY(source); + dy = seg; + } + + if (pt == null || + pt.x < source.x || + pt.x > source.x + source.width) + { + if (pt != null) + { + x = pt.x; + dy = Math.max(Math.abs(y - pt.y), dy); + } + else + { + if (dir == mxConstants.DIRECTION_NORTH) + { + y = source.y - 2 * dx; + } + else if (dir == mxConstants.DIRECTION_SOUTH) + { + y = source.y + source.height + 2 * dx; + } + else if (dir == mxConstants.DIRECTION_EAST) + { + x = source.x - 2 * dy; + } + else + { + x = source.x + source.width + 2 * dy; + } + } + } + else if (pt != null) + { + x = view.getRoutingCenterX(source); + dx = Math.max(Math.abs(x - pt.x), dy); + y = pt.y; + dy = 0; + } + + result.push(new mxPoint(x - dx, y - dy)); + result.push(new mxPoint(x + dx, y + dy)); + } + }, + + /** + * Function: ElbowConnector + * + * Uses either <SideToSide> or <TopToBottom> depending on the horizontal + * flag in the cell style. <SideToSide> is used if horizontal is true or + * unspecified. See <EntityRelation> for a description of the + * parameters. + */ + ElbowConnector: function (state, source, target, points, result) + { + var pt = (points != null && points.length > 0) ? points[0] : null; + + var vertical = false; + var horizontal = false; + + if (source != null && target != null) + { + if (pt != null) + { + var left = Math.min(source.x, target.x); + var right = Math.max(source.x + source.width, + target.x + target.width); + + var top = Math.min(source.y, target.y); + var bottom = Math.max(source.y + source.height, + target.y + target.height); + + pt = state.view.transformControlPoint(state, pt); + + vertical = pt.y < top || pt.y > bottom; + horizontal = pt.x < left || pt.x > right; + } + else + { + var left = Math.max(source.x, target.x); + var right = Math.min(source.x + source.width, + target.x + target.width); + + vertical = left == right; + + if (!vertical) + { + var top = Math.max(source.y, target.y); + var bottom = Math.min(source.y + source.height, + target.y + target.height); + + horizontal = top == bottom; + } + } + } + + if (!horizontal && (vertical || + state.style[mxConstants.STYLE_ELBOW] == mxConstants.ELBOW_VERTICAL)) + { + mxEdgeStyle.TopToBottom(state, source, target, points, result); + } + else + { + mxEdgeStyle.SideToSide(state, source, target, points, result); + } + }, + + /** + * Function: SideToSide + * + * Implements a vertical elbow edge. See <EntityRelation> for a description + * of the parameters. + */ + SideToSide: function (state, source, target, points, result) + { + var view = state.view; + var pt = (points != null && points.length > 0) ? points[0] : null; + var pts = state.absolutePoints; + var p0 = pts[0]; + var pe = pts[pts.length-1]; + + if (pt != null) + { + pt = view.transformControlPoint(state, pt); + } + + if (p0 != null) + { + source = new mxCellState(); + source.x = p0.x; + source.y = p0.y; + } + + if (pe != null) + { + target = new mxCellState(); + target.x = pe.x; + target.y = pe.y; + } + + if (source != null && target != null) + { + var l = Math.max(source.x, target.x); + var r = Math.min(source.x + source.width, + target.x + target.width); + + var x = (pt != null) ? pt.x : r + (l - r) / 2; + + var y1 = view.getRoutingCenterY(source); + var y2 = view.getRoutingCenterY(target); + + if (pt != null) + { + if (pt.y >= source.y && pt.y <= source.y + source.height) + { + y1 = pt.y; + } + + if (pt.y >= target.y && pt.y <= target.y + target.height) + { + y2 = pt.y; + } + } + + if (!mxUtils.contains(target, x, y1) && + !mxUtils.contains(source, x, y1)) + { + result.push(new mxPoint(x, y1)); + } + + if (!mxUtils.contains(target, x, y2) && + !mxUtils.contains(source, x, y2)) + { + result.push(new mxPoint(x, y2)); + } + + if (result.length == 1) + { + if (pt != null) + { + if (!mxUtils.contains(target, x, pt.y) && + !mxUtils.contains(source, x, pt.y)) + { + result.push(new mxPoint(x, pt.y)); + } + } + else + { + var t = Math.max(source.y, target.y); + var b = Math.min(source.y + source.height, + target.y + target.height); + + result.push(new mxPoint(x, t + (b - t) / 2)); + } + } + } + }, + + /** + * Function: TopToBottom + * + * Implements a horizontal elbow edge. See <EntityRelation> for a + * description of the parameters. + */ + TopToBottom: function(state, source, target, points, result) + { + var view = state.view; + var pt = (points != null && points.length > 0) ? points[0] : null; + var pts = state.absolutePoints; + var p0 = pts[0]; + var pe = pts[pts.length-1]; + + if (pt != null) + { + pt = view.transformControlPoint(state, pt); + } + + if (p0 != null) + { + source = new mxCellState(); + source.x = p0.x; + source.y = p0.y; + } + + if (pe != null) + { + target = new mxCellState(); + target.x = pe.x; + target.y = pe.y; + } + + if (source != null && target != null) + { + var t = Math.max(source.y, target.y); + var b = Math.min(source.y + source.height, + target.y + target.height); + + var x = view.getRoutingCenterX(source); + + if (pt != null && + pt.x >= source.x && + pt.x <= source.x + source.width) + { + x = pt.x; + } + + var y = (pt != null) ? pt.y : b + (t - b) / 2; + + if (!mxUtils.contains(target, x, y) && + !mxUtils.contains(source, x, y)) + { + result.push(new mxPoint(x, y)); + } + + if (pt != null && + pt.x >= target.x && + pt.x <= target.x + target.width) + { + x = pt.x; + } + else + { + x = view.getRoutingCenterX(target); + } + + if (!mxUtils.contains(target, x, y) && + !mxUtils.contains(source, x, y)) + { + result.push(new mxPoint(x, y)); + } + + if (result.length == 1) + { + if (pt != null && result.length == 1) + { + if (!mxUtils.contains(target, pt.x, y) && + !mxUtils.contains(source, pt.x, y)) + { + result.push(new mxPoint(pt.x, y)); + } + } + else + { + var l = Math.max(source.x, target.x); + var r = Math.min(source.x + source.width, + target.x + target.width); + + result.push(new mxPoint(l + (r - l) / 2, y)); + } + } + } + }, + + /** + * Function: SegmentConnector + * + * Implements an orthogonal edge style. Use <mxEdgeSegmentHandler> + * as an interactive handler for this style. + */ + SegmentConnector: function(state, source, target, hints, result) + { + // Creates array of all way- and terminalpoints + var pts = state.absolutePoints; + var horizontal = true; + var hint = null; + + // Adds the first point + var pt = pts[0]; + + if (pt == null && source != null) + { + pt = new mxPoint(state.view.getRoutingCenterX(source), state.view.getRoutingCenterY(source)); + } + else if (pt != null) + { + pt = pt.clone(); + } + + var lastInx = pts.length - 1; + + // Adds the waypoints + if (hints != null && hints.length > 0) + { + hint = state.view.transformControlPoint(state, hints[0]); + + var currentTerm = source; + var currentPt = pts[0]; + var hozChan = false; + var vertChan = false; + var currentHint = hint; + var hintsLen = hints.length; + + for (var i = 0; i < 2; i++) + { + var fixedVertAlign = currentPt != null && currentPt.x == currentHint.x; + var fixedHozAlign = currentPt != null && currentPt.y == currentHint.y; + var inHozChan = currentTerm != null && (currentHint.y >= currentTerm.y && + currentHint.y <= currentTerm.y + currentTerm.height); + var inVertChan = currentTerm != null && (currentHint.x >= currentTerm.x && + currentHint.x <= currentTerm.x + currentTerm.width); + + hozChan = fixedHozAlign || (currentPt == null && inHozChan); + vertChan = fixedVertAlign || (currentPt == null && inVertChan); + + if (currentPt != null && (!fixedHozAlign && !fixedVertAlign) && (inHozChan || inVertChan)) + { + horizontal = inHozChan ? false : true; + break; + } + + if (vertChan || hozChan) + { + horizontal = hozChan; + + if (i == 1) + { + // Work back from target end + horizontal = hints.length % 2 == 0 ? hozChan : vertChan; + } + + break; + } + + currentTerm = target; + currentPt = pts[lastInx]; + currentHint = state.view.transformControlPoint(state, hints[hintsLen - 1]); + } + + if (horizontal && ((pts[0] != null && pts[0].y != hint.y) || + (pts[0] == null && source != null && + (hint.y < source.y || hint.y > source.y + source.height)))) + { + result.push(new mxPoint(pt.x, hint.y)); + } + else if (!horizontal && ((pts[0] != null && pts[0].x != hint.x) || + (pts[0] == null && source != null && + (hint.x < source.x || hint.x > source.x + source.width)))) + { + result.push(new mxPoint(hint.x, pt.y)); + } + + if (horizontal) + { + pt.y = hint.y; + } + else + { + pt.x = hint.x; + } + + for (var i = 0; i < hints.length; i++) + { + horizontal = !horizontal; + hint = state.view.transformControlPoint(state, hints[i]); + +// mxLog.show(); +// mxLog.debug('hint', i, hint.x, hint.y); + + if (horizontal) + { + pt.y = hint.y; + } + else + { + pt.x = hint.x; + } + + result.push(pt.clone()); + } + } + else + { + hint = pt; + // FIXME: First click in connect preview toggles orientation + horizontal = true; + } + + // Adds the last point + pt = pts[lastInx]; + + if (pt == null && target != null) + { + pt = new mxPoint(state.view.getRoutingCenterX(target), state.view.getRoutingCenterY(target)); + } + + if (horizontal && ((pts[lastInx] != null && pts[lastInx].y != hint.y) || + (pts[lastInx] == null && target != null && + (hint.y < target.y || hint.y > target.y + target.height)))) + { + result.push(new mxPoint(pt.x, hint.y)); + } + else if (!horizontal && ((pts[lastInx] != null && pts[lastInx].x != hint.x) || + (pts[lastInx] == null && target != null && + (hint.x < target.x || hint.x > target.x + target.width)))) + { + result.push(new mxPoint(hint.x, pt.y)); + } + + // Removes bends inside the source terminal for floating ports + if (pts[0] == null && source != null) + { + while (result.length > 1 && mxUtils.contains(source, result[1].x, result[1].y)) + { + result = result.splice(1, 1); + } + } + + // Removes bends inside the target terminal + if (pts[lastInx] == null && target != null) + { + while (result.length > 1 && mxUtils.contains(target, result[result.length - 1].x, result[result.length - 1].y)) + { + result = result.splice(result.length - 1, 1); + } + } + + }, + + orthBuffer: 10, + + dirVectors: [ [ -1, 0 ], + [ 0, -1 ], [ 1, 0 ], [ 0, 1 ], [ -1, 0 ], [ 0, -1 ], [ 1, 0 ] ], + + wayPoints1: [ [ 0, 0], [ 0, 0], [ 0, 0], [ 0, 0], [ 0, 0], [ 0, 0], + [ 0, 0], [ 0, 0], [ 0, 0], [ 0, 0], [ 0, 0], [ 0, 0] ], + + routePatterns: [ + [ [ 513, 2308, 2081, 2562 ], [ 513, 1090, 514, 2184, 2114, 2561 ], + [ 513, 1090, 514, 2564, 2184, 2562 ], + [ 513, 2308, 2561, 1090, 514, 2568, 2308 ] ], + [ [ 514, 1057, 513, 2308, 2081, 2562 ], [ 514, 2184, 2114, 2561 ], + [ 514, 2184, 2562, 1057, 513, 2564, 2184 ], + [ 514, 1057, 513, 2568, 2308, 2561 ] ], + [ [ 1090, 514, 1057, 513, 2308, 2081, 2562 ], [ 2114, 2561 ], + [ 1090, 2562, 1057, 513, 2564, 2184 ], + [ 1090, 514, 1057, 513, 2308, 2561, 2568 ] ], + [ [ 2081, 2562 ], [ 1057, 513, 1090, 514, 2184, 2114, 2561 ], + [ 1057, 513, 1090, 514, 2184, 2562, 2564 ], + [ 1057, 2561, 1090, 514, 2568, 2308 ] ] ], + + inlineRoutePatterns: [ + [ null, [ 2114, 2568 ], null, null ], + [ null, [ 514, 2081, 2114, 2568 ] , null, null ], + [ null, [ 2114, 2561 ], null, null ], + [ [ 2081, 2562 ], [ 1057, 2114, 2568 ], + [ 2184, 2562 ], + null ] ], + vertexSeperations: [], + + limits: [ + [ 0, 0, 0, 0, 0, 0, 0, 0, 0 ], + [ 0, 0, 0, 0, 0, 0, 0, 0, 0 ] ], + + LEFT_MASK: 32, + + TOP_MASK: 64, + + RIGHT_MASK: 128, + + BOTTOM_MASK: 256, + + LEFT: 1, + + TOP: 2, + + RIGHT: 4, + + BOTTOM: 8, + + // TODO remove magic numbers + SIDE_MASK: 480, + //mxEdgeStyle.LEFT_MASK | mxEdgeStyle.TOP_MASK | mxEdgeStyle.RIGHT_MASK + //| mxEdgeStyle.BOTTOM_MASK, + + CENTER_MASK: 512, + + SOURCE_MASK: 1024, + + TARGET_MASK: 2048, + + VERTEX_MASK: 3072, + // mxEdgeStyle.SOURCE_MASK | mxEdgeStyle.TARGET_MASK, + + /** + * Function: OrthConnector + * + * Implements a local orthogonal router between the given + * cells. + */ + OrthConnector: function(state, source, target, points, result) + { + var graph = state.view.graph; + var sourceEdge = source == null ? false : graph.getModel().isEdge(source.cell); + var targetEdge = target == null ? false : graph.getModel().isEdge(target.cell); + + if ((points != null && points.length > 0) || (sourceEdge) || (targetEdge)) + { + mxEdgeStyle.SegmentConnector(state, source, target, points, result); + return; + } + + var pts = state.absolutePoints; + var p0 = pts[0]; + var pe = pts[pts.length-1]; + + var sourceX = source != null ? source.x : p0.x; + var sourceY = source != null ? source.y : p0.y; + var sourceWidth = source != null ? source.width : 1; + var sourceHeight = source != null ? source.height : 1; + + var targetX = target != null ? target.x : pe.x; + var targetY = target != null ? target.y : pe.y; + var targetWidth = target != null ? target.width : 1; + var targetHeight = target != null ? target.height : 1; + + var scaledOrthBuffer = state.view.scale * mxEdgeStyle.orthBuffer; + // Determine the side(s) of the source and target vertices + // that the edge may connect to + // portConstraint [source, target] + var portConstraint = [mxConstants.DIRECTION_MASK_ALL, mxConstants.DIRECTION_MASK_ALL]; + + if (source != null) + { + portConstraint[0] = mxUtils.getPortConstraints(source, state, true, + mxConstants.DIRECTION_MASK_ALL); + } + + if (target != null) + { + portConstraint[1] = mxUtils.getPortConstraints(target, state, false, + mxConstants.DIRECTION_MASK_ALL); + } + + var dir = [0, 0] ; + + // Work out which faces of the vertices present against each other + // in a way that would allow a 3-segment connection if port constraints + // permitted. + // geo -> [source, target] [x, y, width, height] + var geo = [ [sourceX, sourceY, sourceWidth, sourceHeight] , + [targetX, targetY, targetWidth, targetHeight] ]; + + for (var i = 0; i < 2; i++) + { + mxEdgeStyle.limits[i][1] = geo[i][0] - scaledOrthBuffer; + mxEdgeStyle.limits[i][2] = geo[i][1] - scaledOrthBuffer; + mxEdgeStyle.limits[i][4] = geo[i][0] + geo[i][2] + scaledOrthBuffer; + mxEdgeStyle.limits[i][8] = geo[i][1] + geo[i][3] + scaledOrthBuffer; + } + + // Work out which quad the target is in + var sourceCenX = geo[0][0] + geo[0][2] / 2.0; + var sourceCenY = geo[0][1] + geo[0][3] / 2.0; + var targetCenX = geo[1][0] + geo[1][2] / 2.0; + var targetCenY = geo[1][1] + geo[1][3] / 2.0; + + var dx = sourceCenX - targetCenX; + var dy = sourceCenY - targetCenY; + + var quad = 0; + + if (dx < 0) + { + if (dy < 0) + { + quad = 2; + } + else + { + quad = 1; + } + } + else + { + if (dy <= 0) + { + quad = 3; + + // Special case on x = 0 and negative y + if (dx == 0) + { + quad = 2; + } + } + } + + // Check for connection constraints + var currentTerm = null; + + if (source != null) + { + currentTerm = p0; + } + + var constraint = [ [0.5, 0.5] , [0.5, 0.5] ]; + + for (var i = 0; i < 2; i++) + { + if (currentTerm != null) + { + constraint[i][0] = (currentTerm.x - geo[i][0]) / geo[i][2]; + + if (constraint[i][0] < 0.01) + { + dir[i] = mxConstants.DIRECTION_MASK_WEST; + } + else if (constraint[i][0] > 0.99) + { + dir[i] = mxConstants.DIRECTION_MASK_EAST; + } + + constraint[i][1] = (currentTerm.y - geo[i][1]) / geo[i][3]; + + if (constraint[i][1] < 0.01) + { + dir[i] = mxConstants.DIRECTION_MASK_NORTH; + } + else if (constraint[i][1] > 0.99) + { + dir[i] = mxConstants.DIRECTION_MASK_SOUTH; + } + } + + currentTerm = null; + + if (target != null) + { + currentTerm = pe; + } + } + + var sourceTopDist = geo[0][1] - (geo[1][1] + geo[1][3]); + var sourceLeftDist = geo[0][0] - (geo[1][0] + geo[1][2]); + var sourceBottomDist = geo[1][1] - (geo[0][1] + geo[0][3]); + var sourceRightDist = geo[1][0] - (geo[0][0] + geo[0][2]); + + mxEdgeStyle.vertexSeperations[1] = Math.max( + sourceLeftDist - 2 * scaledOrthBuffer, 0); + mxEdgeStyle.vertexSeperations[2] = Math.max(sourceTopDist - 2 * scaledOrthBuffer, + 0); + mxEdgeStyle.vertexSeperations[4] = Math.max(sourceBottomDist - 2 + * scaledOrthBuffer, 0); + mxEdgeStyle.vertexSeperations[3] = Math.max(sourceRightDist - 2 + * scaledOrthBuffer, 0); + + //============================================================== + // Start of source and target direction determination + + // Work through the preferred orientations by relative positioning + // of the vertices and list them in preferred and available order + + var dirPref = []; + var horPref = []; + var vertPref = []; + + horPref[0] = (sourceLeftDist >= sourceRightDist) ? mxConstants.DIRECTION_MASK_WEST + : mxConstants.DIRECTION_MASK_EAST; + vertPref[0] = (sourceTopDist >= sourceBottomDist) ? mxConstants.DIRECTION_MASK_NORTH + : mxConstants.DIRECTION_MASK_SOUTH; + + horPref[1] = mxUtils.reversePortConstraints(horPref[0]); + vertPref[1] = mxUtils.reversePortConstraints(vertPref[0]); + + var preferredHorizDist = sourceLeftDist >= sourceRightDist ? sourceLeftDist + : sourceRightDist; + var preferredVertDist = sourceTopDist >= sourceBottomDist ? sourceTopDist + : sourceBottomDist; + + var prefOrdering = [ [0, 0] , [0, 0] ]; + var preferredOrderSet = false; + + // If the preferred port isn't available, switch it + for (var i = 0; i < 2; i++) + { + if (dir[i] != 0x0) + { + continue; + } + + if ((horPref[i] & portConstraint[i]) == 0) + { + horPref[i] = mxUtils.reversePortConstraints(horPref[i]); + } + + if ((vertPref[i] & portConstraint[i]) == 0) + { + vertPref[i] = mxUtils + .reversePortConstraints(vertPref[i]); + } + + prefOrdering[i][0] = vertPref[i]; + prefOrdering[i][1] = horPref[i]; + } + + if (preferredVertDist > scaledOrthBuffer * 2 + && preferredHorizDist > scaledOrthBuffer * 2) + { + // Possibility of two segment edge connection + if (((horPref[0] & portConstraint[0]) > 0) + && ((vertPref[1] & portConstraint[1]) > 0)) + { + prefOrdering[0][0] = horPref[0]; + prefOrdering[0][1] = vertPref[0]; + prefOrdering[1][0] = vertPref[1]; + prefOrdering[1][1] = horPref[1]; + preferredOrderSet = true; + } + else if (((vertPref[0] & portConstraint[0]) > 0) + && ((horPref[1] & portConstraint[1]) > 0)) + { + prefOrdering[0][0] = vertPref[0]; + prefOrdering[0][1] = horPref[0]; + prefOrdering[1][0] = horPref[1]; + prefOrdering[1][1] = vertPref[1]; + preferredOrderSet = true; + } + } + if (preferredVertDist > scaledOrthBuffer * 2 && !preferredOrderSet) + { + prefOrdering[0][0] = vertPref[0]; + prefOrdering[0][1] = horPref[0]; + prefOrdering[1][0] = vertPref[1]; + prefOrdering[1][1] = horPref[1]; + preferredOrderSet = true; + + } + if (preferredHorizDist > scaledOrthBuffer * 2 && !preferredOrderSet) + { + prefOrdering[0][0] = horPref[0]; + prefOrdering[0][1] = vertPref[0]; + prefOrdering[1][0] = horPref[1]; + prefOrdering[1][1] = vertPref[1]; + preferredOrderSet = true; + } + + // The source and target prefs are now an ordered list of + // the preferred port selections + // It the list can contain gaps, compact it + + for (var i = 0; i < 2; i++) + { + if (dir[i] != 0x0) + { + continue; + } + + if ((prefOrdering[i][0] & portConstraint[i]) == 0) + { + prefOrdering[i][0] = prefOrdering[i][1]; + } + + dirPref[i] = prefOrdering[i][0] & portConstraint[i]; + dirPref[i] |= (prefOrdering[i][1] & portConstraint[i]) << 8; + dirPref[i] |= (prefOrdering[1 - i][i] & portConstraint[i]) << 16; + dirPref[i] |= (prefOrdering[1 - i][1 - i] & portConstraint[i]) << 24; + + if ((dirPref[i] & 0xF) == 0) + { + dirPref[i] = dirPref[i] << 8; + } + if ((dirPref[i] & 0xF00) == 0) + { + dirPref[i] = (dirPref[i] & 0xF) | dirPref[i] >> 8; + } + if ((dirPref[i] & 0xF0000) == 0) + { + dirPref[i] = (dirPref[i] & 0xFFFF) + | ((dirPref[i] & 0xF000000) >> 8); + } + + dir[i] = dirPref[i] & 0xF; + + if (portConstraint[i] == mxConstants.DIRECTION_MASK_WEST + || portConstraint[i] == mxConstants.DIRECTION_MASK_NORTH + || portConstraint[i] == mxConstants.DIRECTION_MASK_EAST + || portConstraint[i] == mxConstants.DIRECTION_MASK_SOUTH) + { + dir[i] = portConstraint[i]; + } + } + + //============================================================== + // End of source and target direction determination + + var sourceIndex = dir[0] == mxConstants.DIRECTION_MASK_EAST ? 3 + : dir[0]; + var targetIndex = dir[1] == mxConstants.DIRECTION_MASK_EAST ? 3 + : dir[1]; + + sourceIndex -= quad; + targetIndex -= quad; + + if (sourceIndex < 1) + { + sourceIndex += 4; + } + if (targetIndex < 1) + { + targetIndex += 4; + } + + var routePattern = mxEdgeStyle.routePatterns[sourceIndex - 1][targetIndex - 1]; + + mxEdgeStyle.wayPoints1[0][0] = geo[0][0]; + mxEdgeStyle.wayPoints1[0][1] = geo[0][1]; + + switch (dir[0]) + { + case mxConstants.DIRECTION_MASK_WEST: + mxEdgeStyle.wayPoints1[0][0] -= scaledOrthBuffer; + mxEdgeStyle.wayPoints1[0][1] += constraint[0][1] * geo[0][3]; + break; + case mxConstants.DIRECTION_MASK_SOUTH: + mxEdgeStyle.wayPoints1[0][0] += constraint[0][0] * geo[0][2]; + mxEdgeStyle.wayPoints1[0][1] += geo[0][3] + scaledOrthBuffer; + break; + case mxConstants.DIRECTION_MASK_EAST: + mxEdgeStyle.wayPoints1[0][0] += geo[0][2] + scaledOrthBuffer; + mxEdgeStyle.wayPoints1[0][1] += constraint[0][1] * geo[0][3]; + break; + case mxConstants.DIRECTION_MASK_NORTH: + mxEdgeStyle.wayPoints1[0][0] += constraint[0][0] * geo[0][2]; + mxEdgeStyle.wayPoints1[0][1] -= scaledOrthBuffer; + break; + } + + var currentIndex = 0; + + // Orientation, 0 horizontal, 1 vertical + var lastOrientation = (dir[0] & (mxConstants.DIRECTION_MASK_EAST | mxConstants.DIRECTION_MASK_WEST)) > 0 ? 0 + : 1; + var initialOrientation = lastOrientation; + var currentOrientation = 0; + + for (var i = 0; i < routePattern.length; i++) + { + var nextDirection = routePattern[i] & 0xF; + + // Rotate the index of this direction by the quad + // to get the real direction + var directionIndex = nextDirection == mxConstants.DIRECTION_MASK_EAST ? 3 + : nextDirection; + + directionIndex += quad; + + if (directionIndex > 4) + { + directionIndex -= 4; + } + + var direction = mxEdgeStyle.dirVectors[directionIndex - 1]; + + currentOrientation = (directionIndex % 2 > 0) ? 0 : 1; + // Only update the current index if the point moved + // in the direction of the current segment move, + // otherwise the same point is moved until there is + // a segment direction change + if (currentOrientation != lastOrientation) + { + currentIndex++; + // Copy the previous way point into the new one + // We can't base the new position on index - 1 + // because sometime elbows turn out not to exist, + // then we'd have to rewind. + mxEdgeStyle.wayPoints1[currentIndex][0] = mxEdgeStyle.wayPoints1[currentIndex - 1][0]; + mxEdgeStyle.wayPoints1[currentIndex][1] = mxEdgeStyle.wayPoints1[currentIndex - 1][1]; + } + + var tar = (routePattern[i] & mxEdgeStyle.TARGET_MASK) > 0; + var sou = (routePattern[i] & mxEdgeStyle.SOURCE_MASK) > 0; + var side = (routePattern[i] & mxEdgeStyle.SIDE_MASK) >> 5; + side = side << quad; + + if (side > 0xF) + { + side = side >> 4; + } + + var center = (routePattern[i] & mxEdgeStyle.CENTER_MASK) > 0; + + if ((sou || tar) && side < 9) + { + var limit = 0; + var souTar = sou ? 0 : 1; + + if (center && currentOrientation == 0) + { + limit = geo[souTar][0] + constraint[souTar][0] * geo[souTar][2]; + } + else if (center) + { + limit = geo[souTar][1] + constraint[souTar][1] * geo[souTar][3]; + } + else + { + limit = mxEdgeStyle.limits[souTar][side]; + } + + if (currentOrientation == 0) + { + var lastX = mxEdgeStyle.wayPoints1[currentIndex][0]; + var deltaX = (limit - lastX) * direction[0]; + + if (deltaX > 0) + { + mxEdgeStyle.wayPoints1[currentIndex][0] += direction[0] + * deltaX; + } + } + else + { + var lastY = mxEdgeStyle.wayPoints1[currentIndex][1]; + var deltaY = (limit - lastY) * direction[1]; + + if (deltaY > 0) + { + mxEdgeStyle.wayPoints1[currentIndex][1] += direction[1] + * deltaY; + } + } + } + + else if (center) + { + // Which center we're travelling to depend on the current direction + mxEdgeStyle.wayPoints1[currentIndex][0] += direction[0] + * Math.abs(mxEdgeStyle.vertexSeperations[directionIndex] / 2); + mxEdgeStyle.wayPoints1[currentIndex][1] += direction[1] + * Math.abs(mxEdgeStyle.vertexSeperations[directionIndex] / 2); + } + + if (currentIndex > 0 + && mxEdgeStyle.wayPoints1[currentIndex][currentOrientation] == mxEdgeStyle.wayPoints1[currentIndex - 1][currentOrientation]) + { + currentIndex--; + } + else + { + lastOrientation = currentOrientation; + } + } + + for (var i = 0; i <= currentIndex; i++) + { + if (i == currentIndex) + { + // Last point can cause last segment to be in + // same direction as jetty/approach. If so, + // check the number of points is consistent + // with the relative orientation of source and target + // jettys. Same orientation requires an even + // number of turns (points), different requires + // odd. + var targetOrientation = (dir[1] & (mxConstants.DIRECTION_MASK_EAST | mxConstants.DIRECTION_MASK_WEST)) > 0 ? 0 + : 1; + var sameOrient = targetOrientation == initialOrientation ? 0 : 1; + + // (currentIndex + 1) % 2 is 0 for even number of points, + // 1 for odd + if (sameOrient != (currentIndex + 1) % 2) + { + // The last point isn't required + break; + } + } + + result.push(new mxPoint(mxEdgeStyle.wayPoints1[i][0], mxEdgeStyle.wayPoints1[i][1])); + } + }, + + getRoutePattern: function(dir, quad, dx, dy) + { + var sourceIndex = dir[0] == mxConstants.DIRECTION_MASK_EAST ? 3 + : dir[0]; + var targetIndex = dir[1] == mxConstants.DIRECTION_MASK_EAST ? 3 + : dir[1]; + + sourceIndex -= quad; + targetIndex -= quad; + + if (sourceIndex < 1) + { + sourceIndex += 4; + } + if (targetIndex < 1) + { + targetIndex += 4; + } + + var result = routePatterns[sourceIndex - 1][targetIndex - 1]; + + if (dx == 0 || dy == 0) + { + if (inlineRoutePatterns[sourceIndex - 1][targetIndex - 1] != null) + { + result = inlineRoutePatterns[sourceIndex - 1][targetIndex - 1]; + } + } + + return result; + } +};
\ No newline at end of file diff --git a/src/js/view/mxGraph.js b/src/js/view/mxGraph.js new file mode 100644 index 0000000..7c90f9b --- /dev/null +++ b/src/js/view/mxGraph.js @@ -0,0 +1,11176 @@ +/** + * $Id: mxGraph.js,v 1.702 2012-12-13 15:07:34 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxGraph + * + * Extends <mxEventSource> to implement a graph component for + * the browser. This is the main class of the package. To activate + * panning and connections use <setPanning> and <setConnectable>. + * For rubberband selection you must create a new instance of + * <mxRubberband>. The following listeners are added to + * <mouseListeners> by default: + * + * - <tooltipHandler>: <mxTooltipHandler> that displays tooltips + * - <panningHandler>: <mxPanningHandler> for panning and popup menus + * - <connectionHandler>: <mxConnectionHandler> for creating connections + * - <graphHandler>: <mxGraphHandler> for moving and cloning cells + * + * These listeners will be called in the above order if they are enabled. + * + * Background Images: + * + * To display a background image, set the image, image width and + * image height using <setBackgroundImage>. If one of the + * above values has changed then the <view>'s <mxGraphView.validate> + * should be invoked. + * + * Cell Images: + * + * To use images in cells, a shape must be specified in the default + * vertex style (or any named style). Possible shapes are + * <mxConstants.SHAPE_IMAGE> and <mxConstants.SHAPE_LABEL>. + * The code to change the shape used in the default vertex style, + * the following code is used: + * + * (code) + * var style = graph.getStylesheet().getDefaultVertexStyle(); + * style[mxConstants.STYLE_SHAPE] = mxConstants.SHAPE_IMAGE; + * (end) + * + * For the default vertex style, the image to be displayed can be + * specified in a cell's style using the <mxConstants.STYLE_IMAGE> + * key and the image URL as a value, for example: + * + * (code) + * image=http://www.example.com/image.gif + * (end) + * + * For a named style, the the stylename must be the first element + * of the cell style: + * + * (code) + * stylename;image=http://www.example.com/image.gif + * (end) + * + * A cell style can have any number of key=value pairs added, divided + * by a semicolon as follows: + * + * (code) + * [stylename;|key=value;] + * (end) + * + * Labels: + * + * The cell labels are defined by <getLabel> which uses <convertValueToString> + * if <labelsVisible> is true. If a label must be rendered as HTML markup, then + * <isHtmlLabel> should return true for the respective cell. If all labels + * contain HTML markup, <htmlLabels> can be set to true. NOTE: Enabling HTML + * labels carries a possible security risk (see the section on security in + * the manual). + * + * If wrapping is needed for a label, then <isHtmlLabel> and <isWrapping> must + * return true for the cell whose label should be wrapped. See <isWrapping> for + * an example. + * + * If clipping is needed to keep the rendering of a HTML label inside the + * bounds of its vertex, then <isClipping> should return true for the + * respective cell. + * + * By default, edge labels are movable and vertex labels are fixed. This can be + * changed by setting <edgeLabelsMovable> and <vertexLabelsMovable>, or by + * overriding <isLabelMovable>. + * + * In-place Editing: + * + * In-place editing is started with a doubleclick or by typing F2. + * Programmatically, <edit> is used to check if the cell is editable + * (<isCellEditable>) and call <startEditingAtCell>, which invokes + * <mxCellEditor.startEditing>. The editor uses the value returned + * by <getEditingValue> as the editing value. + * + * After in-place editing, <labelChanged> is called, which invokes + * <mxGraphModel.setValue>, which in turn calls + * <mxGraphModel.valueForCellChanged> via <mxValueChange>. + * + * The event that triggers in-place editing is passed through to the + * <cellEditor>, which may take special actions depending on the type of the + * event or mouse location, and is also passed to <getEditingValue>. The event + * is then passed back to the event processing functions which can perform + * specific actions based on the trigger event. + * + * Tooltips: + * + * Tooltips are implemented by <getTooltip>, which calls <getTooltipForCell> + * if a cell is under the mousepointer. The default implementation checks if + * the cell has a getTooltip function and calls it if it exists. Hence, in order + * to provide custom tooltips, the cell must provide a getTooltip function, or + * one of the two above functions must be overridden. + * + * Typically, for custom cell tooltips, the latter function is overridden as + * follows: + * + * (code) + * graph.getTooltipForCell = function(cell) + * { + * var label = this.convertValueToString(cell); + * return 'Tooltip for '+label; + * } + * (end) + * + * When using a config file, the function is overridden in the mxGraph section + * using the following entry: + * + * (code) + * <add as="getTooltipForCell"><![CDATA[ + * function(cell) + * { + * var label = this.convertValueToString(cell); + * return 'Tooltip for '+label; + * } + * ]]></add> + * (end) + * + * "this" refers to the graph in the implementation, so for example to check if + * a cell is an edge, you use this.getModel().isEdge(cell) + * + * For replacing the default implementation of <getTooltipForCell> (rather than + * replacing the function on a specific instance), the following code should be + * used after loading the JavaScript files, but before creating a new mxGraph + * instance using <mxGraph>: + * + * (code) + * mxGraph.prototype.getTooltipForCell = function(cell) + * { + * var label = this.convertValueToString(cell); + * return 'Tooltip for '+label; + * } + * (end) + * + * Shapes & Styles: + * + * The implementation of new shapes is demonstrated in the examples. We'll assume + * that we have implemented a custom shape with the name BoxShape which we want + * to use for drawing vertices. To use this shape, it must first be registered in + * the cell renderer as follows: + * + * (code) + * graph.cellRenderer.registerShape('box', BoxShape); + * (end) + * + * The code registers the BoxShape constructor under the name box in the cell + * renderer of the graph. The shape can now be referenced using the shape-key in + * a style definition. (The cell renderer contains a set of additional shapes, + * namely one for each constant with a SHAPE-prefix in <mxConstants>.) + * + * Styles are a collection of key, value pairs and a stylesheet is a collection + * of named styles. The names are referenced by the cellstyle, which is stored + * in <mxCell.style> with the following format: [stylename;|key=value;]. The + * string is resolved to a collection of key, value pairs, where the keys are + * overridden with the values in the string. + * + * When introducing a new shape, the name under which the shape is registered + * must be used in the stylesheet. There are three ways of doing this: + * + * - By changing the default style, so that all vertices will use the new + * shape + * - By defining a new style, so that only vertices with the respective + * cellstyle will use the new shape + * - By using shape=box in the cellstyle's optional list of key, value pairs + * to be overridden + * + * In the first case, the code to fetch and modify the default style for + * vertices is as follows: + * + * (code) + * var style = graph.getStylesheet().getDefaultVertexStyle(); + * style[mxConstants.STYLE_SHAPE] = 'box'; + * (end) + * + * The code takes the default vertex style, which is used for all vertices that + * do not have a specific cellstyle, and modifies the value for the shape-key + * in-place to use the new BoxShape for drawing vertices. This is done by + * assigning the box value in the second line, which refers to the name of the + * BoxShape in the cell renderer. + * + * In the second case, a collection of key, value pairs is created and then + * added to the stylesheet under a new name. In order to distinguish the + * shapename and the stylename we'll use boxstyle for the stylename: + * + * (code) + * var style = new Object(); + * style[mxConstants.STYLE_SHAPE] = 'box'; + * style[mxConstants.STYLE_STROKECOLOR] = '#000000'; + * style[mxConstants.STYLE_FONTCOLOR] = '#000000'; + * graph.getStylesheet().putCellStyle('boxstyle', style); + * (end) + * + * The code adds a new style with the name boxstyle to the stylesheet. To use + * this style with a cell, it must be referenced from the cellstyle as follows: + * + * (code) + * var vertex = graph.insertVertex(parent, null, 'Hello, World!', 20, 20, 80, 20, + * 'boxstyle'); + * (end) + * + * To summarize, each new shape must be registered in the <mxCellRenderer> with + * a unique name. That name is then used as the value of the shape-key in a + * default or custom style. If there are multiple custom shapes, then there + * should be a separate style for each shape. + * + * Inheriting Styles: + * + * For fill-, stroke-, gradient- and indicatorColors special keywords can be + * used. The inherit keyword for one of these colors will inherit the color + * for the same key from the parent cell. The swimlane keyword does the same, + * but inherits from the nearest swimlane in the ancestor hierarchy. Finally, + * the indicated keyword will use the color of the indicator as the color for + * the given key. + * + * Scrollbars: + * + * The <containers> overflow CSS property defines if scrollbars are used to + * display the graph. For values of 'auto' or 'scroll', the scrollbars will + * be shown. Note that the <resizeContainer> flag is normally not used + * together with scrollbars, as it will resize the container to match the + * size of the graph after each change. + * + * Multiplicities and Validation: + * + * To control the possible connections in mxGraph, <getEdgeValidationError> is + * used. The default implementation of the function uses <multiplicities>, + * which is an array of <mxMultiplicity>. Using this class allows to establish + * simple multiplicities, which are enforced by the graph. + * + * The <mxMultiplicity> uses <mxCell.is> to determine for which terminals it + * applies. The default implementation of <mxCell.is> works with DOM nodes (XML + * nodes) and checks if the given type parameter matches the nodeName of the + * node (case insensitive). Optionally, an attributename and value can be + * specified which are also checked. + * + * <getEdgeValidationError> is called whenever the connectivity of an edge + * changes. It returns an empty string or an error message if the edge is + * invalid or null if the edge is valid. If the returned string is not empty + * then it is displayed as an error message. + * + * <mxMultiplicity> allows to specify the multiplicity between a terminal and + * its possible neighbors. For example, if any rectangle may only be connected + * to, say, a maximum of two circles you can add the following rule to + * <multiplicities>: + * + * (code) + * graph.multiplicities.push(new mxMultiplicity( + * true, 'rectangle', null, null, 0, 2, ['circle'], + * 'Only 2 targets allowed', + * 'Only shape targets allowed')); + * (end) + * + * This will display the first error message whenever a rectangle is connected + * to more than two circles and the second error message if a rectangle is + * connected to anything but a circle. + * + * For certain multiplicities, such as a minimum of 1 connection, which cannot + * be enforced at cell creation time (unless the cell is created together with + * the connection), mxGraph offers <validate> which checks all multiplicities + * for all cells and displays the respective error messages in an overlay icon + * on the cells. + * + * If a cell is collapsed and contains validation errors, a respective warning + * icon is attached to the collapsed cell. + * + * Auto-Layout: + * + * For automatic layout, the <getLayout> hook is provided in <mxLayoutManager>. + * It can be overridden to return a layout algorithm for the children of a + * given cell. + * + * Unconnected edges: + * + * The default values for all switches are designed to meet the requirements of + * general diagram drawing applications. A very typical set of settings to + * avoid edges that are not connected is the following: + * + * (code) + * graph.setAllowDanglingEdges(false); + * graph.setDisconnectOnMove(false); + * (end) + * + * Setting the <cloneInvalidEdges> switch to true is optional. This switch + * controls if edges are inserted after a copy, paste or clone-drag if they are + * invalid. For example, edges are invalid if copied or control-dragged without + * having selected the corresponding terminals and allowDanglingEdges is + * false, in which case the edges will not be cloned if the switch is false. + * + * Output: + * + * To produce an XML representation for a diagram, the following code can be + * used. + * + * (code) + * var enc = new mxCodec(mxUtils.createXmlDocument()); + * var node = enc.encode(graph.getModel()); + * (end) + * + * This will produce an XML node than can be handled using the DOM API or + * turned into a string representation using the following code: + * + * (code) + * var xml = mxUtils.getXml(node); + * (end) + * + * To obtain a formatted string, mxUtils.getPrettyXml can be used instead. + * + * This string can now be stored in a local persistent storage (for example + * using Google Gears) or it can be passed to a backend using mxUtils.post as + * follows. The url variable is the URL of the Java servlet, PHP page or HTTP + * handler, depending on the server. + * + * (code) + * var xmlString = encodeURIComponent(mxUtils.getXml(node)); + * mxUtils.post(url, 'xml='+xmlString, function(req) + * { + * // Process server response using req of type mxXmlRequest + * }); + * (end) + * + * Input: + * + * To load an XML representation of a diagram into an existing graph object + * mxUtils.load can be used as follows. The url variable is the URL of the Java + * servlet, PHP page or HTTP handler that produces the XML string. + * + * (code) + * var xmlDoc = mxUtils.load(url).getXml(); + * var node = xmlDoc.documentElement; + * var dec = new mxCodec(node.ownerDocument); + * dec.decode(node, graph.getModel()); + * (end) + * + * For creating a page that loads the client and a diagram using a single + * request please refer to the deployment examples in the backends. + * + * Functional dependencies: + * + * (see images/callgraph.png) + * + * Resources: + * + * resources/graph - Language resources for mxGraph + * + * Group: Events + * + * Event: mxEvent.ROOT + * + * Fires if the root in the model has changed. This event has no properties. + * + * Event: mxEvent.ALIGN_CELLS + * + * Fires between begin- and endUpdate in <alignCells>. The <code>cells</code> + * and <code>align</code> properties contain the respective arguments that were + * passed to <alignCells>. + * + * Event: mxEvent.FLIP_EDGE + * + * Fires between begin- and endUpdate in <flipEdge>. The <code>edge</code> + * property contains the edge passed to <flipEdge>. + * + * Event: mxEvent.ORDER_CELLS + * + * Fires between begin- and endUpdate in <orderCells>. The <code>cells</code> + * and <code>back</code> properties contain the respective arguments that were + * passed to <orderCells>. + * + * Event: mxEvent.CELLS_ORDERED + * + * Fires between begin- and endUpdate in <cellsOrdered>. The <code>cells</code> + * and <code>back</code> arguments contain the respective arguments that were + * passed to <cellsOrdered>. + * + * Event: mxEvent.GROUP_CELLS + * + * Fires between begin- and endUpdate in <groupCells>. The <code>group</code>, + * <code>cells</code> and <code>border</code> arguments contain the respective + * arguments that were passed to <groupCells>. + * + * Event: mxEvent.UNGROUP_CELLS + * + * Fires between begin- and endUpdate in <ungroupCells>. The <code>cells</code> + * property contains the array of cells that was passed to <ungroupCells>. + * + * Event: mxEvent.REMOVE_CELLS_FROM_PARENT + * + * Fires between begin- and endUpdate in <removeCellsFromParent>. The + * <code>cells</code> property contains the array of cells that was passed to + * <removeCellsFromParent>. + * + * Event: mxEvent.ADD_CELLS + * + * Fires between begin- and endUpdate in <addCells>. The <code>cells</code>, + * <code>parent</code>, <code>index</code>, <code>source</code> and + * <code>target</code> properties contain the respective arguments that were + * passed to <addCells>. + * + * Event: mxEvent.CELLS_ADDED + * + * Fires between begin- and endUpdate in <cellsAdded>. The <code>cells</code>, + * <code>parent</code>, <code>index</code>, <code>source</code>, + * <code>target</code> and <code>absolute</code> properties contain the + * respective arguments that were passed to <cellsAdded>. + * + * Event: mxEvent.REMOVE_CELLS + * + * Fires between begin- and endUpdate in <removeCells>. The <code>cells</code> + * and <code>includeEdges</code> arguments contain the respective arguments + * that were passed to <removeCells>. + * + * Event: mxEvent.CELLS_REMOVED + * + * Fires between begin- and endUpdate in <cellsRemoved>. The <code>cells</code> + * argument contains the array of cells that was removed. + * + * Event: mxEvent.SPLIT_EDGE + * + * Fires between begin- and endUpdate in <splitEdge>. The <code>edge</code> + * property contains the edge to be splitted, the <code>cells</code>, + * <code>newEdge</code>, <code>dx</code> and <code>dy</code> properties contain + * the respective arguments that were passed to <splitEdge>. + * + * Event: mxEvent.TOGGLE_CELLS + * + * Fires between begin- and endUpdate in <toggleCells>. The <code>show</code>, + * <code>cells</code> and <code>includeEdges</code> properties contain the + * respective arguments that were passed to <toggleCells>. + * + * Event: mxEvent.FOLD_CELLS + * + * Fires between begin- and endUpdate in <foldCells>. The + * <code>collapse</code>, <code>cells</code> and <code>recurse</code> + * properties contain the respective arguments that were passed to <foldCells>. + * + * Event: mxEvent.CELLS_FOLDED + * + * Fires between begin- and endUpdate in cellsFolded. The + * <code>collapse</code>, <code>cells</code> and <code>recurse</code> + * properties contain the respective arguments that were passed to + * <cellsFolded>. + * + * Event: mxEvent.UPDATE_CELL_SIZE + * + * Fires between begin- and endUpdate in <updateCellSize>. The + * <code>cell</code> and <code>ignoreChildren</code> properties contain the + * respective arguments that were passed to <updateCellSize>. + * + * Event: mxEvent.RESIZE_CELLS + * + * Fires between begin- and endUpdate in <resizeCells>. The <code>cells</code> + * and <code>bounds</code> properties contain the respective arguments that + * were passed to <resizeCells>. + * + * Event: mxEvent.CELLS_RESIZED + * + * Fires between begin- and endUpdate in <cellsResized>. The <code>cells</code> + * and <code>bounds</code> properties contain the respective arguments that + * were passed to <cellsResized>. + * + * Event: mxEvent.MOVE_CELLS + * + * Fires between begin- and endUpdate in <moveCells>. The <code>cells</code>, + * <code>dx</code>, <code>dy</code>, <code>clone</code>, <code>target</code> + * and <code>event</code> properties contain the respective arguments that + * were passed to <moveCells>. + * + * Event: mxEvent.CELLS_MOVED + * + * Fires between begin- and endUpdate in <cellsMoved>. The <code>cells</code>, + * <code>dx</code>, <code>dy</code> and <code>disconnect</code> properties + * contain the respective arguments that were passed to <cellsMoved>. + * + * Event: mxEvent.CONNECT_CELL + * + * Fires between begin- and endUpdate in <connectCell>. The <code>edge</code>, + * <code>terminal</code> and <code>source</code> properties contain the + * respective arguments that were passed to <connectCell>. + * + * Event: mxEvent.CELL_CONNECTED + * + * Fires between begin- and endUpdate in <cellConnected>. The + * <code>edge</code>, <code>terminal</code> and <code>source</code> properties + * contain the respective arguments that were passed to <cellConnected>. + * + * Event: mxEvent.REFRESH + * + * Fires after <refresh> was executed. This event has no properties. + * + * Event: mxEvent.CLICK + * + * Fires in <click> after a click event. The <code>event</code> property + * contains the original mouse event and <code>cell</code> property contains + * the cell under the mouse or null if the background was clicked. + * + * To handle a click event, use the following code: + * + * (code) + * graph.addListener(mxEvent.CLICK, function(sender, evt) + * { + * var e = evt.getProperty('event'); // mouse event + * var cell = evt.getProperty('cell'); // cell may be null + * + * if (!evt.isConsumed()) + * { + * if (cell != null) + * { + * // Do something useful with cell and consume the event + * evt.consume(); + * } + * } + * }); + * (end) + * + * Event: mxEvent.DOUBLE_CLICK + * + * Fires in <dblClick> after a double click. The <code>event</code> property + * contains the original mouse event and the <code>cell</code> property + * contains the cell under the mouse or null if the background was clicked. + * + * Event: mxEvent.SIZE + * + * Fires after <sizeDidChange> was executed. The <code>bounds</code> property + * contains the new graph bounds. + * + * Event: mxEvent.START_EDITING + * + * Fires before the in-place editor starts in <startEditingAtCell>. The + * <code>cell</code> property contains the cell that is being edited and the + * <code>event</code> property contains the optional event argument that was + * passed to <startEditingAtCell>. + * + * Event: mxEvent.LABEL_CHANGED + * + * Fires between begin- and endUpdate in <cellLabelChanged>. The + * <code>cell</code> property contains the cell, the <code>value</code> + * property contains the new value for the cell and the optional + * <code>event</code> property contains the mouse event that started the edit. + * + * Event: mxEvent.ADD_OVERLAY + * + * Fires after an overlay is added in <addCellOverlay>. The <code>cell</code> + * property contains the cell and the <code>overlay</code> property contains + * the <mxCellOverlay> that was added. + * + * Event: mxEvent.REMOVE_OVERLAY + * + * Fires after an overlay is removed in <removeCellOverlay> and + * <removeCellOverlays>. The <code>cell</code> property contains the cell and + * the <code>overlay</code> property contains the <mxCellOverlay> that was + * removed. + * + * Constructor: mxGraph + * + * Constructs a new mxGraph in the specified container. Model is an optional + * mxGraphModel. If no model is provided, a new mxGraphModel instance is + * used as the model. The container must have a valid owner document prior + * to calling this function in Internet Explorer. RenderHint is a string to + * affect the display performance and rendering in IE, but not in SVG-based + * browsers. The parameter is mapped to <dialect>, which may + * be one of <mxConstants.DIALECT_SVG> for SVG-based browsers, + * <mxConstants.DIALECT_STRICTHTML> for fastest display mode, + * <mxConstants.DIALECT_PREFERHTML> for faster display mode, + * <mxConstants.DIALECT_MIXEDHTML> for fast and <mxConstants.DIALECT_VML> + * for exact display mode (slowest). The dialects are defined in mxConstants. + * The default values are DIALECT_SVG for SVG-based browsers and + * DIALECT_MIXED for IE. + * + * The possible values for the renderingHint parameter are explained below: + * + * fast - The parameter is based on the fact that the display performance is + * highly improved in IE if the VML is not contained within a VML group + * element. The lack of a group element only slightly affects the display while + * panning, but improves the performance by almost a factor of 2, while keeping + * the display sufficiently accurate. This also allows to render certain shapes as HTML + * if the display accuracy is not affected, which is implemented by + * <mxShape.isMixedModeHtml>. This is the default setting and is mapped to + * DIALECT_MIXEDHTML. + * faster - Same as fast, but more expensive shapes are avoided. This is + * controlled by <mxShape.preferModeHtml>. The default implementation will + * avoid gradients and rounded rectangles, but more significant shapes, such + * as rhombus, ellipse, actor and cylinder will be rendered accurately. This + * setting is mapped to DIALECT_PREFERHTML. + * fastest - Almost anything will be rendered in Html. This allows for + * rectangles, labels and images. This setting is mapped to + * DIALECT_STRICTHTML. + * exact - If accurate panning is required and if the diagram is small (up + * to 100 cells), then this value should be used. In this mode, a group is + * created that contains the VML. This allows for accurate panning and is + * mapped to DIALECT_VML. + * + * Example: + * + * To create a graph inside a DOM node with an id of graph: + * (code) + * var container = document.getElementById('graph'); + * var graph = new mxGraph(container); + * (end) + * + * Parameters: + * + * container - Optional DOM node that acts as a container for the graph. + * If this is null then the container can be initialized later using + * <init>. + * model - Optional <mxGraphModel> that constitutes the graph data. + * renderHint - Optional string that specifies the display accuracy and + * performance. Default is mxConstants.DIALECT_MIXEDHTML (for IE). + * stylesheet - Optional <mxStylesheet> to be used in the graph. + */ +function mxGraph(container, model, renderHint, stylesheet) +{ + // Initializes the variable in case the prototype has been + // modified to hold some listeners (which is possible because + // the createHandlers call is executed regardless of the + // arguments passed into the ctor). + this.mouseListeners = null; + + // Converts the renderHint into a dialect + this.renderHint = renderHint; + + if (mxClient.IS_SVG) + { + this.dialect = mxConstants.DIALECT_SVG; + } + else if (renderHint == mxConstants.RENDERING_HINT_EXACT && mxClient.IS_VML) + { + this.dialect = mxConstants.DIALECT_VML; + } + else if (renderHint == mxConstants.RENDERING_HINT_FASTEST) + { + this.dialect = mxConstants.DIALECT_STRICTHTML; + } + else if (renderHint == mxConstants.RENDERING_HINT_FASTER) + { + this.dialect = mxConstants.DIALECT_PREFERHTML; + } + else // default for VML + { + this.dialect = mxConstants.DIALECT_MIXEDHTML; + } + + // Initializes the main members that do not require a container + this.model = (model != null) ? model : new mxGraphModel(); + this.multiplicities = []; + this.imageBundles = []; + this.cellRenderer = this.createCellRenderer(); + this.setSelectionModel(this.createSelectionModel()); + this.setStylesheet((stylesheet != null) ? stylesheet : this.createStylesheet()); + this.view = this.createGraphView(); + + // Adds a graph model listener to update the view + this.graphModelChangeListener = mxUtils.bind(this, function(sender, evt) + { + this.graphModelChanged(evt.getProperty('edit').changes); + }); + + this.model.addListener(mxEvent.CHANGE, this.graphModelChangeListener); + + // Installs basic event handlers with disabled default settings. + this.createHandlers(); + + // Initializes the display if a container was specified + if (container != null) + { + this.init(container); + } + + this.view.revalidate(); +}; + +/** + * Installs the required language resources at class + * loading time. + */ +if (mxLoadResources) +{ + mxResources.add(mxClient.basePath+'/resources/graph'); +} + +/** + * Extends mxEventSource. + */ +mxGraph.prototype = new mxEventSource(); +mxGraph.prototype.constructor = mxGraph; + +/** + * Variable: EMPTY_ARRAY + * + * Immutable empty array instance. + */ +mxGraph.prototype.EMPTY_ARRAY = []; + +/** + * Group: Variables + */ + +/** + * Variable: mouseListeners + * + * Holds the mouse event listeners. See <fireMouseEvent>. + */ +mxGraph.prototype.mouseListeners = null; + +/** + * Variable: isMouseDown + * + * Holds the state of the mouse button. + */ +mxGraph.prototype.isMouseDown = false; + +/** + * Variable: model + * + * Holds the <mxGraphModel> that contains the cells to be displayed. + */ +mxGraph.prototype.model = null; + +/** + * Variable: view + * + * Holds the <mxGraphView> that caches the <mxCellStates> for the cells. + */ +mxGraph.prototype.view = null; + +/** + * Variable: stylesheet + * + * Holds the <mxStylesheet> that defines the appearance of the cells. + * + * + * Example: + * + * Use the following code to read a stylesheet into an existing graph. + * + * (code) + * var req = mxUtils.load('stylesheet.xml'); + * var root = req.getDocumentElement(); + * var dec = new mxCodec(root.ownerDocument); + * dec.decode(root, graph.stylesheet); + * (end) + */ +mxGraph.prototype.stylesheet = null; + +/** + * Variable: selectionModel + * + * Holds the <mxGraphSelectionModel> that models the current selection. + */ +mxGraph.prototype.selectionModel = null; + +/** + * Variable: cellEditor + * + * Holds the <mxCellEditor> that is used as the in-place editing. + */ +mxGraph.prototype.cellEditor = null; + +/** + * Variable: cellRenderer + * + * Holds the <mxCellRenderer> for rendering the cells in the graph. + */ +mxGraph.prototype.cellRenderer = null; + +/** + * Variable: multiplicities + * + * An array of <mxMultiplicities> describing the allowed + * connections in a graph. + */ +mxGraph.prototype.multiplicities = null; + +/** + * Variable: renderHint + * + * RenderHint as it was passed to the constructor. + */ +mxGraph.prototype.renderHint = null; + +/** + * Variable: dialect + * + * Dialect to be used for drawing the graph. Possible values are all + * constants in <mxConstants> with a DIALECT-prefix. + */ +mxGraph.prototype.dialect = null; + +/** + * Variable: gridSize + * + * Specifies the grid size. Default is 10. + */ +mxGraph.prototype.gridSize = 10; + +/** + * Variable: gridEnabled + * + * Specifies if the grid is enabled. This is used in <snap>. Default is + * true. + */ +mxGraph.prototype.gridEnabled = true; + +/** + * Variable: portsEnabled + * + * Specifies if ports are enabled. This is used in <cellConnected> to update + * the respective style. Default is true. + */ +mxGraph.prototype.portsEnabled = true; + +/** + * Variable: doubleTapEnabled + * + * Specifies if double taps on touch-based devices should be handled. Default + * is true. + */ +mxGraph.prototype.doubleTapEnabled = true; + +/** + * Variable: doubleTapTimeout + * + * Specifies the timeout for double taps. Default is 700 ms. + */ +mxGraph.prototype.doubleTapTimeout = 700; + +/** + * Variable: doubleTapTolerance + * + * Specifies the tolerance for double taps. Default is 25 pixels. + */ +mxGraph.prototype.doubleTapTolerance = 25; + +/** + * Variable: lastTouchX + * + * Holds the x-coordinate of the last touch event for double tap detection. + */ +mxGraph.prototype.lastTouchY = 0; + +/** + * Variable: lastTouchX + * + * Holds the y-coordinate of the last touch event for double tap detection. + */ +mxGraph.prototype.lastTouchY = 0; + +/** + * Variable: lastTouchTime + * + * Holds the time of the last touch event for double click detection. + */ +mxGraph.prototype.lastTouchTime = 0; + +/** + * Variable: gestureEnabled + * + * Specifies if the handleGesture method should be invoked. Default is true. This + * is an experimental feature for touch-based devices. + */ +mxGraph.prototype.gestureEnabled = true; + +/** + * Variable: tolerance + * + * Tolerance for a move to be handled as a single click. + * Default is 4 pixels. + */ +mxGraph.prototype.tolerance = 4; + +/** + * Variable: defaultOverlap + * + * Value returned by <getOverlap> if <isAllowOverlapParent> returns + * true for the given cell. <getOverlap> is used in <constrainChild> if + * <isConstrainChild> returns true. The value specifies the + * portion of the child which is allowed to overlap the parent. + */ +mxGraph.prototype.defaultOverlap = 0.5; + +/** + * Variable: defaultParent + * + * Specifies the default parent to be used to insert new cells. + * This is used in <getDefaultParent>. Default is null. + */ +mxGraph.prototype.defaultParent = null; + +/** + * Variable: alternateEdgeStyle + * + * Specifies the alternate edge style to be used if the main control point + * on an edge is being doubleclicked. Default is null. + */ +mxGraph.prototype.alternateEdgeStyle = null; + +/** + * Variable: backgroundImage + * + * Specifies the <mxImage> to be returned by <getBackgroundImage>. Default + * is null. + * + * Example: + * + * (code) + * var img = new mxImage('http://www.example.com/maps/examplemap.jpg', 1024, 768); + * graph.setBackgroundImage(img); + * graph.view.validate(); + * (end) + */ +mxGraph.prototype.backgroundImage = null; + +/** + * Variable: pageVisible + * + * Specifies if the background page should be visible. Default is false. + * Not yet implemented. + */ +mxGraph.prototype.pageVisible = false; + +/** + * Variable: pageBreaksVisible + * + * Specifies if a dashed line should be drawn between multiple pages. Default + * is false. If you change this value while a graph is being displayed then you + * should call <sizeDidChange> to force an update of the display. + */ +mxGraph.prototype.pageBreaksVisible = false; + +/** + * Variable: pageBreakColor + * + * Specifies the color for page breaks. Default is 'gray'. + */ +mxGraph.prototype.pageBreakColor = 'gray'; + +/** + * Variable: pageBreakDashed + * + * Specifies the page breaks should be dashed. Default is true. + */ +mxGraph.prototype.pageBreakDashed = true; + +/** + * Variable: minPageBreakDist + * + * Specifies the minimum distance for page breaks to be visible. Default is + * 20 (in pixels). + */ +mxGraph.prototype.minPageBreakDist = 20; + +/** + * Variable: preferPageSize + * + * Specifies if the graph size should be rounded to the next page number in + * <sizeDidChange>. This is only used if the graph container has scrollbars. + * Default is false. + */ +mxGraph.prototype.preferPageSize = false; + +/** + * Variable: pageFormat + * + * Specifies the page format for the background page. Default is + * <mxConstants.PAGE_FORMAT_A4_PORTRAIT>. This is used as the default in + * <mxPrintPreview> and for painting the background page if <pageVisible> is + * true and the pagebreaks if <pageBreaksVisible> is true. + */ +mxGraph.prototype.pageFormat = mxConstants.PAGE_FORMAT_A4_PORTRAIT; + +/** + * Variable: pageScale + * + * Specifies the scale of the background page. Default is 1.5. + * Not yet implemented. + */ +mxGraph.prototype.pageScale = 1.5; + +/** + * Variable: enabled + * + * Specifies the return value for <isEnabled>. Default is true. + */ +mxGraph.prototype.enabled = true; + +/** + * Variable: escapeEnabled + * + * Specifies if <mxKeyHandler> should invoke <escape> when the escape key + * is pressed. Default is true. + */ +mxGraph.prototype.escapeEnabled = true; + +/** + * Variable: invokesStopCellEditing + * + * If true, when editing is to be stopped by way of selection changing, + * data in diagram changing or other means stopCellEditing is invoked, and + * changes are saved. This is implemented in a focus handler in + * <mxCellEditor>. Default is true. + */ +mxGraph.prototype.invokesStopCellEditing = true; + +/** + * Variable: enterStopsCellEditing + * + * If true, pressing the enter key without pressing control or shift will stop + * editing and accept the new value. This is used in <mxCellEditor> to stop + * cell editing. Note: You can always use F2 and escape to stop editing. + * Default is false. + */ +mxGraph.prototype.enterStopsCellEditing = false; + +/** + * Variable: useScrollbarsForPanning + * + * Specifies if scrollbars should be used for panning in <panGraph> if + * any scrollbars are available. If scrollbars are enabled in CSS, but no + * scrollbars appear because the graph is smaller than the container size, + * then no panning occurs if this is true. Default is true. + */ +mxGraph.prototype.useScrollbarsForPanning = true; + +/** + * Variable: exportEnabled + * + * Specifies the return value for <canExportCell>. Default is true. + */ +mxGraph.prototype.exportEnabled = true; + +/** + * Variable: importEnabled + * + * Specifies the return value for <canImportCell>. Default is true. + */ +mxGraph.prototype.importEnabled = true; + +/** + * Variable: cellsLocked + * + * Specifies the return value for <isCellLocked>. Default is false. + */ +mxGraph.prototype.cellsLocked = false; + +/** + * Variable: cellsCloneable + * + * Specifies the return value for <isCellCloneable>. Default is true. + */ +mxGraph.prototype.cellsCloneable = true; + +/** + * Variable: foldingEnabled + * + * Specifies if folding (collapse and expand via an image icon in the graph + * should be enabled). Default is true. + */ +mxGraph.prototype.foldingEnabled = true; + +/** + * Variable: cellsEditable + * + * Specifies the return value for <isCellEditable>. Default is true. + */ +mxGraph.prototype.cellsEditable = true; + +/** + * Variable: cellsDeletable + * + * Specifies the return value for <isCellDeletable>. Default is true. + */ +mxGraph.prototype.cellsDeletable = true; + +/** + * Variable: cellsMovable + * + * Specifies the return value for <isCellMovable>. Default is true. + */ +mxGraph.prototype.cellsMovable = true; + +/** + * Variable: edgeLabelsMovable + * + * Specifies the return value for edges in <isLabelMovable>. Default is true. + */ +mxGraph.prototype.edgeLabelsMovable = true; + +/** + * Variable: vertexLabelsMovable + * + * Specifies the return value for vertices in <isLabelMovable>. Default is false. + */ +mxGraph.prototype.vertexLabelsMovable = false; + +/** + * Variable: dropEnabled + * + * Specifies the return value for <isDropEnabled>. Default is false. + */ +mxGraph.prototype.dropEnabled = false; + +/** + * Variable: splitEnabled + * + * Specifies if dropping onto edges should be enabled. Default is true. + */ +mxGraph.prototype.splitEnabled = true; + +/** + * Variable: cellsResizable + * + * Specifies the return value for <isCellResizable>. Default is true. + */ +mxGraph.prototype.cellsResizable = true; + +/** + * Variable: cellsBendable + * + * Specifies the return value for <isCellsBendable>. Default is true. + */ +mxGraph.prototype.cellsBendable = true; + +/** + * Variable: cellsSelectable + * + * Specifies the return value for <isCellSelectable>. Default is true. + */ +mxGraph.prototype.cellsSelectable = true; + +/** + * Variable: cellsDisconnectable + * + * Specifies the return value for <isCellDisconntable>. Default is true. + */ +mxGraph.prototype.cellsDisconnectable = true; + +/** + * Variable: autoSizeCells + * + * Specifies if the graph should automatically update the cell size after an + * edit. This is used in <isAutoSizeCell>. Default is false. + */ +mxGraph.prototype.autoSizeCells = false; + +/** + * Variable: autoScroll + * + * Specifies if the graph should automatically scroll if the mouse goes near + * the container edge while dragging. This is only taken into account if the + * container has scrollbars. Default is true. + * + * If you need this to work without scrollbars then set <ignoreScrollbars> to + * true. + */ +mxGraph.prototype.autoScroll = true; + +/** + * Variable: timerAutoScroll + * + * Specifies if timer-based autoscrolling should be used via mxPanningManager. + * Note that this disables the code in <scrollPointToVisible> and uses code in + * mxPanningManager instead. Note that <autoExtend> is disabled if this is + * true and that this should only be used with a scroll buffer or when + * scollbars are visible and scrollable in all directions. Default is false. + */ +mxGraph.prototype.timerAutoScroll = false; + +/** + * Variable: allowAutoPanning + * + * Specifies if panning via <panGraph> should be allowed to implement autoscroll + * if no scrollbars are available in <scrollPointToVisible>. Default is false. + */ +mxGraph.prototype.allowAutoPanning = false; + +/** + * Variable: ignoreScrollbars + * + * Specifies if the graph should automatically scroll regardless of the + * scrollbars. + */ +mxGraph.prototype.ignoreScrollbars = false; + +/** + * Variable: autoExtend + * + * Specifies if the size of the graph should be automatically extended if the + * mouse goes near the container edge while dragging. This is only taken into + * account if the container has scrollbars. Default is true. See <autoScroll>. + */ +mxGraph.prototype.autoExtend = true; + +/** + * Variable: maximumGraphBounds + * + * <mxRectangle> that specifies the area in which all cells in the diagram + * should be placed. Uses in <getMaximumGraphBounds>. Use a width or height of + * 0 if you only want to give a upper, left corner. + */ +mxGraph.prototype.maximumGraphBounds = null; + +/** + * Variable: minimumGraphSize + * + * <mxRectangle> that specifies the minimum size of the graph. This is ignored + * if the graph container has no scrollbars. Default is null. + */ +mxGraph.prototype.minimumGraphSize = null; + +/** + * Variable: minimumContainerSize + * + * <mxRectangle> that specifies the minimum size of the <container> if + * <resizeContainer> is true. + */ +mxGraph.prototype.minimumContainerSize = null; + +/** + * Variable: maximumContainerSize + * + * <mxRectangle> that specifies the maximum size of the container if + * <resizeContainer> is true. + */ +mxGraph.prototype.maximumContainerSize = null; + +/** + * Variable: resizeContainer + * + * Specifies if the container should be resized to the graph size when + * the graph size has changed. Default is false. + */ +mxGraph.prototype.resizeContainer = false; + +/** + * Variable: border + * + * Border to be added to the bottom and right side when the container is + * being resized after the graph has been changed. Default is 0. + */ +mxGraph.prototype.border = 0; + +/** + * Variable: ordered + * + * Specifies if the display should reflect the order of the cells in + * the model. Default is true. This has precendence over + * <keepEdgesInBackground> and <keepEdgesInForeground>. + */ +mxGraph.prototype.ordered = true; + +/** + * Variable: keepEdgesInForeground + * + * Specifies if edges should appear in the foreground regardless of their + * order in the model. This has precendence over <keepEdgeInBackground>, + * but not over <ordered>. Default is false. + */ +mxGraph.prototype.keepEdgesInForeground = false; + +/** + * Variable: keepEdgesInBackground + * + * Specifies if edges should appear in the background regardless of their + * order in the model. <ordered> and <keepEdgesInForeground> have + * precedence over this setting. Default is true. + */ +mxGraph.prototype.keepEdgesInBackground = true; + +/** + * Variable: allowNegativeCoordinates + * + * Specifies if negative coordinates for vertices are allowed. Default is true. + */ +mxGraph.prototype.allowNegativeCoordinates = true; + +/** + * Variable: constrainChildren + * + * Specifies the return value for <isConstrainChildren>. Default is + * true. + */ +mxGraph.prototype.constrainChildren = true; + +/** + * Variable: extendParents + * + * Specifies if a parent should contain the child bounds after a resize of + * the child. Default is true. + */ +mxGraph.prototype.extendParents = true; + +/** + * Variable: extendParentsOnAdd + * + * Specifies if parents should be extended according to the <extendParents> + * switch if cells are added. Default is true. + */ +mxGraph.prototype.extendParentsOnAdd = true; + +/** + * Variable: collapseToPreferredSize + * + * Specifies if the cell size should be changed to the preferred size when + * a cell is first collapsed. Default is true. + */ +mxGraph.prototype.collapseToPreferredSize = true; + +/** + * Variable: zoomFactor + * + * Specifies the factor used for <zoomIn> and <zoomOut>. Default is 1.2 + * (120%). + */ +mxGraph.prototype.zoomFactor = 1.2; + +/** + * Variable: keepSelectionVisibleOnZoom + * + * Specifies if the viewport should automatically contain the selection cells + * after a zoom operation. Default is false. + */ +mxGraph.prototype.keepSelectionVisibleOnZoom = false; + +/** + * Variable: centerZoom + * + * Specifies if the zoom operations should go into the center of the actual + * diagram rather than going from top, left. Default is true. + */ +mxGraph.prototype.centerZoom = true; + +/** + * Variable: resetViewOnRootChange + * + * Specifies if the scale and translate should be reset if the root changes in + * the model. Default is true. + */ +mxGraph.prototype.resetViewOnRootChange = true; + +/** + * Variable: resetEdgesOnResize + * + * Specifies if edge control points should be reset after the resize of a + * connected cell. Default is false. + */ +mxGraph.prototype.resetEdgesOnResize = false; + +/** + * Variable: resetEdgesOnMove + * + * Specifies if edge control points should be reset after the move of a + * connected cell. Default is false. + */ +mxGraph.prototype.resetEdgesOnMove = false; + +/** + * Variable: resetEdgesOnConnect + * + * Specifies if edge control points should be reset after the the edge has been + * reconnected. Default is true. + */ +mxGraph.prototype.resetEdgesOnConnect = true; + +/** + * Variable: allowLoops + * + * Specifies if loops (aka self-references) are allowed. Default is false. + */ +mxGraph.prototype.allowLoops = false; + +/** + * Variable: defaultLoopStyle + * + * <mxEdgeStyle> to be used for loops. This is a fallback for loops if the + * <mxConstants.STYLE_LOOP> is undefined. Default is <mxEdgeStyle.Loop>. + */ +mxGraph.prototype.defaultLoopStyle = mxEdgeStyle.Loop; + +/** + * Variable: multigraph + * + * Specifies if multiple edges in the same direction between the same pair of + * vertices are allowed. Default is true. + */ +mxGraph.prototype.multigraph = true; + +/** + * Variable: connectableEdges + * + * Specifies if edges are connectable. Default is false. This overrides the + * connectable field in edges. + */ +mxGraph.prototype.connectableEdges = false; + +/** + * Variable: allowDanglingEdges + * + * Specifies if edges with disconnected terminals are allowed in the graph. + * Default is true. + */ +mxGraph.prototype.allowDanglingEdges = true; + +/** + * Variable: cloneInvalidEdges + * + * Specifies if edges that are cloned should be validated and only inserted + * if they are valid. Default is true. + */ +mxGraph.prototype.cloneInvalidEdges = false; + +/** + * Variable: disconnectOnMove + * + * Specifies if edges should be disconnected from their terminals when they + * are moved. Default is true. + */ +mxGraph.prototype.disconnectOnMove = true; + +/** + * Variable: labelsVisible + * + * Specifies if labels should be visible. This is used in <getLabel>. Default + * is true. + */ +mxGraph.prototype.labelsVisible = true; + +/** + * Variable: htmlLabels + * + * Specifies the return value for <isHtmlLabel>. Default is false. + */ +mxGraph.prototype.htmlLabels = false; + +/** + * Variable: swimlaneSelectionEnabled + * + * Specifies if swimlanes should be selectable via the content if the + * mouse is released. Default is true. + */ +mxGraph.prototype.swimlaneSelectionEnabled = true; + +/** + * Variable: swimlaneNesting + * + * Specifies if nesting of swimlanes is allowed. Default is true. + */ +mxGraph.prototype.swimlaneNesting = true; + +/** + * Variable: swimlaneIndicatorColorAttribute + * + * The attribute used to find the color for the indicator if the indicator + * color is set to 'swimlane'. Default is <mxConstants.STYLE_FILLCOLOR>. + */ +mxGraph.prototype.swimlaneIndicatorColorAttribute = mxConstants.STYLE_FILLCOLOR; + +/** + * Variable: imageBundles + * + * Holds the list of image bundles. + */ +mxGraph.prototype.imageBundles = null; + +/** + * Variable: minFitScale + * + * Specifies the minimum scale to be applied in <fit>. Default is 0.1. Set this + * to null to allow any value. + */ +mxGraph.prototype.minFitScale = 0.1; + +/** + * Variable: maxFitScale + * + * Specifies the maximum scale to be applied in <fit>. Default is 8. Set this + * to null to allow any value. + */ +mxGraph.prototype.maxFitScale = 8; + +/** + * Variable: panDx + * + * Current horizontal panning value. Default is 0. + */ +mxGraph.prototype.panDx = 0; + +/** + * Variable: panDy + * + * Current vertical panning value. Default is 0. + */ +mxGraph.prototype.panDy = 0; + +/** + * Variable: collapsedImage + * + * Specifies the <mxImage> to indicate a collapsed state. + * Default value is mxClient.imageBasePath + '/collapsed.gif' + */ +mxGraph.prototype.collapsedImage = new mxImage(mxClient.imageBasePath + '/collapsed.gif', 9, 9); + +/** + * Variable: expandedImage + * + * Specifies the <mxImage> to indicate a expanded state. + * Default value is mxClient.imageBasePath + '/expanded.gif' + */ +mxGraph.prototype.expandedImage = new mxImage(mxClient.imageBasePath + '/expanded.gif', 9, 9); + +/** + * Variable: warningImage + * + * Specifies the <mxImage> for the image to be used to display a warning + * overlay. See <setCellWarning>. Default value is mxClient.imageBasePath + + * '/warning'. The extension for the image depends on the platform. It is + * '.png' on the Mac and '.gif' on all other platforms. + */ +mxGraph.prototype.warningImage = new mxImage(mxClient.imageBasePath + '/warning'+ + ((mxClient.IS_MAC) ? '.png' : '.gif'), 16, 16); + +/** + * Variable: alreadyConnectedResource + * + * Specifies the resource key for the error message to be displayed in + * non-multigraphs when two vertices are already connected. If the resource + * for this key does not exist then the value is used as the error message. + * Default is 'alreadyConnected'. + */ +mxGraph.prototype.alreadyConnectedResource = (mxClient.language != 'none') ? 'alreadyConnected' : ''; + +/** + * Variable: containsValidationErrorsResource + * + * Specifies the resource key for the warning message to be displayed when + * a collapsed cell contains validation errors. If the resource for this + * key does not exist then the value is used as the warning message. + * Default is 'containsValidationErrors'. + */ +mxGraph.prototype.containsValidationErrorsResource = (mxClient.language != 'none') ? 'containsValidationErrors' : ''; + +/** + * Variable: collapseExpandResource + * + * Specifies the resource key for the tooltip on the collapse/expand icon. + * If the resource for this key does not exist then the value is used as + * the tooltip. Default is 'collapse-expand'. + */ +mxGraph.prototype.collapseExpandResource = (mxClient.language != 'none') ? 'collapse-expand' : ''; + +/** + * Function: init + * + * Initializes the <container> and creates the respective datastructures. + * + * Parameters: + * + * container - DOM node that will contain the graph display. + */ + mxGraph.prototype.init = function(container) + { + this.container = container; + + // Initializes the in-place editor + this.cellEditor = this.createCellEditor(); + + // Initializes the container using the view + this.view.init(); + + // Updates the size of the container for the current graph + this.sizeDidChange(); + + // Automatic deallocation of memory + if (mxClient.IS_IE) + { + mxEvent.addListener(window, 'unload', mxUtils.bind(this, function() + { + this.destroy(); + })); + + // Disable shift-click for text + mxEvent.addListener(container, 'selectstart', + mxUtils.bind(this, function() + { + return this.isEditing(); + }) + ); + } +}; + +/** + * Function: createHandlers + * + * Creates the tooltip-, panning-, connection- and graph-handler (in this + * order). This is called in the constructor before <init> is called. + */ +mxGraph.prototype.createHandlers = function(container) +{ + this.tooltipHandler = new mxTooltipHandler(this); + this.tooltipHandler.setEnabled(false); + this.panningHandler = new mxPanningHandler(this); + this.panningHandler.panningEnabled = false; + this.selectionCellsHandler = new mxSelectionCellsHandler(this); + this.connectionHandler = new mxConnectionHandler(this); + this.connectionHandler.setEnabled(false); + this.graphHandler = new mxGraphHandler(this); +}; + +/** + * Function: createSelectionModel + * + * Creates a new <mxGraphSelectionModel> to be used in this graph. + */ +mxGraph.prototype.createSelectionModel = function() +{ + return new mxGraphSelectionModel(this); +}; + +/** + * Function: createStylesheet + * + * Creates a new <mxGraphSelectionModel> to be used in this graph. + */ +mxGraph.prototype.createStylesheet = function() +{ + return new mxStylesheet(); +}; + +/** + * Function: createGraphView + * + * Creates a new <mxGraphView> to be used in this graph. + */ +mxGraph.prototype.createGraphView = function() +{ + return new mxGraphView(this); +}; + +/** + * Function: createCellRenderer + * + * Creates a new <mxCellRenderer> to be used in this graph. + */ +mxGraph.prototype.createCellRenderer = function() +{ + return new mxCellRenderer(); +}; + +/** + * Function: createCellEditor + * + * Creates a new <mxCellEditor> to be used in this graph. + */ +mxGraph.prototype.createCellEditor = function() +{ + return new mxCellEditor(this); +}; + +/** + * Function: getModel + * + * Returns the <mxGraphModel> that contains the cells. + */ +mxGraph.prototype.getModel = function() +{ + return this.model; +}; + +/** + * Function: getView + * + * Returns the <mxGraphView> that contains the <mxCellStates>. + */ +mxGraph.prototype.getView = function() +{ + return this.view; +}; + +/** + * Function: getStylesheet + * + * Returns the <mxStylesheet> that defines the style. + */ +mxGraph.prototype.getStylesheet = function() +{ + return this.stylesheet; +}; + +/** + * Function: setStylesheet + * + * Sets the <mxStylesheet> that defines the style. + */ +mxGraph.prototype.setStylesheet = function(stylesheet) +{ + this.stylesheet = stylesheet; +}; + +/** + * Function: getSelectionModel + * + * Returns the <mxGraphSelectionModel> that contains the selection. + */ +mxGraph.prototype.getSelectionModel = function() +{ + return this.selectionModel; +}; + +/** + * Function: setSelectionModel + * + * Sets the <mxSelectionModel> that contains the selection. + */ +mxGraph.prototype.setSelectionModel = function(selectionModel) +{ + this.selectionModel = selectionModel; +}; + +/** + * Function: getSelectionCellsForChanges + * + * Returns the cells to be selected for the given array of changes. + */ +mxGraph.prototype.getSelectionCellsForChanges = function(changes) +{ + var cells = []; + + for (var i = 0; i < changes.length; i++) + { + var change = changes[i]; + + if (change.constructor != mxRootChange) + { + var cell = null; + + if (change instanceof mxChildChange && change.previous == null) + { + cell = change.child; + } + else if (change.cell != null && change.cell instanceof mxCell) + { + cell = change.cell; + } + + if (cell != null && mxUtils.indexOf(cells, cell) < 0) + { + cells.push(cell); + } + } + } + + return this.getModel().getTopmostCells(cells); +}; + +/** + * Function: graphModelChanged + * + * Called when the graph model changes. Invokes <processChange> on each + * item of the given array to update the view accordingly. + * + * Parameters: + * + * changes - Array that contains the individual changes. + */ +mxGraph.prototype.graphModelChanged = function(changes) +{ + for (var i = 0; i < changes.length; i++) + { + this.processChange(changes[i]); + } + + this.removeSelectionCells(this.getRemovedCellsForChanges(changes)); + + this.view.validate(); + this.sizeDidChange(); +}; + +/** + * Function: getRemovedCellsForChanges + * + * Returns the cells that have been removed from the model. + */ +mxGraph.prototype.getRemovedCellsForChanges = function(changes) +{ + var result = []; + + for (var i = 0; i < changes.length; i++) + { + var change = changes[i]; + + // Resets the view settings, removes all cells and clears + // the selection if the root changes. + if (change instanceof mxRootChange) + { + break; + } + else if (change instanceof mxChildChange) + { + if (change.previous != null && change.parent == null) + { + result = result.concat(this.model.getDescendants(change.child)); + } + } + else if (change instanceof mxVisibleChange) + { + result = result.concat(this.model.getDescendants(change.cell)); + } + } + + return result; +}; + +/** + * Function: processChange + * + * Processes the given change and invalidates the respective cached data + * in <view>. This fires a <root> event if the root has changed in the + * model. + * + * Parameters: + * + * change - Object that represents the change on the model. + */ +mxGraph.prototype.processChange = function(change) +{ + // Resets the view settings, removes all cells and clears + // the selection if the root changes. + if (change instanceof mxRootChange) + { + this.clearSelection(); + this.removeStateForCell(change.previous); + + if (this.resetViewOnRootChange) + { + this.view.scale = 1; + this.view.translate.x = 0; + this.view.translate.y = 0; + } + + this.fireEvent(new mxEventObject(mxEvent.ROOT)); + } + + // Adds or removes a child to the view by online invaliding + // the minimal required portions of the cache, namely, the + // old and new parent and the child. + else if (change instanceof mxChildChange) + { + var newParent = this.model.getParent(change.child); + + if (newParent != null) + { + // Flags the cell for updating the order in the renderer + this.view.invalidate(change.child, true, false, change.previous != null); + } + else + { + this.removeStateForCell(change.child); + + // Handles special case of current root of view being removed + if (this.view.currentRoot == change.child) + { + this.home(); + } + } + + if (newParent != change.previous) + { + // Refreshes the collapse/expand icons on the parents + if (newParent != null) + { + this.view.invalidate(newParent, false, false); + } + + if (change.previous != null) + { + this.view.invalidate(change.previous, false, false); + } + } + } + + // Handles two special cases where the shape does not need to be + // recreated from scratch, it only need to be invalidated. + else if (change instanceof mxTerminalChange || + change instanceof mxGeometryChange) + { + this.view.invalidate(change.cell); + } + + // Handles two special cases where only the shape, but no + // descendants need to be recreated + else if (change instanceof mxValueChange) + { + this.view.invalidate(change.cell, false, false); + } + + // Requires a new mxShape in JavaScript + else if (change instanceof mxStyleChange) + { + this.view.invalidate(change.cell, true, true, false); + this.view.removeState(change.cell); + } + + // Removes the state from the cache by default + else if (change.cell != null && + change.cell instanceof mxCell) + { + this.removeStateForCell(change.cell); + } +}; + +/** + * Function: removeStateForCell + * + * Removes all cached information for the given cell and its descendants. + * This is called when a cell was removed from the model. + * + * Paramters: + * + * cell - <mxCell> that was removed from the model. + */ +mxGraph.prototype.removeStateForCell = function(cell) +{ + var childCount = this.model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + this.removeStateForCell(this.model.getChildAt(cell, i)); + } + + this.view.removeState(cell); +}; + +/** + * Group: Overlays + */ + +/** + * Function: addCellOverlay + * + * Adds an <mxCellOverlay> for the specified cell. This method fires an + * <addoverlay> event and returns the new <mxCellOverlay>. + * + * Parameters: + * + * cell - <mxCell> to add the overlay for. + * overlay - <mxCellOverlay> to be added for the cell. + */ +mxGraph.prototype.addCellOverlay = function(cell, overlay) +{ + if (cell.overlays == null) + { + cell.overlays = []; + } + + cell.overlays.push(overlay); + + var state = this.view.getState(cell); + + // Immediately updates the cell display if the state exists + if (state != null) + { + this.cellRenderer.redraw(state); + } + + this.fireEvent(new mxEventObject(mxEvent.ADD_OVERLAY, + 'cell', cell, 'overlay', overlay)); + + return overlay; +}; + +/** + * Function: getCellOverlays + * + * Returns the array of <mxCellOverlays> for the given cell or null, if + * no overlays are defined. + * + * Parameters: + * + * cell - <mxCell> whose overlays should be returned. + */ +mxGraph.prototype.getCellOverlays = function(cell) +{ + return cell.overlays; +}; + +/** + * Function: removeCellOverlay + * + * Removes and returns the given <mxCellOverlay> from the given cell. This + * method fires a <removeoverlay> event. If no overlay is given, then all + * overlays are removed using <removeOverlays>. + * + * Parameters: + * + * cell - <mxCell> whose overlay should be removed. + * overlay - Optional <mxCellOverlay> to be removed. + */ +mxGraph.prototype.removeCellOverlay = function(cell, overlay) +{ + if (overlay == null) + { + this.removeCellOverlays(cell); + } + else + { + var index = mxUtils.indexOf(cell.overlays, overlay); + + if (index >= 0) + { + cell.overlays.splice(index, 1); + + if (cell.overlays.length == 0) + { + cell.overlays = null; + } + + // Immediately updates the cell display if the state exists + var state = this.view.getState(cell); + + if (state != null) + { + this.cellRenderer.redraw(state); + } + + this.fireEvent(new mxEventObject(mxEvent.REMOVE_OVERLAY, + 'cell', cell, 'overlay', overlay)); + } + else + { + overlay = null; + } + } + + return overlay; +}; + +/** + * Function: removeCellOverlays + * + * Removes all <mxCellOverlays> from the given cell. This method + * fires a <removeoverlay> event for each <mxCellOverlay> and returns + * the array of <mxCellOverlays> that was removed from the cell. + * + * Parameters: + * + * cell - <mxCell> whose overlays should be removed + */ +mxGraph.prototype.removeCellOverlays = function(cell) +{ + var overlays = cell.overlays; + + if (overlays != null) + { + cell.overlays = null; + + // Immediately updates the cell display if the state exists + var state = this.view.getState(cell); + + if (state != null) + { + this.cellRenderer.redraw(state); + } + + for (var i = 0; i < overlays.length; i++) + { + this.fireEvent(new mxEventObject(mxEvent.REMOVE_OVERLAY, + 'cell', cell, 'overlay', overlays[i])); + } + } + + return overlays; +}; + +/** + * Function: clearCellOverlays + * + * Removes all <mxCellOverlays> in the graph for the given cell and all its + * descendants. If no cell is specified then all overlays are removed from + * the graph. This implementation uses <removeCellOverlays> to remove the + * overlays from the individual cells. + * + * Parameters: + * + * cell - Optional <mxCell> that represents the root of the subtree to + * remove the overlays from. Default is the root in the model. + */ +mxGraph.prototype.clearCellOverlays = function(cell) +{ + cell = (cell != null) ? cell : this.model.getRoot(); + this.removeCellOverlays(cell); + + // Recursively removes all overlays from the children + var childCount = this.model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + var child = this.model.getChildAt(cell, i); + this.clearCellOverlays(child); // recurse + } +}; + +/** + * Function: setCellWarning + * + * Creates an overlay for the given cell using the warning and image or + * <warningImage> and returns the new <mxCellOverlay>. The warning is + * displayed as a tooltip in a red font and may contain HTML markup. If + * the warning is null or a zero length string, then all overlays are + * removed from the cell. + * + * Example: + * + * (code) + * graph.setCellWarning(cell, '<b>Warning:</b>: Hello, World!'); + * (end) + * + * Parameters: + * + * cell - <mxCell> whose warning should be set. + * warning - String that represents the warning to be displayed. + * img - Optional <mxImage> to be used for the overlay. Default is + * <warningImage>. + * isSelect - Optional boolean indicating if a click on the overlay + * should select the corresponding cell. Default is false. + */ +mxGraph.prototype.setCellWarning = function(cell, warning, img, isSelect) +{ + if (warning != null && warning.length > 0) + { + img = (img != null) ? img : this.warningImage; + + // Creates the overlay with the image and warning + var overlay = new mxCellOverlay(img, + '<font color=red>'+warning+'</font>'); + + // Adds a handler for single mouseclicks to select the cell + if (isSelect) + { + overlay.addListener(mxEvent.CLICK, + mxUtils.bind(this, function(sender, evt) + { + if (this.isEnabled()) + { + this.setSelectionCell(cell); + } + }) + ); + } + + // Sets and returns the overlay in the graph + return this.addCellOverlay(cell, overlay); + } + else + { + this.removeCellOverlays(cell); + } + + return null; +}; + +/** + * Group: In-place editing + */ + +/** + * Function: startEditing + * + * Calls <startEditingAtCell> using the given cell or the first selection + * cell. + * + * Parameters: + * + * evt - Optional mouse event that triggered the editing. + */ +mxGraph.prototype.startEditing = function(evt) +{ + this.startEditingAtCell(null, evt); +}; + +/** + * Function: startEditingAtCell + * + * Fires a <startEditing> event and invokes <mxCellEditor.startEditing> + * on <editor>. + * + * Parameters: + * + * cell - <mxCell> to start the in-place editor for. + * evt - Optional mouse event that triggered the editing. + */ +mxGraph.prototype.startEditingAtCell = function(cell, evt) +{ + if (cell == null) + { + cell = this.getSelectionCell(); + + if (cell != null && !this.isCellEditable(cell)) + { + cell = null; + } + } + + if (cell != null) + { + this.fireEvent(new mxEventObject(mxEvent.START_EDITING, + 'cell', cell, 'event', evt)); + this.cellEditor.startEditing(cell, evt); + } +}; + +/** + * Function: getEditingValue + * + * Returns the initial value for in-place editing. This implementation + * returns <convertValueToString> for the given cell. If this function is + * overridden, then <mxGraphModel.valueForCellChanged> should take care + * of correctly storing the actual new value inside the user object. + * + * Parameters: + * + * cell - <mxCell> for which the initial editing value should be returned. + * evt - Optional mouse event that triggered the editor. + */ +mxGraph.prototype.getEditingValue = function(cell, evt) +{ + return this.convertValueToString(cell); +}; + +/** + * Function: stopEditing + * + * Stops the current editing. + * + * Parameters: + * + * cancel - Boolean that specifies if the current editing value + * should be stored. + */ +mxGraph.prototype.stopEditing = function(cancel) +{ + this.cellEditor.stopEditing(cancel); +}; + +/** + * Function: labelChanged + * + * Sets the label of the specified cell to the given value using + * <cellLabelChanged> and fires <mxEvent.LABEL_CHANGED> while the + * transaction is in progress. Returns the cell whose label was changed. + * + * Parameters: + * + * cell - <mxCell> whose label should be changed. + * value - New label to be assigned. + * evt - Optional event that triggered the change. + */ +mxGraph.prototype.labelChanged = function(cell, value, evt) +{ + this.model.beginUpdate(); + try + { + this.cellLabelChanged(cell, value, this.isAutoSizeCell(cell)); + this.fireEvent(new mxEventObject(mxEvent.LABEL_CHANGED, + 'cell', cell, 'value', value, 'event', evt)); + } + finally + { + this.model.endUpdate(); + } + + return cell; +}; + +/** + * Function: cellLabelChanged + * + * Sets the new label for a cell. If autoSize is true then + * <cellSizeUpdated> will be called. + * + * In the following example, the function is extended to map changes to + * attributes in an XML node, as shown in <convertValueToString>. + * Alternatively, the handling of this can be implemented as shown in + * <mxGraphModel.valueForCellChanged> without the need to clone the + * user object. + * + * (code) + * var graphCellLabelChanged = graph.cellLabelChanged; + * graph.cellLabelChanged = function(cell, newValue, autoSize) + * { + * // Cloned for correct undo/redo + * var elt = cell.value.cloneNode(true); + * elt.setAttribute('label', newValue); + * + * newValue = elt; + * graphCellLabelChanged.apply(this, arguments); + * }; + * (end) + * + * Parameters: + * + * cell - <mxCell> whose label should be changed. + * value - New label to be assigned. + * autoSize - Boolean that specifies if <cellSizeUpdated> should be called. + */ +mxGraph.prototype.cellLabelChanged = function(cell, value, autoSize) +{ + this.model.beginUpdate(); + try + { + this.model.setValue(cell, value); + + if (autoSize) + { + this.cellSizeUpdated(cell, false); + } + } + finally + { + this.model.endUpdate(); + } +}; + +/** + * Group: Event processing + */ + +/** + * Function: escape + * + * Processes an escape keystroke. + * + * Parameters: + * + * evt - Mouseevent that represents the keystroke. + */ +mxGraph.prototype.escape = function(evt) +{ + this.stopEditing(true); + this.connectionHandler.reset(); + this.graphHandler.reset(); + + // Cancels all cell-based editing + var cells = this.getSelectionCells(); + + for (var i = 0; i < cells.length; i++) + { + var state = this.view.getState(cells[i]); + + if (state != null && state.handler != null) + { + state.handler.reset(); + } + } +}; + +/** + * Function: click + * + * Processes a singleclick on an optional cell and fires a <click> event. + * The click event is fired initially. If the graph is enabled and the + * event has not been consumed, then the cell is selected using + * <selectCellForEvent> or the selection is cleared using + * <clearSelection>. The events consumed state is set to true if the + * corresponding <mxMouseEvent> has been consumed. + * + * Parameters: + * + * me - <mxMouseEvent> that represents the single click. + */ +mxGraph.prototype.click = function(me) +{ + var evt = me.getEvent(); + var cell = me.getCell(); + var mxe = new mxEventObject(mxEvent.CLICK, 'event', evt, 'cell', cell); + + if (me.isConsumed()) + { + mxe.consume(); + } + + this.fireEvent(mxe); + + // Handles the event if it has not been consumed + if (this.isEnabled() && !mxEvent.isConsumed(evt) && !mxe.isConsumed()) + { + if (cell != null) + { + this.selectCellForEvent(cell, evt); + } + else + { + var swimlane = null; + + if (this.isSwimlaneSelectionEnabled()) + { + // Gets the swimlane at the location (includes + // content area of swimlanes) + swimlane = this.getSwimlaneAt(me.getGraphX(), me.getGraphY()); + } + + // Selects the swimlane and consumes the event + if (swimlane != null) + { + this.selectCellForEvent(swimlane, evt); + } + + // Ignores the event if the control key is pressed + else if (!this.isToggleEvent(evt)) + { + this.clearSelection(); + } + } + } +}; + +/** + * Function: dblClick + * + * Processes a doubleclick on an optional cell and fires a <dblclick> + * event. The event is fired initially. If the graph is enabled and the + * event has not been consumed, then <edit> is called with the given + * cell. The event is ignored if no cell was specified. + * + * Example for overriding this method. + * + * (code) + * graph.dblClick = function(evt, cell) + * { + * var mxe = new mxEventObject(mxEvent.DOUBLE_CLICK, 'event', evt, 'cell', cell); + * this.fireEvent(mxe); + * + * if (this.isEnabled() && !mxEvent.isConsumed(evt) && !mxe.isConsumed()) + * { + * mxUtils.alert('Hello, World!'); + * mxe.consume(); + * } + * } + * (end) + * + * Example listener for this event. + * + * (code) + * graph.addListener(mxEvent.DOUBLE_CLICK, function(sender, evt) + * { + * var cell = evt.getProperty('cell'); + * // do something with the cell... + * }); + * (end) + * + * Parameters: + * + * evt - Mouseevent that represents the doubleclick. + * cell - Optional <mxCell> under the mousepointer. + */ +mxGraph.prototype.dblClick = function(evt, cell) +{ + var mxe = new mxEventObject(mxEvent.DOUBLE_CLICK, 'event', evt, 'cell', cell); + this.fireEvent(mxe); + + // Handles the event if it has not been consumed + if (this.isEnabled() && !mxEvent.isConsumed(evt) && !mxe.isConsumed() && + cell != null && this.isCellEditable(cell)) + { + this.startEditingAtCell(cell, evt); + } +}; + +/** + * Function: scrollPointToVisible + * + * Scrolls the graph to the given point, extending the graph container if + * specified. + */ +mxGraph.prototype.scrollPointToVisible = function(x, y, extend, border) +{ + if (!this.timerAutoScroll && (this.ignoreScrollbars || mxUtils.hasScrollbars(this.container))) + { + var c = this.container; + border = (border != null) ? border : 20; + + if (x >= c.scrollLeft && y >= c.scrollTop && x <= c.scrollLeft + c.clientWidth && + y <= c.scrollTop + c.clientHeight) + { + var dx = c.scrollLeft + c.clientWidth - x; + + if (dx < border) + { + var old = c.scrollLeft; + c.scrollLeft += border - dx; + + // Automatically extends the canvas size to the bottom, right + // if the event is outside of the canvas and the edge of the + // canvas has been reached. Notes: Needs fix for IE. + if (extend && old == c.scrollLeft) + { + if (this.dialect == mxConstants.DIALECT_SVG) + { + var root = this.view.getDrawPane().ownerSVGElement; + var width = this.container.scrollWidth + border - dx; + + // Updates the clipping region. This is an expensive + // operation that should not be executed too often. + root.setAttribute('width', width); + } + else + { + var width = Math.max(c.clientWidth, c.scrollWidth) + border - dx; + var canvas = this.view.getCanvas(); + canvas.style.width = width + 'px'; + } + + c.scrollLeft += border - dx; + } + } + else + { + dx = x - c.scrollLeft; + + if (dx < border) + { + c.scrollLeft -= border - dx; + } + } + + var dy = c.scrollTop + c.clientHeight - y; + + if (dy < border) + { + var old = c.scrollTop; + c.scrollTop += border - dy; + + if (old == c.scrollTop && extend) + { + if (this.dialect == mxConstants.DIALECT_SVG) + { + var root = this.view.getDrawPane().ownerSVGElement; + var height = this.container.scrollHeight + border - dy; + + // Updates the clipping region. This is an expensive + // operation that should not be executed too often. + root.setAttribute('height', height); + } + else + { + var height = Math.max(c.clientHeight, c.scrollHeight) + border - dy; + var canvas = this.view.getCanvas(); + canvas.style.height = height + 'px'; + } + + c.scrollTop += border - dy; + } + } + else + { + dy = y - c.scrollTop; + + if (dy < border) + { + c.scrollTop -= border - dy; + } + } + } + } + else if (this.allowAutoPanning && !this.panningHandler.active) + { + if (this.panningManager == null) + { + this.panningManager = this.createPanningManager(); + } + + this.panningManager.panTo(x + this.panDx, y + this.panDy); + } +}; + + +/** + * Function: createPanningManager + * + * Creates and returns an <mxPanningManager>. + */ +mxGraph.prototype.createPanningManager = function() +{ + return new mxPanningManager(this); +}; + +/** + * Function: getBorderSizes + * + * Returns the size of the border and padding on all four sides of the + * container. The left, top, right and bottom borders are stored in the x, y, + * width and height of the returned <mxRectangle>, respectively. + */ +mxGraph.prototype.getBorderSizes = function() +{ + // Helper function to handle string values for border widths (approx) + function parseBorder(value) + { + var result = 0; + + if (value == 'thin') + { + result = 2; + } + else if (value == 'medium') + { + result = 4; + } + else if (value == 'thick') + { + result = 6; + } + else + { + result = parseInt(value); + } + + if (isNaN(result)) + { + result = 0; + } + + return result; + } + + var style = mxUtils.getCurrentStyle(this.container); + var result = new mxRectangle(); + result.x = parseBorder(style.borderLeftWidth) + parseInt(style.paddingLeft || 0); + result.y = parseBorder(style.borderTopWidth) + parseInt(style.paddingTop || 0); + result.width = parseBorder(style.borderRightWidth) + parseInt(style.paddingRight || 0); + result.height = parseBorder(style.borderBottomWidth) + parseInt(style.paddingBottom || 0); + + return result; +}; + + +/** + * Function: getPreferredPageSize + * + * Returns the preferred size of the background page if <preferPageSize> is true. + */ +mxGraph.prototype.getPreferredPageSize = function(bounds, width, height) +{ + var scale = this.view.scale; + var tr = this.view.translate; + var fmt = this.pageFormat; + var ps = scale * this.pageScale; + var page = new mxRectangle(0, 0, fmt.width * ps, fmt.height * ps); + + var hCount = (this.pageBreaksVisible) ? Math.ceil(width / page.width) : 1; + var vCount = (this.pageBreaksVisible) ? Math.ceil(height / page.height) : 1; + + return new mxRectangle(0, 0, hCount * page.width + 2 + tr.x / scale, vCount * page.height + 2 + tr.y / scale); +}; + +/** + * Function: sizeDidChange + * + * Called when the size of the graph has changed. This implementation fires + * a <size> event after updating the clipping region of the SVG element in + * SVG-bases browsers. + */ +mxGraph.prototype.sizeDidChange = function() +{ + var bounds = this.getGraphBounds(); + + if (this.container != null) + { + var border = this.getBorder(); + + var width = Math.max(0, bounds.x + bounds.width + 1 + border); + var height = Math.max(0, bounds.y + bounds.height + 1 + border); + + if (this.minimumContainerSize != null) + { + width = Math.max(width, this.minimumContainerSize.width); + height = Math.max(height, this.minimumContainerSize.height); + } + + if (this.resizeContainer) + { + this.doResizeContainer(width, height); + } + + if (this.preferPageSize || (!mxClient.IS_IE && this.pageVisible)) + { + var size = this.getPreferredPageSize(bounds, width, height); + + if (size != null) + { + width = size.width; + height = size.height; + } + } + + if (this.minimumGraphSize != null) + { + width = Math.max(width, this.minimumGraphSize.width * this.view.scale); + height = Math.max(height, this.minimumGraphSize.height * this.view.scale); + } + + width = Math.ceil(width - 1); + height = Math.ceil(height - 1); + + if (this.dialect == mxConstants.DIALECT_SVG) + { + var root = this.view.getDrawPane().ownerSVGElement; + + root.style.minWidth = Math.max(1, width) + 'px'; + root.style.minHeight = Math.max(1, height) + 'px'; + } + else + { + if (mxClient.IS_QUIRKS) + { + // Quirks mode has no minWidth/minHeight support + this.view.updateHtmlCanvasSize(Math.max(1, width), Math.max(1, height)); + } + else + { + this.view.canvas.style.minWidth = Math.max(1, width) + 'px'; + this.view.canvas.style.minHeight = Math.max(1, height) + 'px'; + } + } + + this.updatePageBreaks(this.pageBreaksVisible, width - 1, height - 1); + } + + this.fireEvent(new mxEventObject(mxEvent.SIZE, 'bounds', bounds)); +}; + +/** + * Function: doResizeContainer + * + * Resizes the container for the given graph width and height. + */ +mxGraph.prototype.doResizeContainer = function(width, height) +{ + // Fixes container size for different box models + if (mxClient.IS_IE) + { + if (mxClient.IS_QUIRKS) + { + var borders = this.getBorderSizes(); + + // max(2, ...) required for native IE8 in quirks mode + width += Math.max(2, borders.x + borders.width + 1); + height += Math.max(2, borders.y + borders.height + 1); + } + else if (document.documentMode >= 9) + { + width += 3; + height += 5; + } + else + { + width += 1; + height += 1; + } + } + else + { + height += 1; + } + + if (this.maximumContainerSize != null) + { + width = Math.min(this.maximumContainerSize.width, width); + height = Math.min(this.maximumContainerSize.height, height); + } + + this.container.style.width = Math.ceil(width) + 'px'; + this.container.style.height = Math.ceil(height) + 'px'; +}; + +/** + * Function: redrawPageBreaks + * + * Invokes from <sizeDidChange> to redraw the page breaks. + * + * Parameters: + * + * visible - Boolean that specifies if page breaks should be shown. + * width - Specifies the width of the container in pixels. + * height - Specifies the height of the container in pixels. + */ +mxGraph.prototype.updatePageBreaks = function(visible, width, height) +{ + var scale = this.view.scale; + var tr = this.view.translate; + var fmt = this.pageFormat; + var ps = scale * this.pageScale; + var bounds = new mxRectangle(scale * tr.x, scale * tr.y, + fmt.width * ps, fmt.height * ps); + + // Does not show page breaks if the scale is too small + visible = visible && Math.min(bounds.width, bounds.height) > this.minPageBreakDist; + + // Draws page breaks independent of translate. To ignore + // the translate set bounds.x/y = 0. Note that modulo + // in JavaScript has a bug, so use mxUtils instead. + bounds.x = mxUtils.mod(bounds.x, bounds.width); + bounds.y = mxUtils.mod(bounds.y, bounds.height); + + var horizontalCount = (visible) ? Math.ceil((width - bounds.x) / bounds.width) : 0; + var verticalCount = (visible) ? Math.ceil((height - bounds.y) / bounds.height) : 0; + var right = width; + var bottom = height; + + if (this.horizontalPageBreaks == null && horizontalCount > 0) + { + this.horizontalPageBreaks = []; + } + + if (this.horizontalPageBreaks != null) + { + for (var i = 0; i <= horizontalCount; i++) + { + var pts = [new mxPoint(bounds.x + i * bounds.width, 1), + new mxPoint(bounds.x + i * bounds.width, bottom)]; + + if (this.horizontalPageBreaks[i] != null) + { + this.horizontalPageBreaks[i].scale = 1; + this.horizontalPageBreaks[i].points = pts; + this.horizontalPageBreaks[i].redraw(); + } + else + { + var pageBreak = new mxPolyline(pts, this.pageBreakColor, this.scale); + pageBreak.dialect = this.dialect; + pageBreak.isDashed = this.pageBreakDashed; + pageBreak.scale = scale; + pageBreak.crisp = true; + pageBreak.init(this.view.backgroundPane); + pageBreak.redraw(); + + this.horizontalPageBreaks[i] = pageBreak; + } + } + + for (var i = horizontalCount; i < this.horizontalPageBreaks.length; i++) + { + this.horizontalPageBreaks[i].destroy(); + } + + this.horizontalPageBreaks.splice(horizontalCount, this.horizontalPageBreaks.length - horizontalCount); + } + + if (this.verticalPageBreaks == null && verticalCount > 0) + { + this.verticalPageBreaks = []; + } + + if (this.verticalPageBreaks != null) + { + for (var i = 0; i <= verticalCount; i++) + { + var pts = [new mxPoint(1, bounds.y + i * bounds.height), + new mxPoint(right, bounds.y + i * bounds.height)]; + + if (this.verticalPageBreaks[i] != null) + { + this.verticalPageBreaks[i].scale = 1; + this.verticalPageBreaks[i].points = pts; + this.verticalPageBreaks[i].redraw(); + } + else + { + var pageBreak = new mxPolyline(pts, this.pageBreakColor, scale); + pageBreak.dialect = this.dialect; + pageBreak.isDashed = this.pageBreakDashed; + pageBreak.scale = scale; + pageBreak.crisp = true; + pageBreak.init(this.view.backgroundPane); + pageBreak.redraw(); + + this.verticalPageBreaks[i] = pageBreak; + } + } + + for (var i = verticalCount; i < this.verticalPageBreaks.length; i++) + { + this.verticalPageBreaks[i].destroy(); + } + + this.verticalPageBreaks.splice(verticalCount, this.verticalPageBreaks.length - verticalCount); + } +}; + +/** + * Group: Cell styles + */ + +/** + * Function: getCellStyle + * + * Returns an array of key, value pairs representing the cell style for the + * given cell. If no string is defined in the model that specifies the + * style, then the default style for the cell is returned or <EMPTY_ARRAY>, + * if not style can be found. Note: You should try and get the cell state + * for the given cell and use the cached style in the state before using + * this method. + * + * Parameters: + * + * cell - <mxCell> whose style should be returned as an array. + */ +mxGraph.prototype.getCellStyle = function(cell) +{ + var stylename = this.model.getStyle(cell); + var style = null; + + // Gets the default style for the cell + if (this.model.isEdge(cell)) + { + style = this.stylesheet.getDefaultEdgeStyle(); + } + else + { + style = this.stylesheet.getDefaultVertexStyle(); + } + + // Resolves the stylename using the above as the default + if (stylename != null) + { + style = this.postProcessCellStyle(this.stylesheet.getCellStyle(stylename, style)); + } + + // Returns a non-null value if no style can be found + if (style == null) + { + style = mxGraph.prototype.EMPTY_ARRAY; + } + + return style; +}; + +/** + * Function: postProcessCellStyle + * + * Tries to resolve the value for the image style in the image bundles and + * turns short data URIs as defined in mxImageBundle to data URIs as + * defined in RFC 2397 of the IETF. + */ +mxGraph.prototype.postProcessCellStyle = function(style) +{ + if (style != null) + { + var key = style[mxConstants.STYLE_IMAGE]; + var image = this.getImageFromBundles(key); + + if (image != null) + { + style[mxConstants.STYLE_IMAGE] = image; + } + else + { + image = key; + } + + // Converts short data uris to normal data uris + if (image != null && image.substring(0, 11) == "data:image/") + { + var comma = image.indexOf(','); + + if (comma > 0) + { + image = image.substring(0, comma) + ";base64," + + image.substring(comma + 1); + } + + style[mxConstants.STYLE_IMAGE] = image; + } + } + + return style; +}; + +/** + * Function: setCellStyle + * + * Sets the style of the specified cells. If no cells are given, then the + * selection cells are changed. + * + * Parameters: + * + * style - String representing the new style of the cells. + * cells - Optional array of <mxCells> to set the style for. Default is the + * selection cells. + */ +mxGraph.prototype.setCellStyle = function(style, cells) +{ + cells = cells || this.getSelectionCells(); + + if (cells != null) + { + this.model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + this.model.setStyle(cells[i], style); + } + } + finally + { + this.model.endUpdate(); + } + } +}; + +/** + * Function: toggleCellStyle + * + * Toggles the boolean value for the given key in the style of the + * given cell. If no cell is specified then the selection cell is + * used. + * + * Parameter: + * + * key - String representing the key for the boolean value to be toggled. + * defaultValue - Optional boolean default value if no value is defined. + * Default is false. + * cell - Optional <mxCell> whose style should be modified. Default is + * the selection cell. + */ +mxGraph.prototype.toggleCellStyle = function(key, defaultValue, cell) +{ + cell = cell || this.getSelectionCell(); + + this.toggleCellStyles(key, defaultValue, [cell]); +}; + +/** + * Function: toggleCellStyles + * + * Toggles the boolean value for the given key in the style of the given + * cells. If no cells are specified, then the selection cells are used. For + * example, this can be used to toggle <mxConstants.STYLE_ROUNDED> or any + * other style with a boolean value. + * + * Parameter: + * + * key - String representing the key for the boolean value to be toggled. + * defaultValue - Optional boolean default value if no value is defined. + * Default is false. + * cells - Optional array of <mxCells> whose styles should be modified. + * Default is the selection cells. + */ +mxGraph.prototype.toggleCellStyles = function(key, defaultValue, cells) +{ + defaultValue = (defaultValue != null) ? defaultValue : false; + cells = cells || this.getSelectionCells(); + + if (cells != null && cells.length > 0) + { + var state = this.view.getState(cells[0]); + var style = (state != null) ? state.style : this.getCellStyle(cells[0]); + + if (style != null) + { + var val = (mxUtils.getValue(style, key, defaultValue)) ? 0 : 1; + this.setCellStyles(key, val, cells); + } + } +}; + +/** + * Function: setCellStyles + * + * Sets the key to value in the styles of the given cells. This will modify + * the existing cell styles in-place and override any existing assignment + * for the given key. If no cells are specified, then the selection cells + * are changed. If no value is specified, then the respective key is + * removed from the styles. + * + * Parameters: + * + * key - String representing the key to be assigned. + * value - String representing the new value for the key. + * cells - Optional array of <mxCells> to change the style for. Default is + * the selection cells. + */ +mxGraph.prototype.setCellStyles = function(key, value, cells) +{ + cells = cells || this.getSelectionCells(); + mxUtils.setCellStyles(this.model, cells, key, value); +}; + +/** + * Function: toggleCellStyleFlags + * + * Toggles the given bit for the given key in the styles of the specified + * cells. + * + * Parameters: + * + * key - String representing the key to toggle the flag in. + * flag - Integer that represents the bit to be toggled. + * cells - Optional array of <mxCells> to change the style for. Default is + * the selection cells. + */ +mxGraph.prototype.toggleCellStyleFlags = function(key, flag, cells) +{ + this.setCellStyleFlags(key, flag, null, cells); +}; + +/** + * Function: setCellStyleFlags + * + * Sets or toggles the given bit for the given key in the styles of the + * specified cells. + * + * Parameters: + * + * key - String representing the key to toggle the flag in. + * flag - Integer that represents the bit to be toggled. + * value - Boolean value to be used or null if the value should be toggled. + * cells - Optional array of <mxCells> to change the style for. Default is + * the selection cells. + */ +mxGraph.prototype.setCellStyleFlags = function(key, flag, value, cells) +{ + cells = cells || this.getSelectionCells(); + + if (cells != null && cells.length > 0) + { + if (value == null) + { + var state = this.view.getState(cells[0]); + var style = (state != null) ? state.style : this.getCellStyle(cells[0]); + + if (style != null) + { + var current = parseInt(style[key] || 0); + value = !((current & flag) == flag); + } + } + + mxUtils.setCellStyleFlags(this.model, cells, key, flag, value); + } +}; + +/** + * Group: Cell alignment and orientation + */ + +/** + * Function: alignCells + * + * Aligns the given cells vertically or horizontally according to the given + * alignment using the optional parameter as the coordinate. + * + * Parameters: + * + * align - Specifies the alignment. Possible values are all constants in + * mxConstants with an ALIGN prefix. + * cells - Array of <mxCells> to be aligned. + * param - Optional coordinate for the alignment. + */ +mxGraph.prototype.alignCells = function(align, cells, param) +{ + if (cells == null) + { + cells = this.getSelectionCells(); + } + + if (cells != null && cells.length > 1) + { + // Finds the required coordinate for the alignment + if (param == null) + { + for (var i = 0; i < cells.length; i++) + { + var geo = this.getCellGeometry(cells[i]); + + if (geo != null && !this.model.isEdge(cells[i])) + { + if (param == null) + { + if (align == mxConstants.ALIGN_CENTER) + { + param = geo.x + geo.width / 2; + break; + } + else if (align == mxConstants.ALIGN_RIGHT) + { + param = geo.x + geo.width; + } + else if (align == mxConstants.ALIGN_TOP) + { + param = geo.y; + } + else if (align == mxConstants.ALIGN_MIDDLE) + { + param = geo.y + geo.height / 2; + break; + } + else if (align == mxConstants.ALIGN_BOTTOM) + { + param = geo.y + geo.height; + } + else + { + param = geo.x; + } + } + else + { + if (align == mxConstants.ALIGN_RIGHT) + { + param = Math.max(param, geo.x + geo.width); + } + else if (align == mxConstants.ALIGN_TOP) + { + param = Math.min(param, geo.y); + } + else if (align == mxConstants.ALIGN_BOTTOM) + { + param = Math.max(param, geo.y + geo.height); + } + else + { + param = Math.min(param, geo.x); + } + } + } + } + } + + // Aligns the cells to the coordinate + if (param != null) + { + this.model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + var geo = this.getCellGeometry(cells[i]); + + if (geo != null && !this.model.isEdge(cells[i])) + { + geo = geo.clone(); + + if (align == mxConstants.ALIGN_CENTER) + { + geo.x = param - geo.width / 2; + } + else if (align == mxConstants.ALIGN_RIGHT) + { + geo.x = param - geo.width; + } + else if (align == mxConstants.ALIGN_TOP) + { + geo.y = param; + } + else if (align == mxConstants.ALIGN_MIDDLE) + { + geo.y = param - geo.height / 2; + } + else if (align == mxConstants.ALIGN_BOTTOM) + { + geo.y = param - geo.height; + } + else + { + geo.x = param; + } + + this.model.setGeometry(cells[i], geo); + } + } + + this.fireEvent(new mxEventObject(mxEvent.ALIGN_CELLS, + 'align', align, 'cells', cells)); + } + finally + { + this.model.endUpdate(); + } + } + } + + return cells; +}; + +/** + * Function: flipEdge + * + * Toggles the style of the given edge between null (or empty) and + * <alternateEdgeStyle>. This method fires <mxEvent.FLIP_EDGE> while the + * transaction is in progress. Returns the edge that was flipped. + * + * Here is an example that overrides this implementation to invert the + * value of <mxConstants.STYLE_ELBOW> without removing any existing styles. + * + * (code) + * graph.flipEdge = function(edge) + * { + * if (edge != null) + * { + * var state = this.view.getState(edge); + * var style = (state != null) ? state.style : this.getCellStyle(edge); + * + * if (style != null) + * { + * var elbow = mxUtils.getValue(style, mxConstants.STYLE_ELBOW, + * mxConstants.ELBOW_HORIZONTAL); + * var value = (elbow == mxConstants.ELBOW_HORIZONTAL) ? + * mxConstants.ELBOW_VERTICAL : mxConstants.ELBOW_HORIZONTAL; + * this.setCellStyles(mxConstants.STYLE_ELBOW, value, [edge]); + * } + * } + * }; + * (end) + * + * Parameters: + * + * edge - <mxCell> whose style should be changed. + */ +mxGraph.prototype.flipEdge = function(edge) +{ + if (edge != null && + this.alternateEdgeStyle != null) + { + this.model.beginUpdate(); + try + { + var style = this.model.getStyle(edge); + + if (style == null || style.length == 0) + { + this.model.setStyle(edge, this.alternateEdgeStyle); + } + else + { + this.model.setStyle(edge, null); + } + + // Removes all existing control points + this.resetEdge(edge); + this.fireEvent(new mxEventObject(mxEvent.FLIP_EDGE, 'edge', edge)); + } + finally + { + this.model.endUpdate(); + } + } + + return edge; +}; + +/** + * Function: addImageBundle + * + * Adds the specified <mxImageBundle>. + */ +mxGraph.prototype.addImageBundle = function(bundle) +{ + this.imageBundles.push(bundle); +}; + +/** + * Function: removeImageBundle + * + * Removes the specified <mxImageBundle>. + */ +mxGraph.prototype.removeImageBundle = function(bundle) +{ + var tmp = []; + + for (var i = 0; i < this.imageBundles.length; i++) + { + if (this.imageBundles[i] != bundle) + { + tmp.push(this.imageBundles[i]); + } + } + + this.imageBundles = tmp; +}; + +/** + * Function: getImageFromBundles + * + * Searches all <imageBundles> for the specified key and returns the value + * for the first match or null if the key is not found. + */ +mxGraph.prototype.getImageFromBundles = function(key) +{ + if (key != null) + { + for (var i = 0; i < this.imageBundles.length; i++) + { + var image = this.imageBundles[i].getImage(key); + + if (image != null) + { + return image; + } + } + } + + return null; +}; + +/** + * Group: Order + */ + +/** + * Function: orderCells + * + * Moves the given cells to the front or back. The change is carried out + * using <cellsOrdered>. This method fires <mxEvent.ORDER_CELLS> while the + * transaction is in progress. + * + * Parameters: + * + * back - Boolean that specifies if the cells should be moved to back. + * cells - Array of <mxCells> to move to the background. If null is + * specified then the selection cells are used. + */ + mxGraph.prototype.orderCells = function(back, cells) + { + if (cells == null) + { + cells = mxUtils.sortCells(this.getSelectionCells(), true); + } + + this.model.beginUpdate(); + try + { + this.cellsOrdered(cells, back); + this.fireEvent(new mxEventObject(mxEvent.ORDER_CELLS, + 'back', back, 'cells', cells)); + } + finally + { + this.model.endUpdate(); + } + + return cells; + }; + +/** + * Function: cellsOrdered + * + * Moves the given cells to the front or back. This method fires + * <mxEvent.CELLS_ORDERED> while the transaction is in progress. + * + * Parameters: + * + * cells - Array of <mxCells> whose order should be changed. + * back - Boolean that specifies if the cells should be moved to back. + */ + mxGraph.prototype.cellsOrdered = function(cells, back) + { + if (cells != null) + { + this.model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + var parent = this.model.getParent(cells[i]); + + if (back) + { + this.model.add(parent, cells[i], i); + } + else + { + this.model.add(parent, cells[i], + this.model.getChildCount(parent) - 1); + } + } + + this.fireEvent(new mxEventObject(mxEvent.CELLS_ORDERED, + 'back', back, 'cells', cells)); + } + finally + { + this.model.endUpdate(); + } + } +}; + +/** + * Group: Grouping + */ + +/** + * Function: groupCells + * + * Adds the cells into the given group. The change is carried out using + * <cellsAdded>, <cellsMoved> and <cellsResized>. This method fires + * <mxEvent.GROUP_CELLS> while the transaction is in progress. Returns the + * new group. A group is only created if there is at least one entry in the + * given array of cells. + * + * Parameters: + * + * group - <mxCell> that represents the target group. If null is specified + * then a new group is created using <createGroupCell>. + * border - Optional integer that specifies the border between the child + * area and the group bounds. Default is 0. + * cells - Optional array of <mxCells> to be grouped. If null is specified + * then the selection cells are used. + */ +mxGraph.prototype.groupCells = function(group, border, cells) +{ + if (cells == null) + { + cells = mxUtils.sortCells(this.getSelectionCells(), true); + } + + cells = this.getCellsForGroup(cells); + + if (group == null) + { + group = this.createGroupCell(cells); + } + + var bounds = this.getBoundsForGroup(group, cells, border); + + if (cells.length > 0 && bounds != null) + { + // Uses parent of group or previous parent of first child + var parent = this.model.getParent(group); + + if (parent == null) + { + parent = this.model.getParent(cells[0]); + } + + this.model.beginUpdate(); + try + { + // Checks if the group has a geometry and + // creates one if one does not exist + if (this.getCellGeometry(group) == null) + { + this.model.setGeometry(group, new mxGeometry()); + } + + // Adds the children into the group and moves + var index = this.model.getChildCount(group); + this.cellsAdded(cells, group, index, null, null, false, false); + this.cellsMoved(cells, -bounds.x, -bounds.y, false, true); + + // Adds the group into the parent and resizes + index = this.model.getChildCount(parent); + this.cellsAdded([group], parent, index, null, null, false); + this.cellsResized([group], [bounds]); + + this.fireEvent(new mxEventObject(mxEvent.GROUP_CELLS, + 'group', group, 'border', border, 'cells', cells)); + } + finally + { + this.model.endUpdate(); + } + } + + return group; +}; + +/** + * Function: getCellsForGroup + * + * Returns the cells with the same parent as the first cell + * in the given array. + */ +mxGraph.prototype.getCellsForGroup = function(cells) +{ + var result = []; + + if (cells != null && cells.length > 0) + { + var parent = this.model.getParent(cells[0]); + result.push(cells[0]); + + // Filters selection cells with the same parent + for (var i = 1; i < cells.length; i++) + { + if (this.model.getParent(cells[i]) == parent) + { + result.push(cells[i]); + } + } + } + + return result; +}; + +/** + * Function: getBoundsForGroup + * + * Returns the bounds to be used for the given group and children. + */ +mxGraph.prototype.getBoundsForGroup = function(group, children, border) +{ + var result = this.getBoundingBoxFromGeometry(children); + + if (result != null) + { + if (this.isSwimlane(group)) + { + var size = this.getStartSize(group); + + result.x -= size.width; + result.y -= size.height; + result.width += size.width; + result.height += size.height; + } + + // Adds the border + result.x -= border; + result.y -= border; + result.width += 2 * border; + result.height += 2 * border; + } + + return result; +}; + +/** + * Function: createGroupCell + * + * Hook for creating the group cell to hold the given array of <mxCells> if + * no group cell was given to the <group> function. + * + * The following code can be used to set the style of new group cells. + * + * (code) + * var graphCreateGroupCell = graph.createGroupCell; + * graph.createGroupCell = function(cells) + * { + * var group = graphCreateGroupCell.apply(this, arguments); + * group.setStyle('group'); + * + * return group; + * }; + */ +mxGraph.prototype.createGroupCell = function(cells) +{ + var group = new mxCell(''); + group.setVertex(true); + group.setConnectable(false); + + return group; +}; + +/** + * Function: ungroupCells + * + * Ungroups the given cells by moving the children the children to their + * parents parent and removing the empty groups. Returns the children that + * have been removed from the groups. + * + * Parameters: + * + * cells - Array of cells to be ungrouped. If null is specified then the + * selection cells are used. + */ +mxGraph.prototype.ungroupCells = function(cells) +{ + var result = []; + + if (cells == null) + { + cells = this.getSelectionCells(); + + // Finds the cells with children + var tmp = []; + + for (var i = 0; i < cells.length; i++) + { + if (this.model.getChildCount(cells[i]) > 0) + { + tmp.push(cells[i]); + } + } + + cells = tmp; + } + + if (cells != null && cells.length > 0) + { + this.model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + var children = this.model.getChildren(cells[i]); + + if (children != null && children.length > 0) + { + children = children.slice(); + var parent = this.model.getParent(cells[i]); + var index = this.model.getChildCount(parent); + + this.cellsAdded(children, parent, index, null, null, true); + result = result.concat(children); + } + } + + this.cellsRemoved(this.addAllEdges(cells)); + this.fireEvent(new mxEventObject(mxEvent.UNGROUP_CELLS, + 'cells', cells)); + } + finally + { + this.model.endUpdate(); + } + } + + return result; +}; + +/** + * Function: removeCellsFromParent + * + * Removes the specified cells from their parents and adds them to the + * default parent. Returns the cells that were removed from their parents. + * + * Parameters: + * + * cells - Array of <mxCells> to be removed from their parents. + */ +mxGraph.prototype.removeCellsFromParent = function(cells) +{ + if (cells == null) + { + cells = this.getSelectionCells(); + } + + this.model.beginUpdate(); + try + { + var parent = this.getDefaultParent(); + var index = this.model.getChildCount(parent); + + this.cellsAdded(cells, parent, index, null, null, true); + this.fireEvent(new mxEventObject(mxEvent.REMOVE_CELLS_FROM_PARENT, + 'cells', cells)); + } + finally + { + this.model.endUpdate(); + } + + return cells; +}; + +/** + * Function: updateGroupBounds + * + * Updates the bounds of the given array of groups so that it includes + * all child vertices. + * + * Parameters: + * + * cells - The groups whose bounds should be updated. + * border - Optional border to be added in the group. Default is 0. + * moveGroup - Optional boolean that allows the group to be moved. Default + * is false. + */ +mxGraph.prototype.updateGroupBounds = function(cells, border, moveGroup) +{ + if (cells == null) + { + cells = this.getSelectionCells(); + } + + border = (border != null) ? border : 0; + moveGroup = (moveGroup != null) ? moveGroup : false; + + this.model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + var geo = this.getCellGeometry(cells[i]); + + if (geo != null) + { + var children = this.getChildCells(cells[i]); + + if (children != null && children.length > 0) + { + var childBounds = this.getBoundingBoxFromGeometry(children); + + if (childBounds.width > 0 && childBounds.height > 0) + { + var size = (this.isSwimlane(cells[i])) ? + this.getStartSize(cells[i]) : new mxRectangle(); + + geo = geo.clone(); + + if (moveGroup) + { + geo.x += childBounds.x - size.width - border; + geo.y += childBounds.y - size.height - border; + } + + geo.width = childBounds.width + size.width + 2 * border; + geo.height = childBounds.height + size.height + 2 * border; + + this.model.setGeometry(cells[i], geo); + this.moveCells(children, -childBounds.x + size.width + border, + -childBounds.y + size.height + border); + } + } + } + } + } + finally + { + this.model.endUpdate(); + } + + return cells; +}; + +/** + * Group: Cell cloning, insertion and removal + */ + +/** + * Function: cloneCells + * + * Returns the clones for the given cells. If the terminal of an edge is + * not in the given array, then the respective end is assigned a terminal + * point and the terminal is removed. + * + * Parameters: + * + * cells - Array of <mxCells> to be cloned. + * allowInvalidEdges - Optional boolean that specifies if invalid edges + * should be cloned. Default is true. + */ +mxGraph.prototype.cloneCells = function(cells, allowInvalidEdges) +{ + allowInvalidEdges = (allowInvalidEdges != null) ? allowInvalidEdges : true; + var clones = null; + + if (cells != null) + { + // Creates a hashtable for cell lookups + var hash = new Object(); + var tmp = []; + + for (var i = 0; i < cells.length; i++) + { + var id = mxCellPath.create(cells[i]); + hash[id] = cells[i]; + tmp.push(cells[i]); + } + + if (tmp.length > 0) + { + var scale = this.view.scale; + var trans = this.view.translate; + clones = this.model.cloneCells(cells, true); + + for (var i = 0; i < cells.length; i++) + { + if (!allowInvalidEdges && this.model.isEdge(clones[i]) && + this.getEdgeValidationError(clones[i], + this.model.getTerminal(clones[i], true), + this.model.getTerminal(clones[i], false)) != null) + { + clones[i] = null; + } + else + { + var g = this.model.getGeometry(clones[i]); + + if (g != null) + { + var state = this.view.getState(cells[i]); + var pstate = this.view.getState( + this.model.getParent(cells[i])); + + if (state != null && pstate != null) + { + var dx = pstate.origin.x; + var dy = pstate.origin.y; + + if (this.model.isEdge(clones[i])) + { + var pts = state.absolutePoints; + + // Checks if the source is cloned or sets the terminal point + var src = this.model.getTerminal(cells[i], true); + var srcId = mxCellPath.create(src); + + while (src != null && hash[srcId] == null) + { + src = this.model.getParent(src); + srcId = mxCellPath.create(src); + } + + if (src == null) + { + g.setTerminalPoint( + new mxPoint(pts[0].x / scale - trans.x, + pts[0].y / scale - trans.y), true); + } + + // Checks if the target is cloned or sets the terminal point + var trg = this.model.getTerminal(cells[i], false); + var trgId = mxCellPath.create(trg); + + while (trg != null && hash[trgId] == null) + { + trg = this.model.getParent(trg); + trgId = mxCellPath.create(trg); + } + + if (trg == null) + { + var n = pts.length - 1; + g.setTerminalPoint( + new mxPoint(pts[n].x / scale - trans.x, + pts[n].y / scale - trans.y), false); + } + + // Translates the control points + var points = g.points; + + if (points != null) + { + for (var j = 0; j < points.length; j++) + { + points[j].x += dx; + points[j].y += dy; + } + } + } + else + { + g.x += dx; + g.y += dy; + } + } + } + } + } + } + else + { + clones = []; + } + } + + return clones; +}; + +/** + * Function: insertVertex + * + * Adds a new vertex into the given parent <mxCell> using value as the user + * object and the given coordinates as the <mxGeometry> of the new vertex. + * The id and style are used for the respective properties of the new + * <mxCell>, which is returned. + * + * When adding new vertices from a mouse event, one should take into + * account the offset of the graph container and the scale and translation + * of the view in order to find the correct unscaled, untranslated + * coordinates using <mxGraph.getPointForEvent> as follows: + * + * (code) + * var pt = graph.getPointForEvent(evt); + * var parent = graph.getDefaultParent(); + * graph.insertVertex(parent, null, + * 'Hello, World!', x, y, 220, 30); + * (end) + * + * For adding image cells, the style parameter can be assigned as + * + * (code) + * stylename;image=imageUrl + * (end) + * + * See <mxGraph> for more information on using images. + * + * Parameters: + * + * parent - <mxCell> that specifies the parent of the new vertex. + * id - Optional string that defines the Id of the new vertex. + * value - Object to be used as the user object. + * x - Integer that defines the x coordinate of the vertex. + * y - Integer that defines the y coordinate of the vertex. + * width - Integer that defines the width of the vertex. + * height - Integer that defines the height of the vertex. + * style - Optional string that defines the cell style. + * relative - Optional boolean that specifies if the geometry is relative. + * Default is false. + */ +mxGraph.prototype.insertVertex = function(parent, id, value, + x, y, width, height, style, relative) +{ + var vertex = this.createVertex(parent, id, value, x, y, width, height, style, relative); + + return this.addCell(vertex, parent); +}; + +/** + * Function: createVertex + * + * Hook method that creates the new vertex for <insertVertex>. + */ +mxGraph.prototype.createVertex = function(parent, id, value, + x, y, width, height, style, relative) +{ + // Creates the geometry for the vertex + var geometry = new mxGeometry(x, y, width, height); + geometry.relative = (relative != null) ? relative : false; + + // Creates the vertex + var vertex = new mxCell(value, geometry, style); + vertex.setId(id); + vertex.setVertex(true); + vertex.setConnectable(true); + + return vertex; +}; + +/** + * Function: insertEdge + * + * Adds a new edge into the given parent <mxCell> using value as the user + * object and the given source and target as the terminals of the new edge. + * The id and style are used for the respective properties of the new + * <mxCell>, which is returned. + * + * Parameters: + * + * parent - <mxCell> that specifies the parent of the new edge. + * id - Optional string that defines the Id of the new edge. + * value - JavaScript object to be used as the user object. + * source - <mxCell> that defines the source of the edge. + * target - <mxCell> that defines the target of the edge. + * style - Optional string that defines the cell style. + */ +mxGraph.prototype.insertEdge = function(parent, id, value, source, target, style) +{ + var edge = this.createEdge(parent, id, value, source, target, style); + + return this.addEdge(edge, parent, source, target); +}; + +/** + * Function: createEdge + * + * Hook method that creates the new edge for <insertEdge>. This + * implementation does not set the source and target of the edge, these + * are set when the edge is added to the model. + * + */ +mxGraph.prototype.createEdge = function(parent, id, value, source, target, style) +{ + // Creates the edge + var edge = new mxCell(value, new mxGeometry(), style); + edge.setId(id); + edge.setEdge(true); + edge.geometry.relative = true; + + return edge; +}; + +/** + * Function: addEdge + * + * Adds the edge to the parent and connects it to the given source and + * target terminals. This is a shortcut method. Returns the edge that was + * added. + * + * Parameters: + * + * edge - <mxCell> to be inserted into the given parent. + * parent - <mxCell> that represents the new parent. If no parent is + * given then the default parent is used. + * source - Optional <mxCell> that represents the source terminal. + * target - Optional <mxCell> that represents the target terminal. + * index - Optional index to insert the cells at. Default is to append. + */ +mxGraph.prototype.addEdge = function(edge, parent, source, target, index) +{ + return this.addCell(edge, parent, index, source, target); +}; + +/** + * Function: addCell + * + * Adds the cell to the parent and connects it to the given source and + * target terminals. This is a shortcut method. Returns the cell that was + * added. + * + * Parameters: + * + * cell - <mxCell> to be inserted into the given parent. + * parent - <mxCell> that represents the new parent. If no parent is + * given then the default parent is used. + * index - Optional index to insert the cells at. Default is to append. + * source - Optional <mxCell> that represents the source terminal. + * target - Optional <mxCell> that represents the target terminal. + */ +mxGraph.prototype.addCell = function(cell, parent, index, source, target) +{ + return this.addCells([cell], parent, index, source, target)[0]; +}; + +/** + * Function: addCells + * + * Adds the cells to the parent at the given index, connecting each cell to + * the optional source and target terminal. The change is carried out using + * <cellsAdded>. This method fires <mxEvent.ADD_CELLS> while the + * transaction is in progress. Returns the cells that were added. + * + * Parameters: + * + * cells - Array of <mxCells> to be inserted. + * parent - <mxCell> that represents the new parent. If no parent is + * given then the default parent is used. + * index - Optional index to insert the cells at. Default is to append. + * source - Optional source <mxCell> for all inserted cells. + * target - Optional target <mxCell> for all inserted cells. + */ +mxGraph.prototype.addCells = function(cells, parent, index, source, target) +{ + if (parent == null) + { + parent = this.getDefaultParent(); + } + + if (index == null) + { + index = this.model.getChildCount(parent); + } + + this.model.beginUpdate(); + try + { + this.cellsAdded(cells, parent, index, source, target, false, true); + this.fireEvent(new mxEventObject(mxEvent.ADD_CELLS, 'cells', cells, + 'parent', parent, 'index', index, 'source', source, 'target', target)); + } + finally + { + this.model.endUpdate(); + } + + return cells; +}; + +/** + * Function: cellsAdded + * + * Adds the specified cells to the given parent. This method fires + * <mxEvent.CELLS_ADDED> while the transaction is in progress. + */ +mxGraph.prototype.cellsAdded = function(cells, parent, index, source, target, absolute, constrain) +{ + if (cells != null && parent != null && index != null) + { + this.model.beginUpdate(); + try + { + var parentState = (absolute) ? this.view.getState(parent) : null; + var o1 = (parentState != null) ? parentState.origin : null; + var zero = new mxPoint(0, 0); + + for (var i = 0; i < cells.length; i++) + { + if (cells[i] == null) + { + index--; + } + else + { + var previous = this.model.getParent(cells[i]); + + // Keeps the cell at its absolute location + if (o1 != null && cells[i] != parent && parent != previous) + { + var oldState = this.view.getState(previous); + var o2 = (oldState != null) ? oldState.origin : zero; + var geo = this.model.getGeometry(cells[i]); + + if (geo != null) + { + var dx = o2.x - o1.x; + var dy = o2.y - o1.y; + + // FIXME: Cells should always be inserted first before any other edit + // to avoid forward references in sessions. + geo = geo.clone(); + geo.translate(dx, dy); + + if (!geo.relative && this.model.isVertex(cells[i]) && + !this.isAllowNegativeCoordinates()) + { + geo.x = Math.max(0, geo.x); + geo.y = Math.max(0, geo.y); + } + + this.model.setGeometry(cells[i], geo); + } + } + + // Decrements all following indices + // if cell is already in parent + if (parent == previous) + { + index--; + } + + this.model.add(parent, cells[i], index + i); + + // Extends the parent + if (this.isExtendParentsOnAdd() && this.isExtendParent(cells[i])) + { + this.extendParent(cells[i]); + } + + // Constrains the child + if (constrain == null || constrain) + { + this.constrainChild(cells[i]); + } + + // Sets the source terminal + if (source != null) + { + this.cellConnected(cells[i], source, true); + } + + // Sets the target terminal + if (target != null) + { + this.cellConnected(cells[i], target, false); + } + } + } + + this.fireEvent(new mxEventObject(mxEvent.CELLS_ADDED, 'cells', cells, + 'parent', parent, 'index', index, 'source', source, 'target', target, + 'absolute', absolute)); + } + finally + { + this.model.endUpdate(); + } + } +}; + +/** + * Function: removeCells + * + * Removes the given cells from the graph including all connected edges if + * includeEdges is true. The change is carried out using <cellsRemoved>. + * This method fires <mxEvent.REMOVE_CELLS> while the transaction is in + * progress. The removed cells are returned as an array. + * + * Parameters: + * + * cells - Array of <mxCells> to remove. If null is specified then the + * selection cells which are deletable are used. + * includeEdges - Optional boolean which specifies if all connected edges + * should be removed as well. Default is true. + */ +mxGraph.prototype.removeCells = function(cells, includeEdges) +{ + includeEdges = (includeEdges != null) ? includeEdges : true; + + if (cells == null) + { + cells = this.getDeletableCells(this.getSelectionCells()); + } + + // Adds all edges to the cells + if (includeEdges) + { + cells = this.getDeletableCells(this.addAllEdges(cells)); + } + + this.model.beginUpdate(); + try + { + this.cellsRemoved(cells); + this.fireEvent(new mxEventObject(mxEvent.REMOVE_CELLS, + 'cells', cells, 'includeEdges', includeEdges)); + } + finally + { + this.model.endUpdate(); + } + + return cells; +}; + +/** + * Function: cellsRemoved + * + * Removes the given cells from the model. This method fires + * <mxEvent.CELLS_REMOVED> while the transaction is in progress. + * + * Parameters: + * + * cells - Array of <mxCells> to remove. + */ +mxGraph.prototype.cellsRemoved = function(cells) +{ + if (cells != null && cells.length > 0) + { + var scale = this.view.scale; + var tr = this.view.translate; + + this.model.beginUpdate(); + try + { + // Creates hashtable for faster lookup + var hash = new Object(); + + for (var i = 0; i < cells.length; i++) + { + var id = mxCellPath.create(cells[i]); + hash[id] = cells[i]; + } + + for (var i = 0; i < cells.length; i++) + { + // Disconnects edges which are not in cells + var edges = this.getConnections(cells[i]); + + for (var j = 0; j < edges.length; j++) + { + var id = mxCellPath.create(edges[j]); + + if (hash[id] == null) + { + var geo = this.model.getGeometry(edges[j]); + + if (geo != null) + { + var state = this.view.getState(edges[j]); + + if (state != null) + { + geo = geo.clone(); + var source = state.getVisibleTerminal(true) == cells[i]; + var pts = state.absolutePoints; + var n = (source) ? 0 : pts.length - 1; + + geo.setTerminalPoint( + new mxPoint(pts[n].x / scale - tr.x, + pts[n].y / scale - tr.y), source); + this.model.setTerminal(edges[j], null, source); + this.model.setGeometry(edges[j], geo); + } + } + } + } + + this.model.remove(cells[i]); + } + + this.fireEvent(new mxEventObject(mxEvent.CELLS_REMOVED, + 'cells', cells)); + } + finally + { + this.model.endUpdate(); + } + } +}; + +/** + * Function: splitEdge + * + * Splits the given edge by adding the newEdge between the previous source + * and the given cell and reconnecting the source of the given edge to the + * given cell. This method fires <mxEvent.SPLIT_EDGE> while the transaction + * is in progress. Returns the new edge that was inserted. + * + * Parameters: + * + * edge - <mxCell> that represents the edge to be splitted. + * cells - <mxCells> that represents the cells to insert into the edge. + * newEdge - <mxCell> that represents the edge to be inserted. + * dx - Optional integer that specifies the vector to move the cells. + * dy - Optional integer that specifies the vector to move the cells. + */ +mxGraph.prototype.splitEdge = function(edge, cells, newEdge, dx, dy) +{ + dx = dx || 0; + dy = dy || 0; + + if (newEdge == null) + { + newEdge = this.cloneCells([edge])[0]; + } + + var parent = this.model.getParent(edge); + var source = this.model.getTerminal(edge, true); + + this.model.beginUpdate(); + try + { + this.cellsMoved(cells, dx, dy, false, false); + this.cellsAdded(cells, parent, this.model.getChildCount(parent), null, null, + true); + this.cellsAdded([newEdge], parent, this.model.getChildCount(parent), + source, cells[0], false); + this.cellConnected(edge, cells[0], true); + this.fireEvent(new mxEventObject(mxEvent.SPLIT_EDGE, 'edge', edge, + 'cells', cells, 'newEdge', newEdge, 'dx', dx, 'dy', dy)); + } + finally + { + this.model.endUpdate(); + } + + return newEdge; +}; + +/** + * Group: Cell visibility + */ + +/** + * Function: toggleCells + * + * Sets the visible state of the specified cells and all connected edges + * if includeEdges is true. The change is carried out using <cellsToggled>. + * This method fires <mxEvent.TOGGLE_CELLS> while the transaction is in + * progress. Returns the cells whose visible state was changed. + * + * Parameters: + * + * show - Boolean that specifies the visible state to be assigned. + * cells - Array of <mxCells> whose visible state should be changed. If + * null is specified then the selection cells are used. + * includeEdges - Optional boolean indicating if the visible state of all + * connected edges should be changed as well. Default is true. + */ +mxGraph.prototype.toggleCells = function(show, cells, includeEdges) +{ + if (cells == null) + { + cells = this.getSelectionCells(); + } + + // Adds all connected edges recursively + if (includeEdges) + { + cells = this.addAllEdges(cells); + } + + this.model.beginUpdate(); + try + { + this.cellsToggled(cells, show); + this.fireEvent(new mxEventObject(mxEvent.TOGGLE_CELLS, + 'show', show, 'cells', cells, 'includeEdges', includeEdges)); + } + finally + { + this.model.endUpdate(); + } + + return cells; +}; + +/** + * Function: cellsToggled + * + * Sets the visible state of the specified cells. + * + * Parameters: + * + * cells - Array of <mxCells> whose visible state should be changed. + * show - Boolean that specifies the visible state to be assigned. + */ +mxGraph.prototype.cellsToggled = function(cells, show) +{ + if (cells != null && cells.length > 0) + { + this.model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + this.model.setVisible(cells[i], show); + } + } + finally + { + this.model.endUpdate(); + } + } +}; + +/** + * Group: Folding + */ + +/** + * Function: foldCells + * + * Sets the collapsed state of the specified cells and all descendants + * if recurse is true. The change is carried out using <cellsFolded>. + * This method fires <mxEvent.FOLD_CELLS> while the transaction is in + * progress. Returns the cells whose collapsed state was changed. + * + * Parameters: + * + * collapsed - Boolean indicating the collapsed state to be assigned. + * recurse - Optional boolean indicating if the collapsed state of all + * descendants should be set. Default is false. + * cells - Array of <mxCells> whose collapsed state should be set. If + * null is specified then the foldable selection cells are used. + * checkFoldable - Optional boolean indicating of isCellFoldable should be + * checked. Default is false. + */ +mxGraph.prototype.foldCells = function(collapse, recurse, cells, checkFoldable) +{ + recurse = (recurse != null) ? recurse : false; + + if (cells == null) + { + cells = this.getFoldableCells(this.getSelectionCells(), collapse); + } + + this.stopEditing(false); + + this.model.beginUpdate(); + try + { + this.cellsFolded(cells, collapse, recurse, checkFoldable); + this.fireEvent(new mxEventObject(mxEvent.FOLD_CELLS, + 'collapse', collapse, 'recurse', recurse, 'cells', cells)); + } + finally + { + this.model.endUpdate(); + } + + return cells; +}; + +/** + * Function: cellsFolded + * + * Sets the collapsed state of the specified cells. This method fires + * <mxEvent.CELLS_FOLDED> while the transaction is in progress. Returns the + * cells whose collapsed state was changed. + * + * Parameters: + * + * cells - Array of <mxCells> whose collapsed state should be set. + * collapsed - Boolean indicating the collapsed state to be assigned. + * recurse - Boolean indicating if the collapsed state of all descendants + * should be set. + * checkFoldable - Optional boolean indicating of isCellFoldable should be + * checked. Default is false. + */ +mxGraph.prototype.cellsFolded = function(cells, collapse, recurse, checkFoldable) +{ + if (cells != null && cells.length > 0) + { + this.model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + if ((!checkFoldable || this.isCellFoldable(cells[i], collapse)) && + collapse != this.isCellCollapsed(cells[i])) + { + this.model.setCollapsed(cells[i], collapse); + this.swapBounds(cells[i], collapse); + + if (this.isExtendParent(cells[i])) + { + this.extendParent(cells[i]); + } + + if (recurse) + { + var children = this.model.getChildren(cells[i]); + this.foldCells(children, collapse, recurse); + } + } + } + + this.fireEvent(new mxEventObject(mxEvent.CELLS_FOLDED, + 'cells', cells, 'collapse', collapse, 'recurse', recurse)); + } + finally + { + this.model.endUpdate(); + } + } +}; + +/** + * Function: swapBounds + * + * Swaps the alternate and the actual bounds in the geometry of the given + * cell invoking <updateAlternateBounds> before carrying out the swap. + * + * Parameters: + * + * cell - <mxCell> for which the bounds should be swapped. + * willCollapse - Boolean indicating if the cell is going to be collapsed. + */ +mxGraph.prototype.swapBounds = function(cell, willCollapse) +{ + if (cell != null) + { + var geo = this.model.getGeometry(cell); + + if (geo != null) + { + geo = geo.clone(); + + this.updateAlternateBounds(cell, geo, willCollapse); + geo.swap(); + + this.model.setGeometry(cell, geo); + } + } +}; + +/** + * Function: updateAlternateBounds + * + * Updates or sets the alternate bounds in the given geometry for the given + * cell depending on whether the cell is going to be collapsed. If no + * alternate bounds are defined in the geometry and + * <collapseToPreferredSize> is true, then the preferred size is used for + * the alternate bounds. The top, left corner is always kept at the same + * location. + * + * Parameters: + * + * cell - <mxCell> for which the geometry is being udpated. + * g - <mxGeometry> for which the alternate bounds should be updated. + * willCollapse - Boolean indicating if the cell is going to be collapsed. + */ +mxGraph.prototype.updateAlternateBounds = function(cell, geo, willCollapse) +{ + if (cell != null && geo != null) + { + if (geo.alternateBounds == null) + { + var bounds = geo; + + if (this.collapseToPreferredSize) + { + var tmp = this.getPreferredSizeForCell(cell); + + if (tmp != null) + { + bounds = tmp; + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + var startSize = mxUtils.getValue(style, mxConstants.STYLE_STARTSIZE); + + if (startSize > 0) + { + bounds.height = Math.max(bounds.height, startSize); + } + } + } + + geo.alternateBounds = new mxRectangle( + geo.x, geo.y, bounds.width, bounds.height); + } + else + { + geo.alternateBounds.x = geo.x; + geo.alternateBounds.y = geo.y; + } + } +}; + +/** + * Function: addAllEdges + * + * Returns an array with the given cells and all edges that are connected + * to a cell or one of its descendants. + */ +mxGraph.prototype.addAllEdges = function(cells) +{ + var allCells = cells.slice(); // FIXME: Required? + allCells = allCells.concat(this.getAllEdges(cells)); + + return allCells; +}; + +/** + * Function: getAllEdges + * + * Returns all edges connected to the given cells or its descendants. + */ +mxGraph.prototype.getAllEdges = function(cells) +{ + var edges = []; + + if (cells != null) + { + for (var i = 0; i < cells.length; i++) + { + var edgeCount = this.model.getEdgeCount(cells[i]); + + for (var j = 0; j < edgeCount; j++) + { + edges.push(this.model.getEdgeAt(cells[i], j)); + } + + // Recurses + var children = this.model.getChildren(cells[i]); + edges = edges.concat(this.getAllEdges(children)); + } + } + + return edges; +}; + +/** + * Group: Cell sizing + */ + +/** + * Function: updateCellSize + * + * Updates the size of the given cell in the model using <cellSizeUpdated>. + * This method fires <mxEvent.UPDATE_CELL_SIZE> while the transaction is in + * progress. Returns the cell whose size was updated. + * + * Parameters: + * + * cell - <mxCell> whose size should be updated. + */ +mxGraph.prototype.updateCellSize = function(cell, ignoreChildren) +{ + ignoreChildren = (ignoreChildren != null) ? ignoreChildren : false; + + this.model.beginUpdate(); + try + { + this.cellSizeUpdated(cell, ignoreChildren); + this.fireEvent(new mxEventObject(mxEvent.UPDATE_CELL_SIZE, + 'cell', cell, 'ignoreChildren', ignoreChildren)); + } + finally + { + this.model.endUpdate(); + } + + return cell; +}; + +/** + * Function: cellSizeUpdated + * + * Updates the size of the given cell in the model using + * <getPreferredSizeForCell> to get the new size. + * + * Parameters: + * + * cell - <mxCell> for which the size should be changed. + */ +mxGraph.prototype.cellSizeUpdated = function(cell, ignoreChildren) +{ + if (cell != null) + { + this.model.beginUpdate(); + try + { + var size = this.getPreferredSizeForCell(cell); + var geo = this.model.getGeometry(cell); + + if (size != null && geo != null) + { + var collapsed = this.isCellCollapsed(cell); + geo = geo.clone(); + + if (this.isSwimlane(cell)) + { + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + var cellStyle = this.model.getStyle(cell); + + if (cellStyle == null) + { + cellStyle = ''; + } + + if (mxUtils.getValue(style, mxConstants.STYLE_HORIZONTAL, true)) + { + cellStyle = mxUtils.setStyle(cellStyle, + mxConstants.STYLE_STARTSIZE, size.height + 8); + + if (collapsed) + { + geo.height = size.height + 8; + } + + geo.width = size.width; + } + else + { + cellStyle = mxUtils.setStyle(cellStyle, + mxConstants.STYLE_STARTSIZE, size.width + 8); + + if (collapsed) + { + geo.width = size.width + 8; + } + + geo.height = size.height; + } + + this.model.setStyle(cell, cellStyle); + } + else + { + geo.width = size.width; + geo.height = size.height; + } + + if (!ignoreChildren && !collapsed) + { + var bounds = this.view.getBounds(this.model.getChildren(cell)); + + if (bounds != null) + { + var tr = this.view.translate; + var scale = this.view.scale; + + var width = (bounds.x + bounds.width) / scale - geo.x - tr.x; + var height = (bounds.y + bounds.height) / scale - geo.y - tr.y; + + geo.width = Math.max(geo.width, width); + geo.height = Math.max(geo.height, height); + } + } + + this.cellsResized([cell], [geo]); + } + } + finally + { + this.model.endUpdate(); + } + } +}; + +/** + * Function: getPreferredSizeForCell + * + * Returns the preferred width and height of the given <mxCell> as an + * <mxRectangle>. + * + * Parameters: + * + * cell - <mxCell> for which the preferred size should be returned. + */ +mxGraph.prototype.getPreferredSizeForCell = function(cell) +{ + var result = null; + + if (cell != null) + { + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + if (style != null && !this.model.isEdge(cell)) + { + var fontSize = style[mxConstants.STYLE_FONTSIZE] || mxConstants.DEFAULT_FONTSIZE; + var dx = 0; + var dy = 0; + + // Adds dimension of image if shape is a label + if (this.getImage(state) != null || style[mxConstants.STYLE_IMAGE] != null) + { + if (style[mxConstants.STYLE_SHAPE] == mxConstants.SHAPE_LABEL) + { + if (style[mxConstants.STYLE_VERTICAL_ALIGN] == mxConstants.ALIGN_MIDDLE) + { + dx += parseFloat(style[mxConstants.STYLE_IMAGE_WIDTH]) || mxLabel.prototype.imageSize; + } + + if (style[mxConstants.STYLE_ALIGN] != mxConstants.ALIGN_CENTER) + { + dy += parseFloat(style[mxConstants.STYLE_IMAGE_HEIGHT]) || mxLabel.prototype.imageSize; + } + } + } + + // Adds spacings + dx += 2 * (style[mxConstants.STYLE_SPACING] || 0); + dx += style[mxConstants.STYLE_SPACING_LEFT] || 0; + dx += style[mxConstants.STYLE_SPACING_RIGHT] || 0; + + dy += 2 * (style[mxConstants.STYLE_SPACING] || 0); + dy += style[mxConstants.STYLE_SPACING_TOP] || 0; + dy += style[mxConstants.STYLE_SPACING_BOTTOM] || 0; + + // Add spacing for collapse/expand icon + // LATER: Check alignment and use constants + // for image spacing + var image = this.getFoldingImage(state); + + if (image != null) + { + dx += image.width + 8; + } + + // Adds space for label + var value = this.getLabel(cell); + + if (value != null && value.length > 0) + { + if (!this.isHtmlLabel(cell)) + { + value = value.replace(/\n/g, '<br>'); + } + + var size = mxUtils.getSizeForString(value, + fontSize, style[mxConstants.STYLE_FONTFAMILY]); + var width = size.width + dx; + var height = size.height + dy; + + if (!mxUtils.getValue(style, mxConstants.STYLE_HORIZONTAL, true)) + { + var tmp = height; + + height = width; + width = tmp; + } + + if (this.gridEnabled) + { + width = this.snap(width + this.gridSize / 2); + height = this.snap(height + this.gridSize / 2); + } + + result = new mxRectangle(0, 0, width, height); + } + else + { + var gs2 = 4 * this.gridSize; + result = new mxRectangle(0, 0, gs2, gs2); + } + } + } + + return result; +}; + +/** + * Function: handleGesture + * + * Invokes if a gesture event has been detected on a cell state. + * + * Parameters: + * + * state - <mxCellState> which was pinched. + * evt - Object that represents the gesture event. + */ +mxGraph.prototype.handleGesture = function(state, evt) +{ + if (Math.abs(1 - evt.scale) > 0.2) + { + var scale = this.view.scale; + var tr = this.view.translate; + + var w = state.width * evt.scale; + var h = state.height * evt.scale; + var x = state.x - (w - state.width) / 2; + var y = state.y - (h - state.height) / 2; + + var bounds = new mxRectangle(this.snap(x / scale) - tr.x, + this.snap(y / scale) - tr.y, + this.snap(w / scale), this.snap(h / scale)); + this.resizeCell(state.cell, bounds); + } +}; + +/** + * Function: resizeCell + * + * Sets the bounds of the given cell using <resizeCells>. Returns the + * cell which was passed to the function. + * + * Parameters: + * + * cell - <mxCell> whose bounds should be changed. + * bounds - <mxRectangle> that represents the new bounds. + */ +mxGraph.prototype.resizeCell = function(cell, bounds) +{ + return this.resizeCells([cell], [bounds])[0]; +}; + +/** + * Function: resizeCells + * + * Sets the bounds of the given cells and fires a <mxEvent.RESIZE_CELLS> + * event while the transaction is in progress. Returns the cells which + * have been passed to the function. + * + * Parameters: + * + * cells - Array of <mxCells> whose bounds should be changed. + * bounds - Array of <mxRectangles> that represent the new bounds. + */ +mxGraph.prototype.resizeCells = function(cells, bounds) +{ + this.model.beginUpdate(); + try + { + this.cellsResized(cells, bounds); + this.fireEvent(new mxEventObject(mxEvent.RESIZE_CELLS, + 'cells', cells, 'bounds', bounds)); + } + finally + { + this.model.endUpdate(); + } + + return cells; +}; + +/** + * Function: cellsResized + * + * Sets the bounds of the given cells and fires a <mxEvent.CELLS_RESIZED> + * event. If <extendParents> is true, then the parent is extended if a + * child size is changed so that it overlaps with the parent. + * + * Parameters: + * + * cells - Array of <mxCells> whose bounds should be changed. + * bounds - Array of <mxRectangles> that represent the new bounds. + */ +mxGraph.prototype.cellsResized = function(cells, bounds) +{ + if (cells != null && bounds != null && cells.length == bounds.length) + { + this.model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + var tmp = bounds[i]; + var geo = this.model.getGeometry(cells[i]); + + if (geo != null && (geo.x != tmp.x || geo.y != tmp.y || + geo.width != tmp.width || geo.height != tmp.height)) + { + geo = geo.clone(); + + if (geo.relative) + { + var offset = geo.offset; + + if (offset != null) + { + offset.x += tmp.x - geo.x; + offset.y += tmp.y - geo.y; + } + } + else + { + geo.x = tmp.x; + geo.y = tmp.y; + } + + geo.width = tmp.width; + geo.height = tmp.height; + + if (!geo.relative && this.model.isVertex(cells[i]) && + !this.isAllowNegativeCoordinates()) + { + geo.x = Math.max(0, geo.x); + geo.y = Math.max(0, geo.y); + } + + this.model.setGeometry(cells[i], geo); + + if (this.isExtendParent(cells[i])) + { + this.extendParent(cells[i]); + } + } + } + + if (this.resetEdgesOnResize) + { + this.resetEdges(cells); + } + + this.fireEvent(new mxEventObject(mxEvent.CELLS_RESIZED, + 'cells', cells, 'bounds', bounds)); + } + finally + { + this.model.endUpdate(); + } + } +}; + +/** + * Function: extendParent + * + * Resizes the parents recursively so that they contain the complete area + * of the resized child cell. + * + * Parameters: + * + * cell - <mxCell> that has been resized. + */ +mxGraph.prototype.extendParent = function(cell) +{ + if (cell != null) + { + var parent = this.model.getParent(cell); + var p = this.model.getGeometry(parent); + + if (parent != null && p != null && !this.isCellCollapsed(parent)) + { + var geo = this.model.getGeometry(cell); + + if (geo != null && (p.width < geo.x + geo.width || + p.height < geo.y + geo.height)) + { + p = p.clone(); + + p.width = Math.max(p.width, geo.x + geo.width); + p.height = Math.max(p.height, geo.y + geo.height); + + this.cellsResized([parent], [p]); + } + } + } +}; + +/** + * Group: Cell moving + */ + +/** + * Function: importCells + * + * Clones and inserts the given cells into the graph using the move + * method and returns the inserted cells. This shortcut is used if + * cells are inserted via datatransfer. + */ +mxGraph.prototype.importCells = function(cells, dx, dy, target, evt) +{ + return this.moveCells(cells, dx, dy, true, target, evt); +}; + +/** + * Function: moveCells + * + * Moves or clones the specified cells and moves the cells or clones by the + * given amount, adding them to the optional target cell. The evt is the + * mouse event as the mouse was released. The change is carried out using + * <cellsMoved>. This method fires <mxEvent.MOVE_CELLS> while the + * transaction is in progress. Returns the cells that were moved. + * + * Use the following code to move all cells in the graph. + * + * (code) + * graph.moveCells(graph.getChildCells(null, true, true), 10, 10); + * (end) + * + * Parameters: + * + * cells - Array of <mxCells> to be moved, cloned or added to the target. + * dx - Integer that specifies the x-coordinate of the vector. Default is 0. + * dy - Integer that specifies the y-coordinate of the vector. Default is 0. + * clone - Boolean indicating if the cells should be cloned. Default is false. + * target - <mxCell> that represents the new parent of the cells. + * evt - Mouseevent that triggered the invocation. + */ +mxGraph.prototype.moveCells = function(cells, dx, dy, clone, target, evt) +{ + dx = (dx != null) ? dx : 0; + dy = (dy != null) ? dy : 0; + clone = (clone != null) ? clone : false; + + if (cells != null && (dx != 0 || dy != 0 || clone || target != null)) + { + this.model.beginUpdate(); + try + { + if (clone) + { + cells = this.cloneCells(cells, this.isCloneInvalidEdges()); + + if (target == null) + { + target = this.getDefaultParent(); + } + } + + // FIXME: Cells should always be inserted first before any other edit + // to avoid forward references in sessions. + // Need to disable allowNegativeCoordinates if target not null to + // allow for temporary negative numbers until cellsAdded is called. + var previous = this.isAllowNegativeCoordinates(); + + if (target != null) + { + this.setAllowNegativeCoordinates(true); + } + + this.cellsMoved(cells, dx, dy, !clone && this.isDisconnectOnMove() + && this.isAllowDanglingEdges(), target == null); + + this.setAllowNegativeCoordinates(previous); + + if (target != null) + { + var index = this.model.getChildCount(target); + this.cellsAdded(cells, target, index, null, null, true); + } + + // Dispatches a move event + this.fireEvent(new mxEventObject(mxEvent.MOVE_CELLS, 'cells', cells, + 'dx', dx, 'dy', dy, 'clone', clone, 'target', target, 'event', evt)); + } + finally + { + this.model.endUpdate(); + } + } + + return cells; +}; + +/** + * Function: cellsMoved + * + * Moves the specified cells by the given vector, disconnecting the cells + * using disconnectGraph is disconnect is true. This method fires + * <mxEvent.CELLS_MOVED> while the transaction is in progress. + */ +mxGraph.prototype.cellsMoved = function(cells, dx, dy, disconnect, constrain) +{ + if (cells != null && (dx != 0 || dy != 0)) + { + this.model.beginUpdate(); + try + { + if (disconnect) + { + this.disconnectGraph(cells); + } + + for (var i = 0; i < cells.length; i++) + { + this.translateCell(cells[i], dx, dy); + + if (constrain) + { + this.constrainChild(cells[i]); + } + } + + if (this.resetEdgesOnMove) + { + this.resetEdges(cells); + } + + this.fireEvent(new mxEventObject(mxEvent.CELLS_MOVED, + 'cells', cells, 'dx', dy, 'dy', dy, 'disconnect', disconnect)); + } + finally + { + this.model.endUpdate(); + } + } +}; + +/** + * Function: translateCell + * + * Translates the geometry of the given cell and stores the new, + * translated geometry in the model as an atomic change. + */ +mxGraph.prototype.translateCell = function(cell, dx, dy) +{ + var geo = this.model.getGeometry(cell); + + if (geo != null) + { + geo = geo.clone(); + geo.translate(dx, dy); + + if (!geo.relative && this.model.isVertex(cell) && !this.isAllowNegativeCoordinates()) + { + geo.x = Math.max(0, geo.x); + geo.y = Math.max(0, geo.y); + } + + if (geo.relative && !this.model.isEdge(cell)) + { + if (geo.offset == null) + { + geo.offset = new mxPoint(dx, dy); + } + else + { + geo.offset.x += dx; + geo.offset.y += dy; + } + } + + this.model.setGeometry(cell, geo); + } +}; + +/** + * Function: getCellContainmentArea + * + * Returns the <mxRectangle> inside which a cell is to be kept. + * + * Parameters: + * + * cell - <mxCell> for which the area should be returned. + */ +mxGraph.prototype.getCellContainmentArea = function(cell) +{ + if (cell != null && !this.model.isEdge(cell)) + { + var parent = this.model.getParent(cell); + + if (parent == this.getDefaultParent() || parent == this.getCurrentRoot()) + { + return this.getMaximumGraphBounds(); + } + else if (parent != null && parent != this.getDefaultParent()) + { + var g = this.model.getGeometry(parent); + + if (g != null) + { + var x = 0; + var y = 0; + var w = g.width; + var h = g.height; + + if (this.isSwimlane(parent)) + { + var size = this.getStartSize(parent); + + x = size.width; + w -= size.width; + y = size.height; + h -= size.height; + } + + return new mxRectangle(x, y, w, h); + } + } + } + + return null; +}; + +/** + * Function: getMaximumGraphBounds + * + * Returns the bounds inside which the diagram should be kept as an + * <mxRectangle>. + */ +mxGraph.prototype.getMaximumGraphBounds = function() +{ + return this.maximumGraphBounds; +}; + +/** + * Function: constrainChild + * + * Keeps the given cell inside the bounds returned by + * <getCellContainmentArea> for its parent, according to the rules defined by + * <getOverlap> and <isConstrainChild>. This modifies the cell's geometry + * in-place and does not clone it. + * + * Parameters: + * + * cells - <mxCell> which should be constrained. + */ +mxGraph.prototype.constrainChild = function(cell) +{ + if (cell != null) + { + var geo = this.model.getGeometry(cell); + var area = (this.isConstrainChild(cell)) ? + this.getCellContainmentArea(cell) : + this.getMaximumGraphBounds(); + + if (geo != null && area != null) + { + // Keeps child within the content area of the parent + if (!geo.relative && (geo.x < area.x || geo.y < area.y || + area.width < geo.x + geo.width || area.height < geo.y + geo.height)) + { + var overlap = this.getOverlap(cell); + + if (area.width > 0) + { + geo.x = Math.min(geo.x, area.x + area.width - + (1 - overlap) * geo.width); + } + + if (area.height > 0) + { + geo.y = Math.min(geo.y, area.y + area.height - + (1 - overlap) * geo.height); + } + + geo.x = Math.max(geo.x, area.x - geo.width * overlap); + geo.y = Math.max(geo.y, area.y - geo.height * overlap); + } + } + } +}; + +/** + * Function: resetEdges + * + * Resets the control points of the edges that are connected to the given + * cells if not both ends of the edge are in the given cells array. + * + * Parameters: + * + * cells - Array of <mxCells> for which the connected edges should be + * reset. + */ +mxGraph.prototype.resetEdges = function(cells) +{ + if (cells != null) + { + // Prepares a hashtable for faster cell lookups + var hash = new Object(); + + for (var i = 0; i < cells.length; i++) + { + var id = mxCellPath.create(cells[i]); + hash[id] = cells[i]; + } + + this.model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + var edges = this.model.getEdges(cells[i]); + + if (edges != null) + { + for (var j = 0; j < edges.length; j++) + { + var state = this.view.getState(edges[j]); + + var source = (state != null) ? state.getVisibleTerminal(true) : this.view.getVisibleTerminal(edges[j], true); + var target = (state != null) ? state.getVisibleTerminal(false) : this.view.getVisibleTerminal(edges[j], false); + + var sourceId = mxCellPath.create(source); + var targetId = mxCellPath.create(target); + + // Checks if one of the terminals is not in the given array + if (hash[sourceId] == null || hash[targetId] == null) + { + this.resetEdge(edges[j]); + } + } + } + + this.resetEdges(this.model.getChildren(cells[i])); + } + } + finally + { + this.model.endUpdate(); + } + } +}; + +/** + * Function: resetEdge + * + * Resets the control points of the given edge. + * + * Parameters: + * + * edge - <mxCell> whose points should be reset. + */ +mxGraph.prototype.resetEdge = function(edge) +{ + var geo = this.model.getGeometry(edge); + + // Resets the control points + if (geo != null && geo.points != null && geo.points.length > 0) + { + geo = geo.clone(); + geo.points = []; + this.model.setGeometry(edge, geo); + } + + return edge; +}; + +/** + * Group: Cell connecting and connection constraints + */ + +/** + * Function: getAllConnectionConstraints + * + * Returns an array of all <mxConnectionConstraints> for the given terminal. If + * the shape of the given terminal is a <mxStencilShape> then the constraints + * of the corresponding <mxStencil> are returned. + * + * Parameters: + * + * terminal - <mxCellState> that represents the terminal. + * source - Boolean that specifies if the terminal is the source or target. + */ +mxGraph.prototype.getAllConnectionConstraints = function(terminal, source) +{ + if (terminal != null && terminal.shape != null && + terminal.shape instanceof mxStencilShape) + { + if (terminal.shape.stencil != null) + { + return terminal.shape.stencil.constraints; + } + } + + return null; +}; + +/** + * Function: getConnectionConstraint + * + * Returns an <mxConnectionConstraint> that describes the given connection + * point. This result can then be passed to <getConnectionPoint>. + * + * Parameters: + * + * edge - <mxCellState> that represents the edge. + * terminal - <mxCellState> that represents the terminal. + * source - Boolean indicating if the terminal is the source or target. + */ +mxGraph.prototype.getConnectionConstraint = function(edge, terminal, source) +{ + var point = null; + var x = edge.style[(source) ? + mxConstants.STYLE_EXIT_X : + mxConstants.STYLE_ENTRY_X]; + + if (x != null) + { + var y = edge.style[(source) ? + mxConstants.STYLE_EXIT_Y : + mxConstants.STYLE_ENTRY_Y]; + + if (y != null) + { + point = new mxPoint(parseFloat(x), parseFloat(y)); + } + } + + var perimeter = false; + + if (point != null) + { + perimeter = mxUtils.getValue(edge.style, (source) ? mxConstants.STYLE_EXIT_PERIMETER : + mxConstants.STYLE_ENTRY_PERIMETER, true); + } + + return new mxConnectionConstraint(point, perimeter); +}; + +/** + * Function: setConnectionConstraint + * + * Sets the <mxConnectionConstraint> that describes the given connection point. + * If no constraint is given then nothing is changed. To remove an existing + * constraint from the given edge, use an empty constraint instead. + * + * Parameters: + * + * edge - <mxCell> that represents the edge. + * terminal - <mxCell> that represents the terminal. + * source - Boolean indicating if the terminal is the source or target. + * constraint - Optional <mxConnectionConstraint> to be used for this + * connection. + */ +mxGraph.prototype.setConnectionConstraint = function(edge, terminal, source, constraint) +{ + if (constraint != null) + { + this.model.beginUpdate(); + try + { + if (constraint == null || constraint.point == null) + { + this.setCellStyles((source) ? mxConstants.STYLE_EXIT_X : + mxConstants.STYLE_ENTRY_X, null, [edge]); + this.setCellStyles((source) ? mxConstants.STYLE_EXIT_Y : + mxConstants.STYLE_ENTRY_Y, null, [edge]); + this.setCellStyles((source) ? mxConstants.STYLE_EXIT_PERIMETER : + mxConstants.STYLE_ENTRY_PERIMETER, null, [edge]); + } + else if (constraint.point != null) + { + this.setCellStyles((source) ? mxConstants.STYLE_EXIT_X : + mxConstants.STYLE_ENTRY_X, constraint.point.x, [edge]); + this.setCellStyles((source) ? mxConstants.STYLE_EXIT_Y : + mxConstants.STYLE_ENTRY_Y, constraint.point.y, [edge]); + + // Only writes 0 since 1 is default + if (!constraint.perimeter) + { + this.setCellStyles((source) ? mxConstants.STYLE_EXIT_PERIMETER : + mxConstants.STYLE_ENTRY_PERIMETER, '0', [edge]); + } + else + { + this.setCellStyles((source) ? mxConstants.STYLE_EXIT_PERIMETER : + mxConstants.STYLE_ENTRY_PERIMETER, null, [edge]); + } + } + } + finally + { + this.model.endUpdate(); + } + } +}; + +/** + * Function: getConnectionPoint + * + * Returns the nearest point in the list of absolute points or the center + * of the opposite terminal. + * + * Parameters: + * + * vertex - <mxCellState> that represents the vertex. + * constraint - <mxConnectionConstraint> that represents the connection point + * constraint as returned by <getConnectionConstraint>. + */ +mxGraph.prototype.getConnectionPoint = function(vertex, constraint) +{ + var point = null; + + if (vertex != null) + { + var bounds = this.view.getPerimeterBounds(vertex); + var cx = new mxPoint(bounds.getCenterX(), bounds.getCenterY()); + + var direction = vertex.style[mxConstants.STYLE_DIRECTION]; + var r1 = 0; + + // Bounds need to be rotated by 90 degrees for further computation + if (direction != null) + { + if (direction == 'north') + { + r1 += 270; + } + else if (direction == 'west') + { + r1 += 180; + } + else if (direction == 'south') + { + r1 += 90; + } + + // Bounds need to be rotated by 90 degrees for further computation + if (direction == 'north' || direction == 'south') + { + bounds.x += bounds.width / 2 - bounds.height / 2; + bounds.y += bounds.height / 2 - bounds.width / 2; + var tmp = bounds.width; + bounds.width = bounds.height; + bounds.height = tmp; + } + } + + if (constraint.point != null) + { + var sx = 1; + var sy = 1; + var dx = 0; + var dy = 0; + + // LATER: Add flipping support for image shapes + if (vertex.shape instanceof mxStencilShape) + { + var flipH = vertex.style[mxConstants.STYLE_STENCIL_FLIPH]; + var flipV = vertex.style[mxConstants.STYLE_STENCIL_FLIPV]; + + if (direction == 'north' || direction == 'south') + { + var tmp = flipH; + flipH = flipV; + flipV = tmp; + } + + if (flipH) + { + sx = -1; + dx = -bounds.width; + } + + if (flipV) + { + sy = -1; + dy = -bounds.height ; + } + } + + point = new mxPoint(bounds.x + constraint.point.x * bounds.width * sx - dx, + bounds.y + constraint.point.y * bounds.height * sy - dy); + } + + // Rotation for direction before projection on perimeter + var r2 = vertex.style[mxConstants.STYLE_ROTATION] || 0; + + if (constraint.perimeter) + { + if (r1 != 0 && point != null) + { + // Only 90 degrees steps possible here so no trig needed + var cos = 0; + var sin = 0; + + if (r1 == 90) + { + sin = 1; + } + else if (r1 == 180) + { + cos = -1; + } + else if (r2 == 270) + { + sin = -1; + } + + point = mxUtils.getRotatedPoint(point, cos, sin, cx); + } + + if (point != null && constraint.perimeter) + { + point = this.view.getPerimeterPoint(vertex, point, false); + } + } + else + { + r2 += r1; + } + + // Generic rotation after projection on perimeter + if (r2 != 0 && point != null) + { + var rad = mxUtils.toRadians(r2); + var cos = Math.cos(rad); + var sin = Math.sin(rad); + + point = mxUtils.getRotatedPoint(point, cos, sin, cx); + } + } + + return point; +}; + +/** + * Function: connectCell + * + * Connects the specified end of the given edge to the given terminal + * using <cellConnected> and fires <mxEvent.CONNECT_CELL> while the + * transaction is in progress. Returns the updated edge. + * + * Parameters: + * + * edge - <mxCell> whose terminal should be updated. + * terminal - <mxCell> that represents the new terminal to be used. + * source - Boolean indicating if the new terminal is the source or target. + * constraint - Optional <mxConnectionConstraint> to be used for this + * connection. + */ +mxGraph.prototype.connectCell = function(edge, terminal, source, constraint) +{ + this.model.beginUpdate(); + try + { + var previous = this.model.getTerminal(edge, source); + this.cellConnected(edge, terminal, source, constraint); + this.fireEvent(new mxEventObject(mxEvent.CONNECT_CELL, + 'edge', edge, 'terminal', terminal, 'source', source, + 'previous', previous)); + } + finally + { + this.model.endUpdate(); + } + + return edge; +}; + +/** + * Function: cellConnected + * + * Sets the new terminal for the given edge and resets the edge points if + * <resetEdgesOnConnect> is true. This method fires + * <mxEvent.CELL_CONNECTED> while the transaction is in progress. + * + * Parameters: + * + * edge - <mxCell> whose terminal should be updated. + * terminal - <mxCell> that represents the new terminal to be used. + * source - Boolean indicating if the new terminal is the source or target. + * constraint - <mxConnectionConstraint> to be used for this connection. + */ +mxGraph.prototype.cellConnected = function(edge, terminal, source, constraint) +{ + if (edge != null) + { + this.model.beginUpdate(); + try + { + var previous = this.model.getTerminal(edge, source); + + // Updates the constraint + this.setConnectionConstraint(edge, terminal, source, constraint); + + // Checks if the new terminal is a port, uses the ID of the port in the + // style and the parent of the port as the actual terminal of the edge. + if (this.isPortsEnabled()) + { + var id = null; + + if (this.isPort(terminal)) + { + id = terminal.getId(); + terminal = this.getTerminalForPort(terminal, source); + } + + // Sets or resets all previous information for connecting to a child port + var key = (source) ? mxConstants.STYLE_SOURCE_PORT : + mxConstants.STYLE_TARGET_PORT; + this.setCellStyles(key, id, [edge]); + } + + this.model.setTerminal(edge, terminal, source); + + if (this.resetEdgesOnConnect) + { + this.resetEdge(edge); + } + + this.fireEvent(new mxEventObject(mxEvent.CELL_CONNECTED, + 'edge', edge, 'terminal', terminal, 'source', source, + 'previous', previous)); + } + finally + { + this.model.endUpdate(); + } + } +}; + +/** + * Function: disconnectGraph + * + * Disconnects the given edges from the terminals which are not in the + * given array. + * + * Parameters: + * + * cells - Array of <mxCells> to be disconnected. + */ +mxGraph.prototype.disconnectGraph = function(cells) +{ + if (cells != null) + { + this.model.beginUpdate(); + try + { + var scale = this.view.scale; + var tr = this.view.translate; + + // Prepares a hashtable for faster cell lookups + var hash = new Object(); + + for (var i = 0; i < cells.length; i++) + { + var id = mxCellPath.create(cells[i]); + hash[id] = cells[i]; + } + + for (var i = 0; i < cells.length; i++) + { + if (this.model.isEdge(cells[i])) + { + var geo = this.model.getGeometry(cells[i]); + + if (geo != null) + { + var state = this.view.getState(cells[i]); + var pstate = this.view.getState( + this.model.getParent(cells[i])); + + if (state != null && + pstate != null) + { + geo = geo.clone(); + + var dx = -pstate.origin.x; + var dy = -pstate.origin.y; + var pts = state.absolutePoints; + + var src = this.model.getTerminal(cells[i], true); + + if (src != null && this.isCellDisconnectable(cells[i], src, true)) + { + var srcId = mxCellPath.create(src); + + while (src != null && hash[srcId] == null) + { + src = this.model.getParent(src); + srcId = mxCellPath.create(src); + } + + if (src == null) + { + geo.setTerminalPoint( + new mxPoint(pts[0].x / scale - tr.x + dx, + pts[0].y / scale - tr.y + dy), true); + this.model.setTerminal(cells[i], null, true); + } + } + + var trg = this.model.getTerminal(cells[i], false); + + if (trg != null && this.isCellDisconnectable(cells[i], trg, false)) + { + var trgId = mxCellPath.create(trg); + + while (trg != null && hash[trgId] == null) + { + trg = this.model.getParent(trg); + trgId = mxCellPath.create(trg); + } + + if (trg == null) + { + var n = pts.length - 1; + geo.setTerminalPoint( + new mxPoint(pts[n].x / scale - tr.x + dx, + pts[n].y / scale - tr.y + dy), false); + this.model.setTerminal(cells[i], null, false); + } + } + + this.model.setGeometry(cells[i], geo); + } + } + } + } + } + finally + { + this.model.endUpdate(); + } + } +}; + +/** + * Group: Drilldown + */ + +/** + * Function: getCurrentRoot + * + * Returns the current root of the displayed cell hierarchy. This is a + * shortcut to <mxGraphView.currentRoot> in <view>. + */ + mxGraph.prototype.getCurrentRoot = function() + { + return this.view.currentRoot; + }; + + /** + * Function: getTranslateForRoot + * + * Returns the translation to be used if the given cell is the root cell as + * an <mxPoint>. This implementation returns null. + * + * Example: + * + * To keep the children at their absolute position while stepping into groups, + * this function can be overridden as follows. + * + * (code) + * var offset = new mxPoint(0, 0); + * + * while (cell != null) + * { + * var geo = this.model.getGeometry(cell); + * + * if (geo != null) + * { + * offset.x -= geo.x; + * offset.y -= geo.y; + * } + * + * cell = this.model.getParent(cell); + * } + * + * return offset; + * (end) + * + * Parameters: + * + * cell - <mxCell> that represents the root. + */ +mxGraph.prototype.getTranslateForRoot = function(cell) +{ + return null; +}; + +/** + * Function: isPort + * + * Returns true if the given cell is a "port", that is, when connecting to + * it, the cell returned by getTerminalForPort should be used as the + * terminal and the port should be referenced by the ID in either the + * mxConstants.STYLE_SOURCE_PORT or the or the + * mxConstants.STYLE_TARGET_PORT. Note that a port should not be movable. + * This implementation always returns false. + * + * A typical implementation is the following: + * + * (code) + * graph.isPort = function(cell) + * { + * var geo = this.getCellGeometry(cell); + * + * return (geo != null) ? geo.relative : false; + * }; + * (end) + * + * Parameters: + * + * cell - <mxCell> that represents the port. + */ +mxGraph.prototype.isPort = function(cell) +{ + return false; +}; + +/** + * Function: getTerminalForPort + * + * Returns the terminal to be used for a given port. This implementation + * always returns the parent cell. + * + * Parameters: + * + * cell - <mxCell> that represents the port. + * source - If the cell is the source or target port. + */ +mxGraph.prototype.getTerminalForPort = function(cell, source) +{ + return this.model.getParent(cell); +}; + +/** + * Function: getChildOffsetForCell + * + * Returns the offset to be used for the cells inside the given cell. The + * root and layer cells may be identified using <mxGraphModel.isRoot> and + * <mxGraphModel.isLayer>. For all other current roots, the + * <mxGraphView.currentRoot> field points to the respective cell, so that + * the following holds: cell == this.view.currentRoot. This implementation + * returns null. + * + * Parameters: + * + * cell - <mxCell> whose offset should be returned. + */ +mxGraph.prototype.getChildOffsetForCell = function(cell) +{ + return null; +}; + +/** + * Function: enterGroup + * + * Uses the given cell as the root of the displayed cell hierarchy. If no + * cell is specified then the selection cell is used. The cell is only used + * if <isValidRoot> returns true. + * + * Parameters: + * + * cell - Optional <mxCell> to be used as the new root. Default is the + * selection cell. + */ +mxGraph.prototype.enterGroup = function(cell) +{ + cell = cell || this.getSelectionCell(); + + if (cell != null && this.isValidRoot(cell)) + { + this.view.setCurrentRoot(cell); + this.clearSelection(); + } +}; + +/** + * Function: exitGroup + * + * Changes the current root to the next valid root in the displayed cell + * hierarchy. + */ +mxGraph.prototype.exitGroup = function() +{ + var root = this.model.getRoot(); + var current = this.getCurrentRoot(); + + if (current != null) + { + var next = this.model.getParent(current); + + // Finds the next valid root in the hierarchy + while (next != root && !this.isValidRoot(next) && + this.model.getParent(next) != root) + { + next = this.model.getParent(next); + } + + // Clears the current root if the new root is + // the model's root or one of the layers. + if (next == root || this.model.getParent(next) == root) + { + this.view.setCurrentRoot(null); + } + else + { + this.view.setCurrentRoot(next); + } + + var state = this.view.getState(current); + + // Selects the previous root in the graph + if (state != null) + { + this.setSelectionCell(current); + } + } +}; + +/** + * Function: home + * + * Uses the root of the model as the root of the displayed cell hierarchy + * and selects the previous root. + */ +mxGraph.prototype.home = function() +{ + var current = this.getCurrentRoot(); + + if (current != null) + { + this.view.setCurrentRoot(null); + var state = this.view.getState(current); + + if (state != null) + { + this.setSelectionCell(current); + } + } +}; + +/** + * Function: isValidRoot + * + * Returns true if the given cell is a valid root for the cell display + * hierarchy. This implementation returns true for all non-null values. + * + * Parameters: + * + * cell - <mxCell> which should be checked as a possible root. + */ +mxGraph.prototype.isValidRoot = function(cell) +{ + return (cell != null); +}; + +/** + * Group: Graph display + */ + +/** + * Function: getGraphBounds + * + * Returns the bounds of the visible graph. Shortcut to + * <mxGraphView.getGraphBounds>. See also: <getBoundingBoxFromGeometry>. + */ + mxGraph.prototype.getGraphBounds = function() + { + return this.view.getGraphBounds(); + }; + +/** + * Function: getCellBounds + * + * Returns the scaled, translated bounds for the given cell. See + * <mxGraphView.getBounds> for arrays. + * + * Parameters: + * + * cell - <mxCell> whose bounds should be returned. + * includeEdge - Optional boolean that specifies if the bounds of + * the connected edges should be included. Default is false. + * includeDescendants - Optional boolean that specifies if the bounds + * of all descendants should be included. Default is false. + */ +mxGraph.prototype.getCellBounds = function(cell, includeEdges, includeDescendants) +{ + var cells = [cell]; + + // Includes all connected edges + if (includeEdges) + { + cells = cells.concat(this.model.getEdges(cell)); + } + + var result = this.view.getBounds(cells); + + // Recursively includes the bounds of the children + if (includeDescendants) + { + var childCount = this.model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + var tmp = this.getCellBounds(this.model.getChildAt(cell, i), + includeEdges, true); + + if (result != null) + { + result.add(tmp); + } + else + { + result = tmp; + } + } + } + + return result; +}; + +/** + * Function: getBoundingBoxFromGeometry + * + * Returns the bounding box for the geometries of the vertices in the + * given array of cells. This can be used to find the graph bounds during + * a layout operation (ie. before the last endUpdate) as follows: + * + * (code) + * var cells = graph.getChildCells(graph.getDefaultParent(), true, true); + * var bounds = graph.getBoundingBoxFromGeometry(cells, true); + * (end) + * + * This can then be used to move cells to the origin: + * + * (code) + * if (bounds.x < 0 || bounds.y < 0) + * { + * graph.moveCells(cells, -Math.min(bounds.x, 0), -Math.min(bounds.y, 0)) + * } + * (end) + * + * Or to translate the graph view: + * + * (code) + * if (bounds.x < 0 || bounds.y < 0) + * { + * graph.view.setTranslate(-Math.min(bounds.x, 0), -Math.min(bounds.y, 0)); + * } + * (end) + * + * Parameters: + * + * cells - Array of <mxCells> whose bounds should be returned. + * includeEdges - Specifies if edge bounds should be included by computing + * the bounding box for all points its geometry. Default is false. + */ +mxGraph.prototype.getBoundingBoxFromGeometry = function(cells, includeEdges) +{ + includeEdges = (includeEdges != null) ? includeEdges : false; + var result = null; + + if (cells != null) + { + for (var i = 0; i < cells.length; i++) + { + if (includeEdges || this.model.isVertex(cells[i])) + { + // Computes the bounding box for the points in the geometry + var geo = this.getCellGeometry(cells[i]); + + if (geo != null) + { + var pts = geo.points; + + if (pts != null && pts.length > 0) + { + var tmp = new mxRectangle(pts[0].x, pts[0].y, 0, 0); + var addPoint = function(pt) + { + if (pt != null) + { + tmp.add(new mxRectangle(pt.x, pt.y, 0, 0)); + } + }; + + for (var j = 1; j < pts.length; j++) + { + addPoint(pts[j]); + } + + addPoint(geo.getTerminalPoint(true)); + addPoint(geo.getTerminalPoint(false)); + } + + if (result == null) + { + result = new mxRectangle(geo.x, geo.y, geo.width, geo.height); + } + else + { + result.add(geo); + } + } + } + } + } + + return result; +}; + +/** + * Function: refresh + * + * Clears all cell states or the states for the hierarchy starting at the + * given cell and validates the graph. This fires a refresh event as the + * last step. + * + * Parameters: + * + * cell - Optional <mxCell> for which the cell states should be cleared. + */ +mxGraph.prototype.refresh = function(cell) +{ + this.view.clear(cell, cell == null); + this.view.validate(); + this.sizeDidChange(); + this.fireEvent(new mxEventObject(mxEvent.REFRESH)); +}; + +/** + * Function: snap + * + * Snaps the given numeric value to the grid if <gridEnabled> is true. + * + * Parameters: + * + * value - Numeric value to be snapped to the grid. + */ +mxGraph.prototype.snap = function(value) +{ + if (this.gridEnabled) + { + value = Math.round(value / this.gridSize ) * this.gridSize; + } + + return value; +}; + +/** + * Function: panGraph + * + * Shifts the graph display by the given amount. This is used to preview + * panning operations, use <mxGraphView.setTranslate> to set a persistent + * translation of the view. Fires <mxEvent.PAN>. + * + * Parameters: + * + * dx - Amount to shift the graph along the x-axis. + * dy - Amount to shift the graph along the y-axis. + */ +mxGraph.prototype.panGraph = function(dx, dy) +{ + if (this.useScrollbarsForPanning && mxUtils.hasScrollbars(this.container)) + { + this.container.scrollLeft = -dx; + this.container.scrollTop = -dy; + } + else + { + var canvas = this.view.getCanvas(); + + if (this.dialect == mxConstants.DIALECT_SVG) + { + // Puts everything inside the container in a DIV so that it + // can be moved without changing the state of the container + if (dx == 0 && dy == 0) + { + // Workaround for ignored removeAttribute on SVG element in IE9 standards + if (mxClient.IS_IE) + { + canvas.setAttribute('transform', 'translate('+ dx + ',' + dy + ')'); + } + else + { + canvas.removeAttribute('transform'); + } + + if (this.shiftPreview1 != null) + { + var child = this.shiftPreview1.firstChild; + + while (child != null) + { + var next = child.nextSibling; + this.container.appendChild(child); + child = next; + } + + this.shiftPreview1.parentNode.removeChild(this.shiftPreview1); + this.shiftPreview1 = null; + + this.container.appendChild(canvas.parentNode); + + child = this.shiftPreview2.firstChild; + + while (child != null) + { + var next = child.nextSibling; + this.container.appendChild(child); + child = next; + } + + this.shiftPreview2.parentNode.removeChild(this.shiftPreview2); + this.shiftPreview2 = null; + } + } + else + { + canvas.setAttribute('transform', 'translate('+ dx + ',' + dy + ')'); + + if (this.shiftPreview1 == null) + { + // Needs two divs for stuff before and after the SVG element + this.shiftPreview1 = document.createElement('div'); + this.shiftPreview1.style.position = 'absolute'; + this.shiftPreview1.style.overflow = 'visible'; + + this.shiftPreview2 = document.createElement('div'); + this.shiftPreview2.style.position = 'absolute'; + this.shiftPreview2.style.overflow = 'visible'; + + var current = this.shiftPreview1; + var child = this.container.firstChild; + + while (child != null) + { + var next = child.nextSibling; + + // SVG element is moved via transform attribute + if (child != canvas.parentNode) + { + current.appendChild(child); + } + else + { + current = this.shiftPreview2; + } + + child = next; + } + + this.container.insertBefore(this.shiftPreview1, canvas.parentNode); + this.container.appendChild(this.shiftPreview2); + } + + this.shiftPreview1.style.left = dx + 'px'; + this.shiftPreview1.style.top = dy + 'px'; + this.shiftPreview2.style.left = dx + 'px'; + this.shiftPreview2.style.top = dy + 'px'; + } + } + else + { + canvas.style.left = dx + 'px'; + canvas.style.top = dy + 'px'; + } + + this.panDx = dx; + this.panDy = dy; + + this.fireEvent(new mxEventObject(mxEvent.PAN)); + } +}; + +/** + * Function: zoomIn + * + * Zooms into the graph by <zoomFactor>. + */ +mxGraph.prototype.zoomIn = function() +{ + this.zoom(this.zoomFactor); +}; + +/** + * Function: zoomOut + * + * Zooms out of the graph by <zoomFactor>. + */ +mxGraph.prototype.zoomOut = function() +{ + this.zoom(1 / this.zoomFactor); +}; + +/** + * Function: zoomActual + * + * Resets the zoom and panning in the view. + */ +mxGraph.prototype.zoomActual = function() +{ + if (this.view.scale == 1) + { + this.view.setTranslate(0, 0); + } + else + { + this.view.translate.x = 0; + this.view.translate.y = 0; + + this.view.setScale(1); + } +}; + +/** + * Function: zoomTo + * + * Zooms the graph to the given scale with an optional boolean center + * argument, which is passd to <zoom>. + */ +mxGraph.prototype.zoomTo = function(scale, center) +{ + this.zoom(scale / this.view.scale, center); +}; + +/** + * Function: zoom + * + * Zooms the graph using the given factor. Center is an optional boolean + * argument that keeps the graph scrolled to the center. If the center argument + * is omitted, then <centerZoom> will be used as its value. + */ +mxGraph.prototype.zoom = function(factor, center) +{ + center = (center != null) ? center : this.centerZoom; + var scale = this.view.scale * factor; + var state = this.view.getState(this.getSelectionCell()); + + if (this.keepSelectionVisibleOnZoom && state != null) + { + var rect = new mxRectangle( + state.x * factor, + state.y * factor, + state.width * factor, + state.height * factor); + + // Refreshes the display only once if a + // scroll is carried out + this.view.scale = scale; + + if (!this.scrollRectToVisible(rect)) + { + this.view.revalidate(); + + // Forces an event to be fired but does not revalidate again + this.view.setScale(scale); + } + } + else if (center && !mxUtils.hasScrollbars(this.container)) + { + var dx = this.container.offsetWidth; + var dy = this.container.offsetHeight; + + if (factor > 1) + { + var f = (factor -1) / (scale * 2); + dx *= -f; + dy *= -f; + } + else + { + var f = (1/factor -1) / (this.view.scale * 2); + dx *= f; + dy *= f; + } + + this.view.scaleAndTranslate(scale, + this.view.translate.x + dx, + this.view.translate.y + dy); + } + else + { + this.view.setScale(scale); + + if (mxUtils.hasScrollbars(this.container)) + { + var dx = 0; + var dy = 0; + + if (center) + { + dx = this.container.offsetWidth * (factor - 1) / 2; + dy = this.container.offsetHeight * (factor - 1) / 2; + } + + this.container.scrollLeft = Math.round(this.container.scrollLeft * factor + dx); + this.container.scrollTop = Math.round(this.container.scrollTop * factor + dy); + } + } +}; + +/** + * Function: zoomToRect + * + * Zooms the graph to the specified rectangle. If the rectangle does not have same aspect + * ratio as the display container, it is increased in the smaller relative dimension only + * until the aspect match. The original rectangle is centralised within this expanded one. + * + * Note that the input rectangular must be un-scaled and un-translated. + * + * Parameters: + * + * rect - The un-scaled and un-translated rectangluar region that should be just visible + * after the operation + */ +mxGraph.prototype.zoomToRect = function(rect) +{ + var scaleX = this.container.clientWidth / rect.width; + var scaleY = this.container.clientHeight / rect.height; + var aspectFactor = scaleX / scaleY; + + // Remove any overlap of the rect outside the client area + rect.x = Math.max(0, rect.x); + rect.y = Math.max(0, rect.y); + var rectRight = Math.min(this.container.scrollWidth, rect.x + rect.width); + var rectBottom = Math.min(this.container.scrollHeight, rect.y + rect.height); + rect.width = rectRight - rect.x; + rect.height = rectBottom - rect.y; + + // The selection area has to be increased to the same aspect + // ratio as the container, centred around the centre point of the + // original rect passed in. + if (aspectFactor < 1.0) + { + // Height needs increasing + var newHeight = rect.height / aspectFactor; + var deltaHeightBuffer = (newHeight - rect.height) / 2.0; + rect.height = newHeight; + + // Assign up to half the buffer to the upper part of the rect, not crossing 0 + // put the rest on the bottom + var upperBuffer = Math.min(rect.y , deltaHeightBuffer); + rect.y = rect.y - upperBuffer; + + // Check if the bottom has extended too far + rectBottom = Math.min(this.container.scrollHeight, rect.y + rect.height); + rect.height = rectBottom - rect.y; + } + else + { + // Width needs increasing + var newWidth = rect.width * aspectFactor; + var deltaWidthBuffer = (newWidth - rect.width) / 2.0; + rect.width = newWidth; + + // Assign up to half the buffer to the upper part of the rect, not crossing 0 + // put the rest on the bottom + var leftBuffer = Math.min(rect.x , deltaWidthBuffer); + rect.x = rect.x - leftBuffer; + + // Check if the right hand side has extended too far + rectRight = Math.min(this.container.scrollWidth, rect.x + rect.width); + rect.width = rectRight - rect.x; + } + + var scale = this.container.clientWidth / rect.width; + + if (!mxUtils.hasScrollbars(this.container)) + { + this.view.scaleAndTranslate(scale, -rect.x, -rect.y); + } + else + { + this.view.setScale(scale); + this.container.scrollLeft = Math.round(rect.x * scale); + this.container.scrollTop = Math.round(rect.y * scale); + } +}; + +/** + * Function: fit + * + * Scales the graph such that the complete diagram fits into <container> and + * returns the current scale in the view. To fit an initial graph prior to + * rendering, set <mxGraphView.rendering> to false prior to changing the model + * and execute the following after changing the model. + * + * (code) + * graph.fit(); + * graph.view.rendering = true; + * graph.refresh(); + * (end) + * + * Parameters: + * + * border - Optional number that specifies the border. Default is 0. + * keepOrigin - Optional boolean that specifies if the translate should be + * changed. Default is false. + */ +mxGraph.prototype.fit = function(border, keepOrigin) +{ + if (this.container != null) + { + border = (border != null) ? border : 0; + keepOrigin = (keepOrigin != null) ? keepOrigin : false; + + var w1 = this.container.clientWidth; + var h1 = this.container.clientHeight; + + var bounds = this.view.getGraphBounds(); + + if (keepOrigin && bounds.x != null && bounds.y != null) + { + bounds.width += bounds.x; + bounds.height += bounds.y; + bounds.x = 0; + bounds.y = 0; + } + + var s = this.view.scale; + var w2 = bounds.width / s; + var h2 = bounds.height / s; + + // Fits to the size of the background image if required + if (this.backgroundImage != null) + { + w2 = Math.max(w2, this.backgroundImage.width - bounds.x / s); + h2 = Math.max(h2, this.backgroundImage.height - bounds.y / s); + } + + var b = (keepOrigin) ? border : 2 * border; + var s2 = Math.floor(Math.min(w1 / (w2 + b), h1 / (h2 + b)) * 100) / 100; + + if (this.minFitScale != null) + { + s2 = Math.max(s2, this.minFitScale); + } + + if (this.maxFitScale != null) + { + s2 = Math.min(s2, this.maxFitScale); + } + + if (!keepOrigin) + { + if (!mxUtils.hasScrollbars(this.container)) + { + var x0 = (bounds.x != null) ? Math.floor(this.view.translate.x - bounds.x / s + border + 1) : border; + var y0 = (bounds.y != null) ? Math.floor(this.view.translate.y - bounds.y / s + border + 1) : border; + + this.view.scaleAndTranslate(s2, x0, y0); + } + else + { + this.view.setScale(s2); + + if (bounds.x != null) + { + this.container.scrollLeft = Math.round(bounds.x / s) * s2 - border - + Math.max(0, (this.container.clientWidth - w2 * s2) / 2); + } + + if (bounds.y != null) + { + this.container.scrollTop = Math.round(bounds.y / s) * s2 - border - + Math.max(0, (this.container.clientHeight - h2 * s2) / 2); + } + } + } + else if (this.view.scale != s2) + { + this.view.setScale(s2); + } + } + + return this.view.scale; +}; + +/** + * Function: scrollCellToVisible + * + * Pans the graph so that it shows the given cell. Optionally the cell may + * be centered in the container. + * + * To center a given graph if the <container> has no scrollbars, use the following code. + * + * [code] + * var bounds = graph.getGraphBounds(); + * graph.view.setTranslate(-bounds.x - (bounds.width - container.clientWidth) / 2, + * -bounds.y - (bounds.height - container.clientHeight) / 2); + * [/code] + * + * Parameters: + * + * cell - <mxCell> to be made visible. + * center - Optional boolean flag. Default is false. + */ +mxGraph.prototype.scrollCellToVisible = function(cell, center) +{ + var x = -this.view.translate.x; + var y = -this.view.translate.y; + + var state = this.view.getState(cell); + + if (state != null) + { + var bounds = new mxRectangle(x + state.x, y + state.y, state.width, + state.height); + + if (center && this.container != null) + { + var w = this.container.clientWidth; + var h = this.container.clientHeight; + + bounds.x = bounds.getCenterX() - w / 2; + bounds.width = w; + bounds.y = bounds.getCenterY() - h / 2; + bounds.height = h; + } + + if (this.scrollRectToVisible(bounds)) + { + // Triggers an update via the view's event source + this.view.setTranslate(this.view.translate.x, this.view.translate.y); + } + } +}; + +/** + * Function: scrollRectToVisible + * + * Pans the graph so that it shows the given rectangle. + * + * Parameters: + * + * rect - <mxRectangle> to be made visible. + */ +mxGraph.prototype.scrollRectToVisible = function(rect) +{ + var isChanged = false; + + if (rect != null) + { + var w = this.container.offsetWidth; + var h = this.container.offsetHeight; + + var widthLimit = Math.min(w, rect.width); + var heightLimit = Math.min(h, rect.height); + + if (mxUtils.hasScrollbars(this.container)) + { + var c = this.container; + rect.x += this.view.translate.x; + rect.y += this.view.translate.y; + var dx = c.scrollLeft - rect.x; + var ddx = Math.max(dx - c.scrollLeft, 0); + + if (dx > 0) + { + c.scrollLeft -= dx + 2; + } + else + { + dx = rect.x + widthLimit - c.scrollLeft - c.clientWidth; + + if (dx > 0) + { + c.scrollLeft += dx + 2; + } + } + + var dy = c.scrollTop - rect.y; + var ddy = Math.max(0, dy - c.scrollTop); + + if (dy > 0) + { + c.scrollTop -= dy + 2; + } + else + { + dy = rect.y + heightLimit - c.scrollTop - c.clientHeight; + + if (dy > 0) + { + c.scrollTop += dy + 2; + } + } + + if (!this.useScrollbarsForPanning && (ddx != 0 || ddy != 0)) + { + this.view.setTranslate(ddx, ddy); + } + } + else + { + var x = -this.view.translate.x; + var y = -this.view.translate.y; + + var s = this.view.scale; + + if (rect.x + widthLimit > x + w) + { + this.view.translate.x -= (rect.x + widthLimit - w - x) / s; + isChanged = true; + } + + if (rect.y + heightLimit > y + h) + { + this.view.translate.y -= (rect.y + heightLimit - h - y) / s; + isChanged = true; + } + + if (rect.x < x) + { + this.view.translate.x += (x - rect.x) / s; + isChanged = true; + } + + if (rect.y < y) + { + this.view.translate.y += (y - rect.y) / s; + isChanged = true; + } + + if (isChanged) + { + this.view.refresh(); + + // Repaints selection marker (ticket 18) + if (this.selectionCellsHandler != null) + { + this.selectionCellsHandler.refresh(); + } + } + } + } + + return isChanged; +}; + +/** + * Function: getCellGeometry + * + * Returns the <mxGeometry> for the given cell. This implementation uses + * <mxGraphModel.getGeometry>. Subclasses can override this to implement + * specific geometries for cells in only one graph, that is, it can return + * geometries that depend on the current state of the view. + * + * Parameters: + * + * cell - <mxCell> whose geometry should be returned. + */ +mxGraph.prototype.getCellGeometry = function(cell) +{ + return this.model.getGeometry(cell); +}; + +/** + * Function: isCellVisible + * + * Returns true if the given cell is visible in this graph. This + * implementation uses <mxGraphModel.isVisible>. Subclassers can override + * this to implement specific visibility for cells in only one graph, that + * is, without affecting the visible state of the cell. + * + * When using dynamic filter expressions for cell visibility, then the + * graph should be revalidated after the filter expression has changed. + * + * Parameters: + * + * cell - <mxCell> whose visible state should be returned. + */ +mxGraph.prototype.isCellVisible = function(cell) +{ + return this.model.isVisible(cell); +}; + +/** + * Function: isCellCollapsed + * + * Returns true if the given cell is collapsed in this graph. This + * implementation uses <mxGraphModel.isCollapsed>. Subclassers can override + * this to implement specific collapsed states for cells in only one graph, + * that is, without affecting the collapsed state of the cell. + * + * When using dynamic filter expressions for the collapsed state, then the + * graph should be revalidated after the filter expression has changed. + * + * Parameters: + * + * cell - <mxCell> whose collapsed state should be returned. + */ +mxGraph.prototype.isCellCollapsed = function(cell) +{ + return this.model.isCollapsed(cell); +}; + +/** + * Function: isCellConnectable + * + * Returns true if the given cell is connectable in this graph. This + * implementation uses <mxGraphModel.isConnectable>. Subclassers can override + * this to implement specific connectable states for cells in only one graph, + * that is, without affecting the connectable state of the cell in the model. + * + * Parameters: + * + * cell - <mxCell> whose connectable state should be returned. + */ +mxGraph.prototype.isCellConnectable = function(cell) +{ + return this.model.isConnectable(cell); +}; + +/** + * Function: isOrthogonal + * + * Returns true if perimeter points should be computed such that the + * resulting edge has only horizontal or vertical segments. + * + * Parameters: + * + * edge - <mxCellState> that represents the edge. + */ +mxGraph.prototype.isOrthogonal = function(edge) +{ + var orthogonal = edge.style[mxConstants.STYLE_ORTHOGONAL]; + + if (orthogonal != null) + { + return orthogonal; + } + + var tmp = this.view.getEdgeStyle(edge); + + return tmp == mxEdgeStyle.SegmentConnector || + tmp == mxEdgeStyle.ElbowConnector || + tmp == mxEdgeStyle.SideToSide || + tmp == mxEdgeStyle.TopToBottom || + tmp == mxEdgeStyle.EntityRelation || + tmp == mxEdgeStyle.OrthConnector; +}; + +/** + * Function: isLoop + * + * Returns true if the given cell state is a loop. + * + * Parameters: + * + * state - <mxCellState> that represents a potential loop. + */ +mxGraph.prototype.isLoop = function(state) +{ + var src = state.getVisibleTerminalState(true); + var trg = state.getVisibleTerminalState(false); + + return (src != null && src == trg); +}; + +/** + * Function: isCloneEvent + * + * Returns true if the given event is a clone event. This implementation + * returns true if control is pressed. + */ +mxGraph.prototype.isCloneEvent = function(evt) +{ + return mxEvent.isControlDown(evt); +}; + +/** + * Function: isToggleEvent + * + * Returns true if the given event is a toggle event. This implementation + * returns true if the meta key (Cmd) is pressed on Macs or if control is + * pressed on any other platform. + */ +mxGraph.prototype.isToggleEvent = function(evt) +{ + return (mxClient.IS_MAC) ? mxEvent.isMetaDown(evt) : mxEvent.isControlDown(evt); +}; + +/** + * Function: isGridEnabledEvent + * + * Returns true if the given mouse event should be aligned to the grid. + */ +mxGraph.prototype.isGridEnabledEvent = function(evt) +{ + return evt != null && !mxEvent.isAltDown(evt); +}; + +/** + * Function: isConstrainedEvent + * + * Returns true if the given mouse event should be aligned to the grid. + */ +mxGraph.prototype.isConstrainedEvent = function(evt) +{ + return mxEvent.isShiftDown(evt); +}; + +/** + * Function: isForceMarqueeEvent + * + * Returns true if the given event forces marquee selection. This implementation + * returns true if alt is pressed. + */ +mxGraph.prototype.isForceMarqueeEvent = function(evt) +{ + return mxEvent.isAltDown(evt); +}; + +/** + * Group: Validation + */ + +/** + * Function: validationAlert + * + * Displays the given validation error in a dialog. This implementation uses + * mxUtils.alert. + */ +mxGraph.prototype.validationAlert = function(message) +{ + mxUtils.alert(message); +}; + +/** + * Function: isEdgeValid + * + * Checks if the return value of <getEdgeValidationError> for the given + * arguments is null. + * + * Parameters: + * + * edge - <mxCell> that represents the edge to validate. + * source - <mxCell> that represents the source terminal. + * target - <mxCell> that represents the target terminal. + */ +mxGraph.prototype.isEdgeValid = function(edge, source, target) +{ + return this.getEdgeValidationError(edge, source, target) == null; +}; + +/** + * Function: getEdgeValidationError + * + * Returns the validation error message to be displayed when inserting or + * changing an edges' connectivity. A return value of null means the edge + * is valid, a return value of '' means it's not valid, but do not display + * an error message. Any other (non-empty) string returned from this method + * is displayed as an error message when trying to connect an edge to a + * source and target. This implementation uses the <multiplicities>, and + * checks <multigraph>, <allowDanglingEdges> and <allowLoops> to generate + * validation errors. + * + * For extending this method with specific checks for source/target cells, + * the method can be extended as follows. Returning an empty string means + * the edge is invalid with no error message, a non-null string specifies + * the error message, and null means the edge is valid. + * + * (code) + * graph.getEdgeValidationError = function(edge, source, target) + * { + * if (source != null && target != null && + * this.model.getValue(source) != null && + * this.model.getValue(target) != null) + * { + * if (target is not valid for source) + * { + * return 'Invalid Target'; + * } + * } + * + * // "Supercall" + * return mxGraph.prototype.getEdgeValidationError.apply(this, arguments); + * } + * (end) + * + * Parameters: + * + * edge - <mxCell> that represents the edge to validate. + * source - <mxCell> that represents the source terminal. + * target - <mxCell> that represents the target terminal. + */ +mxGraph.prototype.getEdgeValidationError = function(edge, source, target) +{ + if (edge != null && !this.isAllowDanglingEdges() && (source == null || target == null)) + { + return ''; + } + + if (edge != null && this.model.getTerminal(edge, true) == null && + this.model.getTerminal(edge, false) == null) + { + return null; + } + + // Checks if we're dealing with a loop + if (!this.allowLoops && source == target && source != null) + { + return ''; + } + + // Checks if the connection is generally allowed + if (!this.isValidConnection(source, target)) + { + return ''; + } + + if (source != null && target != null) + { + var error = ''; + + // Checks if the cells are already connected + // and adds an error message if required + if (!this.multigraph) + { + var tmp = this.model.getEdgesBetween(source, target, true); + + // Checks if the source and target are not connected by another edge + if (tmp.length > 1 || (tmp.length == 1 && tmp[0] != edge)) + { + error += (mxResources.get(this.alreadyConnectedResource) || + this.alreadyConnectedResource)+'\n'; + } + } + + // Gets the number of outgoing edges from the source + // and the number of incoming edges from the target + // without counting the edge being currently changed. + var sourceOut = this.model.getDirectedEdgeCount(source, true, edge); + var targetIn = this.model.getDirectedEdgeCount(target, false, edge); + + // Checks the change against each multiplicity rule + if (this.multiplicities != null) + { + for (var i = 0; i < this.multiplicities.length; i++) + { + var err = this.multiplicities[i].check(this, edge, source, + target, sourceOut, targetIn); + + if (err != null) + { + error += err; + } + } + } + + // Validates the source and target terminals independently + var err = this.validateEdge(edge, source, target); + + if (err != null) + { + error += err; + } + + return (error.length > 0) ? error : null; + } + + return (this.allowDanglingEdges) ? null : ''; +}; + +/** + * Function: validateEdge + * + * Hook method for subclassers to return an error message for the given + * edge and terminals. This implementation returns null. + * + * Parameters: + * + * edge - <mxCell> that represents the edge to validate. + * source - <mxCell> that represents the source terminal. + * target - <mxCell> that represents the target terminal. + */ +mxGraph.prototype.validateEdge = function(edge, source, target) +{ + return null; +}; + +/** + * Function: validateGraph + * + * Validates the graph by validating each descendant of the given cell or + * the root of the model. Context is an object that contains the validation + * state for the complete validation run. The validation errors are + * attached to their cells using <setCellWarning>. This function returns true + * if no validation errors exist in the graph. + * + * Paramters: + * + * cell - Optional <mxCell> to start the validation recursion. Default is + * the graph root. + * context - Object that represents the global validation state. + */ +mxGraph.prototype.validateGraph = function(cell, context) +{ + cell = (cell != null) ? cell : this.model.getRoot(); + context = (context != null) ? context : new Object(); + + var isValid = true; + var childCount = this.model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + var tmp = this.model.getChildAt(cell, i); + var ctx = context; + + if (this.isValidRoot(tmp)) + { + ctx = new Object(); + } + + var warn = this.validateGraph(tmp, ctx); + + if (warn != null) + { + this.setCellWarning(tmp, warn.replace(/\n/g, '<br>')); + } + else + { + this.setCellWarning(tmp, null); + } + + isValid = isValid && warn == null; + } + + var warning = ''; + + // Adds error for invalid children if collapsed (children invisible) + if (this.isCellCollapsed(cell) && !isValid) + { + warning += (mxResources.get(this.containsValidationErrorsResource) || + this.containsValidationErrorsResource)+'\n'; + } + + // Checks edges and cells using the defined multiplicities + if (this.model.isEdge(cell)) + { + warning += this.getEdgeValidationError(cell, + this.model.getTerminal(cell, true), + this.model.getTerminal(cell, false)) || ''; + } + else + { + warning += this.getCellValidationError(cell) || ''; + } + + // Checks custom validation rules + var err = this.validateCell(cell, context); + + if (err != null) + { + warning += err; + } + + // Updates the display with the warning icons + // before any potential alerts are displayed. + // LATER: Move this into addCellOverlay. Redraw + // should check if overlay was added or removed. + if (this.model.getParent(cell) == null) + { + this.view.validate(); + } + + return (warning.length > 0 || !isValid) ? warning : null; +}; + +/** + * Function: getCellValidationError + * + * Checks all <multiplicities> that cannot be enforced while the graph is + * being modified, namely, all multiplicities that require a minimum of + * 1 edge. + * + * Parameters: + * + * cell - <mxCell> for which the multiplicities should be checked. + */ +mxGraph.prototype.getCellValidationError = function(cell) +{ + var outCount = this.model.getDirectedEdgeCount(cell, true); + var inCount = this.model.getDirectedEdgeCount(cell, false); + var value = this.model.getValue(cell); + var error = ''; + + if (this.multiplicities != null) + { + for (var i = 0; i < this.multiplicities.length; i++) + { + var rule = this.multiplicities[i]; + + if (rule.source && mxUtils.isNode(value, rule.type, + rule.attr, rule.value) && ((rule.max == 0 && outCount > 0) || + (rule.min == 1 && outCount == 0) || (rule.max == 1 && outCount > 1))) + { + error += rule.countError + '\n'; + } + else if (!rule.source && mxUtils.isNode(value, rule.type, + rule.attr, rule.value) && ((rule.max == 0 && inCount > 0) || + (rule.min == 1 && inCount == 0) || (rule.max == 1 && inCount > 1))) + { + error += rule.countError + '\n'; + } + } + } + + return (error.length > 0) ? error : null; +}; + +/** + * Function: validateCell + * + * Hook method for subclassers to return an error message for the given + * cell and validation context. This implementation returns null. Any HTML + * breaks will be converted to linefeeds in the calling method. + * + * Parameters: + * + * cell - <mxCell> that represents the cell to validate. + * context - Object that represents the global validation state. + */ +mxGraph.prototype.validateCell = function(cell, context) +{ + return null; +}; + +/** + * Group: Graph appearance + */ + +/** + * Function: getBackgroundImage + * + * Returns the <backgroundImage> as an <mxImage>. + */ +mxGraph.prototype.getBackgroundImage = function() +{ + return this.backgroundImage; +}; + +/** + * Function: setBackgroundImage + * + * Sets the new <backgroundImage>. + * + * Parameters: + * + * image - New <mxImage> to be used for the background. + */ +mxGraph.prototype.setBackgroundImage = function(image) +{ + this.backgroundImage = image; +}; + +/** + * Function: getFoldingImage + * + * Returns the <mxImage> used to display the collapsed state of + * the specified cell state. This returns null for all edges. + */ +mxGraph.prototype.getFoldingImage = function(state) +{ + if (state != null && this.foldingEnabled && !this.getModel().isEdge(state.cell)) + { + var tmp = this.isCellCollapsed(state.cell); + + if (this.isCellFoldable(state.cell, !tmp)) + { + return (tmp) ? this.collapsedImage : this.expandedImage; + } + } + + return null; +}; + +/** + * Function: convertValueToString + * + * Returns the textual representation for the given cell. This + * implementation returns the nodename or string-representation of the user + * object. + * + * Example: + * + * The following returns the label attribute from the cells user + * object if it is an XML node. + * + * (code) + * graph.convertValueToString = function(cell) + * { + * return cell.getAttribute('label'); + * } + * (end) + * + * See also: <cellLabelChanged>. + * + * Parameters: + * + * cell - <mxCell> whose textual representation should be returned. + */ +mxGraph.prototype.convertValueToString = function(cell) +{ + var value = this.model.getValue(cell); + + if (value != null) + { + if (mxUtils.isNode(value)) + { + return value.nodeName; + } + else if (typeof(value.toString) == 'function') + { + return value.toString(); + } + } + + return ''; +}; + +/** + * Function: getLabel + * + * Returns a string or DOM node that represents the label for the given + * cell. This implementation uses <convertValueToString> if <labelsVisible> + * is true. Otherwise it returns an empty string. + * + * To truncate label to match the size of the cell, the following code + * can be used. + * + * (code) + * graph.getLabel = function(cell) + * { + * var label = mxGraph.prototype.getLabel.apply(this, arguments); + * + * if (label != null && this.model.isVertex(cell)) + * { + * var geo = this.getCellGeometry(cell); + * + * if (geo != null) + * { + * var max = parseInt(geo.width / 8); + * + * if (label.length > max) + * { + * label = label.substring(0, max)+'...'; + * } + * } + * } + * return mxUtils.htmlEntities(label); + * } + * (end) + * + * A resize listener is needed in the graph to force a repaint of the label + * after a resize. + * + * (code) + * graph.addListener(mxEvent.RESIZE_CELLS, function(sender, evt) + * { + * var cells = evt.getProperty('cells'); + * + * for (var i = 0; i < cells.length; i++) + * { + * this.view.removeState(cells[i]); + * } + * }); + * (end) + * + * Parameters: + * + * cell - <mxCell> whose label should be returned. + */ +mxGraph.prototype.getLabel = function(cell) +{ + var result = ''; + + if (this.labelsVisible && cell != null) + { + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + if (!mxUtils.getValue(style, mxConstants.STYLE_NOLABEL, false)) + { + result = this.convertValueToString(cell); + } + } + + return result; +}; + +/** + * Function: isHtmlLabel + * + * Returns true if the label must be rendered as HTML markup. The default + * implementation returns <htmlLabels>. + * + * Parameters: + * + * cell - <mxCell> whose label should be displayed as HTML markup. + */ +mxGraph.prototype.isHtmlLabel = function(cell) +{ + return this.isHtmlLabels(); +}; + +/** + * Function: isHtmlLabels + * + * Returns <htmlLabels>. + */ +mxGraph.prototype.isHtmlLabels = function() +{ + return this.htmlLabels; +}; + +/** + * Function: setHtmlLabels + * + * Sets <htmlLabels>. + */ +mxGraph.prototype.setHtmlLabels = function(value) +{ + this.htmlLabels = value; +}; + +/** + * Function: isWrapping + * + * This enables wrapping for HTML labels. + * + * Returns true if no white-space CSS style directive should be used for + * displaying the given cells label. This implementation returns true if + * <mxConstants.STYLE_WHITE_SPACE> in the style of the given cell is 'wrap'. + * + * This is used as a workaround for IE ignoring the white-space directive + * of child elements if the directive appears in a parent element. It + * should be overridden to return true if a white-space directive is used + * in the HTML markup that represents the given cells label. In order for + * HTML markup to work in labels, <isHtmlLabel> must also return true + * for the given cell. + * + * Example: + * + * (code) + * graph.getLabel = function(cell) + * { + * var tmp = mxGraph.prototype.getLabel.apply(this, arguments); // "supercall" + * + * if (this.model.isEdge(cell)) + * { + * tmp = '<div style="width: 150px; white-space:normal;">'+tmp+'</div>'; + * } + * + * return tmp; + * } + * + * graph.isWrapping = function(state) + * { + * return this.model.isEdge(state.cell); + * } + * (end) + * + * Makes sure no edge label is wider than 150 pixels, otherwise the content + * is wrapped. Note: No width must be specified for wrapped vertex labels as + * the vertex defines the width in its geometry. + * + * Parameters: + * + * state - <mxCell> whose label should be wrapped. + */ +mxGraph.prototype.isWrapping = function(cell) +{ + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + return (style != null) ? style[mxConstants.STYLE_WHITE_SPACE] == 'wrap' : false; +}; + +/** + * Function: isLabelClipped + * + * Returns true if the overflow portion of labels should be hidden. If this + * returns true then vertex labels will be clipped to the size of the vertices. + * This implementation returns true if <mxConstants.STYLE_OVERFLOW> in the + * style of the given cell is 'hidden'. + * + * Parameters: + * + * state - <mxCell> whose label should be clipped. + */ +mxGraph.prototype.isLabelClipped = function(cell) +{ + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + return (style != null) ? style[mxConstants.STYLE_OVERFLOW] == 'hidden' : false; +}; + +/** + * Function: getTooltip + * + * Returns the string or DOM node that represents the tooltip for the given + * state, node and coordinate pair. This implementation checks if the given + * node is a folding icon or overlay and returns the respective tooltip. If + * this does not result in a tooltip, the handler for the cell is retrieved + * from <selectionCellsHandler> and the optional getTooltipForNode method is + * called. If no special tooltip exists here then <getTooltipForCell> is used + * with the cell in the given state as the argument to return a tooltip for the + * given state. + * + * Parameters: + * + * state - <mxCellState> whose tooltip should be returned. + * node - DOM node that is currently under the mouse. + * x - X-coordinate of the mouse. + * y - Y-coordinate of the mouse. + */ +mxGraph.prototype.getTooltip = function(state, node, x, y) +{ + var tip = null; + + if (state != null) + { + // Checks if the mouse is over the folding icon + if (state.control != null && (node == state.control.node || + node.parentNode == state.control.node)) + { + tip = this.collapseExpandResource; + tip = mxResources.get(tip) || tip; + } + + if (tip == null && state.overlays != null) + { + state.overlays.visit(function(id, shape) + { + // LATER: Exit loop if tip is not null + if (tip == null && (node == shape.node || node.parentNode == shape.node)) + { + tip = shape.overlay.toString(); + } + }); + } + + if (tip == null) + { + var handler = this.selectionCellsHandler.getHandler(state.cell); + + if (handler != null && typeof(handler.getTooltipForNode) == 'function') + { + tip = handler.getTooltipForNode(node); + } + } + + if (tip == null) + { + tip = this.getTooltipForCell(state.cell); + } + } + + return tip; +}; + +/** + * Function: getTooltipForCell + * + * Returns the string or DOM node to be used as the tooltip for the given + * cell. This implementation uses the cells getTooltip function if it + * exists, or else it returns <convertValueToString> for the cell. + * + * Example: + * + * (code) + * graph.getTooltipForCell = function(cell) + * { + * return 'Hello, World!'; + * } + * (end) + * + * Replaces all tooltips with the string Hello, World! + * + * Parameters: + * + * cell - <mxCell> whose tooltip should be returned. + */ +mxGraph.prototype.getTooltipForCell = function(cell) +{ + var tip = null; + + if (cell != null && cell.getTooltip != null) + { + tip = cell.getTooltip(); + } + else + { + tip = this.convertValueToString(cell); + } + + return tip; +}; + +/** + * Function: getCursorForCell + * + * Returns the cursor value to be used for the CSS of the shape for the + * given cell. This implementation returns null. + * + * Parameters: + * + * cell - <mxCell> whose cursor should be returned. + */ +mxGraph.prototype.getCursorForCell = function(cell) +{ + return null; +}; + +/** + * Function: getStartSize + * + * Returns the start size of the given swimlane, that is, the width or + * height of the part that contains the title, depending on the + * horizontal style. The return value is an <mxRectangle> with either + * width or height set as appropriate. + * + * Parameters: + * + * swimlane - <mxCell> whose start size should be returned. + */ +mxGraph.prototype.getStartSize = function(swimlane) +{ + var result = new mxRectangle(); + var state = this.view.getState(swimlane); + var style = (state != null) ? state.style : this.getCellStyle(swimlane); + + if (style != null) + { + var size = parseInt(mxUtils.getValue(style, + mxConstants.STYLE_STARTSIZE, mxConstants.DEFAULT_STARTSIZE)); + + if (mxUtils.getValue(style, mxConstants.STYLE_HORIZONTAL, true)) + { + result.height = size; + } + else + { + result.width = size; + } + } + + return result; +}; + +/** + * Function: getImage + * + * Returns the image URL for the given cell state. This implementation + * returns the value stored under <mxConstants.STYLE_IMAGE> in the cell + * style. + * + * Parameters: + * + * state - <mxCellState> whose image URL should be returned. + */ +mxGraph.prototype.getImage = function(state) +{ + return (state != null && state.style != null) ? + state.style[mxConstants.STYLE_IMAGE] : null; +}; + +/** + * Function: getVerticalAlign + * + * Returns the vertical alignment for the given cell state. This + * implementation returns the value stored under + * <mxConstants.STYLE_VERTICAL_ALIGN> in the cell style. + * + * Parameters: + * + * state - <mxCellState> whose vertical alignment should be + * returned. + */ +mxGraph.prototype.getVerticalAlign = function(state) +{ + return (state != null && state.style != null) ? + (state.style[mxConstants.STYLE_VERTICAL_ALIGN] || + mxConstants.ALIGN_MIDDLE ): + null; +}; + +/** + * Function: getIndicatorColor + * + * Returns the indicator color for the given cell state. This + * implementation returns the value stored under + * <mxConstants.STYLE_INDICATOR_COLOR> in the cell style. + * + * Parameters: + * + * state - <mxCellState> whose indicator color should be + * returned. + */ +mxGraph.prototype.getIndicatorColor = function(state) +{ + return (state != null && state.style != null) ? + state.style[mxConstants.STYLE_INDICATOR_COLOR] : null; +}; + +/** + * Function: getIndicatorGradientColor + * + * Returns the indicator gradient color for the given cell state. This + * implementation returns the value stored under + * <mxConstants.STYLE_INDICATOR_GRADIENTCOLOR> in the cell style. + * + * Parameters: + * + * state - <mxCellState> whose indicator gradient color should be + * returned. + */ +mxGraph.prototype.getIndicatorGradientColor = function(state) +{ + return (state != null && state.style != null) ? + state.style[mxConstants.STYLE_INDICATOR_GRADIENTCOLOR] : null; +}; + +/** + * Function: getIndicatorShape + * + * Returns the indicator shape for the given cell state. This + * implementation returns the value stored under + * <mxConstants.STYLE_INDICATOR_SHAPE> in the cell style. + * + * Parameters: + * + * state - <mxCellState> whose indicator shape should be returned. + */ +mxGraph.prototype.getIndicatorShape = function(state) +{ + return (state != null && state.style != null) ? + state.style[mxConstants.STYLE_INDICATOR_SHAPE] : null; +}; + +/** + * Function: getIndicatorImage + * + * Returns the indicator image for the given cell state. This + * implementation returns the value stored under + * <mxConstants.STYLE_INDICATOR_IMAGE> in the cell style. + * + * Parameters: + * + * state - <mxCellState> whose indicator image should be returned. + */ +mxGraph.prototype.getIndicatorImage = function(state) +{ + return (state != null && state.style != null) ? + state.style[mxConstants.STYLE_INDICATOR_IMAGE] : null; +}; + +/** + * Function: getBorder + * + * Returns the value of <border>. + */ +mxGraph.prototype.getBorder = function() +{ + return this.border; +}; + +/** + * Function: setBorder + * + * Sets the value of <border>. + * + * Parameters: + * + * value - Positive integer that represents the border to be used. + */ +mxGraph.prototype.setBorder = function(value) +{ + this.border = value; +}; + +/** + * Function: isSwimlane + * + * Returns true if the given cell is a swimlane in the graph. A swimlane is + * a container cell with some specific behaviour. This implementation + * checks if the shape associated with the given cell is a <mxSwimlane>. + * + * Parameters: + * + * cell - <mxCell> to be checked. + */ +mxGraph.prototype.isSwimlane = function (cell) +{ + if (cell != null) + { + if (this.model.getParent(cell) != this.model.getRoot()) + { + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + if (style != null && !this.model.isEdge(cell)) + { + return style[mxConstants.STYLE_SHAPE] == + mxConstants.SHAPE_SWIMLANE; + } + } + } + + return false; +}; + +/** + * Group: Graph behaviour + */ + +/** + * Function: isResizeContainer + * + * Returns <resizeContainer>. + */ +mxGraph.prototype.isResizeContainer = function() +{ + return this.resizeContainer; +}; + +/** + * Function: setResizeContainer + * + * Sets <resizeContainer>. + * + * Parameters: + * + * value - Boolean indicating if the container should be resized. + */ +mxGraph.prototype.setResizeContainer = function(value) +{ + this.resizeContainer = value; +}; + +/** + * Function: isEnabled + * + * Returns true if the graph is <enabled>. + */ +mxGraph.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Specifies if the graph should allow any interactions. This + * implementation updates <enabled>. + * + * Parameters: + * + * value - Boolean indicating if the graph should be enabled. + */ +mxGraph.prototype.setEnabled = function(value) +{ + this.enabled = value; +}; + +/** + * Function: isEscapeEnabled + * + * Returns <escapeEnabled>. + */ +mxGraph.prototype.isEscapeEnabled = function() +{ + return this.escapeEnabled; +}; + +/** + * Function: setEscapeEnabled + * + * Sets <escapeEnabled>. + * + * Parameters: + * + * enabled - Boolean indicating if escape should be enabled. + */ +mxGraph.prototype.setEscapeEnabled = function(value) +{ + this.escapeEnabled = value; +}; + +/** + * Function: isInvokesStopCellEditing + * + * Returns <invokesStopCellEditing>. + */ +mxGraph.prototype.isInvokesStopCellEditing = function() +{ + return this.invokesStopCellEditing; +}; + +/** + * Function: setInvokesStopCellEditing + * + * Sets <invokesStopCellEditing>. + */ +mxGraph.prototype.setInvokesStopCellEditing = function(value) +{ + this.invokesStopCellEditing = value; +}; + +/** + * Function: isEnterStopsCellEditing + * + * Returns <enterStopsCellEditing>. + */ +mxGraph.prototype.isEnterStopsCellEditing = function() +{ + return this.enterStopsCellEditing; +}; + +/** + * Function: setEnterStopsCellEditing + * + * Sets <enterStopsCellEditing>. + */ +mxGraph.prototype.setEnterStopsCellEditing = function(value) +{ + this.enterStopsCellEditing = value; +}; + +/** + * Function: isCellLocked + * + * Returns true if the given cell may not be moved, sized, bended, + * disconnected, edited or selected. This implementation returns true for + * all vertices with a relative geometry if <locked> is false. + * + * Parameters: + * + * cell - <mxCell> whose locked state should be returned. + */ +mxGraph.prototype.isCellLocked = function(cell) +{ + var geometry = this.model.getGeometry(cell); + + return this.isCellsLocked() || (geometry != null && + this.model.isVertex(cell) && geometry.relative); +}; + +/** + * Function: isCellsLocked + * + * Returns true if the given cell may not be moved, sized, bended, + * disconnected, edited or selected. This implementation returns true for + * all vertices with a relative geometry if <locked> is false. + * + * Parameters: + * + * cell - <mxCell> whose locked state should be returned. + */ +mxGraph.prototype.isCellsLocked = function() +{ + return this.cellsLocked; +}; + +/** + * Function: setLocked + * + * Sets if any cell may be moved, sized, bended, disconnected, edited or + * selected. + * + * Parameters: + * + * value - Boolean that defines the new value for <cellsLocked>. + */ +mxGraph.prototype.setCellsLocked = function(value) +{ + this.cellsLocked = value; +}; + +/** + * Function: getCloneableCells + * + * Returns the cells which may be exported in the given array of cells. + */ +mxGraph.prototype.getCloneableCells = function(cells) +{ + return this.model.filterCells(cells, mxUtils.bind(this, function(cell) + { + return this.isCellCloneable(cell); + })); +}; + +/** + * Function: isCellCloneable + * + * Returns true if the given cell is cloneable. This implementation returns + * <isCellsCloneable> for all cells unless a cell style specifies + * <mxConstants.STYLE_CLONEABLE> to be 0. + * + * Parameters: + * + * cell - Optional <mxCell> whose cloneable state should be returned. + */ +mxGraph.prototype.isCellCloneable = function(cell) +{ + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + return this.isCellsCloneable() && style[mxConstants.STYLE_CLONEABLE] != 0; +}; + +/** + * Function: isCellsCloneable + * + * Returns <cellsCloneable>, that is, if the graph allows cloning of cells + * by using control-drag. + */ +mxGraph.prototype.isCellsCloneable = function() +{ + return this.cellsCloneable; +}; + +/** + * Function: setCellsCloneable + * + * Specifies if the graph should allow cloning of cells by holding down the + * control key while cells are being moved. This implementation updates + * <cellsCloneable>. + * + * Parameters: + * + * value - Boolean indicating if the graph should be cloneable. + */ +mxGraph.prototype.setCellsCloneable = function(value) +{ + this.cellsCloneable = value; +}; + +/** + * Function: getExportableCells + * + * Returns the cells which may be exported in the given array of cells. + */ +mxGraph.prototype.getExportableCells = function(cells) +{ + return this.model.filterCells(cells, mxUtils.bind(this, function(cell) + { + return this.canExportCell(cell); + })); +}; + +/** + * Function: canExportCell + * + * Returns true if the given cell may be exported to the clipboard. This + * implementation returns <exportEnabled> for all cells. + * + * Parameters: + * + * cell - <mxCell> that represents the cell to be exported. + */ +mxGraph.prototype.canExportCell = function(cell) +{ + return this.exportEnabled; +}; + +/** + * Function: getImportableCells + * + * Returns the cells which may be imported in the given array of cells. + */ +mxGraph.prototype.getImportableCells = function(cells) +{ + return this.model.filterCells(cells, mxUtils.bind(this, function(cell) + { + return this.canImportCell(cell); + })); +}; + +/** + * Function: canImportCell + * + * Returns true if the given cell may be imported from the clipboard. + * This implementation returns <importEnabled> for all cells. + * + * Parameters: + * + * cell - <mxCell> that represents the cell to be imported. + */ +mxGraph.prototype.canImportCell = function(cell) +{ + return this.importEnabled; +}; + +/** + * Function: isCellSelectable + * + * Returns true if the given cell is selectable. This implementation + * returns <cellsSelectable>. + * + * To add a new style for making cells (un)selectable, use the following code. + * + * (code) + * mxGraph.prototype.isCellSelectable = function(cell) + * { + * var state = this.view.getState(cell); + * var style = (state != null) ? state.style : this.getCellStyle(cell); + * + * return this.isCellsSelectable() && !this.isCellLocked(cell) && style['selectable'] != 0; + * }; + * (end) + * + * You can then use the new style as shown in this example. + * + * (code) + * graph.insertVertex(parent, null, 'Hello,', 20, 20, 80, 30, 'selectable=0'); + * (end) + * + * Parameters: + * + * cell - <mxCell> whose selectable state should be returned. + */ +mxGraph.prototype.isCellSelectable = function(cell) +{ + return this.isCellsSelectable(); +}; + +/** + * Function: isCellsSelectable + * + * Returns <cellsSelectable>. + */ +mxGraph.prototype.isCellsSelectable = function() +{ + return this.cellsSelectable; +}; + +/** + * Function: setCellsSelectable + * + * Sets <cellsSelectable>. + */ +mxGraph.prototype.setCellsSelectable = function(value) +{ + this.cellsSelectable = value; +}; + +/** + * Function: getDeletableCells + * + * Returns the cells which may be exported in the given array of cells. + */ +mxGraph.prototype.getDeletableCells = function(cells) +{ + return this.model.filterCells(cells, mxUtils.bind(this, function(cell) + { + return this.isCellDeletable(cell); + })); +}; + +/** + * Function: isCellDeletable + * + * Returns true if the given cell is moveable. This returns + * <cellsDeletable> for all given cells if a cells style does not specify + * <mxConstants.STYLE_DELETABLE> to be 0. + * + * Parameters: + * + * cell - <mxCell> whose deletable state should be returned. + */ +mxGraph.prototype.isCellDeletable = function(cell) +{ + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + return this.isCellsDeletable() && style[mxConstants.STYLE_DELETABLE] != 0; +}; + +/** + * Function: isCellsDeletable + * + * Returns <cellsDeletable>. + */ +mxGraph.prototype.isCellsDeletable = function() +{ + return this.cellsDeletable; +}; + +/** + * Function: setCellsDeletable + * + * Sets <cellsDeletable>. + * + * Parameters: + * + * value - Boolean indicating if the graph should allow deletion of cells. + */ +mxGraph.prototype.setCellsDeletable = function(value) +{ + this.cellsDeletable = value; +}; + +/** + * Function: isLabelMovable + * + * Returns true if the given edges's label is moveable. This returns + * <movable> for all given cells if <isLocked> does not return true + * for the given cell. + * + * Parameters: + * + * cell - <mxCell> whose label should be moved. + */ +mxGraph.prototype.isLabelMovable = function(cell) +{ + return !this.isCellLocked(cell) && + ((this.model.isEdge(cell) && this.edgeLabelsMovable) || + (this.model.isVertex(cell) && this.vertexLabelsMovable)); +}; + +/** + * Function: getMovableCells + * + * Returns the cells which are movable in the given array of cells. + */ +mxGraph.prototype.getMovableCells = function(cells) +{ + return this.model.filterCells(cells, mxUtils.bind(this, function(cell) + { + return this.isCellMovable(cell); + })); +}; + +/** + * Function: isCellMovable + * + * Returns true if the given cell is moveable. This returns <cellsMovable> + * for all given cells if <isCellLocked> does not return true for the given + * cell and its style does not specify <mxConstants.STYLE_MOVABLE> to be 0. + * + * Parameters: + * + * cell - <mxCell> whose movable state should be returned. + */ +mxGraph.prototype.isCellMovable = function(cell) +{ + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + return this.isCellsMovable() && !this.isCellLocked(cell) && style[mxConstants.STYLE_MOVABLE] != 0; +}; + +/** + * Function: isCellsMovable + * + * Returns <cellsMovable>. + */ +mxGraph.prototype.isCellsMovable = function() +{ + return this.cellsMovable; +}; + +/** + * Function: setCellsMovable + * + * Specifies if the graph should allow moving of cells. This implementation + * updates <cellsMsovable>. + * + * Parameters: + * + * value - Boolean indicating if the graph should allow moving of cells. + */ +mxGraph.prototype.setCellsMovable = function(value) +{ + this.cellsMovable = value; +}; + +/** + * Function: isGridEnabled + * + * Returns <gridEnabled> as a boolean. + */ +mxGraph.prototype.isGridEnabled = function() +{ + return this.gridEnabled; +}; + +/** + * Function: setGridEnabled + * + * Specifies if the grid should be enabled. + * + * Parameters: + * + * value - Boolean indicating if the grid should be enabled. + */ +mxGraph.prototype.setGridEnabled = function(value) +{ + this.gridEnabled = value; +}; + +/** + * Function: isPortsEnabled + * + * Returns <portsEnabled> as a boolean. + */ +mxGraph.prototype.isPortsEnabled = function() +{ + return this.portsEnabled; +}; + +/** + * Function: setPortsEnabled + * + * Specifies if the ports should be enabled. + * + * Parameters: + * + * value - Boolean indicating if the ports should be enabled. + */ +mxGraph.prototype.setPortsEnabled = function(value) +{ + this.portsEnabled = value; +}; + +/** + * Function: getGridSize + * + * Returns <gridSize>. + */ +mxGraph.prototype.getGridSize = function() +{ + return this.gridSize; +}; + +/** + * Function: setGridSize + * + * Sets <gridSize>. + */ +mxGraph.prototype.setGridSize = function(value) +{ + this.gridSize = value; +}; + +/** + * Function: getTolerance + * + * Returns <tolerance>. + */ +mxGraph.prototype.getTolerance = function() +{ + return this.tolerance; +}; + +/** + * Function: setTolerance + * + * Sets <tolerance>. + */ +mxGraph.prototype.setTolerance = function(value) +{ + this.tolerance = value; +}; + +/** + * Function: isVertexLabelsMovable + * + * Returns <vertexLabelsMovable>. + */ +mxGraph.prototype.isVertexLabelsMovable = function() +{ + return this.vertexLabelsMovable; +}; + +/** + * Function: setVertexLabelsMovable + * + * Sets <vertexLabelsMovable>. + */ +mxGraph.prototype.setVertexLabelsMovable = function(value) +{ + this.vertexLabelsMovable = value; +}; + +/** + * Function: isEdgeLabelsMovable + * + * Returns <edgeLabelsMovable>. + */ +mxGraph.prototype.isEdgeLabelsMovable = function() +{ + return this.edgeLabelsMovable; +}; + +/** + * Function: isEdgeLabelsMovable + * + * Sets <edgeLabelsMovable>. + */ +mxGraph.prototype.setEdgeLabelsMovable = function(value) +{ + this.edgeLabelsMovable = value; +}; + +/** + * Function: isSwimlaneNesting + * + * Returns <swimlaneNesting> as a boolean. + */ +mxGraph.prototype.isSwimlaneNesting = function() +{ + return this.swimlaneNesting; +}; + +/** + * Function: setSwimlaneNesting + * + * Specifies if swimlanes can be nested by drag and drop. This is only + * taken into account if dropEnabled is true. + * + * Parameters: + * + * value - Boolean indicating if swimlanes can be nested. + */ +mxGraph.prototype.setSwimlaneNesting = function(value) +{ + this.swimlaneNesting = value; +}; + +/** + * Function: isSwimlaneSelectionEnabled + * + * Returns <swimlaneSelectionEnabled> as a boolean. + */ +mxGraph.prototype.isSwimlaneSelectionEnabled = function() +{ + return this.swimlaneSelectionEnabled; +}; + +/** + * Function: setSwimlaneSelectionEnabled + * + * Specifies if swimlanes should be selected if the mouse is released + * over their content area. + * + * Parameters: + * + * value - Boolean indicating if swimlanes content areas + * should be selected when the mouse is released over them. + */ +mxGraph.prototype.setSwimlaneSelectionEnabled = function(value) +{ + this.swimlaneSelectionEnabled = value; +}; + +/** + * Function: isMultigraph + * + * Returns <multigraph> as a boolean. + */ +mxGraph.prototype.isMultigraph = function() +{ + return this.multigraph; +}; + +/** + * Function: setMultigraph + * + * Specifies if the graph should allow multiple connections between the + * same pair of vertices. + * + * Parameters: + * + * value - Boolean indicating if the graph allows multiple connections + * between the same pair of vertices. + */ +mxGraph.prototype.setMultigraph = function(value) +{ + this.multigraph = value; +}; + +/** + * Function: isAllowLoops + * + * Returns <allowLoops> as a boolean. + */ +mxGraph.prototype.isAllowLoops = function() +{ + return this.allowLoops; +}; + +/** + * Function: setAllowDanglingEdges + * + * Specifies if dangling edges are allowed, that is, if edges are allowed + * that do not have a source and/or target terminal defined. + * + * Parameters: + * + * value - Boolean indicating if dangling edges are allowed. + */ +mxGraph.prototype.setAllowDanglingEdges = function(value) +{ + this.allowDanglingEdges = value; +}; + +/** + * Function: isAllowDanglingEdges + * + * Returns <allowDanglingEdges> as a boolean. + */ +mxGraph.prototype.isAllowDanglingEdges = function() +{ + return this.allowDanglingEdges; +}; + +/** + * Function: setConnectableEdges + * + * Specifies if edges should be connectable. + * + * Parameters: + * + * value - Boolean indicating if edges should be connectable. + */ +mxGraph.prototype.setConnectableEdges = function(value) +{ + this.connectableEdges = value; +}; + +/** + * Function: isConnectableEdges + * + * Returns <connectableEdges> as a boolean. + */ +mxGraph.prototype.isConnectableEdges = function() +{ + return this.connectableEdges; +}; + +/** + * Function: setCloneInvalidEdges + * + * Specifies if edges should be inserted when cloned but not valid wrt. + * <getEdgeValidationError>. If false such edges will be silently ignored. + * + * Parameters: + * + * value - Boolean indicating if cloned invalid edges should be + * inserted into the graph or ignored. + */ +mxGraph.prototype.setCloneInvalidEdges = function(value) +{ + this.cloneInvalidEdges = value; +}; + +/** + * Function: isCloneInvalidEdges + * + * Returns <cloneInvalidEdges> as a boolean. + */ +mxGraph.prototype.isCloneInvalidEdges = function() +{ + return this.cloneInvalidEdges; +}; + +/** + * Function: setAllowLoops + * + * Specifies if loops are allowed. + * + * Parameters: + * + * value - Boolean indicating if loops are allowed. + */ +mxGraph.prototype.setAllowLoops = function(value) +{ + this.allowLoops = value; +}; + +/** + * Function: isDisconnectOnMove + * + * Returns <disconnectOnMove> as a boolean. + */ +mxGraph.prototype.isDisconnectOnMove = function() +{ + return this.disconnectOnMove; +}; + +/** + * Function: setDisconnectOnMove + * + * Specifies if edges should be disconnected when moved. (Note: Cloned + * edges are always disconnected.) + * + * Parameters: + * + * value - Boolean indicating if edges should be disconnected + * when moved. + */ +mxGraph.prototype.setDisconnectOnMove = function(value) +{ + this.disconnectOnMove = value; +}; + +/** + * Function: isDropEnabled + * + * Returns <dropEnabled> as a boolean. + */ +mxGraph.prototype.isDropEnabled = function() +{ + return this.dropEnabled; +}; + +/** + * Function: setDropEnabled + * + * Specifies if the graph should allow dropping of cells onto or into other + * cells. + * + * Parameters: + * + * dropEnabled - Boolean indicating if the graph should allow dropping + * of cells into other cells. + */ +mxGraph.prototype.setDropEnabled = function(value) +{ + this.dropEnabled = value; +}; + +/** + * Function: isSplitEnabled + * + * Returns <splitEnabled> as a boolean. + */ +mxGraph.prototype.isSplitEnabled = function() +{ + return this.splitEnabled; +}; + +/** + * Function: setSplitEnabled + * + * Specifies if the graph should allow dropping of cells onto or into other + * cells. + * + * Parameters: + * + * dropEnabled - Boolean indicating if the graph should allow dropping + * of cells into other cells. + */ +mxGraph.prototype.setSplitEnabled = function(value) +{ + this.splitEnabled = value; +}; + +/** + * Function: isCellResizable + * + * Returns true if the given cell is resizable. This returns + * <cellsResizable> for all given cells if <isCellLocked> does not return + * true for the given cell and its style does not specify + * <mxConstants.STYLE_RESIZABLE> to be 0. + * + * Parameters: + * + * cell - <mxCell> whose resizable state should be returned. + */ +mxGraph.prototype.isCellResizable = function(cell) +{ + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + return this.isCellsResizable() && !this.isCellLocked(cell) && style[mxConstants.STYLE_RESIZABLE] != 0; +}; + +/** + * Function: isCellsResizable + * + * Returns <cellsResizable>. + */ +mxGraph.prototype.isCellsResizable = function() +{ + return this.cellsResizable; +}; + +/** + * Function: setCellsResizable + * + * Specifies if the graph should allow resizing of cells. This + * implementation updates <cellsResizable>. + * + * Parameters: + * + * value - Boolean indicating if the graph should allow resizing of + * cells. + */ +mxGraph.prototype.setCellsResizable = function(value) +{ + this.cellsResizable = value; +}; + +/** + * Function: isTerminalPointMovable + * + * Returns true if the given terminal point is movable. This is independent + * from <isCellConnectable> and <isCellDisconnectable> and controls if terminal + * points can be moved in the graph if the edge is not connected. Note that it + * is required for this to return true to connect unconnected edges. This + * implementation returns true. + * + * Parameters: + * + * cell - <mxCell> whose terminal point should be moved. + * source - Boolean indicating if the source or target terminal should be moved. + */ +mxGraph.prototype.isTerminalPointMovable = function(cell, source) +{ + return true; +}; + +/** + * Function: isCellBendable + * + * Returns true if the given cell is bendable. This returns <cellsBendable> + * for all given cells if <isLocked> does not return true for the given + * cell and its style does not specify <mxConstants.STYLE_BENDABLE> to be 0. + * + * Parameters: + * + * cell - <mxCell> whose bendable state should be returned. + */ +mxGraph.prototype.isCellBendable = function(cell) +{ + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + return this.isCellsBendable() && !this.isCellLocked(cell) && style[mxConstants.STYLE_BENDABLE] != 0; +}; + +/** + * Function: isCellsBendable + * + * Returns <cellsBenadable>. + */ +mxGraph.prototype.isCellsBendable = function() +{ + return this.cellsBendable; +}; + +/** + * Function: setCellsBendable + * + * Specifies if the graph should allow bending of edges. This + * implementation updates <bendable>. + * + * Parameters: + * + * value - Boolean indicating if the graph should allow bending of + * edges. + */ +mxGraph.prototype.setCellsBendable = function(value) +{ + this.cellsBendable = value; +}; + +/** + * Function: isCellEditable + * + * Returns true if the given cell is editable. This returns <cellsEditable> for + * all given cells if <isCellLocked> does not return true for the given cell + * and its style does not specify <mxConstants.STYLE_EDITABLE> to be 0. + * + * Parameters: + * + * cell - <mxCell> whose editable state should be returned. + */ +mxGraph.prototype.isCellEditable = function(cell) +{ + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + return this.isCellsEditable() && !this.isCellLocked(cell) && style[mxConstants.STYLE_EDITABLE] != 0; +}; + +/** + * Function: isCellsEditable + * + * Returns <cellsEditable>. + */ +mxGraph.prototype.isCellsEditable = function() +{ + return this.cellsEditable; +}; + +/** + * Function: setCellsEditable + * + * Specifies if the graph should allow in-place editing for cell labels. + * This implementation updates <cellsEditable>. + * + * Parameters: + * + * value - Boolean indicating if the graph should allow in-place + * editing. + */ +mxGraph.prototype.setCellsEditable = function(value) +{ + this.cellsEditable = value; +}; + +/** + * Function: isCellDisconnectable + * + * Returns true if the given cell is disconnectable from the source or + * target terminal. This returns <isCellsDisconnectable> for all given + * cells if <isCellLocked> does not return true for the given cell. + * + * Parameters: + * + * cell - <mxCell> whose disconnectable state should be returned. + * terminal - <mxCell> that represents the source or target terminal. + * source - Boolean indicating if the source or target terminal is to be + * disconnected. + */ +mxGraph.prototype.isCellDisconnectable = function(cell, terminal, source) +{ + return this.isCellsDisconnectable() && !this.isCellLocked(cell); +}; + +/** + * Function: isCellsDisconnectable + * + * Returns <cellsDisconnectable>. + */ +mxGraph.prototype.isCellsDisconnectable = function() +{ + return this.cellsDisconnectable; +}; + +/** + * Function: setCellsDisconnectable + * + * Sets <cellsDisconnectable>. + */ +mxGraph.prototype.setCellsDisconnectable = function(value) +{ + this.cellsDisconnectable = value; +}; + +/** + * Function: isValidSource + * + * Returns true if the given cell is a valid source for new connections. + * This implementation returns true for all non-null values and is + * called by is called by <isValidConnection>. + * + * Parameters: + * + * cell - <mxCell> that represents a possible source or null. + */ +mxGraph.prototype.isValidSource = function(cell) +{ + return (cell == null && this.allowDanglingEdges) || + (cell != null && (!this.model.isEdge(cell) || + this.connectableEdges) && this.isCellConnectable(cell)); +}; + +/** + * Function: isValidTarget + * + * Returns <isValidSource> for the given cell. This is called by + * <isValidConnection>. + * + * Parameters: + * + * cell - <mxCell> that represents a possible target or null. + */ +mxGraph.prototype.isValidTarget = function(cell) +{ + return this.isValidSource(cell); +}; + +/** + * Function: isValidConnection + * + * Returns true if the given target cell is a valid target for source. + * This is a boolean implementation for not allowing connections between + * certain pairs of vertices and is called by <getEdgeValidationError>. + * This implementation returns true if <isValidSource> returns true for + * the source and <isValidTarget> returns true for the target. + * + * Parameters: + * + * source - <mxCell> that represents the source cell. + * target - <mxCell> that represents the target cell. + */ +mxGraph.prototype.isValidConnection = function(source, target) +{ + return this.isValidSource(source) && this.isValidTarget(target); +}; + +/** + * Function: setConnectable + * + * Specifies if the graph should allow new connections. This implementation + * updates <mxConnectionHandler.enabled> in <connectionHandler>. + * + * Parameters: + * + * connectable - Boolean indicating if new connections should be allowed. + */ +mxGraph.prototype.setConnectable = function(connectable) +{ + this.connectionHandler.setEnabled(connectable); +}; + +/** + * Function: isConnectable + * + * Returns true if the <connectionHandler> is enabled. + */ +mxGraph.prototype.isConnectable = function(connectable) +{ + return this.connectionHandler.isEnabled(); +}; + +/** + * Function: setTooltips + * + * Specifies if tooltips should be enabled. This implementation updates + * <mxTooltipHandler.enabled> in <tooltipHandler>. + * + * Parameters: + * + * enabled - Boolean indicating if tooltips should be enabled. + */ +mxGraph.prototype.setTooltips = function (enabled) +{ + this.tooltipHandler.setEnabled(enabled); +}; + +/** + * Function: setPanning + * + * Specifies if panning should be enabled. This implementation updates + * <mxPanningHandler.panningEnabled> in <panningHandler>. + * + * Parameters: + * + * enabled - Boolean indicating if panning should be enabled. + */ +mxGraph.prototype.setPanning = function(enabled) +{ + this.panningHandler.panningEnabled = enabled; +}; + +/** + * Function: isEditing + * + * Returns true if the given cell is currently being edited. + * If no cell is specified then this returns true if any + * cell is currently being edited. + * + * Parameters: + * + * cell - <mxCell> that should be checked. + */ +mxGraph.prototype.isEditing = function(cell) +{ + if (this.cellEditor != null) + { + var editingCell = this.cellEditor.getEditingCell(); + + return (cell == null) ? + editingCell != null : + cell == editingCell; + } + + return false; +}; + +/** + * Function: isAutoSizeCell + * + * Returns true if the size of the given cell should automatically be + * updated after a change of the label. This implementation returns + * <autoSizeCells> or checks if the cell style does specify + * <mxConstants.STYLE_AUTOSIZE> to be 1. + * + * Parameters: + * + * cell - <mxCell> that should be resized. + */ +mxGraph.prototype.isAutoSizeCell = function(cell) +{ + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + return this.isAutoSizeCells() || style[mxConstants.STYLE_AUTOSIZE] == 1; +}; + +/** + * Function: isAutoSizeCells + * + * Returns <autoSizeCells>. + */ +mxGraph.prototype.isAutoSizeCells = function() +{ + return this.autoSizeCells; +}; + +/** + * Function: setAutoSizeCells + * + * Specifies if cell sizes should be automatically updated after a label + * change. This implementation sets <autoSizeCells> to the given parameter. + * + * Parameters: + * + * value - Boolean indicating if cells should be resized + * automatically. + */ +mxGraph.prototype.setAutoSizeCells = function(value) +{ + this.autoSizeCells = value; +}; + +/** + * Function: isExtendParent + * + * Returns true if the parent of the given cell should be extended if the + * child has been resized so that it overlaps the parent. This + * implementation returns <isExtendParents> if the cell is not an edge. + * + * Parameters: + * + * cell - <mxCell> that has been resized. + */ +mxGraph.prototype.isExtendParent = function(cell) +{ + return !this.getModel().isEdge(cell) && this.isExtendParents(); +}; + +/** + * Function: isExtendParents + * + * Returns <extendParents>. + */ +mxGraph.prototype.isExtendParents = function() +{ + return this.extendParents; +}; + +/** + * Function: setExtendParents + * + * Sets <extendParents>. + * + * Parameters: + * + * value - New boolean value for <extendParents>. + */ +mxGraph.prototype.setExtendParents = function(value) +{ + this.extendParents = value; +}; + +/** + * Function: isExtendParentsOnAdd + * + * Returns <extendParentsOnAdd>. + */ +mxGraph.prototype.isExtendParentsOnAdd = function() +{ + return this.extendParentsOnAdd; +}; + +/** + * Function: setExtendParentsOnAdd + * + * Sets <extendParentsOnAdd>. + * + * Parameters: + * + * value - New boolean value for <extendParentsOnAdd>. + */ +mxGraph.prototype.setExtendParentsOnAdd = function(value) +{ + this.extendParentsOnAdd = value; +}; + +/** + * Function: isConstrainChild + * + * Returns true if the given cell should be kept inside the bounds of its + * parent according to the rules defined by <getOverlap> and + * <isAllowOverlapParent>. This implementation returns false for all children + * of edges and <isConstrainChildren> otherwise. + * + * Parameters: + * + * cell - <mxCell> that should be constrained. + */ +mxGraph.prototype.isConstrainChild = function(cell) +{ + return this.isConstrainChildren() && !this.getModel().isEdge(this.getModel().getParent(cell)); + +}; + +/** + * Function: isConstrainChildren + * + * Returns <constrainChildren>. + */ +mxGraph.prototype.isConstrainChildren = function() +{ + return this.constrainChildren; +}; + +/** + * Function: setConstrainChildren + * + * Sets <constrainChildren>. + */ +mxGraph.prototype.setConstrainChildren = function(value) +{ + this.constrainChildren = value; +}; + +/** + * Function: isConstrainChildren + * + * Returns <allowNegativeCoordinates>. + */ +mxGraph.prototype.isAllowNegativeCoordinates = function() +{ + return this.allowNegativeCoordinates; +}; + +/** + * Function: setConstrainChildren + * + * Sets <allowNegativeCoordinates>. + */ +mxGraph.prototype.setAllowNegativeCoordinates = function(value) +{ + this.allowNegativeCoordinates = value; +}; + +/** + * Function: getOverlap + * + * Returns a decimal number representing the amount of the width and height + * of the given cell that is allowed to overlap its parent. A value of 0 + * means all children must stay inside the parent, 1 means the child is + * allowed to be placed outside of the parent such that it touches one of + * the parents sides. If <isAllowOverlapParent> returns false for the given + * cell, then this method returns 0. + * + * Parameters: + * + * cell - <mxCell> for which the overlap ratio should be returned. + */ +mxGraph.prototype.getOverlap = function(cell) +{ + return (this.isAllowOverlapParent(cell)) ? this.defaultOverlap : 0; +}; + +/** + * Function: isAllowOverlapParent + * + * Returns true if the given cell is allowed to be placed outside of the + * parents area. + * + * Parameters: + * + * cell - <mxCell> that represents the child to be checked. + */ +mxGraph.prototype.isAllowOverlapParent = function(cell) +{ + return false; +}; + +/** + * Function: getFoldableCells + * + * Returns the cells which are movable in the given array of cells. + */ +mxGraph.prototype.getFoldableCells = function(cells, collapse) +{ + return this.model.filterCells(cells, mxUtils.bind(this, function(cell) + { + return this.isCellFoldable(cell, collapse); + })); +}; + +/** + * Function: isCellFoldable + * + * Returns true if the given cell is foldable. This implementation + * returns true if the cell has at least one child and its style + * does not specify <mxConstants.STYLE_FOLDABLE> to be 0. + * + * Parameters: + * + * cell - <mxCell> whose foldable state should be returned. + */ +mxGraph.prototype.isCellFoldable = function(cell, collapse) +{ + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + return this.model.getChildCount(cell) > 0 && style[mxConstants.STYLE_FOLDABLE] != 0; +}; + +/** + * Function: isValidDropTarget + * + * Returns true if the given cell is a valid drop target for the specified + * cells. If the given cell is an edge, then <isSplitDropTarget> is used, + * else <isParentDropTarget> is used to compute the return value. + * + * Parameters: + * + * cell - <mxCell> that represents the possible drop target. + * cells - <mxCells> that should be dropped into the target. + * evt - Mouseevent that triggered the invocation. + */ +mxGraph.prototype.isValidDropTarget = function(cell, cells, evt) +{ + return cell != null && ((this.isSplitEnabled() && + this.isSplitTarget(cell, cells, evt)) || (!this.model.isEdge(cell) && + (this.isSwimlane(cell) || (this.model.getChildCount(cell) > 0 && + !this.isCellCollapsed(cell))))); +}; + +/** + * Function: isSplitTarget + * + * Returns true if the given edge may be splitted into two edges with the + * given cell as a new terminal between the two. + * + * Parameters: + * + * target - <mxCell> that represents the edge to be splitted. + * cells - <mxCells> that should split the edge. + * evt - Mouseevent that triggered the invocation. + */ +mxGraph.prototype.isSplitTarget = function(target, cells, evt) +{ + if (this.model.isEdge(target) && cells != null && cells.length == 1 && + this.isCellConnectable(cells[0]) && this.getEdgeValidationError(target, + this.model.getTerminal(target, true), cells[0]) == null) + { + var src = this.model.getTerminal(target, true); + var trg = this.model.getTerminal(target, false); + + return (!this.model.isAncestor(cells[0], src) && + !this.model.isAncestor(cells[0], trg)); + } + + return false; +}; + +/** + * Function: getDropTarget + * + * Returns the given cell if it is a drop target for the given cells or the + * nearest ancestor that may be used as a drop target for the given cells. + * If the given array contains a swimlane and <swimlaneNesting> is false + * then this always returns null. If no cell is given, then the bottommost + * swimlane at the location of the given event is returned. + * + * This function should only be used if <isDropEnabled> returns true. + * + * Parameters: + * + * cells - Array of <mxCells> which are to be dropped onto the target. + * evt - Mouseevent for the drag and drop. + * cell - <mxCell> that is under the mousepointer. + */ +mxGraph.prototype.getDropTarget = function(cells, evt, cell) +{ + if (!this.isSwimlaneNesting()) + { + for (var i = 0; i < cells.length; i++) + { + if (this.isSwimlane(cells[i])) + { + return null; + } + } + } + + var pt = mxUtils.convertPoint(this.container, + mxEvent.getClientX(evt), mxEvent.getClientY(evt)); + pt.x -= this.panDx; + pt.y -= this.panDy; + var swimlane = this.getSwimlaneAt(pt.x, pt.y); + + if (cell == null) + { + cell = swimlane; + } + else if (swimlane != null) + { + // Checks if the cell is an ancestor of the swimlane + // under the mouse and uses the swimlane in that case + var tmp = this.model.getParent(swimlane); + + while (tmp != null && this.isSwimlane(tmp) && tmp != cell) + { + tmp = this.model.getParent(tmp); + } + + if (tmp == cell) + { + cell = swimlane; + } + } + + while (cell != null && !this.isValidDropTarget(cell, cells, evt) && + !this.model.isLayer(cell)) + { + cell = this.model.getParent(cell); + } + + return (!this.model.isLayer(cell) && mxUtils.indexOf(cells, cell) < 0) ? cell : null; +}; + +/** + * Group: Cell retrieval + */ + +/** + * Function: getDefaultParent + * + * Returns <defaultParent> or <mxGraphView.currentRoot> or the first child + * child of <mxGraphModel.root> if both are null. The value returned by + * this function should be used as the parent for new cells (aka default + * layer). + */ +mxGraph.prototype.getDefaultParent = function() +{ + var parent = this.defaultParent; + + if (parent == null) + { + parent = this.getCurrentRoot(); + + if (parent == null) + { + var root = this.model.getRoot(); + parent = this.model.getChildAt(root, 0); + } + } + + return parent; +}; + +/** + * Function: setDefaultParent + * + * Sets the <defaultParent> to the given cell. Set this to null to return + * the first child of the root in getDefaultParent. + */ +mxGraph.prototype.setDefaultParent = function(cell) +{ + this.defaultParent = cell; +}; + +/** + * Function: getSwimlane + * + * Returns the nearest ancestor of the given cell which is a swimlane, or + * the given cell, if it is itself a swimlane. + * + * Parameters: + * + * cell - <mxCell> for which the ancestor swimlane should be returned. + */ +mxGraph.prototype.getSwimlane = function(cell) +{ + while (cell != null && !this.isSwimlane(cell)) + { + cell = this.model.getParent(cell); + } + + return cell; +}; + +/** + * Function: getSwimlaneAt + * + * Returns the bottom-most swimlane that intersects the given point (x, y) + * in the cell hierarchy that starts at the given parent. + * + * Parameters: + * + * x - X-coordinate of the location to be checked. + * y - Y-coordinate of the location to be checked. + * parent - <mxCell> that should be used as the root of the recursion. + * Default is <defaultParent>. + */ +mxGraph.prototype.getSwimlaneAt = function (x, y, parent) +{ + parent = parent || this.getDefaultParent(); + + if (parent != null) + { + var childCount = this.model.getChildCount(parent); + + for (var i = 0; i < childCount; i++) + { + var child = this.model.getChildAt(parent, i); + var result = this.getSwimlaneAt(x, y, child); + + if (result != null) + { + return result; + } + else if (this.isSwimlane(child)) + { + var state = this.view.getState(child); + + if (this.intersects(state, x, y)) + { + return child; + } + } + } + } + + return null; +}; + +/** + * Function: getCellAt + * + * Returns the bottom-most cell that intersects the given point (x, y) in + * the cell hierarchy starting at the given parent. This will also return + * swimlanes if the given location intersects the content area of the + * swimlane. If this is not desired, then the <hitsSwimlaneContent> may be + * used if the returned cell is a swimlane to determine if the location + * is inside the content area or on the actual title of the swimlane. + * + * Parameters: + * + * x - X-coordinate of the location to be checked. + * y - Y-coordinate of the location to be checked. + * parent - <mxCell> that should be used as the root of the recursion. + * Default is <defaultParent>. + * vertices - Optional boolean indicating if vertices should be returned. + * Default is true. + * edges - Optional boolean indicating if edges should be returned. Default + * is true. + */ +mxGraph.prototype.getCellAt = function(x, y, parent, vertices, edges) +{ + vertices = (vertices != null) ? vertices : true; + edges = (edges != null) ? edges : true; + parent = (parent != null) ? parent : this.getDefaultParent(); + + if (parent != null) + { + var childCount = this.model.getChildCount(parent); + + for (var i = childCount - 1; i >= 0; i--) + { + var cell = this.model.getChildAt(parent, i); + var result = this.getCellAt(x, y, cell, vertices, edges); + + if (result != null) + { + return result; + } + else if (this.isCellVisible(cell) && (edges && this.model.isEdge(cell) || + vertices && this.model.isVertex(cell))) + { + var state = this.view.getState(cell); + + if (this.intersects(state, x, y)) + { + return cell; + } + } + } + } + + return null; +}; + +/** + * Function: intersects + * + * Returns the bottom-most cell that intersects the given point (x, y) in + * the cell hierarchy that starts at the given parent. + * + * Parameters: + * + * state - <mxCellState> that represents the cell state. + * x - X-coordinate of the location to be checked. + * y - Y-coordinate of the location to be checked. + */ +mxGraph.prototype.intersects = function(state, x, y) +{ + if (state != null) + { + var pts = state.absolutePoints; + + if (pts != null) + { + var t2 = this.tolerance * this.tolerance; + + var pt = pts[0]; + + for (var i = 1; i<pts.length; i++) + { + var next = pts[i]; + var dist = mxUtils.ptSegDistSq( + pt.x, pt.y, next.x, next.y, x, y); + + if (dist <= t2) + { + return true; + } + + pt = next; + } + } + else if (mxUtils.contains(state, x, y)) + { + return true; + } + } + + return false; +}; + +/** + * Function: hitsSwimlaneContent + * + * Returns true if the given coordinate pair is inside the content + * are of the given swimlane. + * + * Parameters: + * + * swimlane - <mxCell> that specifies the swimlane. + * x - X-coordinate of the mouse event. + * y - Y-coordinate of the mouse event. + */ +mxGraph.prototype.hitsSwimlaneContent = function(swimlane, x, y) +{ + var state = this.getView().getState(swimlane); + var size = this.getStartSize(swimlane); + + if (state != null) + { + var scale = this.getView().getScale(); + x -= state.x; + y -= state.y; + + if (size.width > 0 && x > 0 && x > size.width * scale) + { + return true; + } + else if (size.height > 0 && y > 0 && y > size.height * scale) + { + return true; + } + } + + return false; +}; + +/** + * Function: getChildVertices + * + * Returns the visible child vertices of the given parent. + * + * Parameters: + * + * parent - <mxCell> whose children should be returned. + */ +mxGraph.prototype.getChildVertices = function(parent) +{ + return this.getChildCells(parent, true, false); +}; + +/** + * Function: getChildEdges + * + * Returns the visible child edges of the given parent. + * + * Parameters: + * + * parent - <mxCell> whose child vertices should be returned. + */ +mxGraph.prototype.getChildEdges = function(parent) +{ + return this.getChildCells(parent, false, true); +}; + +/** + * Function: getChildCells + * + * Returns the visible child vertices or edges in the given parent. If + * vertices and edges is false, then all children are returned. + * + * Parameters: + * + * parent - <mxCell> whose children should be returned. + * vertices - Optional boolean that specifies if child vertices should + * be returned. Default is false. + * edges - Optional boolean that specifies if child edges should + * be returned. Default is false. + */ +mxGraph.prototype.getChildCells = function(parent, vertices, edges) +{ + parent = (parent != null) ? parent : this.getDefaultParent(); + vertices = (vertices != null) ? vertices : false; + edges = (edges != null) ? edges : false; + + var cells = this.model.getChildCells(parent, vertices, edges); + var result = []; + + // Filters out the non-visible child cells + for (var i = 0; i < cells.length; i++) + { + if (this.isCellVisible(cells[i])) + { + result.push(cells[i]); + } + } + + return result; +}; + +/** + * Function: getConnections + * + * Returns all visible edges connected to the given cell without loops. + * + * Parameters: + * + * cell - <mxCell> whose connections should be returned. + * parent - Optional parent of the opposite end for a connection to be + * returned. + */ +mxGraph.prototype.getConnections = function(cell, parent) +{ + return this.getEdges(cell, parent, true, true, false); +}; + +/** + * Function: getIncomingEdges + * + * Returns the visible incoming edges for the given cell. If the optional + * parent argument is specified, then only child edges of the given parent + * are returned. + * + * Parameters: + * + * cell - <mxCell> whose incoming edges should be returned. + * parent - Optional parent of the opposite end for an edge to be + * returned. + */ +mxGraph.prototype.getIncomingEdges = function(cell, parent) +{ + return this.getEdges(cell, parent, true, false, false); +}; + +/** + * Function: getOutgoingEdges + * + * Returns the visible outgoing edges for the given cell. If the optional + * parent argument is specified, then only child edges of the given parent + * are returned. + * + * Parameters: + * + * cell - <mxCell> whose outgoing edges should be returned. + * parent - Optional parent of the opposite end for an edge to be + * returned. + */ +mxGraph.prototype.getOutgoingEdges = function(cell, parent) +{ + return this.getEdges(cell, parent, false, true, false); +}; + +/** + * Function: getEdges + * + * Returns the incoming and/or outgoing edges for the given cell. + * If the optional parent argument is specified, then only edges are returned + * where the opposite is in the given parent cell. If at least one of incoming + * or outgoing is true, then loops are ignored, if both are false, then all + * edges connected to the given cell are returned including loops. + * + * Parameters: + * + * cell - <mxCell> whose edges should be returned. + * parent - Optional parent of the opposite end for an edge to be + * returned. + * incoming - Optional boolean that specifies if incoming edges should + * be included in the result. Default is true. + * outgoing - Optional boolean that specifies if outgoing edges should + * be included in the result. Default is true. + * includeLoops - Optional boolean that specifies if loops should be + * included in the result. Default is true. + * recurse - Optional boolean the specifies if the parent specified only + * need be an ancestral parent, true, or the direct parent, false. + * Default is false + */ +mxGraph.prototype.getEdges = function(cell, parent, incoming, outgoing, includeLoops, recurse) +{ + incoming = (incoming != null) ? incoming : true; + outgoing = (outgoing != null) ? outgoing : true; + includeLoops = (includeLoops != null) ? includeLoops : true; + recurse = (recurse != null) ? recurse : false; + + var edges = []; + var isCollapsed = this.isCellCollapsed(cell); + var childCount = this.model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + var child = this.model.getChildAt(cell, i); + + if (isCollapsed || !this.isCellVisible(child)) + { + edges = edges.concat(this.model.getEdges(child, incoming, outgoing)); + } + } + + edges = edges.concat(this.model.getEdges(cell, incoming, outgoing)); + var result = []; + + for (var i = 0; i < edges.length; i++) + { + var state = this.view.getState(edges[i]); + + var source = (state != null) ? state.getVisibleTerminal(true) : this.view.getVisibleTerminal(edges[i], true); + var target = (state != null) ? state.getVisibleTerminal(false) : this.view.getVisibleTerminal(edges[i], false); + + if ((includeLoops && source == target) || ((source != target) && ((incoming && + target == cell && (parent == null || this.isValidAncestor(source, parent, recurse))) || + (outgoing && source == cell && (parent == null || + this.isValidAncestor(target, parent, recurse)))))) + { + result.push(edges[i]); + } + } + + return result; +}; + +/** + * Function: isValidAncestor + * + * Returns whether or not the specified parent is a valid + * ancestor of the specified cell, either direct or indirectly + * based on whether ancestor recursion is enabled. + * + * Parameters: + * + * cell - <mxCell> the possible child cell + * parent - <mxCell> the possible parent cell + * recurse - boolean whether or not to recurse the child ancestors + */ +mxGraph.prototype.isValidAncestor = function(cell, parent, recurse) +{ + return (recurse ? this.model.isAncestor(parent, cell) : this.model + .getParent(cell) == parent); +}; + +/** + * Function: getOpposites + * + * Returns all distinct visible opposite cells for the specified terminal + * on the given edges. + * + * Parameters: + * + * edges - Array of <mxCells> that contains the edges whose opposite + * terminals should be returned. + * terminal - Terminal that specifies the end whose opposite should be + * returned. + * source - Optional boolean that specifies if source terminals should be + * included in the result. Default is true. + * targets - Optional boolean that specifies if targer terminals should be + * included in the result. Default is true. + */ +mxGraph.prototype.getOpposites = function(edges, terminal, sources, targets) +{ + sources = (sources != null) ? sources : true; + targets = (targets != null) ? targets : true; + + var terminals = []; + + // Implements set semantic on the terminals array using a string + // representation of each cell in an associative array lookup + var hash = new Object(); + + if (edges != null) + { + for (var i = 0; i < edges.length; i++) + { + var state = this.view.getState(edges[i]); + + var source = (state != null) ? state.getVisibleTerminal(true) : this.view.getVisibleTerminal(edges[i], true); + var target = (state != null) ? state.getVisibleTerminal(false) : this.view.getVisibleTerminal(edges[i], false); + + // Checks if the terminal is the source of the edge and if the + // target should be stored in the result + if (source == terminal && target != null && + target != terminal && targets) + { + var id = mxCellPath.create(target); + + if (hash[id] == null) + { + hash[id] = target; + terminals.push(target); + } + } + + // Checks if the terminal is the taget of the edge and if the + // source should be stored in the result + else if (target == terminal && source != null && + source != terminal && sources) + { + var id = mxCellPath.create(source); + + if (hash[id] == null) + { + hash[id] = source; + terminals.push(source); + } + } + } + } + + return terminals; +}; + +/** + * Function: getEdgesBetween + * + * Returns the edges between the given source and target. This takes into + * account collapsed and invisible cells and returns the connected edges + * as displayed on the screen. + * + * Parameters: + * + * source - + * target - + * directed - + */ +mxGraph.prototype.getEdgesBetween = function(source, target, directed) +{ + directed = (directed != null) ? directed : false; + var edges = this.getEdges(source); + var result = []; + + // Checks if the edge is connected to the correct + // cell and returns the first match + for (var i = 0; i < edges.length; i++) + { + var state = this.view.getState(edges[i]); + + var src = (state != null) ? state.getVisibleTerminal(true) : this.view.getVisibleTerminal(edges[i], true); + var trg = (state != null) ? state.getVisibleTerminal(false) : this.view.getVisibleTerminal(edges[i], false); + + if ((src == source && trg == target) || (!directed && src == target && trg == source)) + { + result.push(edges[i]); + } + } + + return result; +}; + +/** + * Function: getPointForEvent + * + * Returns an <mxPoint> representing the given event in the unscaled, + * non-translated coordinate space of <container> and applies the grid. + * + * Parameters: + * + * evt - Mousevent that contains the mouse pointer location. + * addOffset - Optional boolean that specifies if the position should be + * offset by half of the <gridSize>. Default is true. + */ + mxGraph.prototype.getPointForEvent = function(evt, addOffset) + { + var p = mxUtils.convertPoint(this.container, + mxEvent.getClientX(evt), mxEvent.getClientY(evt)); + + var s = this.view.scale; + var tr = this.view.translate; + var off = (addOffset != false) ? this.gridSize / 2 : 0; + + p.x = this.snap(p.x / s - tr.x - off); + p.y = this.snap(p.y / s - tr.y - off); + + return p; + }; + +/** + * Function: getCells + * + * Returns the children of the given parent that are contained in the given + * rectangle (x, y, width, height). The result is added to the optional + * result array, which is returned from the function. If no result array + * is specified then a new array is created and returned. + * + * Parameters: + * + * x - X-coordinate of the rectangle. + * y - Y-coordinate of the rectangle. + * width - Width of the rectangle. + * height - Height of the rectangle. + * parent - <mxCell> whose children should be checked. Default is + * <defaultParent>. + * result - Optional array to store the result in. + */ +mxGraph.prototype.getCells = function(x, y, width, height, parent, result) +{ + result = (result != null) ? result : []; + + if (width > 0 || height > 0) + { + var right = x + width; + var bottom = y + height; + + parent = parent || this.getDefaultParent(); + + if (parent != null) + { + var childCount = this.model.getChildCount(parent); + + for (var i = 0; i < childCount; i++) + { + var cell = this.model.getChildAt(parent, i); + var state = this.view.getState(cell); + + if (this.isCellVisible(cell) && state != null) + { + if (state.x >= x && state.y >= y && + state.x + state.width <= right && + state.y + state.height <= bottom) + { + result.push(cell); + } + else + { + this.getCells(x, y, width, height, cell, result); + } + } + } + } + } + + return result; +}; + +/** + * Function: getCellsBeyond + * + * Returns the children of the given parent that are contained in the + * halfpane from the given point (x0, y0) rightwards and/or downwards + * depending on rightHalfpane and bottomHalfpane. + * + * Parameters: + * + * x0 - X-coordinate of the origin. + * y0 - Y-coordinate of the origin. + * parent - Optional <mxCell> whose children should be checked. Default is + * <defaultParent>. + * rightHalfpane - Boolean indicating if the cells in the right halfpane + * from the origin should be returned. + * bottomHalfpane - Boolean indicating if the cells in the bottom halfpane + * from the origin should be returned. + */ +mxGraph.prototype.getCellsBeyond = function(x0, y0, parent, rightHalfpane, bottomHalfpane) +{ + var result = []; + + if (rightHalfpane || bottomHalfpane) + { + if (parent == null) + { + parent = this.getDefaultParent(); + } + + if (parent != null) + { + var childCount = this.model.getChildCount(parent); + + for (var i = 0; i < childCount; i++) + { + var child = this.model.getChildAt(parent, i); + var state = this.view.getState(child); + + if (this.isCellVisible(child) && state != null) + { + if ((!rightHalfpane || + state.x >= x0) && + (!bottomHalfpane || + state.y >= y0)) + { + result.push(child); + } + } + } + } + } + + return result; +}; + +/** + * Function: findTreeRoots + * + * Returns all children in the given parent which do not have incoming + * edges. If the result is empty then the with the greatest difference + * between incoming and outgoing edges is returned. + * + * Parameters: + * + * parent - <mxCell> whose children should be checked. + * isolate - Optional boolean that specifies if edges should be ignored if + * the opposite end is not a child of the given parent cell. Default is + * false. + * invert - Optional boolean that specifies if outgoing or incoming edges + * should be counted for a tree root. If false then outgoing edges will be + * counted. Default is false. + */ +mxGraph.prototype.findTreeRoots = function(parent, isolate, invert) +{ + isolate = (isolate != null) ? isolate : false; + invert = (invert != null) ? invert : false; + var roots = []; + + if (parent != null) + { + var model = this.getModel(); + var childCount = model.getChildCount(parent); + var best = null; + var maxDiff = 0; + + for (var i=0; i<childCount; i++) + { + var cell = model.getChildAt(parent, i); + + if (this.model.isVertex(cell) && this.isCellVisible(cell)) + { + var conns = this.getConnections(cell, (isolate) ? parent : null); + var fanOut = 0; + var fanIn = 0; + + for (var j = 0; j < conns.length; j++) + { + var src = this.view.getVisibleTerminal(conns[j], true); + + if (src == cell) + { + fanOut++; + } + else + { + fanIn++; + } + } + + if ((invert && fanOut == 0 && fanIn > 0) || + (!invert && fanIn == 0 && fanOut > 0)) + { + roots.push(cell); + } + + var diff = (invert) ? fanIn - fanOut : fanOut - fanIn; + + if (diff > maxDiff) + { + maxDiff = diff; + best = cell; + } + } + } + + if (roots.length == 0 && best != null) + { + roots.push(best); + } + } + + return roots; +}; + +/** + * Function: traverse + * + * Traverses the (directed) graph invoking the given function for each + * visited vertex and edge. The function is invoked with the current vertex + * and the incoming edge as a parameter. This implementation makes sure + * each vertex is only visited once. The function may return false if the + * traversal should stop at the given vertex. + * + * Example: + * + * (code) + * mxLog.show(); + * var cell = graph.getSelectionCell(); + * graph.traverse(cell, false, function(vertex, edge) + * { + * mxLog.debug(graph.getLabel(vertex)); + * }); + * (end) + * + * Parameters: + * + * vertex - <mxCell> that represents the vertex where the traversal starts. + * directed - Optional boolean indicating if edges should only be traversed + * from source to target. Default is true. + * func - Visitor function that takes the current vertex and the incoming + * edge as arguments. The traversal stops if the function returns false. + * edge - Optional <mxCell> that represents the incoming edge. This is + * null for the first step of the traversal. + * visited - Optional array of cell paths for the visited cells. + */ +mxGraph.prototype.traverse = function(vertex, directed, func, edge, visited) +{ + if (func != null && vertex != null) + { + directed = (directed != null) ? directed : true; + visited = visited || []; + var id = mxCellPath.create(vertex); + + if (visited[id] == null) + { + visited[id] = vertex; + var result = func(vertex, edge); + + if (result == null || result) + { + var edgeCount = this.model.getEdgeCount(vertex); + + if (edgeCount > 0) + { + for (var i = 0; i < edgeCount; i++) + { + var e = this.model.getEdgeAt(vertex, i); + var isSource = this.model.getTerminal(e, true) == vertex; + + if (!directed || isSource) + { + var next = this.model.getTerminal(e, !isSource); + this.traverse(next, directed, func, e, visited); + } + } + } + } + } + } +}; + +/** + * Group: Selection + */ + +/** + * Function: isCellSelected + * + * Returns true if the given cell is selected. + * + * Parameters: + * + * cell - <mxCell> for which the selection state should be returned. + */ +mxGraph.prototype.isCellSelected = function(cell) +{ + return this.getSelectionModel().isSelected(cell); +}; + +/** + * Function: isSelectionEmpty + * + * Returns true if the selection is empty. + */ +mxGraph.prototype.isSelectionEmpty = function() +{ + return this.getSelectionModel().isEmpty(); +}; + +/** + * Function: clearSelection + * + * Clears the selection using <mxGraphSelectionModel.clear>. + */ +mxGraph.prototype.clearSelection = function() +{ + return this.getSelectionModel().clear(); +}; + +/** + * Function: getSelectionCount + * + * Returns the number of selected cells. + */ +mxGraph.prototype.getSelectionCount = function() +{ + return this.getSelectionModel().cells.length; +}; + +/** + * Function: getSelectionCell + * + * Returns the first cell from the array of selected <mxCells>. + */ +mxGraph.prototype.getSelectionCell = function() +{ + return this.getSelectionModel().cells[0]; +}; + +/** + * Function: getSelectionCells + * + * Returns the array of selected <mxCells>. + */ +mxGraph.prototype.getSelectionCells = function() +{ + return this.getSelectionModel().cells.slice(); +}; + +/** + * Function: setSelectionCell + * + * Sets the selection cell. + * + * Parameters: + * + * cell - <mxCell> to be selected. + */ +mxGraph.prototype.setSelectionCell = function(cell) +{ + this.getSelectionModel().setCell(cell); +}; + +/** + * Function: setSelectionCells + * + * Sets the selection cell. + * + * Parameters: + * + * cells - Array of <mxCells> to be selected. + */ +mxGraph.prototype.setSelectionCells = function(cells) +{ + this.getSelectionModel().setCells(cells); +}; + +/** + * Function: addSelectionCell + * + * Adds the given cell to the selection. + * + * Parameters: + * + * cell - <mxCell> to be add to the selection. + */ +mxGraph.prototype.addSelectionCell = function(cell) +{ + this.getSelectionModel().addCell(cell); +}; + +/** + * Function: addSelectionCells + * + * Adds the given cells to the selection. + * + * Parameters: + * + * cells - Array of <mxCells> to be added to the selection. + */ +mxGraph.prototype.addSelectionCells = function(cells) +{ + this.getSelectionModel().addCells(cells); +}; + +/** + * Function: removeSelectionCell + * + * Removes the given cell from the selection. + * + * Parameters: + * + * cell - <mxCell> to be removed from the selection. + */ +mxGraph.prototype.removeSelectionCell = function(cell) +{ + this.getSelectionModel().removeCell(cell); +}; + +/** + * Function: removeSelectionCells + * + * Removes the given cells from the selection. + * + * Parameters: + * + * cells - Array of <mxCells> to be removed from the selection. + */ +mxGraph.prototype.removeSelectionCells = function(cells) +{ + this.getSelectionModel().removeCells(cells); +}; + +/** + * Function: selectRegion + * + * Selects and returns the cells inside the given rectangle for the + * specified event. + * + * Parameters: + * + * rect - <mxRectangle> that represents the region to be selected. + * evt - Mouseevent that triggered the selection. + */ +mxGraph.prototype.selectRegion = function(rect, evt) +{ + var cells = this.getCells(rect.x, rect.y, rect.width, rect.height); + this.selectCellsForEvent(cells, evt); + + return cells; +}; + +/** + * Function: selectNextCell + * + * Selects the next cell. + */ +mxGraph.prototype.selectNextCell = function() +{ + this.selectCell(true); +}; + +/** + * Function: selectPreviousCell + * + * Selects the previous cell. + */ +mxGraph.prototype.selectPreviousCell = function() +{ + this.selectCell(); +}; + +/** + * Function: selectParentCell + * + * Selects the parent cell. + */ +mxGraph.prototype.selectParentCell = function() +{ + this.selectCell(false, true); +}; + +/** + * Function: selectChildCell + * + * Selects the first child cell. + */ +mxGraph.prototype.selectChildCell = function() +{ + this.selectCell(false, false, true); +}; + +/** + * Function: selectCell + * + * Selects the next, parent, first child or previous cell, if all arguments + * are false. + * + * Parameters: + * + * isNext - Boolean indicating if the next cell should be selected. + * isParent - Boolean indicating if the parent cell should be selected. + * isChild - Boolean indicating if the first child cell should be selected. + */ +mxGraph.prototype.selectCell = function(isNext, isParent, isChild) +{ + var sel = this.selectionModel; + var cell = (sel.cells.length > 0) ? sel.cells[0] : null; + + if (sel.cells.length > 1) + { + sel.clear(); + } + + var parent = (cell != null) ? + this.model.getParent(cell) : + this.getDefaultParent(); + + var childCount = this.model.getChildCount(parent); + + if (cell == null && childCount > 0) + { + var child = this.model.getChildAt(parent, 0); + this.setSelectionCell(child); + } + else if ((cell == null || isParent) && + this.view.getState(parent) != null && + this.model.getGeometry(parent) != null) + { + if (this.getCurrentRoot() != parent) + { + this.setSelectionCell(parent); + } + } + else if (cell != null && isChild) + { + var tmp = this.model.getChildCount(cell); + + if (tmp > 0) + { + var child = this.model.getChildAt(cell, 0); + this.setSelectionCell(child); + } + } + else if (childCount > 0) + { + var i = parent.getIndex(cell); + + if (isNext) + { + i++; + var child = this.model.getChildAt(parent, i % childCount); + this.setSelectionCell(child); + } + else + { + i--; + var index = (i < 0) ? childCount - 1 : i; + var child = this.model.getChildAt(parent, index); + this.setSelectionCell(child); + } + } +}; + +/** + * Function: selectAll + * + * Selects all children of the given parent cell or the children of the + * default parent if no parent is specified. To select leaf vertices and/or + * edges use <selectCells>. + * + * Parameters: + * + * parent - Optional <mxCell> whose children should be selected. + * Default is <defaultParent>. + */ +mxGraph.prototype.selectAll = function(parent) +{ + parent = parent || this.getDefaultParent(); + + var children = this.model.getChildren(parent); + + if (children != null) + { + this.setSelectionCells(children); + } +}; + +/** + * Function: selectVertices + * + * Select all vertices inside the given parent or the default parent. + */ +mxGraph.prototype.selectVertices = function(parent) +{ + this.selectCells(true, false, parent); +}; + +/** + * Function: selectVertices + * + * Select all vertices inside the given parent or the default parent. + */ +mxGraph.prototype.selectEdges = function(parent) +{ + this.selectCells(false, true, parent); +}; + +/** + * Function: selectCells + * + * Selects all vertices and/or edges depending on the given boolean + * arguments recursively, starting at the given parent or the default + * parent if no parent is specified. Use <selectAll> to select all cells. + * + * Parameters: + * + * vertices - Boolean indicating if vertices should be selected. + * edges - Boolean indicating if edges should be selected. + * parent - Optional <mxCell> that acts as the root of the recursion. + * Default is <defaultParent>. + */ +mxGraph.prototype.selectCells = function(vertices, edges, parent) +{ + parent = parent || this.getDefaultParent(); + + var filter = mxUtils.bind(this, function(cell) + { + return this.view.getState(cell) != null && + this.model.getChildCount(cell) == 0 && + ((this.model.isVertex(cell) && vertices) || + (this.model.isEdge(cell) && edges)); + }); + + var cells = this.model.filterDescendants(filter, parent); + this.setSelectionCells(cells); +}; + +/** + * Function: selectCellForEvent + * + * Selects the given cell by either adding it to the selection or + * replacing the selection depending on whether the given mouse event is a + * toggle event. + * + * Parameters: + * + * cell - <mxCell> to be selected. + * evt - Optional mouseevent that triggered the selection. + */ +mxGraph.prototype.selectCellForEvent = function(cell, evt) +{ + var isSelected = this.isCellSelected(cell); + + if (this.isToggleEvent(evt)) + { + if (isSelected) + { + this.removeSelectionCell(cell); + } + else + { + this.addSelectionCell(cell); + } + } + else if (!isSelected || this.getSelectionCount() != 1) + { + this.setSelectionCell(cell); + } +}; + +/** + * Function: selectCellsForEvent + * + * Selects the given cells by either adding them to the selection or + * replacing the selection depending on whether the given mouse event is a + * toggle event. + * + * Parameters: + * + * cells - Array of <mxCells> to be selected. + * evt - Optional mouseevent that triggered the selection. + */ +mxGraph.prototype.selectCellsForEvent = function(cells, evt) +{ + if (this.isToggleEvent(evt)) + { + this.addSelectionCells(cells); + } + else + { + this.setSelectionCells(cells); + } +}; + +/** + * Group: Selection state + */ + +/** + * Function: createHandler + * + * Creates a new handler for the given cell state. This implementation + * returns a new <mxEdgeHandler> of the corresponding cell is an edge, + * otherwise it returns an <mxVertexHandler>. + * + * Parameters: + * + * state - <mxCellState> whose handler should be created. + */ +mxGraph.prototype.createHandler = function(state) +{ + var result = null; + + if (state != null) + { + if (this.model.isEdge(state.cell)) + { + var style = this.view.getEdgeStyle(state); + + if (this.isLoop(state) || + style == mxEdgeStyle.ElbowConnector || + style == mxEdgeStyle.SideToSide || + style == mxEdgeStyle.TopToBottom) + { + result = new mxElbowEdgeHandler(state); + } + else if (style == mxEdgeStyle.SegmentConnector || + style == mxEdgeStyle.OrthConnector) + { + result = new mxEdgeSegmentHandler(state); + } + else + { + result = new mxEdgeHandler(state); + } + } + else + { + result = new mxVertexHandler(state); + } + } + + return result; +}; + +/** + * Group: Graph events + */ + +/** + * Function: addMouseListener + * + * Adds a listener to the graph event dispatch loop. The listener + * must implement the mouseDown, mouseMove and mouseUp methods + * as shown in the <mxMouseEvent> class. + * + * Parameters: + * + * listener - Listener to be added to the graph event listeners. + */ +mxGraph.prototype.addMouseListener = function(listener) +{ + if (this.mouseListeners == null) + { + this.mouseListeners = []; + } + + this.mouseListeners.push(listener); +}; + +/** + * Function: removeMouseListener + * + * Removes the specified graph listener. + * + * Parameters: + * + * listener - Listener to be removed from the graph event listeners. + */ +mxGraph.prototype.removeMouseListener = function(listener) +{ + if (this.mouseListeners != null) + { + for (var i = 0; i < this.mouseListeners.length; i++) + { + if (this.mouseListeners[i] == listener) + { + this.mouseListeners.splice(i, 1); + break; + } + } + } +}; + +/** + * Function: updateMouseEvent + * + * Sets the graphX and graphY properties if the given <mxMouseEvent> if + * required. + */ +mxGraph.prototype.updateMouseEvent = function(me) +{ + if (me.graphX == null || me.graphY == null) + { + var pt = mxUtils.convertPoint(this.container, me.getX(), me.getY()); + + me.graphX = pt.x - this.panDx; + me.graphY = pt.y - this.panDy; + } +}; + +/** + * Function: fireMouseEvent + * + * Dispatches the given event in the graph event dispatch loop. Possible + * event names are <mxEvent.MOUSE_DOWN>, <mxEvent.MOUSE_MOVE> and + * <mxEvent.MOUSE_UP>. All listeners are invoked for all events regardless + * of the consumed state of the event. + * + * Parameters: + * + * evtName - String that specifies the type of event to be dispatched. + * me - <mxMouseEvent> to be fired. + * sender - Optional sender argument. Default is this. + */ +mxGraph.prototype.fireMouseEvent = function(evtName, me, sender) +{ + if (sender == null) + { + sender = this; + } + + // Updates the graph coordinates in the event + this.updateMouseEvent(me); + + // Makes sure we have a uniform event-sequence across all + // browsers for a double click. Since evt.detail == 2 is only + // available on Firefox we use the fact that each mousedown + // must be followed by a mouseup, all out-of-sync downs + // will be dropped silently. + if (evtName == mxEvent.MOUSE_DOWN) + { + this.isMouseDown = true; + } + + // Detects and processes double taps for touch-based devices + // which do not have native double click events + if (mxClient.IS_TOUCH && this.doubleTapEnabled && evtName == mxEvent.MOUSE_DOWN) + { + var currentTime = new Date().getTime(); + + if (currentTime - this.lastTouchTime < this.doubleTapTimeout && + Math.abs(this.lastTouchX - me.getX()) < this.doubleTapTolerance && + Math.abs(this.lastTouchY - me.getY()) < this.doubleTapTolerance) + { + // FIXME: The actual editing should start on MOUSE_UP event but + // the detection of the double click should use the mouse_down event + // to make it consistent with behaviour in browser with mouse. + this.lastTouchTime = 0; + this.dblClick(me.getEvent(), me.getCell()); + + // Stop bubbling but do not consume to make sure the device + // can bring up the virtual keyboard for editing + me.getEvent().cancelBubble = true; + } + else + { + this.lastTouchX = me.getX(); + this.lastTouchY = me.getY(); + this.lastTouchTime = currentTime; + } + } + + // Workaround for IE9 standards mode ignoring tolerance for double clicks + var noDoubleClick = me.getEvent().detail/*clickCount*/ != 2; + + if (mxClient.IS_IE && document.compatMode == 'CSS1Compat') + { + if ((this.lastMouseX != null && Math.abs(this.lastMouseX - me.getX()) > this.doubleTapTolerance) || + (this.lastMouseY != null && Math.abs(this.lastMouseY - me.getY()) > this.doubleTapTolerance)) + { + noDoubleClick = true; + } + + if (evtName == mxEvent.MOUSE_UP) + { + this.lastMouseX = me.getX(); + this.lastMouseY = me.getY(); + } + } + + // Filters too many mouse ups when the mouse is down + if ((evtName != mxEvent.MOUSE_UP || this.isMouseDown) && noDoubleClick) + { + if (evtName == mxEvent.MOUSE_UP) + { + this.isMouseDown = false; + } + + if (!this.isEditing() && (mxClient.IS_OP || mxClient.IS_SF || mxClient.IS_GC || + (mxClient.IS_IE && mxClient.IS_SVG) || me.getEvent().target != this.container)) + { + if (evtName == mxEvent.MOUSE_MOVE && this.isMouseDown && this.autoScroll) + { + this.scrollPointToVisible(me.getGraphX(), me.getGraphY(), this.autoExtend); + } + + if (this.mouseListeners != null) + { + var args = [sender, me]; + + // Does not change returnValue in Opera + me.getEvent().returnValue = true; + + for (var i = 0; i < this.mouseListeners.length; i++) + { + var l = this.mouseListeners[i]; + + if (evtName == mxEvent.MOUSE_DOWN) + { + l.mouseDown.apply(l, args); + } + else if (evtName == mxEvent.MOUSE_MOVE) + { + l.mouseMove.apply(l, args); + } + else if (evtName == mxEvent.MOUSE_UP) + { + l.mouseUp.apply(l, args); + } + } + } + + // Invokes the click handler + if (evtName == mxEvent.MOUSE_UP) + { + this.click(me); + } + } + } + else if (evtName == mxEvent.MOUSE_UP) + { + this.isMouseDown = false; + } +}; + +/** + * Function: destroy + * + * Destroys the graph and all its resources. + */ +mxGraph.prototype.destroy = function() +{ + if (!this.destroyed) + { + this.destroyed = true; + + if (this.tooltipHandler != null) + { + this.tooltipHandler.destroy(); + } + + if (this.selectionCellsHandler != null) + { + this.selectionCellsHandler.destroy(); + } + + if (this.panningHandler != null) + { + this.panningHandler.destroy(); + } + + if (this.connectionHandler != null) + { + this.connectionHandler.destroy(); + } + + if (this.graphHandler != null) + { + this.graphHandler.destroy(); + } + + if (this.cellEditor != null) + { + this.cellEditor.destroy(); + } + + if (this.view != null) + { + this.view.destroy(); + } + + if (this.model != null && this.graphModelChangeListener != null) + { + this.model.removeListener(this.graphModelChangeListener); + this.graphModelChangeListener = null; + } + + this.container = null; + } +}; diff --git a/src/js/view/mxGraphSelectionModel.js b/src/js/view/mxGraphSelectionModel.js new file mode 100644 index 0000000..5cd16a8 --- /dev/null +++ b/src/js/view/mxGraphSelectionModel.js @@ -0,0 +1,435 @@ +/** + * $Id: mxGraphSelectionModel.js,v 1.14 2011-11-25 10:16:08 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxGraphSelectionModel + * + * Implements the selection model for a graph. Here is a listener that handles + * all removed selection cells. + * + * (code) + * graph.getSelectionModel().addListener(mxEvent.CHANGE, function(sender, evt) + * { + * var cells = evt.getProperty('added'); + * + * for (var i = 0; i < cells.length; i++) + * { + * // Handle cells[i]... + * } + * }); + * (end) + * + * Event: mxEvent.UNDO + * + * Fires after the selection was changed in <changeSelection>. The + * <code>edit</code> property contains the <mxUndoableEdit> which contains the + * <mxSelectionChange>. + * + * Event: mxEvent.CHANGE + * + * Fires after the selection changes by executing an <mxSelectionChange>. The + * <code>added</code> and <code>removed</code> properties contain arrays of + * cells that have been added to or removed from the selection, respectively. + * + * Constructor: mxGraphSelectionModel + * + * Constructs a new graph selection model for the given <mxGraph>. + * + * Parameters: + * + * graph - Reference to the enclosing <mxGraph>. + */ +function mxGraphSelectionModel(graph) +{ + this.graph = graph; + this.cells = []; +}; + +/** + * Extends mxEventSource. + */ +mxGraphSelectionModel.prototype = new mxEventSource(); +mxGraphSelectionModel.prototype.constructor = mxGraphSelectionModel; + +/** + * Variable: doneResource + * + * Specifies the resource key for the status message after a long operation. + * If the resource for this key does not exist then the value is used as + * the status message. Default is 'done'. + */ +mxGraphSelectionModel.prototype.doneResource = (mxClient.language != 'none') ? 'done' : ''; + +/** + * Variable: updatingSelectionResource + * + * Specifies the resource key for the status message while the selection is + * being updated. If the resource for this key does not exist then the + * value is used as the status message. Default is 'updatingSelection'. + */ +mxGraphSelectionModel.prototype.updatingSelectionResource = (mxClient.language != 'none') ? 'updatingSelection' : ''; + +/** + * Variable: graph + * + * Reference to the enclosing <mxGraph>. + */ +mxGraphSelectionModel.prototype.graph = null; + +/** + * Variable: singleSelection + * + * Specifies if only one selected item at a time is allowed. + * Default is false. + */ +mxGraphSelectionModel.prototype.singleSelection = false; + +/** + * Function: isSingleSelection + * + * Returns <singleSelection> as a boolean. + */ +mxGraphSelectionModel.prototype.isSingleSelection = function() +{ + return this.singleSelection; +}; + +/** + * Function: setSingleSelection + * + * Sets the <singleSelection> flag. + * + * Parameters: + * + * singleSelection - Boolean that specifies the new value for + * <singleSelection>. + */ +mxGraphSelectionModel.prototype.setSingleSelection = function(singleSelection) +{ + this.singleSelection = singleSelection; +}; + +/** + * Function: isSelected + * + * Returns true if the given <mxCell> is selected. + */ +mxGraphSelectionModel.prototype.isSelected = function(cell) +{ + if (cell != null) + { + return mxUtils.indexOf(this.cells, cell) >= 0; + } + + return false; +}; + +/** + * Function: isEmpty + * + * Returns true if no cells are currently selected. + */ +mxGraphSelectionModel.prototype.isEmpty = function() +{ + return this.cells.length == 0; +}; + +/** + * Function: clear + * + * Clears the selection and fires a <change> event if the selection was not + * empty. + */ +mxGraphSelectionModel.prototype.clear = function() +{ + this.changeSelection(null, this.cells); +}; + +/** + * Function: setCell + * + * Selects the specified <mxCell> using <setCells>. + * + * Parameters: + * + * cell - <mxCell> to be selected. + */ +mxGraphSelectionModel.prototype.setCell = function(cell) +{ + if (cell != null) + { + this.setCells([cell]); + } +}; + +/** + * Function: setCells + * + * Selects the given array of <mxCells> and fires a <change> event. + * + * Parameters: + * + * cells - Array of <mxCells> to be selected. + */ +mxGraphSelectionModel.prototype.setCells = function(cells) +{ + if (cells != null) + { + if (this.singleSelection) + { + cells = [this.getFirstSelectableCell(cells)]; + } + + var tmp = []; + + for (var i = 0; i < cells.length; i++) + { + if (this.graph.isCellSelectable(cells[i])) + { + tmp.push(cells[i]); + } + } + + this.changeSelection(tmp, this.cells); + } +}; + +/** + * Function: getFirstSelectableCell + * + * Returns the first selectable cell in the given array of cells. + */ +mxGraphSelectionModel.prototype.getFirstSelectableCell = function(cells) +{ + if (cells != null) + { + for (var i = 0; i < cells.length; i++) + { + if (this.graph.isCellSelectable(cells[i])) + { + return cells[i]; + } + } + } + + return null; +}; + +/** + * Function: addCell + * + * Adds the given <mxCell> to the selection and fires a <select> event. + * + * Parameters: + * + * cell - <mxCell> to add to the selection. + */ +mxGraphSelectionModel.prototype.addCell = function(cell) +{ + if (cell != null) + { + this.addCells([cell]); + } +}; + +/** + * Function: addCells + * + * Adds the given array of <mxCells> to the selection and fires a <select> + * event. + * + * Parameters: + * + * cells - Array of <mxCells> to add to the selection. + */ +mxGraphSelectionModel.prototype.addCells = function(cells) +{ + if (cells != null) + { + var remove = null; + + if (this.singleSelection) + { + remove = this.cells; + cells = [this.getFirstSelectableCell(cells)]; + } + + var tmp = []; + + for (var i = 0; i < cells.length; i++) + { + if (!this.isSelected(cells[i]) && + this.graph.isCellSelectable(cells[i])) + { + tmp.push(cells[i]); + } + } + + this.changeSelection(tmp, remove); + } +}; + +/** + * Function: removeCell + * + * Removes the specified <mxCell> from the selection and fires a <select> + * event for the remaining cells. + * + * Parameters: + * + * cell - <mxCell> to remove from the selection. + */ +mxGraphSelectionModel.prototype.removeCell = function(cell) +{ + if (cell != null) + { + this.removeCells([cell]); + } +}; + +/** + * Function: removeCells + */ +mxGraphSelectionModel.prototype.removeCells = function(cells) +{ + if (cells != null) + { + var tmp = []; + + for (var i = 0; i < cells.length; i++) + { + if (this.isSelected(cells[i])) + { + tmp.push(cells[i]); + } + } + + this.changeSelection(null, tmp); + } +}; + +/** + * Function: changeSelection + * + * Inner callback to add the specified <mxCell> to the selection. No event + * is fired in this implementation. + * + * Paramters: + * + * cell - <mxCell> to add to the selection. + */ +mxGraphSelectionModel.prototype.changeSelection = function(added, removed) +{ + if ((added != null && + added.length > 0 && + added[0] != null) || + (removed != null && + removed.length > 0 && + removed[0] != null)) + { + var change = new mxSelectionChange(this, added, removed); + change.execute(); + var edit = new mxUndoableEdit(this, false); + edit.add(change); + this.fireEvent(new mxEventObject(mxEvent.UNDO, 'edit', edit)); + } +}; + +/** + * Function: cellAdded + * + * Inner callback to add the specified <mxCell> to the selection. No event + * is fired in this implementation. + * + * Paramters: + * + * cell - <mxCell> to add to the selection. + */ +mxGraphSelectionModel.prototype.cellAdded = function(cell) +{ + if (cell != null && + !this.isSelected(cell)) + { + this.cells.push(cell); + } +}; + +/** + * Function: cellRemoved + * + * Inner callback to remove the specified <mxCell> from the selection. No + * event is fired in this implementation. + * + * Parameters: + * + * cell - <mxCell> to remove from the selection. + */ +mxGraphSelectionModel.prototype.cellRemoved = function(cell) +{ + if (cell != null) + { + var index = mxUtils.indexOf(this.cells, cell); + + if (index >= 0) + { + this.cells.splice(index, 1); + } + } +}; + +/** + * Class: mxSelectionChange + * + * Action to change the current root in a view. + * + * Constructor: mxCurrentRootChange + * + * Constructs a change of the current root in the given view. + */ +function mxSelectionChange(selectionModel, added, removed) +{ + this.selectionModel = selectionModel; + this.added = (added != null) ? added.slice() : null; + this.removed = (removed != null) ? removed.slice() : null; +}; + +/** + * Function: execute + * + * Changes the current root of the view. + */ +mxSelectionChange.prototype.execute = function() +{ + var t0 = mxLog.enter('mxSelectionChange.execute'); + window.status = mxResources.get( + this.selectionModel.updatingSelectionResource) || + this.selectionModel.updatingSelectionResource; + + if (this.removed != null) + { + for (var i = 0; i < this.removed.length; i++) + { + this.selectionModel.cellRemoved(this.removed[i]); + } + } + + if (this.added != null) + { + for (var i = 0; i < this.added.length; i++) + { + this.selectionModel.cellAdded(this.added[i]); + } + } + + var tmp = this.added; + this.added = this.removed; + this.removed = tmp; + + window.status = mxResources.get(this.selectionModel.doneResource) || + this.selectionModel.doneResource; + mxLog.leave('mxSelectionChange.execute', t0); + + this.selectionModel.fireEvent(new mxEventObject(mxEvent.CHANGE, + 'added', this.added, 'removed', this.removed)); +}; diff --git a/src/js/view/mxGraphView.js b/src/js/view/mxGraphView.js new file mode 100644 index 0000000..0ef2dc8 --- /dev/null +++ b/src/js/view/mxGraphView.js @@ -0,0 +1,2545 @@ +/** + * $Id: mxGraphView.js,v 1.195 2012-11-20 09:06:07 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxGraphView + * + * Extends <mxEventSource> to implement a view for a graph. This class is in + * charge of computing the absolute coordinates for the relative child + * geometries, the points for perimeters and edge styles and keeping them + * cached in <mxCellStates> for faster retrieval. The states are updated + * whenever the model or the view state (translate, scale) changes. The scale + * and translate are honoured in the bounds. + * + * Event: mxEvent.UNDO + * + * Fires after the root was changed in <setCurrentRoot>. The <code>edit</code> + * property contains the <mxUndoableEdit> which contains the + * <mxCurrentRootChange>. + * + * Event: mxEvent.SCALE_AND_TRANSLATE + * + * Fires after the scale and translate have been changed in <scaleAndTranslate>. + * The <code>scale</code>, <code>previousScale</code>, <code>translate</code> + * and <code>previousTranslate</code> properties contain the new and previous + * scale and translate, respectively. + * + * Event: mxEvent.SCALE + * + * Fires after the scale was changed in <setScale>. The <code>scale</code> and + * <code>previousScale</code> properties contain the new and previous scale. + * + * Event: mxEvent.TRANSLATE + * + * Fires after the translate was changed in <setTranslate>. The + * <code>translate</code> and <code>previousTranslate</code> properties contain + * the new and previous value for translate. + * + * Event: mxEvent.DOWN and mxEvent.UP + * + * Fire if the current root is changed by executing an <mxCurrentRootChange>. + * The event name depends on the location of the root in the cell hierarchy + * with respect to the current root. The <code>root</code> and + * <code>previous</code> properties contain the new and previous root, + * respectively. + * + * Constructor: mxGraphView + * + * Constructs a new view for the given <mxGraph>. + * + * Parameters: + * + * graph - Reference to the enclosing <mxGraph>. + */ +function mxGraphView(graph) +{ + this.graph = graph; + this.translate = new mxPoint(); + this.graphBounds = new mxRectangle(); + this.states = new mxDictionary(); +}; + +/** + * Extends mxEventSource. + */ +mxGraphView.prototype = new mxEventSource(); +mxGraphView.prototype.constructor = mxGraphView; + +/** + * + */ +mxGraphView.prototype.EMPTY_POINT = new mxPoint(); + +/** + * Variable: doneResource + * + * Specifies the resource key for the status message after a long operation. + * If the resource for this key does not exist then the value is used as + * the status message. Default is 'done'. + */ +mxGraphView.prototype.doneResource = (mxClient.language != 'none') ? 'done' : ''; + +/** + * Function: updatingDocumentResource + * + * Specifies the resource key for the status message while the document is + * being updated. If the resource for this key does not exist then the + * value is used as the status message. Default is 'updatingDocument'. + */ +mxGraphView.prototype.updatingDocumentResource = (mxClient.language != 'none') ? 'updatingDocument' : ''; + +/** + * Variable: allowEval + * + * Specifies if string values in cell styles should be evaluated using + * <mxUtils.eval>. This will only be used if the string values can't be mapped + * to objects using <mxStyleRegistry>. Default is false. NOTE: Enabling this + * switch carries a possible security risk (see the section on security in + * the manual). + */ +mxGraphView.prototype.allowEval = false; + +/** + * Variable: captureDocumentGesture + * + * Specifies if a gesture should be captured when it goes outside of the + * graph container. Default is true. + */ +mxGraphView.prototype.captureDocumentGesture = true; + +/** + * Variable: rendering + * + * Specifies if shapes should be created, updated and destroyed using the + * methods of <mxCellRenderer> in <graph>. Default is true. + */ +mxGraphView.prototype.rendering = true; + +/** + * Variable: graph + * + * Reference to the enclosing <mxGraph>. + */ +mxGraphView.prototype.graph = null; + +/** + * Variable: currentRoot + * + * <mxCell> that acts as the root of the displayed cell hierarchy. + */ +mxGraphView.prototype.currentRoot = null; + +/** + * Variable: graphBounds + * + * <mxRectangle> that caches the scales, translated bounds of the current view. + */ +mxGraphView.prototype.graphBounds = null; + +/** + * Variable: scale + * + * Specifies the scale. Default is 1 (100%). + */ +mxGraphView.prototype.scale = 1; + +/** + * Variable: translate + * + * <mxPoint> that specifies the current translation. Default is a new + * empty <mxPoint>. + */ +mxGraphView.prototype.translate = null; + +/** + * Variable: updateStyle + * + * Specifies if the style should be updated in each validation step. If this + * is false then the style is only updated if the state is created or if the + * style of the cell was changed. Default is false. + */ +mxGraphView.prototype.updateStyle = false; + +/** + * Function: getGraphBounds + * + * Returns <graphBounds>. + */ +mxGraphView.prototype.getGraphBounds = function() +{ + return this.graphBounds; +}; + +/** + * Function: setGraphBounds + * + * Sets <graphBounds>. + */ +mxGraphView.prototype.setGraphBounds = function(value) +{ + this.graphBounds = value; +}; + +/** + * Function: getBounds + * + * Returns the bounds (on the screen) for the given array of <mxCells>. + * + * Parameters: + * + * cells - Array of <mxCells> to return the bounds for. + */ +mxGraphView.prototype.getBounds = function(cells) +{ + var result = null; + + if (cells != null && cells.length > 0) + { + var model = this.graph.getModel(); + + for (var i = 0; i < cells.length; i++) + { + if (model.isVertex(cells[i]) || model.isEdge(cells[i])) + { + var state = this.getState(cells[i]); + + if (state != null) + { + if (result == null) + { + result = new mxRectangle(state.x, state.y, + state.width, state.height); + } + else + { + result.add(state); + } + } + } + } + } + + return result; +}; + +/** + * Function: setCurrentRoot + * + * Sets and returns the current root and fires an <undo> event before + * calling <mxGraph.sizeDidChange>. + * + * Parameters: + * + * root - <mxCell> that specifies the root of the displayed cell hierarchy. + */ +mxGraphView.prototype.setCurrentRoot = function(root) +{ + if (this.currentRoot != root) + { + var change = new mxCurrentRootChange(this, root); + change.execute(); + var edit = new mxUndoableEdit(this, false); + edit.add(change); + this.fireEvent(new mxEventObject(mxEvent.UNDO, 'edit', edit)); + this.graph.sizeDidChange(); + } + + return root; +}; + +/** + * Function: scaleAndTranslate + * + * Sets the scale and translation and fires a <scale> and <translate> event + * before calling <revalidate> followed by <mxGraph.sizeDidChange>. + * + * Parameters: + * + * scale - Decimal value that specifies the new scale (1 is 100%). + * dx - X-coordinate of the translation. + * dy - Y-coordinate of the translation. + */ +mxGraphView.prototype.scaleAndTranslate = function(scale, dx, dy) +{ + var previousScale = this.scale; + var previousTranslate = new mxPoint(this.translate.x, this.translate.y); + + if (this.scale != scale || this.translate.x != dx || this.translate.y != dy) + { + this.scale = scale; + + this.translate.x = dx; + this.translate.y = dy; + + if (this.isEventsEnabled()) + { + this.revalidate(); + this.graph.sizeDidChange(); + } + } + + this.fireEvent(new mxEventObject(mxEvent.SCALE_AND_TRANSLATE, + 'scale', scale, 'previousScale', previousScale, + 'translate', this.translate, 'previousTranslate', previousTranslate)); +}; + +/** + * Function: getScale + * + * Returns the <scale>. + */ +mxGraphView.prototype.getScale = function() +{ + return this.scale; +}; + +/** + * Function: setScale + * + * Sets the scale and fires a <scale> event before calling <revalidate> followed + * by <mxGraph.sizeDidChange>. + * + * Parameters: + * + * value - Decimal value that specifies the new scale (1 is 100%). + */ +mxGraphView.prototype.setScale = function(value) +{ + var previousScale = this.scale; + + if (this.scale != value) + { + this.scale = value; + + if (this.isEventsEnabled()) + { + this.revalidate(); + this.graph.sizeDidChange(); + } + } + + this.fireEvent(new mxEventObject(mxEvent.SCALE, + 'scale', value, 'previousScale', previousScale)); +}; + +/** + * Function: getTranslate + * + * Returns the <translate>. + */ +mxGraphView.prototype.getTranslate = function() +{ + return this.translate; +}; + +/** + * Function: setTranslate + * + * Sets the translation and fires a <translate> event before calling + * <revalidate> followed by <mxGraph.sizeDidChange>. The translation is the + * negative of the origin. + * + * Parameters: + * + * dx - X-coordinate of the translation. + * dy - Y-coordinate of the translation. + */ +mxGraphView.prototype.setTranslate = function(dx, dy) +{ + var previousTranslate = new mxPoint(this.translate.x, this.translate.y); + + if (this.translate.x != dx || this.translate.y != dy) + { + this.translate.x = dx; + this.translate.y = dy; + + if (this.isEventsEnabled()) + { + this.revalidate(); + this.graph.sizeDidChange(); + } + } + + this.fireEvent(new mxEventObject(mxEvent.TRANSLATE, + 'translate', this.translate, 'previousTranslate', previousTranslate)); +}; + +/** + * Function: refresh + * + * Clears the view if <currentRoot> is not null and revalidates. + */ +mxGraphView.prototype.refresh = function() +{ + if (this.currentRoot != null) + { + this.clear(); + } + + this.revalidate(); +}; + +/** + * Function: revalidate + * + * Revalidates the complete view with all cell states. + */ +mxGraphView.prototype.revalidate = function() +{ + this.invalidate(); + this.validate(); +}; + +/** + * Function: clear + * + * Removes the state of the given cell and all descendants if the given + * cell is not the current root. + * + * Parameters: + * + * cell - Optional <mxCell> for which the state should be removed. Default + * is the root of the model. + * force - Boolean indicating if the current root should be ignored for + * recursion. + */ +mxGraphView.prototype.clear = function(cell, force, recurse) +{ + var model = this.graph.getModel(); + cell = cell || model.getRoot(); + force = (force != null) ? force : false; + recurse = (recurse != null) ? recurse : true; + + this.removeState(cell); + + if (recurse && (force || cell != this.currentRoot)) + { + var childCount = model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + this.clear(model.getChildAt(cell, i), force); + } + } + else + { + this.invalidate(cell); + } +}; + +/** + * Function: invalidate + * + * Invalidates the state of the given cell, all its descendants and + * connected edges. + * + * Parameters: + * + * cell - Optional <mxCell> to be invalidated. Default is the root of the + * model. + */ +mxGraphView.prototype.invalidate = function(cell, recurse, includeEdges, orderChanged) +{ + var model = this.graph.getModel(); + cell = cell || model.getRoot(); + recurse = (recurse != null) ? recurse : true; + includeEdges = (includeEdges != null) ? includeEdges : true; + orderChanged = (orderChanged != null) ? orderChanged : false; + + var state = this.getState(cell); + + if (state != null) + { + state.invalid = true; + + if (orderChanged) + { + state.orderChanged = true; + } + } + + // Recursively invalidates all descendants + if (recurse) + { + var childCount = model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + var child = model.getChildAt(cell, i); + this.invalidate(child, recurse, includeEdges, orderChanged); + } + } + + // Propagates invalidation to all connected edges + if (includeEdges) + { + var edgeCount = model.getEdgeCount(cell); + + for (var i = 0; i < edgeCount; i++) + { + this.invalidate(model.getEdgeAt(cell, i), recurse, includeEdges); + } + } +}; + +/** + * Function: validate + * + * First validates all bounds and then validates all points recursively on + * all visible cells starting at the given cell. Finally the background + * is validated using <validateBackground>. + * + * Parameters: + * + * cell - Optional <mxCell> to be used as the root of the validation. + * Default is <currentRoot> or the root of the model. + */ +mxGraphView.prototype.validate = function(cell) +{ + var t0 = mxLog.enter('mxGraphView.validate'); + window.status = mxResources.get(this.updatingDocumentResource) || + this.updatingDocumentResource; + + cell = cell || ((this.currentRoot != null) ? + this.currentRoot : + this.graph.getModel().getRoot()); + this.validateBounds(null, cell); + var graphBounds = this.validatePoints(null, cell); + + if (graphBounds == null) + { + graphBounds = new mxRectangle(); + } + + this.setGraphBounds(graphBounds); + this.validateBackground(); + + window.status = mxResources.get(this.doneResource) || + this.doneResource; + mxLog.leave('mxGraphView.validate', t0); +}; + +/** + * Function: createBackgroundPageShape + * + * Creates and returns the shape used as the background page. + * + * Parameters: + * + * bounds - <mxRectangle> that represents the bounds of the shape. + */ +mxGraphView.prototype.createBackgroundPageShape = function(bounds) +{ + return new mxRectangleShape(bounds, 'white', 'black'); +}; + +/** + * Function: validateBackground + * + * Validates the background image. + */ +mxGraphView.prototype.validateBackground = function() +{ + var bg = this.graph.getBackgroundImage(); + + if (bg != null) + { + if (this.backgroundImage == null || this.backgroundImage.image != bg.src) + { + if (this.backgroundImage != null) + { + this.backgroundImage.destroy(); + } + + var bounds = new mxRectangle(0, 0, 1, 1); + + this.backgroundImage = new mxImageShape(bounds, bg.src); + this.backgroundImage.dialect = this.graph.dialect; + this.backgroundImage.init(this.backgroundPane); + this.backgroundImage.redraw(); + } + + this.redrawBackgroundImage(this.backgroundImage, bg); + } + else if (this.backgroundImage != null) + { + this.backgroundImage.destroy(); + this.backgroundImage = null; + } + + if (this.graph.pageVisible) + { + var bounds = this.getBackgroundPageBounds(); + + if (this.backgroundPageShape == null) + { + this.backgroundPageShape = this.createBackgroundPageShape(bounds); + this.backgroundPageShape.scale = this.scale; + this.backgroundPageShape.isShadow = true; + this.backgroundPageShape.dialect = this.graph.dialect; + this.backgroundPageShape.init(this.backgroundPane); + this.backgroundPageShape.redraw(); + + // Adds listener for double click handling on background + mxEvent.addListener(this.backgroundPageShape.node, 'dblclick', + mxUtils.bind(this, function(evt) + { + this.graph.dblClick(evt); + }) + ); + + var md = (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown'; + var mm = (mxClient.IS_TOUCH) ? 'touchmove' : 'mousemove'; + var mu = (mxClient.IS_TOUCH) ? 'touchend' : 'mouseup'; + + // Adds basic listeners for graph event dispatching outside of the + // container and finishing the handling of a single gesture + mxEvent.addListener(this.backgroundPageShape.node, md, + mxUtils.bind(this, function(evt) + { + this.graph.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt)); + }) + ); + mxEvent.addListener(this.backgroundPageShape.node, mm, + mxUtils.bind(this, function(evt) + { + // Hides the tooltip if mouse is outside container + if (this.graph.tooltipHandler != null && + this.graph.tooltipHandler.isHideOnHover()) + { + this.graph.tooltipHandler.hide(); + } + + if (this.graph.isMouseDown && + !mxEvent.isConsumed(evt)) + { + this.graph.fireMouseEvent(mxEvent.MOUSE_MOVE, + new mxMouseEvent(evt)); + } + }) + ); + mxEvent.addListener(this.backgroundPageShape.node, mu, + mxUtils.bind(this, function(evt) + { + this.graph.fireMouseEvent(mxEvent.MOUSE_UP, + new mxMouseEvent(evt)); + }) + ); + } + else + { + this.backgroundPageShape.scale = this.scale; + this.backgroundPageShape.bounds = bounds; + this.backgroundPageShape.redraw(); + } + } + else if (this.backgroundPageShape != null) + { + this.backgroundPageShape.destroy(); + this.backgroundPageShape = null; + } +}; + +/** + * Function: getBackgroundPageBounds + * + * Returns the bounds for the background page. + */ +mxGraphView.prototype.getBackgroundPageBounds = function() +{ + var fmt = this.graph.pageFormat; + var ps = this.scale * this.graph.pageScale; + var bounds = new mxRectangle(this.scale * this.translate.x, this.scale * this.translate.y, + fmt.width * ps, fmt.height * ps); + + return bounds; +}; + +/** + * Function: redrawBackgroundImage + * + * Updates the bounds and redraws the background image. + * + * Example: + * + * If the background image should not be scaled, this can be replaced with + * the following. + * + * (code) + * mxGraphView.prototype.redrawBackground = function(backgroundImage, bg) + * { + * backgroundImage.bounds.x = this.translate.x; + * backgroundImage.bounds.y = this.translate.y; + * backgroundImage.bounds.width = bg.width; + * backgroundImage.bounds.height = bg.height; + * + * backgroundImage.redraw(); + * }; + * (end) + * + * Parameters: + * + * backgroundImage - <mxImageShape> that represents the background image. + * bg - <mxImage> that specifies the image and its dimensions. + */ +mxGraphView.prototype.redrawBackgroundImage = function(backgroundImage, bg) +{ + backgroundImage.scale = this.scale; + backgroundImage.bounds.x = this.scale * this.translate.x; + backgroundImage.bounds.y = this.scale * this.translate.y; + backgroundImage.bounds.width = this.scale * bg.width; + backgroundImage.bounds.height = this.scale * bg.height; + + backgroundImage.redraw(); +}; + +/** + * Function: validateBounds + * + * Validates the bounds of the given parent's child using the given parent + * state as the origin for the child. The validation is carried out + * recursively for all non-collapsed descendants. + * + * Parameters: + * + * parentState - <mxCellState> for the given parent. + * cell - <mxCell> for which the bounds in the state should be updated. + */ +mxGraphView.prototype.validateBounds = function(parentState, cell) +{ + var model = this.graph.getModel(); + var state = this.getState(cell, true); + + if (state != null && state.invalid) + { + if (!this.graph.isCellVisible(cell)) + { + this.removeState(cell); + } + + // Updates the cell state's origin + else if (cell != this.currentRoot && parentState != null) + { + state.absoluteOffset.x = 0; + state.absoluteOffset.y = 0; + state.origin.x = parentState.origin.x; + state.origin.y = parentState.origin.y; + var geo = this.graph.getCellGeometry(cell); + + if (geo != null) + { + if (!model.isEdge(cell)) + { + var offset = geo.offset || this.EMPTY_POINT; + + if (geo.relative) + { + state.origin.x += geo.x * parentState.width / + this.scale + offset.x; + state.origin.y += geo.y * parentState.height / + this.scale + offset.y; + } + else + { + state.absoluteOffset.x = this.scale * offset.x; + state.absoluteOffset.y = this.scale * offset.y; + state.origin.x += geo.x; + state.origin.y += geo.y; + } + } + + // Updates cell state's bounds + state.x = this.scale * (this.translate.x + state.origin.x); + state.y = this.scale * (this.translate.y + state.origin.y); + state.width = this.scale * geo.width; + state.height = this.scale * geo.height; + + if (model.isVertex(cell)) + { + this.updateVertexLabelOffset(state); + } + } + } + + // Applies child offset to origin + var offset = this.graph.getChildOffsetForCell(cell); + + if (offset != null) + { + state.origin.x += offset.x; + state.origin.y += offset.y; + } + } + + // Recursively validates the child bounds + if (state != null && (!this.graph.isCellCollapsed(cell) || + cell == this.currentRoot)) + { + var childCount = model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + var child = model.getChildAt(cell, i); + this.validateBounds(state, child); + } + } +}; + +/** + * Function: updateVertexLabelOffset + * + * Updates the absoluteOffset of the given vertex cell state. This takes + * into account the label position styles. + * + * Parameters: + * + * state - <mxCellState> whose absolute offset should be updated. + */ +mxGraphView.prototype.updateVertexLabelOffset = function(state) +{ + var horizontal = mxUtils.getValue(state.style, + mxConstants.STYLE_LABEL_POSITION, + mxConstants.ALIGN_CENTER); + + if (horizontal == mxConstants.ALIGN_LEFT) + { + state.absoluteOffset.x -= state.width; + } + else if (horizontal == mxConstants.ALIGN_RIGHT) + { + state.absoluteOffset.x += state.width; + } + + var vertical = mxUtils.getValue(state.style, + mxConstants.STYLE_VERTICAL_LABEL_POSITION, + mxConstants.ALIGN_MIDDLE); + + if (vertical == mxConstants.ALIGN_TOP) + { + state.absoluteOffset.y -= state.height; + } + else if (vertical == mxConstants.ALIGN_BOTTOM) + { + state.absoluteOffset.y += state.height; + } +}; + +/** + * Function: validatePoints + * + * Validates the points for the state of the given cell recursively if the + * cell is not collapsed and returns the bounding box of all visited states + * as an <mxRectangle>. + * + * Parameters: + * + * parentState - <mxCellState> for the parent cell. + * cell - <mxCell> whose points in the state should be updated. + */ +mxGraphView.prototype.validatePoints = function(parentState, cell) +{ + var model = this.graph.getModel(); + var state = this.getState(cell); + var bbox = null; + + if (state != null) + { + if (state.invalid) + { + var geo = this.graph.getCellGeometry(cell); + + if (geo != null && model.isEdge(cell)) + { + // Updates the points on the source terminal if its an edge + var source = this.getState(this.getVisibleTerminal(cell, true)); + state.setVisibleTerminalState(source, true); + + if (source != null && model.isEdge(source.cell) && + !model.isAncestor(source.cell, cell)) + { + var tmp = this.getState(model.getParent(source.cell)); + this.validatePoints(tmp, source.cell); + } + + // Updates the points on the target terminal if its an edge + var target = this.getState(this.getVisibleTerminal(cell, false)); + state.setVisibleTerminalState(target, false); + + if (target != null && model.isEdge(target.cell) && + !model.isAncestor(target.cell, cell)) + { + var tmp = this.getState(model.getParent(target.cell)); + this.validatePoints(tmp, target.cell); + } + + this.updateFixedTerminalPoints(state, source, target); + this.updatePoints(state, geo.points, source, target); + this.updateFloatingTerminalPoints(state, source, target); + this.updateEdgeBounds(state); + this.updateEdgeLabelOffset(state); + } + else if (geo != null && geo.relative && parentState != null && + model.isEdge(parentState.cell)) + { + var origin = this.getPoint(parentState, geo); + + if (origin != null) + { + state.x = origin.x; + state.y = origin.y; + + origin.x = (origin.x / this.scale) - this.translate.x; + origin.y = (origin.y / this.scale) - this.translate.y; + state.origin = origin; + + this.childMoved(parentState, state); + } + } + + state.invalid = false; + + if (cell != this.currentRoot) + { + // NOTE: Label bounds currently ignored if rendering is false + this.graph.cellRenderer.redraw(state, false, this.isRendering()); + } + } + + if (model.isEdge(cell) || model.isVertex(cell)) + { + if (state.shape != null && state.shape.boundingBox != null) + { + bbox = state.shape.boundingBox.clone(); + } + + if (state.text != null && !this.graph.isLabelClipped(state.cell)) + { + // Adds label bounding box to graph bounds + if (state.text.boundingBox != null) + { + if (bbox != null) + { + bbox.add(state.text.boundingBox); + } + else + { + bbox = state.text.boundingBox.clone(); + } + } + } + } + } + + if (state != null && (!this.graph.isCellCollapsed(cell) || + cell == this.currentRoot)) + { + var childCount = model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + var child = model.getChildAt(cell, i); + var bounds = this.validatePoints(state, child); + + if (bounds != null) + { + if (bbox == null) + { + bbox = bounds; + } + else + { + bbox.add(bounds); + } + } + } + } + + return bbox; +}; + +/** + * Function: childMoved + * + * Invoked when a child state was moved as a result of late evaluation + * of its position. This is invoked for relative edge children whose + * position can only be determined after the points of the parent edge + * are updated in validatePoints, and validates the bounds of all + * descendants of the child using validateBounds. + * + * Parameters: + * + * parent - <mxCellState> that represents the parent state. + * child - <mxCellState> that represents the child state. + */ +mxGraphView.prototype.childMoved = function(parent, child) +{ + var cell = child.cell; + + // Children of relative edge children need to validate + // their bounds after their parent state was updated + if (!this.graph.isCellCollapsed(cell) || cell == this.currentRoot) + { + var model = this.graph.getModel(); + var childCount = model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + this.validateBounds(child, model.getChildAt(cell, i)); + } + } +}; + +/** + * Function: updateFixedTerminalPoints + * + * Sets the initial absolute terminal points in the given state before the edge + * style is computed. + * + * Parameters: + * + * edge - <mxCellState> whose initial terminal points should be updated. + * source - <mxCellState> which represents the source terminal. + * target - <mxCellState> which represents the target terminal. + */ +mxGraphView.prototype.updateFixedTerminalPoints = function(edge, source, target) +{ + this.updateFixedTerminalPoint(edge, source, true, + this.graph.getConnectionConstraint(edge, source, true)); + this.updateFixedTerminalPoint(edge, target, false, + this.graph.getConnectionConstraint(edge, target, false)); +}; + +/** + * Function: updateFixedTerminalPoint + * + * Sets the fixed source or target terminal point on the given edge. + * + * Parameters: + * + * edge - <mxCellState> whose terminal point should be updated. + * terminal - <mxCellState> which represents the actual terminal. + * source - Boolean that specifies if the terminal is the source. + * constraint - <mxConnectionConstraint> that specifies the connection. + */ +mxGraphView.prototype.updateFixedTerminalPoint = function(edge, terminal, source, constraint) +{ + var pt = null; + + if (constraint != null) + { + pt = this.graph.getConnectionPoint(terminal, constraint); + } + + if (pt == null && terminal == null) + { + var s = this.scale; + var tr = this.translate; + var orig = edge.origin; + var geo = this.graph.getCellGeometry(edge.cell); + pt = geo.getTerminalPoint(source); + + if (pt != null) + { + pt = new mxPoint(s * (tr.x + pt.x + orig.x), + s * (tr.y + pt.y + orig.y)); + } + } + + edge.setAbsoluteTerminalPoint(pt, source); +}; + +/** + * Function: updatePoints + * + * Updates the absolute points in the given state using the specified array + * of <mxPoints> as the relative points. + * + * Parameters: + * + * edge - <mxCellState> whose absolute points should be updated. + * points - Array of <mxPoints> that constitute the relative points. + * source - <mxCellState> that represents the source terminal. + * target - <mxCellState> that represents the target terminal. + */ +mxGraphView.prototype.updatePoints = function(edge, points, source, target) +{ + if (edge != null) + { + var pts = []; + pts.push(edge.absolutePoints[0]); + var edgeStyle = this.getEdgeStyle(edge, points, source, target); + + if (edgeStyle != null) + { + var src = this.getTerminalPort(edge, source, true); + var trg = this.getTerminalPort(edge, target, false); + + edgeStyle(edge, src, trg, points, pts); + } + else if (points != null) + { + for (var i = 0; i < points.length; i++) + { + if (points[i] != null) + { + var pt = mxUtils.clone(points[i]); + pts.push(this.transformControlPoint(edge, pt)); + } + } + } + + var tmp = edge.absolutePoints; + pts.push(tmp[tmp.length-1]); + + edge.absolutePoints = pts; + } +}; + +/** + * Function: transformControlPoint + * + * Transforms the given control point to an absolute point. + */ +mxGraphView.prototype.transformControlPoint = function(state, pt) +{ + var orig = state.origin; + + return new mxPoint(this.scale * (pt.x + this.translate.x + orig.x), + this.scale * (pt.y + this.translate.y + orig.y)); +}; + +/** + * Function: getEdgeStyle + * + * Returns the edge style function to be used to render the given edge + * state. + */ +mxGraphView.prototype.getEdgeStyle = function(edge, points, source, target) +{ + var edgeStyle = (source != null && source == target) ? + mxUtils.getValue(edge.style, mxConstants.STYLE_LOOP, + this.graph.defaultLoopStyle) : + (!mxUtils.getValue(edge.style, + mxConstants.STYLE_NOEDGESTYLE, false) ? + edge.style[mxConstants.STYLE_EDGE] : + null); + + // Converts string values to objects + if (typeof(edgeStyle) == "string") + { + var tmp = mxStyleRegistry.getValue(edgeStyle); + + if (tmp == null && this.isAllowEval()) + { + tmp = mxUtils.eval(edgeStyle); + } + + edgeStyle = tmp; + } + + if (typeof(edgeStyle) == "function") + { + return edgeStyle; + } + + return null; +}; + +/** + * Function: updateFloatingTerminalPoints + * + * Updates the terminal points in the given state after the edge style was + * computed for the edge. + * + * Parameters: + * + * state - <mxCellState> whose terminal points should be updated. + * source - <mxCellState> that represents the source terminal. + * target - <mxCellState> that represents the target terminal. + */ +mxGraphView.prototype.updateFloatingTerminalPoints = function(state, source, target) +{ + var pts = state.absolutePoints; + var p0 = pts[0]; + var pe = pts[pts.length - 1]; + + if (pe == null && target != null) + { + this.updateFloatingTerminalPoint(state, target, source, false); + } + + if (p0 == null && source != null) + { + this.updateFloatingTerminalPoint(state, source, target, true); + } +}; + +/** + * Function: updateFloatingTerminalPoint + * + * Updates the absolute terminal point in the given state for the given + * start and end state, where start is the source if source is true. + * + * Parameters: + * + * edge - <mxCellState> whose terminal point should be updated. + * start - <mxCellState> for the terminal on "this" side of the edge. + * end - <mxCellState> for the terminal on the other side of the edge. + * source - Boolean indicating if start is the source terminal state. + */ +mxGraphView.prototype.updateFloatingTerminalPoint = function(edge, start, end, source) +{ + start = this.getTerminalPort(edge, start, source); + var next = this.getNextPoint(edge, end, source); + + var alpha = mxUtils.toRadians(Number(start.style[mxConstants.STYLE_ROTATION] || '0')); + var center = new mxPoint(start.getCenterX(), start.getCenterY()); + + if (alpha != 0) + { + var cos = Math.cos(-alpha); + var sin = Math.sin(-alpha); + next = mxUtils.getRotatedPoint(next, cos, sin, center); + } + + var border = parseFloat(edge.style[mxConstants.STYLE_PERIMETER_SPACING] || 0); + border += parseFloat(edge.style[(source) ? + mxConstants.STYLE_SOURCE_PERIMETER_SPACING : + mxConstants.STYLE_TARGET_PERIMETER_SPACING] || 0); + var pt = this.getPerimeterPoint(start, next, this.graph.isOrthogonal(edge), border); + + if (alpha != 0) + { + var cos = Math.cos(alpha); + var sin = Math.sin(alpha); + pt = mxUtils.getRotatedPoint(pt, cos, sin, center); + } + + edge.setAbsoluteTerminalPoint(pt, source); +}; + +/** + * Function: getTerminalPort + * + * Returns an <mxCellState> that represents the source or target terminal or + * port for the given edge. + * + * Parameters: + * + * state - <mxCellState> that represents the state of the edge. + * terminal - <mxCellState> that represents the terminal. + * source - Boolean indicating if the given terminal is the source terminal. + */ +mxGraphView.prototype.getTerminalPort = function(state, terminal, source) +{ + var key = (source) ? mxConstants.STYLE_SOURCE_PORT : + mxConstants.STYLE_TARGET_PORT; + var id = mxUtils.getValue(state.style, key); + + if (id != null) + { + var tmp = this.getState(this.graph.getModel().getCell(id)); + + // Only uses ports where a cell state exists + if (tmp != null) + { + terminal = tmp; + } + } + + return terminal; +}; + +/** + * Function: getPerimeterPoint + * + * Returns an <mxPoint> that defines the location of the intersection point between + * the perimeter and the line between the center of the shape and the given point. + * + * Parameters: + * + * terminal - <mxCellState> for the source or target terminal. + * next - <mxPoint> that lies outside of the given terminal. + * orthogonal - Boolean that specifies if the orthogonal projection onto + * the perimeter should be returned. If this is false then the intersection + * of the perimeter and the line between the next and the center point is + * returned. + * border - Optional border between the perimeter and the shape. + */ +mxGraphView.prototype.getPerimeterPoint = function(terminal, next, orthogonal, border) +{ + var point = null; + + if (terminal != null) + { + var perimeter = this.getPerimeterFunction(terminal); + + if (perimeter != null && next != null) + { + var bounds = this.getPerimeterBounds(terminal, border); + + if (bounds.width > 0 || bounds.height > 0) + { + point = perimeter(bounds, terminal, next, orthogonal); + } + } + + if (point == null) + { + point = this.getPoint(terminal); + } + } + + return point; +}; + +/** + * Function: getRoutingCenterX + * + * Returns the x-coordinate of the center point for automatic routing. + */ +mxGraphView.prototype.getRoutingCenterX = function (state) +{ + var f = (state.style != null) ? parseFloat(state.style + [mxConstants.STYLE_ROUTING_CENTER_X]) || 0 : 0; + + return state.getCenterX() + f * state.width; +}; + +/** + * Function: getRoutingCenterY + * + * Returns the y-coordinate of the center point for automatic routing. + */ +mxGraphView.prototype.getRoutingCenterY = function (state) +{ + var f = (state.style != null) ? parseFloat(state.style + [mxConstants.STYLE_ROUTING_CENTER_Y]) || 0 : 0; + + return state.getCenterY() + f * state.height; +}; + +/** + * Function: getPerimeterBounds + * + * Returns the perimeter bounds for the given terminal, edge pair as an + * <mxRectangle>. + * + * If you have a model where each terminal has a relative child that should + * act as the graphical endpoint for a connection from/to the terminal, then + * this method can be replaced as follows: + * + * (code) + * var oldGetPerimeterBounds = mxGraphView.prototype.getPerimeterBounds; + * mxGraphView.prototype.getPerimeterBounds = function(terminal, edge, isSource) + * { + * var model = this.graph.getModel(); + * var childCount = model.getChildCount(terminal.cell); + * + * if (childCount > 0) + * { + * var child = model.getChildAt(terminal.cell, 0); + * var geo = model.getGeometry(child); + * + * if (geo != null && + * geo.relative) + * { + * var state = this.getState(child); + * + * if (state != null) + * { + * terminal = state; + * } + * } + * } + * + * return oldGetPerimeterBounds.apply(this, arguments); + * }; + * (end) + * + * Parameters: + * + * terminal - <mxCellState> that represents the terminal. + * border - Number that adds a border between the shape and the perimeter. + */ +mxGraphView.prototype.getPerimeterBounds = function(terminal, border) +{ + border = (border != null) ? border : 0; + + if (terminal != null) + { + border += parseFloat(terminal.style[mxConstants.STYLE_PERIMETER_SPACING] || 0); + } + + return terminal.getPerimeterBounds(border * this.scale); +}; + +/** + * Function: getPerimeterFunction + * + * Returns the perimeter function for the given state. + */ +mxGraphView.prototype.getPerimeterFunction = function(state) +{ + var perimeter = state.style[mxConstants.STYLE_PERIMETER]; + + // Converts string values to objects + if (typeof(perimeter) == "string") + { + var tmp = mxStyleRegistry.getValue(perimeter); + + if (tmp == null && this.isAllowEval()) + { + tmp = mxUtils.eval(perimeter); + } + + perimeter = tmp; + } + + if (typeof(perimeter) == "function") + { + return perimeter; + } + + return null; +}; + +/** + * Function: getNextPoint + * + * Returns the nearest point in the list of absolute points or the center + * of the opposite terminal. + * + * Parameters: + * + * edge - <mxCellState> that represents the edge. + * opposite - <mxCellState> that represents the opposite terminal. + * source - Boolean indicating if the next point for the source or target + * should be returned. + */ +mxGraphView.prototype.getNextPoint = function(edge, opposite, source) +{ + var pts = edge.absolutePoints; + var point = null; + + if (pts != null && (source || pts.length > 2 || opposite == null)) + { + var count = pts.length; + point = pts[(source) ? Math.min(1, count - 1) : Math.max(0, count - 2)]; + } + + if (point == null && opposite != null) + { + point = new mxPoint(opposite.getCenterX(), opposite.getCenterY()); + } + + return point; +}; + +/** + * Function: getVisibleTerminal + * + * Returns the nearest ancestor terminal that is visible. The edge appears + * to be connected to this terminal on the display. The result of this method + * is cached in <mxCellState.getVisibleTerminalState>. + * + * Parameters: + * + * edge - <mxCell> whose visible terminal should be returned. + * source - Boolean that specifies if the source or target terminal + * should be returned. + */ +mxGraphView.prototype.getVisibleTerminal = function(edge, source) +{ + var model = this.graph.getModel(); + var result = model.getTerminal(edge, source); + var best = result; + + while (result != null && result != this.currentRoot) + { + if (!this.graph.isCellVisible(best) || this.graph.isCellCollapsed(result)) + { + best = result; + } + + result = model.getParent(result); + } + + // Checks if the result is not a layer + if (model.getParent(best) == model.getRoot()) + { + best = null; + } + + return best; +}; + +/** + * Function: updateEdgeBounds + * + * Updates the given state using the bounding box of the absolute points. + * Also updates <mxCellState.terminalDistance>, <mxCellState.length> and + * <mxCellState.segments>. + * + * Parameters: + * + * state - <mxCellState> whose bounds should be updated. + */ +mxGraphView.prototype.updateEdgeBounds = function(state) +{ + var points = state.absolutePoints; + state.length = 0; + + if (points != null && points.length > 0) + { + var p0 = points[0]; + var pe = points[points.length - 1]; + + if (p0 == null || pe == null) + { + // Drops the edge state if the edge is not the root + if (state.cell != this.currentRoot) + { + // Note: This condition normally occurs if a connected edge has a + // null-terminal, ie. edge.source == null or edge.target == null, + // and no corresponding terminal point defined, which happens for + // example if the terminal-id was not resolved at cell decoding time. + this.clear(state.cell, true); + } + } + else + { + if (p0.x != pe.x || p0.y != pe.y) + { + var dx = pe.x - p0.x; + var dy = pe.y - p0.y; + state.terminalDistance = Math.sqrt(dx * dx + dy * dy); + } + else + { + state.terminalDistance = 0; + } + + var length = 0; + var segments = []; + var pt = p0; + + if (pt != null) + { + var minX = pt.x; + var minY = pt.y; + var maxX = minX; + var maxY = minY; + + for (var i = 1; i < points.length; i++) + { + var tmp = points[i]; + + if (tmp != null) + { + var dx = pt.x - tmp.x; + var dy = pt.y - tmp.y; + + var segment = Math.sqrt(dx * dx + dy * dy); + segments.push(segment); + length += segment; + + pt = tmp; + + minX = Math.min(pt.x, minX); + minY = Math.min(pt.y, minY); + maxX = Math.max(pt.x, maxX); + maxY = Math.max(pt.y, maxY); + } + } + + state.length = length; + state.segments = segments; + + var markerSize = 1; // TODO: include marker size + + state.x = minX; + state.y = minY; + state.width = Math.max(markerSize, maxX - minX); + state.height = Math.max(markerSize, maxY - minY); + } + } + } +}; + +/** + * Function: getPoint + * + * Returns the absolute point on the edge for the given relative + * <mxGeometry> as an <mxPoint>. The edge is represented by the given + * <mxCellState>. + * + * Parameters: + * + * state - <mxCellState> that represents the state of the parent edge. + * geometry - <mxGeometry> that represents the relative location. + */ +mxGraphView.prototype.getPoint = function(state, geometry) +{ + var x = state.getCenterX(); + var y = state.getCenterY(); + + if (state.segments != null && (geometry == null || geometry.relative)) + { + var gx = (geometry != null) ? geometry.x / 2 : 0; + var pointCount = state.absolutePoints.length; + var dist = (gx + 0.5) * state.length; + var segment = state.segments[0]; + var length = 0; + var index = 1; + + while (dist > length + segment && index < pointCount-1) + { + length += segment; + segment = state.segments[index++]; + } + + var factor = (segment == 0) ? 0 : (dist - length) / segment; + var p0 = state.absolutePoints[index-1]; + var pe = state.absolutePoints[index]; + + if (p0 != null && pe != null) + { + var gy = 0; + var offsetX = 0; + var offsetY = 0; + + if (geometry != null) + { + gy = geometry.y; + var offset = geometry.offset; + + if (offset != null) + { + offsetX = offset.x; + offsetY = offset.y; + } + } + + var dx = pe.x - p0.x; + var dy = pe.y - p0.y; + var nx = (segment == 0) ? 0 : dy / segment; + var ny = (segment == 0) ? 0 : dx / segment; + + x = p0.x + dx * factor + (nx * gy + offsetX) * this.scale; + y = p0.y + dy * factor - (ny * gy - offsetY) * this.scale; + } + } + else if (geometry != null) + { + var offset = geometry.offset; + + if (offset != null) + { + x += offset.x; + y += offset.y; + } + } + + return new mxPoint(x, y); +}; + +/** + * Function: getRelativePoint + * + * Gets the relative point that describes the given, absolute label + * position for the given edge state. + * + * Parameters: + * + * state - <mxCellState> that represents the state of the parent edge. + * x - Specifies the x-coordinate of the absolute label location. + * y - Specifies the y-coordinate of the absolute label location. + */ +mxGraphView.prototype.getRelativePoint = function(edgeState, x, y) +{ + var model = this.graph.getModel(); + var geometry = model.getGeometry(edgeState.cell); + + if (geometry != null) + { + var pointCount = edgeState.absolutePoints.length; + + if (geometry.relative && pointCount > 1) + { + var totalLength = edgeState.length; + var segments = edgeState.segments; + + // Works which line segment the point of the label is closest to + var p0 = edgeState.absolutePoints[0]; + var pe = edgeState.absolutePoints[1]; + var minDist = mxUtils.ptSegDistSq(p0.x, p0.y, pe.x, pe.y, x, y); + + var index = 0; + var tmp = 0; + var length = 0; + + for (var i = 2; i < pointCount; i++) + { + tmp += segments[i - 2]; + pe = edgeState.absolutePoints[i]; + var dist = mxUtils.ptSegDistSq(p0.x, p0.y, pe.x, pe.y, x, y); + + if (dist <= minDist) + { + minDist = dist; + index = i - 1; + length = tmp; + } + + p0 = pe; + } + + var seg = segments[index]; + p0 = edgeState.absolutePoints[index]; + pe = edgeState.absolutePoints[index + 1]; + + var x2 = p0.x; + var y2 = p0.y; + + var x1 = pe.x; + var y1 = pe.y; + + var px = x; + var py = y; + + var xSegment = x2 - x1; + var ySegment = y2 - y1; + + px -= x1; + py -= y1; + var projlenSq = 0; + + px = xSegment - px; + py = ySegment - py; + var dotprod = px * xSegment + py * ySegment; + + if (dotprod <= 0.0) + { + projlenSq = 0; + } + else + { + projlenSq = dotprod * dotprod + / (xSegment * xSegment + ySegment * ySegment); + } + + var projlen = Math.sqrt(projlenSq); + + if (projlen > seg) + { + projlen = seg; + } + + var yDistance = Math.sqrt(mxUtils.ptSegDistSq(p0.x, p0.y, pe + .x, pe.y, x, y)); + var direction = mxUtils.relativeCcw(p0.x, p0.y, pe.x, pe.y, x, y); + + if (direction == -1) + { + yDistance = -yDistance; + } + + // Constructs the relative point for the label + return new mxPoint(((totalLength / 2 - length - projlen) / totalLength) * -2, + yDistance / this.scale); + } + } + + return new mxPoint(); +}; + +/** + * Function: updateEdgeLabelOffset + * + * Updates <mxCellState.absoluteOffset> for the given state. The absolute + * offset is normally used for the position of the edge label. Is is + * calculated from the geometry as an absolute offset from the center + * between the two endpoints if the geometry is absolute, or as the + * relative distance between the center along the line and the absolute + * orthogonal distance if the geometry is relative. + * + * Parameters: + * + * state - <mxCellState> whose absolute offset should be updated. + */ +mxGraphView.prototype.updateEdgeLabelOffset = function(state) +{ + var points = state.absolutePoints; + + state.absoluteOffset.x = state.getCenterX(); + state.absoluteOffset.y = state.getCenterY(); + + if (points != null && points.length > 0 && state.segments != null) + { + var geometry = this.graph.getCellGeometry(state.cell); + + if (geometry.relative) + { + var offset = this.getPoint(state, geometry); + + if (offset != null) + { + state.absoluteOffset = offset; + } + } + else + { + var p0 = points[0]; + var pe = points[points.length - 1]; + + if (p0 != null && pe != null) + { + var dx = pe.x - p0.x; + var dy = pe.y - p0.y; + var x0 = 0; + var y0 = 0; + + var off = geometry.offset; + + if (off != null) + { + x0 = off.x; + y0 = off.y; + } + + var x = p0.x + dx / 2 + x0 * this.scale; + var y = p0.y + dy / 2 + y0 * this.scale; + + state.absoluteOffset.x = x; + state.absoluteOffset.y = y; + } + } + } +}; + +/** + * Function: getState + * + * Returns the <mxCellState> for the given cell. If create is true, then + * the state is created if it does not yet exist. + * + * Parameters: + * + * cell - <mxCell> for which the <mxCellState> should be returned. + * create - Optional boolean indicating if a new state should be created + * if it does not yet exist. Default is false. + */ +mxGraphView.prototype.getState = function(cell, create) +{ + create = create || false; + var state = null; + + if (cell != null) + { + state = this.states.get(cell); + + if (this.graph.isCellVisible(cell)) + { + if (state == null && create && this.graph.isCellVisible(cell)) + { + state = this.createState(cell); + this.states.put(cell, state); + } + else if (create && state != null && this.updateStyle) + { + state.style = this.graph.getCellStyle(cell); + } + } + } + + return state; +}; + +/** + * Function: isRendering + * + * Returns <rendering>. + */ +mxGraphView.prototype.isRendering = function() +{ + return this.rendering; +}; + +/** + * Function: setRendering + * + * Sets <rendering>. + */ +mxGraphView.prototype.setRendering = function(value) +{ + this.rendering = value; +}; + +/** + * Function: isAllowEval + * + * Returns <allowEval>. + */ +mxGraphView.prototype.isAllowEval = function() +{ + return this.allowEval; +}; + +/** + * Function: setAllowEval + * + * Sets <allowEval>. + */ +mxGraphView.prototype.setAllowEval = function(value) +{ + this.allowEval = value; +}; + +/** + * Function: getStates + * + * Returns <states>. + */ +mxGraphView.prototype.getStates = function() +{ + return this.states; +}; + +/** + * Function: setStates + * + * Sets <states>. + */ +mxGraphView.prototype.setStates = function(value) +{ + this.states = value; +}; + +/** + * Function: getCellStates + * + * Returns the <mxCellStates> for the given array of <mxCells>. The array + * contains all states that are not null, that is, the returned array may + * have less elements than the given array. If no argument is given, then + * this returns <states>. + */ +mxGraphView.prototype.getCellStates = function(cells) +{ + if (cells == null) + { + return this.states; + } + else + { + var result = []; + + for (var i = 0; i < cells.length; i++) + { + var state = this.getState(cells[i]); + + if (state != null) + { + result.push(state); + } + } + + return result; + } +}; + +/** + * Function: removeState + * + * Removes and returns the <mxCellState> for the given cell. + * + * Parameters: + * + * cell - <mxCell> for which the <mxCellState> should be removed. + */ +mxGraphView.prototype.removeState = function(cell) +{ + var state = null; + + if (cell != null) + { + state = this.states.remove(cell); + + if (state != null) + { + this.graph.cellRenderer.destroy(state); + state.destroy(); + } + } + + return state; +}; + +/** + * Function: createState + * + * Creates and returns an <mxCellState> for the given cell and initializes + * it using <mxCellRenderer.initialize>. + * + * Parameters: + * + * cell - <mxCell> for which a new <mxCellState> should be created. + */ +mxGraphView.prototype.createState = function(cell) +{ + var style = this.graph.getCellStyle(cell); + var state = new mxCellState(this, cell, style); + this.graph.cellRenderer.initialize(state, this.isRendering()); + + return state; +}; + +/** + * Function: getCanvas + * + * Returns the DOM node that contains the background-, draw- and + * overlaypane. + */ +mxGraphView.prototype.getCanvas = function() +{ + return this.canvas; +}; + +/** + * Function: getBackgroundPane + * + * Returns the DOM node that represents the background layer. + */ +mxGraphView.prototype.getBackgroundPane = function() +{ + return this.backgroundPane; +}; + +/** + * Function: getDrawPane + * + * Returns the DOM node that represents the main drawing layer. + */ +mxGraphView.prototype.getDrawPane = function() +{ + return this.drawPane; +}; + +/** + * Function: getOverlayPane + * + * Returns the DOM node that represents the topmost drawing layer. + */ +mxGraphView.prototype.getOverlayPane = function() +{ + return this.overlayPane; +}; + +/** + * Function: isContainerEvent + * + * Returns true if the event origin is one of the drawing panes or + * containers of the view. + */ +mxGraphView.prototype.isContainerEvent = function(evt) +{ + var source = mxEvent.getSource(evt); + + return (source == this.graph.container || + source.parentNode == this.backgroundPane || + (source.parentNode != null && + source.parentNode.parentNode == this.backgroundPane) || + source == this.canvas.parentNode || + source == this.canvas || + source == this.backgroundPane || + source == this.drawPane || + source == this.overlayPane); +}; + +/** + * Function: isScrollEvent + * + * Returns true if the event origin is one of the scrollbars of the + * container in IE. Such events are ignored. + */ + mxGraphView.prototype.isScrollEvent = function(evt) +{ + var offset = mxUtils.getOffset(this.graph.container); + var pt = new mxPoint(evt.clientX - offset.x, evt.clientY - offset.y); + + var outWidth = this.graph.container.offsetWidth; + var inWidth = this.graph.container.clientWidth; + + if (outWidth > inWidth && pt.x > inWidth + 2 && pt.x <= outWidth) + { + return true; + } + + var outHeight = this.graph.container.offsetHeight; + var inHeight = this.graph.container.clientHeight; + + if (outHeight > inHeight && pt.y > inHeight + 2 && pt.y <= outHeight) + { + return true; + } + + return false; +}; + +/** + * Function: init + * + * Initializes the graph event dispatch loop for the specified container + * and invokes <create> to create the required DOM nodes for the display. + */ +mxGraphView.prototype.init = function() +{ + this.installListeners(); + + // Creates the DOM nodes for the respective display dialect + var graph = this.graph; + + if (graph.dialect == mxConstants.DIALECT_SVG) + { + this.createSvg(); + } + else if (graph.dialect == mxConstants.DIALECT_VML) + { + this.createVml(); + } + else + { + this.createHtml(); + } +}; + +/** + * Function: installListeners + * + * Installs the required listeners in the container. + */ +mxGraphView.prototype.installListeners = function() +{ + var graph = this.graph; + var container = graph.container; + + if (container != null) + { + var md = (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown'; + var mm = (mxClient.IS_TOUCH) ? 'touchmove' : 'mousemove'; + var mu = (mxClient.IS_TOUCH) ? 'touchend' : 'mouseup'; + + // Adds basic listeners for graph event dispatching + mxEvent.addListener(container, md, + mxUtils.bind(this, function(evt) + { + // Workaround for touch-based device not transferring + // the focus while editing with virtual keyboard + if (mxClient.IS_TOUCH && graph.isEditing()) + { + graph.stopEditing(!graph.isInvokesStopCellEditing()); + } + + // Condition to avoid scrollbar events starting a rubberband + // selection + if (this.isContainerEvent(evt) && ((!mxClient.IS_IE && + !mxClient.IS_GC && !mxClient.IS_OP && !mxClient.IS_SF) || + !this.isScrollEvent(evt))) + { + graph.fireMouseEvent(mxEvent.MOUSE_DOWN, + new mxMouseEvent(evt)); + } + }) + ); + mxEvent.addListener(container, mm, + mxUtils.bind(this, function(evt) + { + if (this.isContainerEvent(evt)) + { + graph.fireMouseEvent(mxEvent.MOUSE_MOVE, + new mxMouseEvent(evt)); + } + }) + ); + mxEvent.addListener(container, mu, + mxUtils.bind(this, function(evt) + { + if (this.isContainerEvent(evt)) + { + graph.fireMouseEvent(mxEvent.MOUSE_UP, + new mxMouseEvent(evt)); + } + }) + ); + + // Adds listener for double click handling on background + mxEvent.addListener(container, 'dblclick', + mxUtils.bind(this, function(evt) + { + graph.dblClick(evt); + }) + ); + + // Workaround for touch events which started on some DOM node + // on top of the container, in which case the cells under the + // mouse for the move and up events are not detected. + var getState = function(evt) + { + var state = null; + + // Workaround for touch events which started on some DOM node + // on top of the container, in which case the cells under the + // mouse for the move and up events are not detected. + if (mxClient.IS_TOUCH) + { + var x = mxEvent.getClientX(evt); + var y = mxEvent.getClientY(evt); + + // Dispatches the drop event to the graph which + // consumes and executes the source function + var pt = mxUtils.convertPoint(container, x, y); + state = graph.view.getState(graph.getCellAt(pt.x, pt.y)); + } + + return state; + }; + + // Adds basic listeners for graph event dispatching outside of the + // container and finishing the handling of a single gesture + // Implemented via graph event dispatch loop to avoid duplicate events + // in Firefox and Chrome + graph.addMouseListener( + { + mouseDown: function(sender, me) + { + graph.panningHandler.hideMenu(); + }, + mouseMove: function() { }, + mouseUp: function() { } + }); + mxEvent.addListener(document, mm, + mxUtils.bind(this, function(evt) + { + // Hides the tooltip if mouse is outside container + if (graph.tooltipHandler != null && + graph.tooltipHandler.isHideOnHover()) + { + graph.tooltipHandler.hide(); + } + + if (this.captureDocumentGesture && graph.isMouseDown && + !mxEvent.isConsumed(evt)) + { + graph.fireMouseEvent(mxEvent.MOUSE_MOVE, + new mxMouseEvent(evt, getState(evt))); + } + }) + ); + mxEvent.addListener(document, mu, + mxUtils.bind(this, function(evt) + { + if (this.captureDocumentGesture) + { + graph.fireMouseEvent(mxEvent.MOUSE_UP, + new mxMouseEvent(evt)); + } + }) + ); + } +}; + +/** + * Function: create + * + * Creates the DOM nodes for the HTML display. + */ +mxGraphView.prototype.createHtml = function() +{ + var container = this.graph.container; + + if (container != null) + { + this.canvas = this.createHtmlPane('100%', '100%'); + + // Uses minimal size for inner DIVs on Canvas. This is required + // for correct event processing in IE. If we have an overlapping + // DIV then the events on the cells are only fired for labels. + this.backgroundPane = this.createHtmlPane('1px', '1px'); + this.drawPane = this.createHtmlPane('1px', '1px'); + this.overlayPane = this.createHtmlPane('1px', '1px'); + + this.canvas.appendChild(this.backgroundPane); + this.canvas.appendChild(this.drawPane); + this.canvas.appendChild(this.overlayPane); + + container.appendChild(this.canvas); + + // Implements minWidth/minHeight in quirks mode + if (mxClient.IS_QUIRKS) + { + var onResize = mxUtils.bind(this, function(evt) + { + var bounds = this.getGraphBounds(); + var width = bounds.x + bounds.width + this.graph.border; + var height = bounds.y + bounds.height + this.graph.border; + + this.updateHtmlCanvasSize(width, height); + }); + + mxEvent.addListener(window, 'resize', onResize); + } + } +}; + +/** + * Function: updateHtmlCanvasSize + * + * Updates the size of the HTML canvas. + */ +mxGraphView.prototype.updateHtmlCanvasSize = function(width, height) +{ + if (this.graph.container != null) + { + var ow = this.graph.container.offsetWidth; + var oh = this.graph.container.offsetHeight; + + if (ow < width) + { + this.canvas.style.width = width + 'px'; + } + else + { + this.canvas.style.width = '100%'; + } + + if (oh < height) + { + this.canvas.style.height = height + 'px'; + } + else + { + this.canvas.style.height = '100%'; + } + } +}; + +/** + * Function: createHtmlPane + * + * Creates and returns a drawing pane in HTML (DIV). + */ +mxGraphView.prototype.createHtmlPane = function(width, height) +{ + var pane = document.createElement('DIV'); + + if (width != null && height != null) + { + pane.style.position = 'absolute'; + pane.style.left = '0px'; + pane.style.top = '0px'; + + pane.style.width = width; + pane.style.height = height; + } + else + { + pane.style.position = 'relative'; + } + + return pane; +}; + +/** + * Function: create + * + * Creates the DOM nodes for the VML display. + */ +mxGraphView.prototype.createVml = function() +{ + var container = this.graph.container; + + if (container != null) + { + var width = container.offsetWidth; + var height = container.offsetHeight; + this.canvas = this.createVmlPane(width, height); + + this.backgroundPane = this.createVmlPane(width, height); + this.drawPane = this.createVmlPane(width, height); + this.overlayPane = this.createVmlPane(width, height); + + this.canvas.appendChild(this.backgroundPane); + this.canvas.appendChild(this.drawPane); + this.canvas.appendChild(this.overlayPane); + + container.appendChild(this.canvas); + } +}; + +/** + * Function: createVmlPane + * + * Creates a drawing pane in VML (group). + */ +mxGraphView.prototype.createVmlPane = function(width, height) +{ + var pane = document.createElement('v:group'); + + // At this point the width and height are potentially + // uninitialized. That's OK. + pane.style.position = 'absolute'; + pane.style.left = '0px'; + pane.style.top = '0px'; + + pane.style.width = width+'px'; + pane.style.height = height+'px'; + + pane.setAttribute('coordsize', width+','+height); + pane.setAttribute('coordorigin', '0,0'); + + return pane; +}; + +/** + * Function: create + * + * Creates and returns the DOM nodes for the SVG display. + */ +mxGraphView.prototype.createSvg = function() +{ + var container = this.graph.container; + this.canvas = document.createElementNS(mxConstants.NS_SVG, 'g'); + + // For background image + this.backgroundPane = document.createElementNS(mxConstants.NS_SVG, 'g'); + this.canvas.appendChild(this.backgroundPane); + + // Adds two layers (background is early feature) + this.drawPane = document.createElementNS(mxConstants.NS_SVG, 'g'); + this.canvas.appendChild(this.drawPane); + + this.overlayPane = document.createElementNS(mxConstants.NS_SVG, 'g'); + this.canvas.appendChild(this.overlayPane); + + var root = document.createElementNS(mxConstants.NS_SVG, 'svg'); + root.style.width = '100%'; + root.style.height = '100%'; + + if (mxClient.IS_IE) + { + root.style.marginBottom = '-4px'; + } + + root.appendChild(this.canvas); + + if (container != null) + { + container.appendChild(root); + + // Workaround for offset of container + var style = mxUtils.getCurrentStyle(container); + + if (style.position == 'static') + { + container.style.position = 'relative'; + } + } +}; + +/** + * Function: destroy + * + * Destroys the view and all its resources. + */ +mxGraphView.prototype.destroy = function() +{ + var root = (this.canvas != null) ? this.canvas.ownerSVGElement : null; + + if (root == null) + { + root = this.canvas; + } + + if (root != null && root.parentNode != null) + { + this.clear(this.currentRoot, true); + mxEvent.removeAllListeners(document); + mxEvent.release(this.graph.container); + root.parentNode.removeChild(root); + + this.canvas = null; + this.backgroundPane = null; + this.drawPane = null; + this.overlayPane = null; + } +}; + +/** + * Class: mxCurrentRootChange + * + * Action to change the current root in a view. + * + * Constructor: mxCurrentRootChange + * + * Constructs a change of the current root in the given view. + */ +function mxCurrentRootChange(view, root) +{ + this.view = view; + this.root = root; + this.previous = root; + this.isUp = root == null; + + if (!this.isUp) + { + var tmp = this.view.currentRoot; + var model = this.view.graph.getModel(); + + while (tmp != null) + { + if (tmp == root) + { + this.isUp = true; + break; + } + + tmp = model.getParent(tmp); + } + } +}; + +/** + * Function: execute + * + * Changes the current root of the view. + */ +mxCurrentRootChange.prototype.execute = function() +{ + var tmp = this.view.currentRoot; + this.view.currentRoot = this.previous; + this.previous = tmp; + + var translate = this.view.graph.getTranslateForRoot(this.view.currentRoot); + + if (translate != null) + { + this.view.translate = new mxPoint(-translate.x, -translate.y); + } + + var name = (this.isUp) ? mxEvent.UP : mxEvent.DOWN; + this.view.fireEvent(new mxEventObject(name, + 'root', this.view.currentRoot, 'previous', this.previous)); + + if (this.isUp) + { + this.view.clear(this.view.currentRoot, true); + this.view.validate(); + } + else + { + this.view.refresh(); + } + + this.isUp = !this.isUp; +}; diff --git a/src/js/view/mxLayoutManager.js b/src/js/view/mxLayoutManager.js new file mode 100644 index 0000000..ee8ec65 --- /dev/null +++ b/src/js/view/mxLayoutManager.js @@ -0,0 +1,375 @@ +/** + * $Id: mxLayoutManager.js,v 1.21 2012-01-04 10:01:16 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxLayoutManager + * + * Implements a layout manager that updates the layout for a given transaction. + * + * Example: + * + * (code) + * var layoutMgr = new mxLayoutManager(graph); + * layoutMgr.getLayout = function(cell) + * { + * return layout; + * }; + * (end) + * + * Event: mxEvent.LAYOUT_CELLS + * + * Fires between begin- and endUpdate after all cells have been layouted in + * <layoutCells>. The <code>cells</code> property contains all cells that have + * been passed to <layoutCells>. + * + * Constructor: mxLayoutManager + * + * Constructs a new automatic layout for the given graph. + * + * Arguments: + * + * graph - Reference to the enclosing graph. + */ +function mxLayoutManager(graph) +{ + // Executes the layout before the changes are dispatched + this.undoHandler = mxUtils.bind(this, function(sender, evt) + { + if (this.isEnabled()) + { + this.beforeUndo(evt.getProperty('edit')); + } + }); + + // Notifies the layout of a move operation inside a parent + this.moveHandler = mxUtils.bind(this, function(sender, evt) + { + if (this.isEnabled()) + { + this.cellsMoved(evt.getProperty('cells'), evt.getProperty('event')); + } + }); + + this.setGraph(graph); +}; + +/** + * Extends mxEventSource. + */ +mxLayoutManager.prototype = new mxEventSource(); +mxLayoutManager.prototype.constructor = mxLayoutManager; + +/** + * Variable: graph + * + * Reference to the enclosing <mxGraph>. + */ +mxLayoutManager.prototype.graph = null; + +/** + * Variable: bubbling + * + * Specifies if the layout should bubble along + * the cell hierarchy. Default is true. + */ +mxLayoutManager.prototype.bubbling = true; + +/** + * Variable: enabled + * + * Specifies if event handling is enabled. Default is true. + */ +mxLayoutManager.prototype.enabled = true; + +/** + * Variable: updateHandler + * + * Holds the function that handles the endUpdate event. + */ +mxLayoutManager.prototype.updateHandler = null; + +/** + * Variable: moveHandler + * + * Holds the function that handles the move event. + */ +mxLayoutManager.prototype.moveHandler = null; + +/** + * Function: isEnabled + * + * Returns true if events are handled. This implementation + * returns <enabled>. + */ +mxLayoutManager.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Enables or disables event handling. This implementation + * updates <enabled>. + * + * Parameters: + * + * enabled - Boolean that specifies the new enabled state. + */ +mxLayoutManager.prototype.setEnabled = function(enabled) +{ + this.enabled = enabled; +}; + +/** + * Function: isBubbling + * + * Returns true if a layout should bubble, that is, if the parent layout + * should be executed whenever a cell layout (layout of the children of + * a cell) has been executed. This implementation returns <bubbling>. + */ +mxLayoutManager.prototype.isBubbling = function() +{ + return this.bubbling; +}; + +/** + * Function: setBubbling + * + * Sets <bubbling>. + */ +mxLayoutManager.prototype.setBubbling = function(value) +{ + this.bubbling = value; +}; + +/** + * Function: getGraph + * + * Returns the graph that this layout operates on. + */ +mxLayoutManager.prototype.getGraph = function() +{ + return this.graph; +}; + +/** + * Function: setGraph + * + * Sets the graph that the layouts operate on. + */ +mxLayoutManager.prototype.setGraph = function(graph) +{ + if (this.graph != null) + { + var model = this.graph.getModel(); + model.removeListener(this.undoHandler); + this.graph.removeListener(this.moveHandler); + } + + this.graph = graph; + + if (this.graph != null) + { + var model = this.graph.getModel(); + model.addListener(mxEvent.BEFORE_UNDO, this.undoHandler); + this.graph.addListener(mxEvent.MOVE_CELLS, this.moveHandler); + } +}; + +/** + * Function: getLayout + * + * Returns the layout to be executed for the given graph and parent. + */ +mxLayoutManager.prototype.getLayout = function(parent) +{ + return null; +}; + +/** + * Function: beforeUndo + * + * Called from the undoHandler. + * + * Parameters: + * + * cell - Array of <mxCells> that have been moved. + * evt - Mouse event that represents the mousedown. + */ +mxLayoutManager.prototype.beforeUndo = function(undoableEdit) +{ + var cells = this.getCellsForChanges(undoableEdit.changes); + var model = this.getGraph().getModel(); + + // Adds all parent ancestors + if (this.isBubbling()) + { + var tmp = model.getParents(cells); + + while (tmp.length > 0) + { + cells = cells.concat(tmp); + tmp = model.getParents(tmp); + } + } + + this.layoutCells(mxUtils.sortCells(cells, false)); +}; + +/** + * Function: cellsMoved + * + * Called from the moveHandler. + * + * Parameters: + * + * cell - Array of <mxCells> that have been moved. + * evt - Mouse event that represents the mousedown. + */ +mxLayoutManager.prototype.cellsMoved = function(cells, evt) +{ + if (cells != null && + evt != null) + { + var point = mxUtils.convertPoint(this.getGraph().container, + mxEvent.getClientX(evt), mxEvent.getClientY(evt)); + var model = this.getGraph().getModel(); + + // Checks if a layout exists to take care of the moving + for (var i = 0; i < cells.length; i++) + { + var layout = this.getLayout(model.getParent(cells[i])); + + if (layout != null) + { + layout.moveCell(cells[i], point.x, point.y); + } + } + } +}; + +/** + * Function: getCellsForEdit + * + * Returns the cells to be layouted for the given sequence of changes. + */ +mxLayoutManager.prototype.getCellsForChanges = function(changes) +{ + var result = []; + var hash = new Object(); + + for (var i = 0; i < changes.length; i++) + { + var change = changes[i]; + + if (change instanceof mxRootChange) + { + return []; + } + else + { + var cells = this.getCellsForChange(change); + + for (var j = 0; j < cells.length; j++) + { + if (cells[j] != null) + { + var id = mxCellPath.create(cells[j]); + + if (hash[id] == null) + { + hash[id] = cells[j]; + result.push(cells[j]); + } + } + } + } + } + + return result; +}; + +/** + * Function: getCellsForChange + * + * Executes all layouts which have been scheduled during the + * changes. + */ +mxLayoutManager.prototype.getCellsForChange = function(change) +{ + var model = this.getGraph().getModel(); + + if (change instanceof mxChildChange) + { + return [change.child, change.previous, model.getParent(change.child)]; + } + else if (change instanceof mxTerminalChange || + change instanceof mxGeometryChange) + { + return [change.cell, model.getParent(change.cell)]; + } + + return []; +}; + +/** + * Function: layoutCells + * + * Executes all layouts which have been scheduled during the + * changes. + */ +mxLayoutManager.prototype.layoutCells = function(cells) +{ + if (cells.length > 0) + { + // Invokes the layouts while removing duplicates + var model = this.getGraph().getModel(); + + model.beginUpdate(); + try + { + var last = null; + + for (var i = 0; i < cells.length; i++) + { + if (cells[i] != model.getRoot() && + cells[i] != last) + { + last = cells[i]; + this.executeLayout(this.getLayout(last), last); + } + } + + this.fireEvent(new mxEventObject(mxEvent.LAYOUT_CELLS, 'cells', cells)); + } + finally + { + model.endUpdate(); + } + } +}; + +/** + * Function: executeLayout + * + * Executes the given layout on the given parent. + */ +mxLayoutManager.prototype.executeLayout = function(layout, parent) +{ + if (layout != null && parent != null) + { + layout.execute(parent); + } +}; + +/** + * Function: destroy + * + * Removes all handlers from the <graph> and deletes the reference to it. + */ +mxLayoutManager.prototype.destroy = function() +{ + this.setGraph(null); +}; diff --git a/src/js/view/mxMultiplicity.js b/src/js/view/mxMultiplicity.js new file mode 100644 index 0000000..c927d3f --- /dev/null +++ b/src/js/view/mxMultiplicity.js @@ -0,0 +1,257 @@ +/** + * $Id: mxMultiplicity.js,v 1.24 2010-11-03 14:52:40 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxMultiplicity + * + * Defines invalid connections along with the error messages that they produce. + * To add or remove rules on a graph, you must add/remove instances of this + * class to <mxGraph.multiplicities>. + * + * Example: + * + * (code) + * graph.multiplicities.push(new mxMultiplicity( + * true, 'rectangle', null, null, 0, 2, ['circle'], + * 'Only 2 targets allowed', + * 'Only circle targets allowed')); + * (end) + * + * Defines a rule where each rectangle must be connected to no more than 2 + * circles and no other types of targets are allowed. + * + * Constructor: mxMultiplicity + * + * Instantiate class mxMultiplicity in order to describe allowed + * connections in a graph. Not all constraints can be enforced while + * editing, some must be checked at validation time. The <countError> and + * <typeError> are treated as resource keys in <mxResources>. + * + * Parameters: + * + * source - Boolean indicating if this rule applies to the source or target + * terminal. + * type - Type of the source or target terminal that this rule applies to. + * See <type> for more information. + * attr - Optional attribute name to match the source or target terminal. + * value - Optional attribute value to match the source or target terminal. + * min - Minimum number of edges for this rule. Default is 1. + * max - Maximum number of edges for this rule. n means infinite. Default + * is n. + * validNeighbors - Array of types of the opposite terminal for which this + * rule applies. + * countError - Error to be displayed for invalid number of edges. + * typeError - Error to be displayed for invalid opposite terminals. + * validNeighborsAllowed - Optional boolean indicating if the array of + * opposite types should be valid or invalid. + */ +function mxMultiplicity(source, type, attr, value, min, max, + validNeighbors, countError, typeError, validNeighborsAllowed) +{ + this.source = source; + this.type = type; + this.attr = attr; + this.value = value; + this.min = (min != null) ? min : 0; + this.max = (max != null) ? max : 'n'; + this.validNeighbors = validNeighbors; + this.countError = mxResources.get(countError) || countError; + this.typeError = mxResources.get(typeError) || typeError; + this.validNeighborsAllowed = (validNeighborsAllowed != null) ? + validNeighborsAllowed : true; +}; + +/** + * Variable: type + * + * Defines the type of the source or target terminal. The type is a string + * passed to <mxUtils.isNode> together with the source or target vertex + * value as the first argument. + */ +mxMultiplicity.prototype.type = null; + +/** + * Variable: attr + * + * Optional string that specifies the attributename to be passed to + * <mxUtils.isNode> to check if the rule applies to a cell. + */ +mxMultiplicity.prototype.attr = null; + +/** + * Variable: value + * + * Optional string that specifies the value of the attribute to be passed + * to <mxUtils.isNode> to check if the rule applies to a cell. + */ +mxMultiplicity.prototype.value = null; + +/** + * Variable: source + * + * Boolean that specifies if the rule is applied to the source or target + * terminal of an edge. + */ +mxMultiplicity.prototype.source = null; + +/** + * Variable: min + * + * Defines the minimum number of connections for which this rule applies. + * Default is 0. + */ +mxMultiplicity.prototype.min = null; + +/** + * Variable: max + * + * Defines the maximum number of connections for which this rule applies. + * A value of 'n' means unlimited times. Default is 'n'. + */ +mxMultiplicity.prototype.max = null; + +/** + * Variable: validNeighbors + * + * Holds an array of strings that specify the type of neighbor for which + * this rule applies. The strings are used in <mxCell.is> on the opposite + * terminal to check if the rule applies to the connection. + */ +mxMultiplicity.prototype.validNeighbors = null; + +/** + * Variable: validNeighborsAllowed + * + * Boolean indicating if the list of validNeighbors are those that are allowed + * for this rule or those that are not allowed for this rule. + */ +mxMultiplicity.prototype.validNeighborsAllowed = true; + +/** + * Variable: countError + * + * Holds the localized error message to be displayed if the number of + * connections for which the rule applies is smaller than <min> or greater + * than <max>. + */ +mxMultiplicity.prototype.countError = null; + +/** + * Variable: typeError + * + * Holds the localized error message to be displayed if the type of the + * neighbor for a connection does not match the rule. + */ +mxMultiplicity.prototype.typeError = null; + +/** + * Function: check + * + * Checks the multiplicity for the given arguments and returns the error + * for the given connection or null if the multiplicity does not apply. + * + * Parameters: + * + * graph - Reference to the enclosing <mxGraph> instance. + * edge - <mxCell> that represents the edge to validate. + * source - <mxCell> that represents the source terminal. + * target - <mxCell> that represents the target terminal. + * sourceOut - Number of outgoing edges from the source terminal. + * targetIn - Number of incoming edges for the target terminal. + */ +mxMultiplicity.prototype.check = function(graph, edge, source, target, sourceOut, targetIn) +{ + var error = ''; + + if ((this.source && this.checkTerminal(graph, source, edge)) || + (!this.source && this.checkTerminal(graph, target, edge))) + { + if (this.countError != null && + ((this.source && (this.max == 0 || (sourceOut >= this.max))) || + (!this.source && (this.max == 0 || (targetIn >= this.max))))) + { + error += this.countError + '\n'; + } + + if (this.validNeighbors != null && this.typeError != null && this.validNeighbors.length > 0) + { + var isValid = this.checkNeighbors(graph, edge, source, target); + + if (!isValid) + { + error += this.typeError + '\n'; + } + } + } + + return (error.length > 0) ? error : null; +}; + +/** + * Function: checkNeighbors + * + * Checks if there are any valid neighbours in <validNeighbors>. This is only + * called if <validNeighbors> is a non-empty array. + */ +mxMultiplicity.prototype.checkNeighbors = function(graph, edge, source, target) +{ + var sourceValue = graph.model.getValue(source); + var targetValue = graph.model.getValue(target); + var isValid = !this.validNeighborsAllowed; + var valid = this.validNeighbors; + + for (var j = 0; j < valid.length; j++) + { + if (this.source && + this.checkType(graph, targetValue, valid[j])) + { + isValid = this.validNeighborsAllowed; + break; + } + else if (!this.source && + this.checkType(graph, sourceValue, valid[j])) + { + isValid = this.validNeighborsAllowed; + break; + } + } + + return isValid; +}; + +/** + * Function: checkTerminal + * + * Checks the given terminal cell and returns true if this rule applies. The + * given cell is the source or target of the given edge, depending on + * <source>. This implementation uses <checkType> on the terminal's value. + */ +mxMultiplicity.prototype.checkTerminal = function(graph, terminal, edge) +{ + var value = graph.model.getValue(terminal); + + return this.checkType(graph, value, this.type, this.attr, this.value); +}; + +/** + * Function: checkType + * + * Checks the type of the given value. + */ +mxMultiplicity.prototype.checkType = function(graph, value, type, attr, attrValue) +{ + if (value != null) + { + if (!isNaN(value.nodeType)) // Checks if value is a DOM node + { + return mxUtils.isNode(value, type, attr, attrValue); + } + else + { + return value == type; + } + } + + return false; +}; diff --git a/src/js/view/mxOutline.js b/src/js/view/mxOutline.js new file mode 100644 index 0000000..a0d6fd3 --- /dev/null +++ b/src/js/view/mxOutline.js @@ -0,0 +1,649 @@ +/** + * $Id: mxOutline.js,v 1.81 2012-06-20 14:13:37 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxOutline + * + * Implements an outline (aka overview) for a graph. Set <updateOnPan> to true + * to enable updates while the source graph is panning. + * + * Example: + * + * (code) + * var outline = new mxOutline(graph, div); + * (end) + * + * If the selection border in the outline appears behind the contents of the + * graph, then you can use the following code. (This may happen when using a + * transparent container for the outline in IE.) + * + * (code) + * mxOutline.prototype.graphRenderHint = mxConstants.RENDERING_HINT_EXACT; + * (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 - <mxGraph> 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 <mxGraph>. + */ +mxOutline.prototype.source = null; + +/** + * Function: outline + * + * Reference to the outline <mxGraph>. + */ +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: updateOnPan + * + * Specifies if <update> should be called for <mxEvent.PAN> in the source + * graph. Default is false. + */ +mxOutline.prototype.updateOnPan = false; + +/** + * Variable: sizerImage + * + * Optional <mxImage> to be used for the sizer. Default is null. + */ +mxOutline.prototype.sizerImage = null; + +/** + * 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; + +/** + * Function: init + * + * Initializes the outline inside the given container. + */ +mxOutline.prototype.init = function(container) +{ + this.outline = new mxGraph(container, this.source.getModel(), this.graphRenderHint, this.source.getStylesheet()); + this.outline.foldingEnabled = false; + this.outline.autoScroll = false; + + // 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 = false; + 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 != mxConstants.DIALECT_SVG) ? + mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG; + this.selectionBorder.crisp = true; + this.selectionBorder.init(this.outline.getView().getOverlayPane()); + mxEvent.redirectMouseEvents(this.selectionBorder.node, this.outline); + this.selectionBorder.node.style.background = ''; + + // Creates a small blue rectangle for sizing (sizer handle) + this.sizer = this.createSizer(); + this.sizer.init(this.outline.getView().getOverlayPane()); + + if (this.enabled) + { + this.sizer.node.style.cursor = 'pointer'; + } + + // Redirects all events from the sizerhandle to the outline + mxEvent.addListener(this.sizer.node, (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown', + mxUtils.bind(this, function(evt) + { + this.outline.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt)); + }) + ); + + 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 <enabled>. + */ +mxOutline.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Enables or disables event handling. This implementation + * updates <enabled>. + * + * 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 <update> 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; + sizer.crisp = true; + + 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: update + * + * Updates the outline. + */ +mxOutline.prototype.update = function(revalidate) +{ + if (this.source != null) + { + var sourceScale = this.source.view.scale; + var scaledGraphBounds = this.source.getGraphBounds(); + 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 = 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) + { + this.zoom = me.isSource(this.sizer); + 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 <graph> 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 dx = me.getX() - this.startX; + var dy = me.getY() - this.startY; + 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: mouseUp + * + * Handles the event by applying the translation or zoom to <graph>. + */ +mxOutline.prototype.mouseUp = function(sender, me) +{ + if (this.active) + { + var dx = me.getX() - this.startX; + var dy = me.getY() - this.startY; + + 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(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 <source>. + */ +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.addListener(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; + } +}; diff --git a/src/js/view/mxPerimeter.js b/src/js/view/mxPerimeter.js new file mode 100644 index 0000000..7aaa187 --- /dev/null +++ b/src/js/view/mxPerimeter.js @@ -0,0 +1,484 @@ +/** + * $Id: mxPerimeter.js,v 1.28 2012-01-11 09:06:56 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +var mxPerimeter = +{ + /** + * Class: mxPerimeter + * + * Provides various perimeter functions to be used in a style + * as the value of <mxConstants.STYLE_PERIMETER>. Perimeters for + * rectangle, circle, rhombus and triangle are available. + * + * Example: + * + * (code) + * <add as="perimeter">mxPerimeter.RightAngleRectanglePerimeter</add> + * (end) + * + * Or programmatically: + * + * (code) + * style[mxConstants.STYLE_PERIMETER] = mxPerimeter.RectanglePerimeter; + * (end) + * + * When adding new perimeter functions, it is recommended to use the + * mxPerimeter-namespace as follows: + * + * (code) + * mxPerimeter.CustomPerimeter = function (bounds, vertex, next, orthogonal) + * { + * var x = 0; // Calculate x-coordinate + * var y = 0; // Calculate y-coordainte + * + * return new mxPoint(x, y); + * } + * (end) + * + * The new perimeter should then be registered in the <mxStyleRegistry> as follows: + * (code) + * mxStyleRegistry.putValue('customPerimeter', mxPerimeter.CustomPerimeter); + * (end) + * + * The custom perimeter above can now be used in a specific vertex as follows: + * + * (code) + * model.setStyle(vertex, 'perimeter=customPerimeter'); + * (end) + * + * Note that the key of the <mxStyleRegistry> entry for the function should + * be used in string values, unless <mxGraphView.allowEval> is true, in + * which case you can also use mxPerimeter.CustomPerimeter for the value in + * the cell style above. + * + * Or it can be used for all vertices in the graph as follows: + * + * (code) + * var style = graph.getStylesheet().getDefaultVertexStyle(); + * style[mxConstants.STYLE_PERIMETER] = mxPerimeter.CustomPerimeter; + * (end) + * + * Note that the object can be used directly when programmatically setting + * the value, but the key in the <mxStyleRegistry> should be used when + * setting the value via a key, value pair in a cell style. + * + * The parameters are explained in <RectanglePerimeter>. + * + * Function: RectanglePerimeter + * + * Describes a rectangular perimeter for the given bounds. + * + * Parameters: + * + * bounds - <mxRectangle> that represents the absolute bounds of the + * vertex. + * vertex - <mxCellState> that represents the vertex. + * next - <mxPoint> that represents the nearest neighbour point on the + * given edge. + * orthogonal - Boolean that specifies if the orthogonal projection onto + * the perimeter should be returned. If this is false then the intersection + * of the perimeter and the line between the next and the center point is + * returned. + */ + RectanglePerimeter: function (bounds, vertex, next, orthogonal) + { + var cx = bounds.getCenterX(); + var cy = bounds.getCenterY(); + var dx = next.x - cx; + var dy = next.y - cy; + var alpha = Math.atan2(dy, dx); + var p = new mxPoint(0, 0); + var pi = Math.PI; + var pi2 = Math.PI/2; + var beta = pi2 - alpha; + var t = Math.atan2(bounds.height, bounds.width); + + if (alpha < -pi + t || alpha > pi - t) + { + // Left edge + p.x = bounds.x; + p.y = cy - bounds.width * Math.tan(alpha) / 2; + } + else if (alpha < -t) + { + // Top Edge + p.y = bounds.y; + p.x = cx - bounds.height * Math.tan(beta) / 2; + } + else if (alpha < t) + { + // Right Edge + p.x = bounds.x + bounds.width; + p.y = cy + bounds.width * Math.tan(alpha) / 2; + } + else + { + // Bottom Edge + p.y = bounds.y + bounds.height; + p.x = cx + bounds.height * Math.tan(beta) / 2; + } + + if (orthogonal) + { + if (next.x >= bounds.x && + next.x <= bounds.x + bounds.width) + { + p.x = next.x; + } + else if (next.y >= bounds.y && + next.y <= bounds.y + bounds.height) + { + p.y = next.y; + } + if (next.x < bounds.x) + { + p.x = bounds.x; + } + else if (next.x > bounds.x + bounds.width) + { + p.x = bounds.x + bounds.width; + } + if (next.y < bounds.y) + { + p.y = bounds.y; + } + else if (next.y > bounds.y + bounds.height) + { + p.y = bounds.y + bounds.height; + } + } + + return p; + }, + + /** + * Function: EllipsePerimeter + * + * Describes an elliptic perimeter. See <RectanglePerimeter> + * for a description of the parameters. + */ + EllipsePerimeter: function (bounds, vertex, next, orthogonal) + { + var x = bounds.x; + var y = bounds.y; + var a = bounds.width / 2; + var b = bounds.height / 2; + var cx = x + a; + var cy = y + b; + var px = next.x; + var py = next.y; + + // Calculates straight line equation through + // point and ellipse center y = d * x + h + var dx = parseInt(px - cx); + var dy = parseInt(py - cy); + + if (dx == 0 && dy != 0) + { + return new mxPoint(cx, cy + b * dy / Math.abs(dy)); + } + else if (dx == 0 && dy == 0) + { + return new mxPoint(px, py); + } + + if (orthogonal) + { + if (py >= y && py <= y + bounds.height) + { + var ty = py - cy; + var tx = Math.sqrt(a*a*(1-(ty*ty)/(b*b))) || 0; + + if (px <= x) + { + tx = -tx; + } + + return new mxPoint(cx+tx, py); + } + + if (px >= x && px <= x + bounds.width) + { + var tx = px - cx; + var ty = Math.sqrt(b*b*(1-(tx*tx)/(a*a))) || 0; + + if (py <= y) + { + ty = -ty; + } + + return new mxPoint(px, cy+ty); + } + } + + // Calculates intersection + var d = dy / dx; + var h = cy - d * cx; + var e = a * a * d * d + b * b; + var f = -2 * cx * e; + var g = a * a * d * d * cx * cx + + b * b * cx * cx - + a * a * b * b; + var det = Math.sqrt(f * f - 4 * e * g); + + // Two solutions (perimeter points) + var xout1 = (-f + det) / (2 * e); + var xout2 = (-f - det) / (2 * e); + var yout1 = d * xout1 + h; + var yout2 = d * xout2 + h; + var dist1 = Math.sqrt(Math.pow((xout1 - px), 2) + + Math.pow((yout1 - py), 2)); + var dist2 = Math.sqrt(Math.pow((xout2 - px), 2) + + Math.pow((yout2 - py), 2)); + + // Correct solution + var xout = 0; + var yout = 0; + + if (dist1 < dist2) + { + xout = xout1; + yout = yout1; + } + else + { + xout = xout2; + yout = yout2; + } + + return new mxPoint(xout, yout); + }, + + /** + * Function: RhombusPerimeter + * + * Describes a rhombus (aka diamond) perimeter. See <RectanglePerimeter> + * for a description of the parameters. + */ + RhombusPerimeter: function (bounds, vertex, next, orthogonal) + { + var x = bounds.x; + var y = bounds.y; + var w = bounds.width; + var h = bounds.height; + + var cx = x + w / 2; + var cy = y + h / 2; + + var px = next.x; + var py = next.y; + + // Special case for intersecting the diamond's corners + if (cx == px) + { + if (cy > py) + { + return new mxPoint(cx, y); // top + } + else + { + return new mxPoint(cx, y + h); // bottom + } + } + else if (cy == py) + { + if (cx > px) + { + return new mxPoint(x, cy); // left + } + else + { + return new mxPoint(x + w, cy); // right + } + } + + var tx = cx; + var ty = cy; + + if (orthogonal) + { + if (px >= x && px <= x + w) + { + tx = px; + } + else if (py >= y && py <= y + h) + { + ty = py; + } + } + + // In which quadrant will the intersection be? + // set the slope and offset of the border line accordingly + if (px < cx) + { + if (py < cy) + { + return mxUtils.intersection(px, py, tx, ty, cx, y, x, cy); + } + else + { + return mxUtils.intersection(px, py, tx, ty, cx, y + h, x, cy); + } + } + else if (py < cy) + { + return mxUtils.intersection(px, py, tx, ty, cx, y, x + w, cy); + } + else + { + return mxUtils.intersection(px, py, tx, ty, cx, y + h, x + w, cy); + } + }, + + /** + * Function: TrianglePerimeter + * + * Describes a triangle perimeter. See <RectanglePerimeter> + * for a description of the parameters. + */ + TrianglePerimeter: function (bounds, vertex, next, orthogonal) + { + var direction = (vertex != null) ? + vertex.style[mxConstants.STYLE_DIRECTION] : null; + var vertical = direction == mxConstants.DIRECTION_NORTH || + direction == mxConstants.DIRECTION_SOUTH; + + var x = bounds.x; + var y = bounds.y; + var w = bounds.width; + var h = bounds.height; + + var cx = x + w / 2; + var cy = y + h / 2; + + var start = new mxPoint(x, y); + var corner = new mxPoint(x + w, cy); + var end = new mxPoint(x, y + h); + + if (direction == mxConstants.DIRECTION_NORTH) + { + start = end; + corner = new mxPoint(cx, y); + end = new mxPoint(x + w, y + h); + } + else if (direction == mxConstants.DIRECTION_SOUTH) + { + corner = new mxPoint(cx, y + h); + end = new mxPoint(x + w, y); + } + else if (direction == mxConstants.DIRECTION_WEST) + { + start = new mxPoint(x + w, y); + corner = new mxPoint(x, cy); + end = new mxPoint(x + w, y + h); + } + + var dx = next.x - cx; + var dy = next.y - cy; + + var alpha = (vertical) ? Math.atan2(dx, dy) : Math.atan2(dy, dx); + var t = (vertical) ? Math.atan2(w, h) : Math.atan2(h, w); + + var base = false; + + if (direction == mxConstants.DIRECTION_NORTH || + direction == mxConstants.DIRECTION_WEST) + { + base = alpha > -t && alpha < t; + } + else + { + base = alpha < -Math.PI + t || alpha > Math.PI - t; + } + + var result = null; + + if (base) + { + if (orthogonal && ((vertical && next.x >= start.x && next.x <= end.x) || + (!vertical && next.y >= start.y && next.y <= end.y))) + { + if (vertical) + { + result = new mxPoint(next.x, start.y); + } + else + { + result = new mxPoint(start.x, next.y); + } + } + else + { + if (direction == mxConstants.DIRECTION_NORTH) + { + result = new mxPoint(x + w / 2 + h * Math.tan(alpha) / 2, + y + h); + } + else if (direction == mxConstants.DIRECTION_SOUTH) + { + result = new mxPoint(x + w / 2 - h * Math.tan(alpha) / 2, + y); + } + else if (direction == mxConstants.DIRECTION_WEST) + { + result = new mxPoint(x + w, y + h / 2 + + w * Math.tan(alpha) / 2); + } + else + { + result = new mxPoint(x, y + h / 2 - + w * Math.tan(alpha) / 2); + } + } + } + else + { + if (orthogonal) + { + var pt = new mxPoint(cx, cy); + + if (next.y >= y && next.y <= y + h) + { + pt.x = (vertical) ? cx : ( + (direction == mxConstants.DIRECTION_WEST) ? + x + w : x); + pt.y = next.y; + } + else if (next.x >= x && next.x <= x + w) + { + pt.x = next.x; + pt.y = (!vertical) ? cy : ( + (direction == mxConstants.DIRECTION_NORTH) ? + y + h : y); + } + + // Compute angle + dx = next.x - pt.x; + dy = next.y - pt.y; + + cx = pt.x; + cy = pt.y; + } + + if ((vertical && next.x <= x + w / 2) || + (!vertical && next.y <= y + h / 2)) + { + result = mxUtils.intersection(next.x, next.y, cx, cy, + start.x, start.y, corner.x, corner.y); + } + else + { + result = mxUtils.intersection(next.x, next.y, cx, cy, + corner.x, corner.y, end.x, end.y); + } + } + + if (result == null) + { + result = new mxPoint(cx, cy); + } + + return result; + } +}; diff --git a/src/js/view/mxPrintPreview.js b/src/js/view/mxPrintPreview.js new file mode 100644 index 0000000..24a65e6 --- /dev/null +++ b/src/js/view/mxPrintPreview.js @@ -0,0 +1,801 @@ +/** + * $Id: mxPrintPreview.js,v 1.61 2012-05-15 14:12:40 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxPrintPreview + * + * Implements printing of a diagram across multiple pages. The following opens + * a print preview for an existing graph: + * + * (code) + * var preview = new mxPrintPreview(graph); + * preview.open(); + * (end) + * + * Use <mxUtils.getScaleForPageCount> as follows in order to print the graph + * across a given number of pages: + * + * (code) + * var pageCount = mxUtils.prompt('Enter page count', '1'); + * + * if (pageCount != null) + * { + * var scale = mxUtils.getScaleForPageCount(pageCount, graph); + * var preview = new mxPrintPreview(graph, scale); + * preview.open(); + * } + * (end) + * + * Headers: + * + * Apart from setting the title argument in the mxPrintPreview constructor you + * can override <renderPage> as follows to add a header to any page: + * + * (code) + * var oldRenderPage = mxPrintPreview.prototype.renderPage; + * mxPrintPreview.prototype.renderPage = function(w, h, dx, dy, scale, pageNumber) + * { + * var div = oldRenderPage.apply(this, arguments); + * + * var header = document.createElement('div'); + * header.style.position = 'absolute'; + * header.style.top = '0px'; + * header.style.width = '100%'; + * header.style.textAlign = 'right'; + * mxUtils.write(header, 'Your header here - Page ' + pageNumber + ' / ' + this.pageCount); + * div.firstChild.appendChild(header); + * + * return div; + * }; + * (end) + * + * Page Format: + * + * For landscape printing, use <mxConstants.PAGE_FORMAT_A4_LANDSCAPE> as + * the pageFormat in <mxUtils.getScaleForPageCount> and <mxPrintPreview>. + * Keep in mind that one can not set the defaults for the print dialog + * of the operating system from JavaScript so the user must manually choose + * a page format that matches this setting. + * + * You can try passing the following CSS directive to <open> to set the + * page format in the print dialog to landscape. However, this CSS + * directive seems to be ignored in most major browsers, including IE. + * + * (code) + * @page { + * size: landscape; + * } + * (end) + * + * Note that the print preview behaves differently in IE when used from the + * filesystem or via HTTP so printing should always be tested via HTTP. + * + * If you are using a DOCTYPE in the source page you can override <getDoctype> + * and provide the same DOCTYPE for the print preview if required. Here is + * an example for IE8 standards mode. + * + * (code) + * var preview = new mxPrintPreview(graph); + * preview.getDoctype = function() + * { + * return '<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=5,IE=8" ><![endif]-->'; + * }; + * preview.open(); + * (end) + * + * Constructor: mxPrintPreview + * + * Constructs a new print preview for the given parameters. + * + * Parameters: + * + * graph - <mxGraph> to be previewed. + * scale - Optional scale of the output. Default is 1 / <mxGraph.pageScale>. + * border - Border in pixels along each side of every page. Note that the + * actual print function in the browser will add another border for + * printing. + * pageFormat - <mxRectangle> that specifies the page format (in pixels). + * This should match the page format of the printer. Default uses the + * <mxGraph.pageFormat> of the given graph. + * x0 - Optional left offset of the output. Default is 0. + * y0 - Optional top offset of the output. Default is 0. + * borderColor - Optional color of the page border. Default is no border. + * Note that a border is sometimes useful to highlight the printed page + * border in the print preview of the browser. + * title - Optional string that is used for the window title. Default + * is 'Printer-friendly version'. + * pageSelector - Optional boolean that specifies if the page selector + * should appear in the window with the print preview. Default is true. + */ +function mxPrintPreview(graph, scale, pageFormat, border, x0, y0, borderColor, title, pageSelector) +{ + this.graph = graph; + this.scale = (scale != null) ? scale : 1 / graph.pageScale; + this.border = (border != null) ? border : 0; + this.pageFormat = (pageFormat != null) ? pageFormat : graph.pageFormat; + this.title = (title != null) ? title : 'Printer-friendly version'; + this.x0 = (x0 != null) ? x0 : 0; + this.y0 = (y0 != null) ? y0 : 0; + this.borderColor = borderColor; + this.pageSelector = (pageSelector != null) ? pageSelector : true; +}; + +/** + * Variable: graph + * + * Reference to the <mxGraph> that should be previewed. + */ +mxPrintPreview.prototype.graph = null; + +/** + * Variable: pageFormat + * + * Holds the <mxRectangle> that defines the page format. + */ +mxPrintPreview.prototype.pageFormat = null; + +/** + * Variable: scale + * + * Holds the scale of the print preview. + */ +mxPrintPreview.prototype.scale = null; + +/** + * Variable: border + * + * The border inset around each side of every page in the preview. This is set + * to 0 if autoOrigin is false. + */ +mxPrintPreview.prototype.border = 0; + +/** +/** + * Variable: x0 + * + * Holds the horizontal offset of the output. + */ +mxPrintPreview.prototype.x0 = 0; + +/** + * Variable: y0 + * + * Holds the vertical offset of the output. + */ +mxPrintPreview.prototype.y0 = 0; + +/** + * Variable: autoOrigin + * + * Specifies if the origin should be automatically computed based on the top, + * left corner of the actual diagram contents. If this is set to false then the + * values for <x0> and <y0> will be overridden in <open>. Default is true. + */ +mxPrintPreview.prototype.autoOrigin = true; + +/** + * Variable: printOverlays + * + * Specifies if overlays should be printed. Default is false. + */ +mxPrintPreview.prototype.printOverlays = false; + +/** + * Variable: borderColor + * + * Holds the color value for the page border. + */ +mxPrintPreview.prototype.borderColor = null; + +/** + * Variable: title + * + * Holds the title of the preview window. + */ +mxPrintPreview.prototype.title = null; + +/** + * Variable: pageSelector + * + * Boolean that specifies if the page selector should be + * displayed. Default is true. + */ +mxPrintPreview.prototype.pageSelector = null; + +/** + * Variable: wnd + * + * Reference to the preview window. + */ +mxPrintPreview.prototype.wnd = null; + +/** + * Variable: pageCount + * + * Holds the actual number of pages in the preview. + */ +mxPrintPreview.prototype.pageCount = 0; + +/** + * Function: getWindow + * + * Returns <wnd>. + */ +mxPrintPreview.prototype.getWindow = function() +{ + return this.wnd; +}; + +/** + * Function: getDocType + * + * Returns the string that should go before the HTML tag in the print preview + * page. This implementation returns an empty string. + */ +mxPrintPreview.prototype.getDoctype = function() +{ + return ''; +}; + +/** + * Function: open + * + * Shows the print preview window. The window is created here if it does + * not exist. + * + * Parameters: + * + * css - Optional CSS string to be used in the new page's head section. + */ +mxPrintPreview.prototype.open = function(css) +{ + // Closing the window while the page is being rendered may cause an + // exception in IE. This and any other exceptions are simply ignored. + var previousInitializeOverlay = this.graph.cellRenderer.initializeOverlay; + var div = null; + + try + { + // Temporarily overrides the method to redirect rendering of overlays + // to the draw pane so that they are visible in the printout + if (this.printOverlays) + { + this.graph.cellRenderer.initializeOverlay = function(state, overlay) + { + overlay.init(state.view.getDrawPane()); + }; + } + + if (this.wnd == null) + { + this.wnd = window.open(); + var doc = this.wnd.document; + var dt = this.getDoctype(); + + if (dt != null && dt.length > 0) + { + doc.writeln(dt); + } + + doc.writeln('<html>'); + doc.writeln('<head>'); + this.writeHead(doc, css); + doc.writeln('</head>'); + doc.writeln('<body class="mxPage">'); + + // Adds all required stylesheets and namespaces + mxClient.link('stylesheet', mxClient.basePath + '/css/common.css', doc); + + if (mxClient.IS_IE && document.documentMode != 9) + { + doc.namespaces.add('v', 'urn:schemas-microsoft-com:vml'); + doc.namespaces.add('o', 'urn:schemas-microsoft-com:office:office'); + var ss = doc.createStyleSheet(); + ss.cssText = 'v\\:*{behavior:url(#default#VML)}o\\:*{behavior:url(#default#VML)}'; + mxClient.link('stylesheet', mxClient.basePath + '/css/explorer.css', doc); + } + + // Computes the horizontal and vertical page count + var bounds = this.graph.getGraphBounds().clone(); + var currentScale = this.graph.getView().getScale(); + var sc = currentScale / this.scale; + var tr = this.graph.getView().getTranslate(); + + // Uses the absolute origin with no offset for all printing + if (!this.autoOrigin) + { + this.x0 = -tr.x * this.scale; + this.y0 = -tr.y * this.scale; + bounds.width += bounds.x; + bounds.height += bounds.y; + bounds.x = 0; + bounds.y = 0; + this.border = 0; + } + + // Compute the unscaled, untranslated bounds to find + // the number of vertical and horizontal pages + bounds.width /= sc; + bounds.height /= sc; + + // Store the available page area + var availableWidth = this.pageFormat.width - (this.border * 2); + var availableHeight = this.pageFormat.height - (this.border * 2); + + var hpages = Math.max(1, Math.ceil((bounds.width + this.x0) / availableWidth)); + var vpages = Math.max(1, Math.ceil((bounds.height + this.y0) / availableHeight)); + this.pageCount = hpages * vpages; + + var writePageSelector = mxUtils.bind(this, function() + { + if (this.pageSelector && (vpages > 1 || hpages > 1)) + { + var table = this.createPageSelector(vpages, hpages); + doc.body.appendChild(table); + + // Workaround for position: fixed which isn't working in IE + if (mxClient.IS_IE) + { + table.style.position = 'absolute'; + + var update = function() + { + table.style.top = (doc.body.scrollTop + 10) + 'px'; + }; + + mxEvent.addListener(this.wnd, 'scroll', function(evt) + { + update(); + }); + + mxEvent.addListener(this.wnd, 'resize', function(evt) + { + update(); + }); + } + } + }); + + // Stores pages for later retrieval + var pages = null; + + // Workaround for aspect of image shapes updated asynchronously + // in VML so we need to fetch the markup of the DIV containing + // the image after the udpate of the style of the DOM node. + // LATER: Allow document for display markup to be customized. + if (mxClient.IS_IE && document.documentMode != 9) + { + pages = []; + + // Overrides asynchronous loading of images for fetching HTML markup + var waitCounter = 0; + var isDone = false; + + var mxImageShapeScheduleUpdateAspect = mxImageShape.prototype.scheduleUpdateAspect; + var mxImageShapeUpdateAspect = mxImageShape.prototype.updateAspect; + + var writePages = function() + { + if (isDone && waitCounter == 0) + { + // Restores previous implementations + mxImageShape.prototype.scheduleUpdateAspect = mxImageShapeScheduleUpdateAspect; + mxImageShape.prototype.updateAspect = mxImageShapeUpdateAspect; + + var markup = ''; + + for (var i = 0; i < pages.length; i++) + { + markup += pages[i].outerHTML; + pages[i].parentNode.removeChild(pages[i]); + + if (i < pages.length - 1) + { + markup += '<hr/>'; + } + } + + doc.body.innerHTML = markup; + writePageSelector(); + } + }; + + // Overrides functions to implement wait counter + mxImageShape.prototype.scheduleUpdateAspect = function() + { + waitCounter++; + mxImageShapeScheduleUpdateAspect.apply(this, arguments); + }; + + // Overrides functions to implement wait counter + mxImageShape.prototype.updateAspect = function() + { + mxImageShapeUpdateAspect.apply(this, arguments); + waitCounter--; + writePages(); + }; + } + + // Appends each page to the page output for printing, making + // sure there will be a page break after each page (ie. div) + for (var i = 0; i < vpages; i++) + { + var dy = i * availableHeight / this.scale - this.y0 / this.scale + + (bounds.y - tr.y * currentScale) / currentScale; + + for (var j = 0; j < hpages; j++) + { + if (this.wnd == null) + { + return null; + } + + var dx = j * availableWidth / this.scale - this.x0 / this.scale + + (bounds.x - tr.x * currentScale) / currentScale; + var pageNum = i * hpages + j + 1; + + div = this.renderPage(this.pageFormat.width, this.pageFormat.height, + -dx, -dy, this.scale, pageNum); + + // Gives the page a unique ID for later accessing the page + div.setAttribute('id', 'mxPage-'+pageNum); + + // Border of the DIV (aka page) inside the document + if (this.borderColor != null) + { + div.style.borderColor = this.borderColor; + div.style.borderStyle = 'solid'; + div.style.borderWidth = '1px'; + } + + // Needs to be assigned directly because IE doesn't support + // child selectors, eg. body > div { background: white; } + div.style.background = 'white'; + + if (i < vpages - 1 || j < hpages - 1) + { + div.style.pageBreakAfter = 'always'; + } + + // NOTE: We are dealing with cross-window DOM here, which + // is a problem in IE, so we copy the HTML markup instead. + // The underlying problem is that the graph display markup + // creation (in mxShape, mxGraphView) is hardwired to using + // document.createElement and hence we must use document + // to create the complete page and then copy it over to the + // new window.document. This can be fixed later by using the + // ownerDocument of the container in mxShape and mxGraphView. + if (mxClient.IS_IE) + { + // For some obscure reason, removing the DIV from the + // parent before fetching its outerHTML has missing + // fillcolor properties and fill children, so the div + // must be removed afterwards to keep the fillcolors. + // For delayed output we remote the DIV from the + // original document when we write out all pages. + doc.writeln(div.outerHTML); + + if (pages != null) + { + pages.push(div); + } + else + { + div.parentNode.removeChild(div); + } + } + else + { + div.parentNode.removeChild(div); + doc.body.appendChild(div); + } + + if (i < vpages - 1 || j < hpages - 1) + { + var hr = doc.createElement('hr'); + hr.className = 'mxPageBreak'; + doc.body.appendChild(hr); + } + } + } + + doc.writeln('</body>'); + doc.writeln('</html>'); + doc.close(); + + // Marks the printing complete for async handling + if (pages != null) + { + isDone = true; + writePages(); + } + else + { + writePageSelector(); + } + + // Removes all event handlers in the print output + mxEvent.release(doc.body); + } + + this.wnd.focus(); + } + catch (e) + { + // Removes the DIV from the document in case of an error + if (div != null && div.parentNode != null) + { + div.parentNode.removeChild(div); + } + } + finally + { + this.graph.cellRenderer.initializeOverlay = previousInitializeOverlay; + } + + return this.wnd; +}; + +/** + * Function: writeHead + * + * Writes the HEAD section into the given document, without the opening + * and closing HEAD tags. + */ +mxPrintPreview.prototype.writeHead = function(doc, css) +{ + if (this.title != null) + { + doc.writeln('<title>' + this.title + '</title>'); + } + + // Makes sure no horizontal rulers are printed + doc.writeln('<style type="text/css">'); + doc.writeln('@media print {'); + doc.writeln(' table.mxPageSelector { display: none; }'); + doc.writeln(' hr.mxPageBreak { display: none; }'); + doc.writeln('}'); + doc.writeln('@media screen {'); + + // NOTE: position: fixed is not supported in IE, so the page selector + // position (absolute) needs to be updated in IE (see below) + doc.writeln(' table.mxPageSelector { position: fixed; right: 10px; top: 10px;' + + 'font-family: Arial; font-size:10pt; border: solid 1px darkgray;' + + 'background: white; border-collapse:collapse; }'); + doc.writeln(' table.mxPageSelector td { border: solid 1px gray; padding:4px; }'); + doc.writeln(' body.mxPage { background: gray; }'); + doc.writeln('}'); + + if (css != null) + { + doc.writeln(css); + } + + doc.writeln('</style>'); +}; + +/** + * Function: createPageSelector + * + * Creates the page selector table. + */ +mxPrintPreview.prototype.createPageSelector = function(vpages, hpages) +{ + var doc = this.wnd.document; + var table = doc.createElement('table'); + table.className = 'mxPageSelector'; + table.setAttribute('border', '0'); + + var tbody = doc.createElement('tbody'); + + for (var i = 0; i < vpages; i++) + { + var row = doc.createElement('tr'); + + for (var j = 0; j < hpages; j++) + { + var pageNum = i * hpages + j + 1; + var cell = doc.createElement('td'); + + // Needs anchor for all browers to work without JavaScript + // LATER: Does not work in Firefox because the generated document + // has the URL of the opening document, the anchor is appended + // to that URL and the full URL is loaded on click. + if (!mxClient.IS_NS || mxClient.IS_SF || mxClient.IS_GC) + { + var a = doc.createElement('a'); + a.setAttribute('href', '#mxPage-' + pageNum); + mxUtils.write(a, pageNum, doc); + cell.appendChild(a); + } + else + { + mxUtils.write(cell, pageNum, doc); + } + + row.appendChild(cell); + } + + tbody.appendChild(row); + } + + table.appendChild(tbody); + + return table; +}; + +/** + * Function: renderPage + * + * Creates a DIV that prints a single page of the given + * graph using the given scale and returns the DIV that + * represents the page. + * + * Parameters: + * + * w - Width of the page in pixels. + * h - Height of the page in pixels. + * dx - Horizontal translation for the diagram. + * dy - Vertical translation for the diagram. + * scale - Scale for the diagram. + * pageNumber - Number of the page to be rendered. + */ +mxPrintPreview.prototype.renderPage = function(w, h, dx, dy, scale, pageNumber) +{ + var div = document.createElement('div'); + + try + { + div.style.width = w + 'px'; + div.style.height = h + 'px'; + div.style.overflow = 'hidden'; + div.style.pageBreakInside = 'avoid'; + + var innerDiv = document.createElement('div'); + innerDiv.style.top = this.border + 'px'; + innerDiv.style.left = this.border + 'px'; + innerDiv.style.width = (w - 2 * this.border) + 'px'; + innerDiv.style.height = (h - 2 * this.border) + 'px'; + innerDiv.style.overflow = 'hidden'; + + if (this.graph.dialect == mxConstants.DIALECT_VML) + { + innerDiv.style.position = 'absolute'; + } + + div.appendChild(innerDiv); + document.body.appendChild(div); + var view = this.graph.getView(); + + var previousContainer = this.graph.container; + this.graph.container = innerDiv; + + var canvas = view.getCanvas(); + var backgroundPane = view.getBackgroundPane(); + var drawPane = view.getDrawPane(); + var overlayPane = view.getOverlayPane(); + + if (this.graph.dialect == mxConstants.DIALECT_SVG) + { + view.createSvg(); + } + else if (this.graph.dialect == mxConstants.DIALECT_VML) + { + view.createVml(); + } + else + { + view.createHtml(); + } + + // Disables events on the view + var eventsEnabled = view.isEventsEnabled(); + view.setEventsEnabled(false); + + // Disables the graph to avoid cursors + var graphEnabled = this.graph.isEnabled(); + this.graph.setEnabled(false); + + // Resets the translation + var translate = view.getTranslate(); + view.translate = new mxPoint(dx, dy); + + var temp = null; + + try + { + // Creates the temporary cell states in the view and + // draws them onto the temporary DOM nodes in the view + var model = this.graph.getModel(); + var cells = [model.getRoot()]; + temp = new mxTemporaryCellStates(view, scale, cells); + } + finally + { + // Removes overlay pane with selection handles + // controls and icons from the print output + if (mxClient.IS_IE) + { + view.overlayPane.innerHTML = ''; + } + else + { + // Removes everything but the SVG node + var tmp = innerDiv.firstChild; + + while (tmp != null) + { + var next = tmp.nextSibling; + var name = tmp.nodeName.toLowerCase(); + + // Note: Width and heigh are required in FF 11 + if (name == 'svg') + { + tmp.setAttribute('width', parseInt(innerDiv.style.width)); + tmp.setAttribute('height', parseInt(innerDiv.style.height)); + } + // Tries to fetch all text labels and only text labels + else if (tmp.style.cursor != 'default' && name != 'table') + { + tmp.parentNode.removeChild(tmp); + } + + tmp = next; + } + } + + // Completely removes the overlay pane to remove more handles + view.overlayPane.parentNode.removeChild(view.overlayPane); + + // Restores the state of the view + this.graph.setEnabled(graphEnabled); + this.graph.container = previousContainer; + view.canvas = canvas; + view.backgroundPane = backgroundPane; + view.drawPane = drawPane; + view.overlayPane = overlayPane; + view.translate = translate; + temp.destroy(); + view.setEventsEnabled(eventsEnabled); + } + } + catch (e) + { + div.parentNode.removeChild(div); + div = null; + + throw e; + } + + return div; +}; + +/** + * Function: print + * + * Opens the print preview and shows the print dialog. + */ +mxPrintPreview.prototype.print = function() +{ + var wnd = this.open(); + + if (wnd != null) + { + wnd.print(); + } +}; + +/** + * Function: close + * + * Closes the print preview window. + */ +mxPrintPreview.prototype.close = function() +{ + if (this.wnd != null) + { + this.wnd.close(); + this.wnd = null; + } +}; diff --git a/src/js/view/mxSpaceManager.js b/src/js/view/mxSpaceManager.js new file mode 100644 index 0000000..2a2dd11 --- /dev/null +++ b/src/js/view/mxSpaceManager.js @@ -0,0 +1,460 @@ +/** + * $Id: mxSpaceManager.js,v 1.9 2010-01-02 09:45:15 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxSpaceManager + * + * In charge of moving cells after a resize. + * + * Constructor: mxSpaceManager + * + * Constructs a new automatic layout for the given graph. + * + * Arguments: + * + * graph - Reference to the enclosing graph. + */ +function mxSpaceManager(graph, shiftRightwards, shiftDownwards, extendParents) +{ + this.resizeHandler = mxUtils.bind(this, function(sender, evt) + { + if (this.isEnabled()) + { + this.cellsResized(evt.getProperty('cells')); + } + }); + + this.foldHandler = mxUtils.bind(this, function(sender, evt) + { + if (this.isEnabled()) + { + this.cellsResized(evt.getProperty('cells')); + } + }); + + this.shiftRightwards = (shiftRightwards != null) ? shiftRightwards : true; + this.shiftDownwards = (shiftDownwards != null) ? shiftDownwards : true; + this.extendParents = (extendParents != null) ? extendParents : true; + this.setGraph(graph); +}; + +/** + * Extends mxEventSource. + */ +mxSpaceManager.prototype = new mxEventSource(); +mxSpaceManager.prototype.constructor = mxSpaceManager; + +/** + * Variable: graph + * + * Reference to the enclosing <mxGraph>. + */ +mxSpaceManager.prototype.graph = null; + +/** + * Variable: enabled + * + * Specifies if event handling is enabled. Default is true. + */ +mxSpaceManager.prototype.enabled = true; + +/** + * Variable: shiftRightwards + * + * Specifies if event handling is enabled. Default is true. + */ +mxSpaceManager.prototype.shiftRightwards = true; + +/** + * Variable: shiftDownwards + * + * Specifies if event handling is enabled. Default is true. + */ +mxSpaceManager.prototype.shiftDownwards = true; + +/** + * Variable: extendParents + * + * Specifies if event handling is enabled. Default is true. + */ +mxSpaceManager.prototype.extendParents = true; + +/** + * Variable: resizeHandler + * + * Holds the function that handles the move event. + */ +mxSpaceManager.prototype.resizeHandler = null; + +/** + * Variable: foldHandler + * + * Holds the function that handles the fold event. + */ +mxSpaceManager.prototype.foldHandler = null; + +/** + * Function: isCellIgnored + * + * Sets the graph that the layouts operate on. + */ +mxSpaceManager.prototype.isCellIgnored = function(cell) +{ + return !this.getGraph().getModel().isVertex(cell); +}; + +/** + * Function: isCellShiftable + * + * Sets the graph that the layouts operate on. + */ +mxSpaceManager.prototype.isCellShiftable = function(cell) +{ + return this.getGraph().getModel().isVertex(cell) && + this.getGraph().isCellMovable(cell); +}; + +/** + * Function: isEnabled + * + * Returns true if events are handled. This implementation + * returns <enabled>. + */ +mxSpaceManager.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Enables or disables event handling. This implementation + * updates <enabled>. + * + * Parameters: + * + * enabled - Boolean that specifies the new enabled state. + */ +mxSpaceManager.prototype.setEnabled = function(value) +{ + this.enabled = value; +}; + +/** + * Function: isShiftRightwards + * + * Returns true if events are handled. This implementation + * returns <enabled>. + */ +mxSpaceManager.prototype.isShiftRightwards = function() +{ + return this.shiftRightwards; +}; + +/** + * Function: setShiftRightwards + * + * Enables or disables event handling. This implementation + * updates <enabled>. + * + * Parameters: + * + * enabled - Boolean that specifies the new enabled state. + */ +mxSpaceManager.prototype.setShiftRightwards = function(value) +{ + this.shiftRightwards = value; +}; + +/** + * Function: isShiftDownwards + * + * Returns true if events are handled. This implementation + * returns <enabled>. + */ +mxSpaceManager.prototype.isShiftDownwards = function() +{ + return this.shiftDownwards; +}; + +/** + * Function: setShiftDownwards + * + * Enables or disables event handling. This implementation + * updates <enabled>. + * + * Parameters: + * + * enabled - Boolean that specifies the new enabled state. + */ +mxSpaceManager.prototype.setShiftDownwards = function(value) +{ + this.shiftDownwards = value; +}; + +/** + * Function: isExtendParents + * + * Returns true if events are handled. This implementation + * returns <enabled>. + */ +mxSpaceManager.prototype.isExtendParents = function() +{ + return this.extendParents; +}; + +/** + * Function: setShiftDownwards + * + * Enables or disables event handling. This implementation + * updates <enabled>. + * + * Parameters: + * + * enabled - Boolean that specifies the new enabled state. + */ +mxSpaceManager.prototype.setExtendParents = function(value) +{ + this.extendParents = value; +}; + +/** + * Function: getGraph + * + * Returns the graph that this layout operates on. + */ +mxSpaceManager.prototype.getGraph = function() +{ + return this.graph; +}; + +/** + * Function: setGraph + * + * Sets the graph that the layouts operate on. + */ +mxSpaceManager.prototype.setGraph = function(graph) +{ + if (this.graph != null) + { + this.graph.removeListener(this.resizeHandler); + this.graph.removeListener(this.foldHandler); + } + + this.graph = graph; + + if (this.graph != null) + { + this.graph.addListener(mxEvent.RESIZE_CELLS, this.resizeHandler); + this.graph.addListener(mxEvent.FOLD_CELLS, this.foldHandler); + } +}; + +/** + * Function: cellsResized + * + * Called from <moveCellsIntoParent> to invoke the <move> hook in the + * automatic layout of each modified cell's parent. The event is used to + * define the x- and y-coordinates passed to the move function. + * + * Parameters: + * + * cell - Array of <mxCells> that have been resized. + */ +mxSpaceManager.prototype.cellsResized = function(cells) +{ + if (cells != null) + { + var model = this.graph.getModel(); + + // Raising the update level should not be required + // since only one call is made below + model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + if (!this.isCellIgnored(cells[i])) + { + this.cellResized(cells[i]); + break; + } + } + } + finally + { + model.endUpdate(); + } + } +}; + +/** + * Function: cellResized + * + * Called from <moveCellsIntoParent> to invoke the <move> hook in the + * automatic layout of each modified cell's parent. The event is used to + * define the x- and y-coordinates passed to the move function. + * + * Parameters: + * + * cell - <mxCell> that has been resized. + */ +mxSpaceManager.prototype.cellResized = function(cell) +{ + var graph = this.getGraph(); + var view = graph.getView(); + var model = graph.getModel(); + + var state = view.getState(cell); + var pstate = view.getState(model.getParent(cell)); + + if (state != null && + pstate != null) + { + var cells = this.getCellsToShift(state); + var geo = model.getGeometry(cell); + + if (cells != null && + geo != null) + { + var tr = view.translate; + var scale = view.scale; + + var x0 = state.x - pstate.origin.x - tr.x * scale; + var y0 = state.y - pstate.origin.y - tr.y * scale; + var right = state.x + state.width; + var bottom = state.y + state.height; + + var dx = state.width - geo.width * scale + x0 - geo.x * scale; + var dy = state.height - geo.height * scale + y0 - geo.y * scale; + + var fx = 1 - geo.width * scale / state.width; + var fy = 1 - geo.height * scale / state.height; + + model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + if (cells[i] != cell && + this.isCellShiftable(cells[i])) + { + this.shiftCell(cells[i], dx, dy, x0, y0, right, bottom, fx, fy, + this.isExtendParents() && + graph.isExtendParent(cells[i])); + } + } + } + finally + { + model.endUpdate(); + } + } + } +}; + +/** + * Function: shiftCell + * + * Called from <moveCellsIntoParent> to invoke the <move> hook in the + * automatic layout of each modified cell's parent. The event is used to + * define the x- and y-coordinates passed to the move function. + * + * Parameters: + * + * cell - Array of <mxCells> that have been moved. + * evt - Mouse event that represents the mousedown. + */ +mxSpaceManager.prototype.shiftCell = function(cell, dx, dy, Ox0, y0, right, + bottom, fx, fy, extendParent) +{ + var graph = this.getGraph(); + var state = graph.getView().getState(cell); + + if (state != null) + { + var model = graph.getModel(); + var geo = model.getGeometry(cell); + + if (geo != null) + { + model.beginUpdate(); + try + { + if (this.isShiftRightwards()) + { + if (state.x >= right) + { + geo = geo.clone(); + geo.translate(-dx, 0); + } + else + { + var tmpDx = Math.max(0, state.x - x0); + geo = geo.clone(); + geo.translate(-fx * tmpDx, 0); + } + } + + if (this.isShiftDownwards()) + { + if (state.y >= bottom) + { + geo = geo.clone(); + geo.translate(0, -dy); + } + else + { + var tmpDy = Math.max(0, state.y - y0); + geo = geo.clone(); + geo.translate(0, -fy * tmpDy); + } + } + + if (geo != model.getGeometry(cell)) + { + model.setGeometry(cell, geo); + + // Parent size might need to be updated if this + // is seen as part of the resize + if (extendParent) + { + graph.extendParent(cell); + } + } + } + finally + { + model.endUpdate(); + } + } + } +}; + +/** + * Function: getCellsToShift + * + * Returns the cells to shift after a resize of the + * specified <mxCellState>. + */ +mxSpaceManager.prototype.getCellsToShift = function(state) +{ + var graph = this.getGraph(); + var parent = graph.getModel().getParent(state.cell); + var down = this.isShiftDownwards(); + var right = this.isShiftRightwards(); + + return graph.getCellsBeyond(state.x + ((down) ? 0 : state.width), + state.y + ((down && right) ? 0 : state.height), parent, right, down); +}; + +/** + * Function: destroy + * + * Removes all handlers from the <graph> and deletes the reference to it. + */ +mxSpaceManager.prototype.destroy = function() +{ + this.setGraph(null); +}; diff --git a/src/js/view/mxStyleRegistry.js b/src/js/view/mxStyleRegistry.js new file mode 100644 index 0000000..6ad878d --- /dev/null +++ b/src/js/view/mxStyleRegistry.js @@ -0,0 +1,70 @@ +/** + * $Id: mxStyleRegistry.js,v 1.10 2011-04-27 10:15:39 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +var mxStyleRegistry = +{ + /** + * Class: mxStyleRegistry + * + * Singleton class that acts as a global converter from string to object values + * in a style. This is currently only used to perimeters and edge styles. + * + * Variable: values + * + * Maps from strings to objects. + */ + values: [], + + /** + * Function: putValue + * + * Puts the given object into the registry under the given name. + */ + putValue: function(name, obj) + { + mxStyleRegistry.values[name] = obj; + }, + + /** + * Function: getValue + * + * Returns the value associated with the given name. + */ + getValue: function(name) + { + return mxStyleRegistry.values[name]; + }, + + /** + * Function: getName + * + * Returns the name for the given value. + */ + getName: function(value) + { + for (var key in mxStyleRegistry.values) + { + if (mxStyleRegistry.values[key] == value) + { + return key; + } + } + + return null; + } + +}; + +mxStyleRegistry.putValue(mxConstants.EDGESTYLE_ELBOW, mxEdgeStyle.ElbowConnector); +mxStyleRegistry.putValue(mxConstants.EDGESTYLE_ENTITY_RELATION, mxEdgeStyle.EntityRelation); +mxStyleRegistry.putValue(mxConstants.EDGESTYLE_LOOP, mxEdgeStyle.Loop); +mxStyleRegistry.putValue(mxConstants.EDGESTYLE_SIDETOSIDE, mxEdgeStyle.SideToSide); +mxStyleRegistry.putValue(mxConstants.EDGESTYLE_TOPTOBOTTOM, mxEdgeStyle.TopToBottom); +mxStyleRegistry.putValue(mxConstants.EDGESTYLE_ORTHOGONAL, mxEdgeStyle.OrthConnector); +mxStyleRegistry.putValue(mxConstants.EDGESTYLE_SEGMENT, mxEdgeStyle.SegmentConnector); + +mxStyleRegistry.putValue(mxConstants.PERIMETER_ELLIPSE, mxPerimeter.EllipsePerimeter); +mxStyleRegistry.putValue(mxConstants.PERIMETER_RECTANGLE, mxPerimeter.RectanglePerimeter); +mxStyleRegistry.putValue(mxConstants.PERIMETER_RHOMBUS, mxPerimeter.RhombusPerimeter); +mxStyleRegistry.putValue(mxConstants.PERIMETER_TRIANGLE, mxPerimeter.TrianglePerimeter); diff --git a/src/js/view/mxStylesheet.js b/src/js/view/mxStylesheet.js new file mode 100644 index 0000000..82a520e --- /dev/null +++ b/src/js/view/mxStylesheet.js @@ -0,0 +1,266 @@ +/** + * $Id: mxStylesheet.js,v 1.35 2010-03-26 10:24:58 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxStylesheet + * + * Defines the appearance of the cells in a graph. See <putCellStyle> for an + * example of creating a new cell style. It is recommended to use objects, not + * arrays for holding cell styles. Existing styles can be cloned using + * <mxUtils.clone> and turned into a string for debugging using + * <mxUtils.toString>. + * + * Default Styles: + * + * The stylesheet contains two built-in styles, which are used if no style is + * defined for a cell: + * + * defaultVertex - Default style for vertices + * defaultEdge - Default style for edges + * + * Example: + * + * (code) + * var vertexStyle = stylesheet.getDefaultVertexStyle(); + * vertexStyle[mxConstants.ROUNDED] = true; + * var edgeStyle = stylesheet.getDefaultEdgeStyle(); + * edgeStyle[mxConstants.STYLE_EDGE] = mxEdgeStyle.EntityRelation; + * (end) + * + * Modifies the built-in default styles. + * + * To avoid the default style for a cell, add a leading semicolon + * to the style definition, eg. + * + * (code) + * ;shadow=1 + * (end) + * + * Removing keys: + * + * For removing a key in a cell style of the form [stylename;|key=value;] the + * special value none can be used, eg. highlight;fillColor=none + * + * See also the helper methods in mxUtils to modify strings of this format, + * namely <mxUtils.setStyle>, <mxUtils.indexOfStylename>, + * <mxUtils.addStylename>, <mxUtils.removeStylename>, + * <mxUtils.removeAllStylenames> and <mxUtils.setStyleFlag>. + * + * Constructor: mxStylesheet + * + * Constructs a new stylesheet and assigns default styles. + */ +function mxStylesheet() +{ + this.styles = new Object(); + + this.putDefaultVertexStyle(this.createDefaultVertexStyle()); + this.putDefaultEdgeStyle(this.createDefaultEdgeStyle()); +}; + +/** + * Function: styles + * + * Maps from names to cell styles. Each cell style is a map of key, + * value pairs. + */ +mxStylesheet.prototype.styles; + +/** + * Function: createDefaultVertexStyle + * + * Creates and returns the default vertex style. + */ +mxStylesheet.prototype.createDefaultVertexStyle = function() +{ + var style = new Object(); + + style[mxConstants.STYLE_SHAPE] = mxConstants.SHAPE_RECTANGLE; + style[mxConstants.STYLE_PERIMETER] = mxPerimeter.RectanglePerimeter; + style[mxConstants.STYLE_VERTICAL_ALIGN] = mxConstants.ALIGN_MIDDLE; + style[mxConstants.STYLE_ALIGN] = mxConstants.ALIGN_CENTER; + style[mxConstants.STYLE_FILLCOLOR] = '#C3D9FF'; + style[mxConstants.STYLE_STROKECOLOR] = '#6482B9'; + style[mxConstants.STYLE_FONTCOLOR] = '#774400'; + + return style; +}; + +/** + * Function: createDefaultEdgeStyle + * + * Creates and returns the default edge style. + */ +mxStylesheet.prototype.createDefaultEdgeStyle = function() +{ + var style = new Object(); + + style[mxConstants.STYLE_SHAPE] = mxConstants.SHAPE_CONNECTOR; + style[mxConstants.STYLE_ENDARROW] = mxConstants.ARROW_CLASSIC; + style[mxConstants.STYLE_VERTICAL_ALIGN] = mxConstants.ALIGN_MIDDLE; + style[mxConstants.STYLE_ALIGN] = mxConstants.ALIGN_CENTER; + style[mxConstants.STYLE_STROKECOLOR] = '#6482B9'; + style[mxConstants.STYLE_FONTCOLOR] = '#446299'; + + return style; +}; + +/** + * Function: putDefaultVertexStyle + * + * Sets the default style for vertices using defaultVertex as the + * stylename. + * + * Parameters: + * style - Key, value pairs that define the style. + */ +mxStylesheet.prototype.putDefaultVertexStyle = function(style) +{ + this.putCellStyle('defaultVertex', style); +}; + +/** + * Function: putDefaultEdgeStyle + * + * Sets the default style for edges using defaultEdge as the stylename. + */ +mxStylesheet.prototype.putDefaultEdgeStyle = function(style) +{ + this.putCellStyle('defaultEdge', style); +}; + +/** + * Function: getDefaultVertexStyle + * + * Returns the default style for vertices. + */ +mxStylesheet.prototype.getDefaultVertexStyle = function() +{ + return this.styles['defaultVertex']; +}; + +/** + * Function: getDefaultEdgeStyle + * + * Sets the default style for edges. + */ +mxStylesheet.prototype.getDefaultEdgeStyle = function() +{ + return this.styles['defaultEdge']; +}; + +/** + * Function: putCellStyle + * + * Stores the given map of key, value pairs under the given name in + * <styles>. + * + * Example: + * + * The following example adds a new style called 'rounded' into an + * existing stylesheet: + * + * (code) + * var style = new Object(); + * style[mxConstants.STYLE_SHAPE] = mxConstants.SHAPE_RECTANGLE; + * style[mxConstants.STYLE_PERIMETER] = mxPerimeter.RectanglePerimeter; + * style[mxConstants.STYLE_ROUNDED] = true; + * graph.getStylesheet().putCellStyle('rounded', style); + * (end) + * + * In the above example, the new style is an object. The possible keys of + * the object are all the constants in <mxConstants> that start with STYLE + * and the values are either JavaScript objects, such as + * <mxPerimeter.RightAngleRectanglePerimeter> (which is in fact a function) + * or expressions, such as true. Note that not all keys will be + * interpreted by all shapes (eg. the line shape ignores the fill color). + * The final call to this method associates the style with a name in the + * stylesheet. The style is used in a cell with the following code: + * + * (code) + * model.setStyle(cell, 'rounded'); + * (end) + * + * Parameters: + * + * name - Name for the style to be stored. + * style - Key, value pairs that define the style. + */ +mxStylesheet.prototype.putCellStyle = function(name, style) +{ + this.styles[name] = style; +}; + +/** + * Function: getCellStyle + * + * Returns the cell style for the specified stylename or the given + * defaultStyle if no style can be found for the given stylename. + * + * Parameters: + * + * name - String of the form [(stylename|key=value);] that represents the + * style. + * defaultStyle - Default style to be returned if no style can be found. + */ +mxStylesheet.prototype.getCellStyle = function(name, defaultStyle) +{ + var style = defaultStyle; + + if (name != null && name.length > 0) + { + var pairs = name.split(';'); + + if (style != null && + name.charAt(0) != ';') + { + style = mxUtils.clone(style); + } + else + { + style = new Object(); + } + + // Parses each key, value pair into the existing style + for (var i = 0; i < pairs.length; i++) + { + var tmp = pairs[i]; + var pos = tmp.indexOf('='); + + if (pos >= 0) + { + var key = tmp.substring(0, pos); + var value = tmp.substring(pos + 1); + + if (value == mxConstants.NONE) + { + delete style[key]; + } + else if (mxUtils.isNumeric(value)) + { + style[key] = parseFloat(value); + } + else + { + style[key] = value; + } + } + else + { + // Merges the entries from a named style + var tmpStyle = this.styles[tmp]; + + if (tmpStyle != null) + { + for (var key in tmpStyle) + { + style[key] = tmpStyle[key]; + } + } + } + } + } + + return style; +}; diff --git a/src/js/view/mxSwimlaneManager.js b/src/js/view/mxSwimlaneManager.js new file mode 100644 index 0000000..fe40613 --- /dev/null +++ b/src/js/view/mxSwimlaneManager.js @@ -0,0 +1,449 @@ +/** + * $Id: mxSwimlaneManager.js,v 1.17 2011-01-14 15:21:10 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxSwimlaneManager + * + * Manager for swimlanes and nested swimlanes that sets the size of newly added + * swimlanes to that of their siblings, and propagates changes to the size of a + * swimlane to its siblings, if <siblings> is true, and its ancestors, if + * <bubbling> is true. + * + * Constructor: mxSwimlaneManager + * + * Constructs a new swimlane manager for the given graph. + * + * Arguments: + * + * graph - Reference to the enclosing graph. + */ +function mxSwimlaneManager(graph, horizontal, addEnabled, resizeEnabled) +{ + this.horizontal = (horizontal != null) ? horizontal : true; + this.addEnabled = (addEnabled != null) ? addEnabled : true; + this.resizeEnabled = (resizeEnabled != null) ? resizeEnabled : true; + + this.addHandler = mxUtils.bind(this, function(sender, evt) + { + if (this.isEnabled() && this.isAddEnabled()) + { + this.cellsAdded(evt.getProperty('cells')); + } + }); + + this.resizeHandler = mxUtils.bind(this, function(sender, evt) + { + if (this.isEnabled() && this.isResizeEnabled()) + { + this.cellsResized(evt.getProperty('cells')); + } + }); + + this.setGraph(graph); +}; + +/** + * Extends mxEventSource. + */ +mxSwimlaneManager.prototype = new mxEventSource(); +mxSwimlaneManager.prototype.constructor = mxSwimlaneManager; + +/** + * Variable: graph + * + * Reference to the enclosing <mxGraph>. + */ +mxSwimlaneManager.prototype.graph = null; + +/** + * Variable: enabled + * + * Specifies if event handling is enabled. Default is true. + */ +mxSwimlaneManager.prototype.enabled = true; + +/** + * Variable: horizontal + * + * Specifies the orientation of the swimlanes. Default is true. + */ +mxSwimlaneManager.prototype.horizontal = true; + +/** + * Variable: addEnabled + * + * Specifies if newly added cells should be resized to match the size of their + * existing siblings. Default is true. + */ +mxSwimlaneManager.prototype.addEnabled = true; + +/** + * Variable: resizeEnabled + * + * Specifies if resizing of swimlanes should be handled. Default is true. + */ +mxSwimlaneManager.prototype.resizeEnabled = true; + +/** + * Variable: moveHandler + * + * Holds the function that handles the move event. + */ +mxSwimlaneManager.prototype.addHandler = null; + +/** + * Variable: moveHandler + * + * Holds the function that handles the move event. + */ +mxSwimlaneManager.prototype.resizeHandler = null; + +/** + * Function: isEnabled + * + * Returns true if events are handled. This implementation + * returns <enabled>. + */ +mxSwimlaneManager.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Enables or disables event handling. This implementation + * updates <enabled>. + * + * Parameters: + * + * enabled - Boolean that specifies the new enabled state. + */ +mxSwimlaneManager.prototype.setEnabled = function(value) +{ + this.enabled = value; +}; + +/** + * Function: isHorizontal + * + * Returns <horizontal>. + */ +mxSwimlaneManager.prototype.isHorizontal = function() +{ + return this.horizontal; +}; + +/** + * Function: setHorizontal + * + * Sets <horizontal>. + */ +mxSwimlaneManager.prototype.setHorizontal = function(value) +{ + this.horizontal = value; +}; + +/** + * Function: isAddEnabled + * + * Returns <addEnabled>. + */ +mxSwimlaneManager.prototype.isAddEnabled = function() +{ + return this.addEnabled; +}; + +/** + * Function: setAddEnabled + * + * Sets <addEnabled>. + */ +mxSwimlaneManager.prototype.setAddEnabled = function(value) +{ + this.addEnabled = value; +}; + +/** + * Function: isResizeEnabled + * + * Returns <resizeEnabled>. + */ +mxSwimlaneManager.prototype.isResizeEnabled = function() +{ + return this.resizeEnabled; +}; + +/** + * Function: setResizeEnabled + * + * Sets <resizeEnabled>. + */ +mxSwimlaneManager.prototype.setResizeEnabled = function(value) +{ + this.resizeEnabled = value; +}; + +/** + * Function: getGraph + * + * Returns the graph that this manager operates on. + */ +mxSwimlaneManager.prototype.getGraph = function() +{ + return this.graph; +}; + +/** + * Function: setGraph + * + * Sets the graph that the manager operates on. + */ +mxSwimlaneManager.prototype.setGraph = function(graph) +{ + if (this.graph != null) + { + this.graph.removeListener(this.addHandler); + this.graph.removeListener(this.resizeHandler); + } + + this.graph = graph; + + if (this.graph != null) + { + this.graph.addListener(mxEvent.ADD_CELLS, this.addHandler); + this.graph.addListener(mxEvent.CELLS_RESIZED, this.resizeHandler); + } +}; + +/** + * Function: isSwimlaneIgnored + * + * Returns true if the given swimlane should be ignored. + */ +mxSwimlaneManager.prototype.isSwimlaneIgnored = function(swimlane) +{ + return !this.getGraph().isSwimlane(swimlane); +}; + +/** + * Function: isCellHorizontal + * + * Returns true if the given cell is horizontal. If the given cell is not a + * swimlane, then the global orientation is returned. + */ +mxSwimlaneManager.prototype.isCellHorizontal = function(cell) +{ + if (this.graph.isSwimlane(cell)) + { + var state = this.graph.view.getState(cell); + var style = (state != null) ? state.style : this.graph.getCellStyle(cell); + + return mxUtils.getValue(style, mxConstants.STYLE_HORIZONTAL, 1) == 1; + } + + return !this.isHorizontal(); +}; + +/** + * Function: cellsAdded + * + * Called if any cells have been added. + * + * Parameters: + * + * cell - Array of <mxCells> that have been added. + */ +mxSwimlaneManager.prototype.cellsAdded = function(cells) +{ + if (cells != null) + { + var model = this.getGraph().getModel(); + + model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + if (!this.isSwimlaneIgnored(cells[i])) + { + this.swimlaneAdded(cells[i]); + } + } + } + finally + { + model.endUpdate(); + } + } +}; + +/** + * Function: swimlaneAdded + * + * Updates the size of the given swimlane to match that of any existing + * siblings swimlanes. + * + * Parameters: + * + * swimlane - <mxCell> that represents the new swimlane. + */ +mxSwimlaneManager.prototype.swimlaneAdded = function(swimlane) +{ + var model = this.getGraph().getModel(); + var parent = model.getParent(swimlane); + var childCount = model.getChildCount(parent); + var geo = null; + + // Finds the first valid sibling swimlane as reference + for (var i = 0; i < childCount; i++) + { + var child = model.getChildAt(parent, i); + + if (child != swimlane && !this.isSwimlaneIgnored(child)) + { + geo = model.getGeometry(child); + + if (geo != null) + { + break; + } + } + } + + // Applies the size of the refernece to the newly added swimlane + if (geo != null) + { + this.resizeSwimlane(swimlane, geo.width, geo.height); + } +}; + +/** + * Function: cellsResized + * + * Called if any cells have been resizes. Calls <swimlaneResized> for all + * swimlanes where <isSwimlaneIgnored> returns false. + * + * Parameters: + * + * cells - Array of <mxCells> whose size was changed. + */ +mxSwimlaneManager.prototype.cellsResized = function(cells) +{ + if (cells != null) + { + var model = this.getGraph().getModel(); + + model.beginUpdate(); + try + { + // Finds the top-level swimlanes and adds offsets + for (var i = 0; i < cells.length; i++) + { + if (!this.isSwimlaneIgnored(cells[i])) + { + var geo = model.getGeometry(cells[i]); + + if (geo != null) + { + var size = new mxRectangle(0, 0, geo.width, geo.height); + var top = cells[i]; + var current = top; + + while (current != null) + { + top = current; + current = model.getParent(current); + var tmp = (this.graph.isSwimlane(current)) ? + this.graph.getStartSize(current) : + new mxRectangle(); + size.width += tmp.width; + size.height += tmp.height; + } + + this.resizeSwimlane(top, size.width, size.height); + } + } + } + } + finally + { + model.endUpdate(); + } + } +}; + +/** + * Function: resizeSwimlane + * + * Called from <cellsResized> for all swimlanes that are not ignored to update + * the size of the siblings and the size of the parent swimlanes, recursively, + * if <bubbling> is true. + * + * Parameters: + * + * swimlane - <mxCell> whose size has changed. + */ +mxSwimlaneManager.prototype.resizeSwimlane = function(swimlane, w, h) +{ + var model = this.getGraph().getModel(); + + model.beginUpdate(); + try + { + if (!this.isSwimlaneIgnored(swimlane)) + { + var geo = model.getGeometry(swimlane); + + if (geo != null) + { + var horizontal = this.isCellHorizontal(swimlane); + + if ((horizontal && geo.height != h) || (!horizontal && geo.width != w)) + { + geo = geo.clone(); + + if (horizontal) + { + geo.height = h; + } + else + { + geo.width = w; + } + + model.setGeometry(swimlane, geo); + } + } + } + + var tmp = (this.graph.isSwimlane(swimlane)) ? + this.graph.getStartSize(swimlane) : + new mxRectangle(); + w -= tmp.width; + h -= tmp.height; + + var childCount = model.getChildCount(swimlane); + + for (var i = 0; i < childCount; i++) + { + var child = model.getChildAt(swimlane, i); + this.resizeSwimlane(child, w, h); + } + } + finally + { + model.endUpdate(); + } +}; + +/** + * Function: destroy + * + * Removes all handlers from the <graph> and deletes the reference to it. + */ +mxSwimlaneManager.prototype.destroy = function() +{ + this.setGraph(null); +}; diff --git a/src/js/view/mxTemporaryCellStates.js b/src/js/view/mxTemporaryCellStates.js new file mode 100644 index 0000000..ce8232c --- /dev/null +++ b/src/js/view/mxTemporaryCellStates.js @@ -0,0 +1,105 @@ +/** + * $Id: mxTemporaryCellStates.js,v 1.10 2010-04-20 14:43:12 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxTemporaryCellStates + * + * Extends <mxPoint> to implement a 2-dimensional rectangle with double + * precision coordinates. + * + * Constructor: mxRectangle + * + * Constructs a new rectangle for the optional parameters. If no parameters + * are given then the respective default values are used. + */ +function mxTemporaryCellStates(view, scale, cells) +{ + this.view = view; + scale = (scale != null) ? scale : 1; + + // Stores the previous state + this.oldBounds = view.getGraphBounds(); + this.oldStates = view.getStates(); + this.oldScale = view.getScale(); + + // Creates space for new states + view.setStates(new mxDictionary()); + view.setScale(scale); + + if (cells != null) + { + // Creates virtual parent state for validation + var state = view.createState(new mxCell()); + + // Validates the vertices and edges without adding them to + // the model so that the original cells are not modified + for (var i = 0; i < cells.length; i++) + { + view.validateBounds(state, cells[i]); + } + + var bbox = null; + + for (var i = 0; i < cells.length; i++) + { + var bounds = view.validatePoints(state, cells[i]); + + if (bbox == null) + { + bbox = bounds; + } + else + { + bbox.add(bounds); + } + } + + if (bbox == null) + { + bbox = new mxRectangle(); + } + + view.setGraphBounds(bbox); + } +}; + +/** + * Variable: view + * + * Holds the width of the rectangle. Default is 0. + */ +mxTemporaryCellStates.prototype.view = null; + +/** + * Variable: oldStates + * + * Holds the height of the rectangle. Default is 0. + */ +mxTemporaryCellStates.prototype.oldStates = null; + +/** + * Variable: oldBounds + * + * Holds the height of the rectangle. Default is 0. + */ +mxTemporaryCellStates.prototype.oldBounds = null; + +/** + * Variable: oldScale + * + * Holds the height of the rectangle. Default is 0. + */ +mxTemporaryCellStates.prototype.oldScale = null; + +/** + * Function: destroy + * + * Returns the top, left corner as a new <mxPoint>. + */ +mxTemporaryCellStates.prototype.destroy = function() +{ + this.view.setScale(this.oldScale); + this.view.setStates(this.oldStates); + this.view.setGraphBounds(this.oldBounds); +}; diff --git a/src/js/xcos/core/details.js b/src/js/xcos/core/details.js new file mode 100644 index 0000000..fbeffda --- /dev/null +++ b/src/js/xcos/core/details.js @@ -0,0 +1,204 @@ +// All arrays - separated by ',' or ';' or ' ' are taken to be 1 Dimensional +// Only during printing, their nomenclature will change +// Good read: http://javascript.info/tutorial/arguments#keyword-arguments + +function scicos_block() { + var options = arguments[0] || new Object(); + this.graphics = options.graphics || new scicos_graphics(); + this.model = options.model || new scicos_model(); + this.gui = options.gui || ''; + this.docs = options.docs || []; +} + +function scicos_graphics() { + var options = arguments[0] || new Object(); + this.orig = options.orig || [0, 0]; + this.sz = options.sz || [80, 80]; // Space and comma works the same! + this.flip = options.flip || true; + this.theta = options.theta || 0; + this.exprs = options.exprs || []; + this.pin = options.pin || []; + this.pout = options.pout || []; + this.pein = options.pein || []; + this.peout = options.peout || []; + this.gr_i = options.gr_i || []; + this.id = options.id || ''; + this.in_implicit = options.in_implicit || []; + this.out_implicit = options.out_implicit || ''; // There is only one! + this.in_style = options.in_style || []; + this.out_style = options.out_style || ''; + this.in_label = options.in_label || []; + this.out_label = options.out_label || ''; + this.style = options.style || ''; +} + +function scicos_model() { + var options = arguments[0] || new Object(); + this.sim = options.sim || ''; + this.in = options.in || []; + this.in2 = options.in2 || []; + this.intyp = options.intyp || []; + this.out = options.out || []; + this.out2 = options.out2 || []; + this.outtyp = options.outtyp || 1; + this.evtin = options.evtin || []; + this.evtout = options.evtout || []; + this.state = options.state || []; + this.dstate = options.dstate || []; + this.odstate = options.odstate || []; + this.ipar = options.ipar || []; + this.rpar = options.rpar || []; + this.opar = options.opar || []; + this.blocktype = options.blocktype || 'c'; + this.firing = options.firing || []; + this.dep_ut = options.dep_ut || [false, false]; + this.label = options.label || ''; // If label not available, use image + this.nzcross = options.nzcross || 0; + this.nmode = options.nmode || 0; + this.equations = options.equations || []; + this.uid = options.uid || ''; +} + +// This might also have to be overloaded +function scicos_diagram() { + this.props = new scicos_params(); + this.objs = []; + this.version = ''; + this.contrib = []; +} + +// This might also have to be overloaded +function scicos_params() { + this.wpar = [600, 450, 0, 0, 600, 450]; + this.titlex = 'Untitled'; + this.tf = 100000; + this.tol = [Math.pow(10, -6), Math.pow(10, -6), Math.pow(10, -10), this.tf+1, 0, 1, 0]; + this.context = []; + this.void1 = []; + this.options = new default_options(); + this.void2 = []; + this.void3 = []; + this.doc = []; +} + +// This might also have to be overloaded +function default_options() { + var options = new Object(); + var col3d = [0.8, 0.8, 0.8]; + options['3D'] = [true, 33]; + options['Background'] = [8, 1]; // white,black + options['Link'] = [1, 5]; // black,red + options['ID'] = [[4, 1, 10, 1], [4, 1, 2, 1]]; + options['Cmap'] = col3d; + return options; +} + +function zeros(n){ + return new Array(n+1).join('0').split('').map(parseFloat); +} + +function standard_define() { + var sz = arguments[0]; + var model = arguments[1]; + var label = arguments[2]; + var gr_i = arguments[3] || []; + + var pin = []; + var pout = []; + var pein = []; + var peout = []; + + var nin = model.in.length; + if(nin > 0){ + pin = zeros(nin); + } + var nout = model.out.length; + if(nout > 0){ + pout = zeros(nout); + } + var ncin = model.evtin.length; + if(ncin > 0){ + pein = zeros(ncin); + } + var ncout = model.evtout.length; + if(ncout > 0){ + peout = zeros(ncout); + } + gr_i = [gr_i, 8]; + if(gr_i[1] == []){ + gr_i[1] = 8; + } + if(gr_i[1] == 0){ + gr_i[1] = []; + } + var graphics_options = { + sz: sz, + pin: pin, + pout: pout, + pein: pein, + peout: peout, + gr_i: gr_i, + exprs: label + }; + var graphics = new scicos_graphics(graphics_options); + var block_options = { + graphics: graphics, + model: model, + gui: arguments.callee.caller.name + }; + return new scicos_block(block_options); +} + +function scicos_link (){ + this.xx = []; + this.yy = []; + this.id = ''; + this.thick = [0, 0]; + this.ct = [1, 1]; + this.from = []; + this.to = []; +} + +function ANDLOG_f(){ + var model = new scicos_model(); + model.sim = "andlog"; + model.out = [1]; + model.out2 = [1]; // null -> 1 + model.evtin = [-1,-1]; // 1, 1 -> -1, -1 + model.blocktype = "d"; + model.firing = []; + model.dep_ut = [false, false]; + var gr_i = "xstringb(orig(1),orig(2),txt,sz(1),sz(2),'fill');"; + var block = new standard_define([80,80], model, 'LOGICAL<BR>AND', gr_i); // 3 -> 80 + + // Style + block.graphics.out_implicit = "E"; + block.graphics.out_style = "ExplicitOutputPort;align=right;verticalAlign=middle;spacing=10.0;rotation=0"; + block.graphics.style = "ANDLOG_f"; + return block; +} + + + + + + + + + + + + + + + + + + + + + + + + + |