<!-- Copyright (c) 2006-2013, JGraph Ltd Schema example for mxGraph. This example demonstrates implementing a SQL schema editor. --> <html> <head> <title>Schema example</title> <!-- Sets the basepath for the library if not in same directory --> <script type="text/javascript"> mxBasePath = '../src'; </script> <!-- Loads and initializes the library --> <script type="text/javascript" src="../src/js/mxClient.js"></script> <!-- Example code --> <script type="text/javascript"> // Program starts here. Creates a sample graph in the // DOM node with the specified ID. This function is invoked // from the onLoad event handler of the document (see below). function main(container, outline, toolbar, sidebar, status) { // Checks if the browser is supported if (!mxClient.isBrowserSupported()) { // Displays an error message if the browser is not supported. mxUtils.error('Browser is not supported!', 200, false); } else { // Specifies shadow opacity, color and offset mxConstants.SHADOW_OPACITY = 0.5; mxConstants.SHADOWCOLOR = '#C0C0C0'; mxConstants.SHADOW_OFFSET_X = 5; mxConstants.SHADOW_OFFSET_Y = 6; // Table icon dimensions and position mxSwimlane.prototype.imageSize = 20; mxSwimlane.prototype.imageDx = 16; mxSwimlane.prototype.imageDy = 4; // Changes swimlane icon bounds mxSwimlane.prototype.getImageBounds = function(x, y, w, h) { return new mxRectangle(x + this.imageDx, y + this.imageDy, this.imageSize, this.imageSize); }; // Defines an icon for creating new connections in the connection handler. // This will automatically disable the highlighting of the source vertex. mxConnectionHandler.prototype.connectImage = new mxImage('images/connector.gif', 16, 16); // Prefetches all images that appear in colums // to avoid problems with the auto-layout var keyImage = new Image(); keyImage.src = "images/key.png"; var plusImage = new Image(); plusImage.src = "images/plus.png"; var checkImage = new Image(); checkImage.src = "images/check.png"; // Workaround for Internet Explorer ignoring certain CSS directives if (mxClient.IS_QUIRKS) { document.body.style.overflow = 'hidden'; new mxDivResizer(container); new mxDivResizer(outline); new mxDivResizer(toolbar); new mxDivResizer(sidebar); new mxDivResizer(status); } // Creates the graph inside the given container. The // editor is used to create certain functionality for the // graph, such as the rubberband selection, but most parts // of the UI are custom in this example. var editor = new mxEditor(); var graph = editor.graph; var model = graph.model; // Disables some global features graph.setConnectable(true); graph.setCellsDisconnectable(false); graph.setCellsCloneable(false); graph.swimlaneNesting = false; graph.dropEnabled = true; // Does not allow dangling edges graph.setAllowDanglingEdges(false); // Forces use of default edge in mxConnectionHandler graph.connectionHandler.factoryMethod = null; // Only tables are resizable graph.isCellResizable = function(cell) { return this.isSwimlane(cell); }; // Only tables are movable graph.isCellMovable = function(cell) { return this.isSwimlane(cell); }; // Sets the graph container and configures the editor editor.setGraphContainer(container); var config = mxUtils.load( 'editors/config/keyhandler-minimal.xml'). getDocumentElement(); editor.configure(config); // Configures the automatic layout for the table columns editor.layoutSwimlanes = true; editor.createSwimlaneLayout = function () { var layout = new mxStackLayout(this.graph, false); layout.fill = true; layout.resizeParent = true; // Overrides the function to always return true layout.isVertexMovable = function(cell) { return true; }; return layout; }; // Text label changes will go into the name field of the user object graph.model.valueForCellChanged = function(cell, value) { if (value.name != null) { return mxGraphModel.prototype.valueForCellChanged.apply(this, arguments); } else { var old = cell.value.name; cell.value.name = value; return old; } }; // Columns are dynamically created HTML labels graph.isHtmlLabel = function(cell) { return !this.isSwimlane(cell) && !this.model.isEdge(cell); }; // Edges are not editable graph.isCellEditable = function(cell) { return !this.model.isEdge(cell); }; // Returns the name field of the user object for the label graph.convertValueToString = function(cell) { if (cell.value != null && cell.value.name != null) { return cell.value.name; } return mxGraph.prototype.convertValueToString.apply(this, arguments); // "supercall" }; // Returns the type as the tooltip for column cells graph.getTooltip = function(state) { if (this.isHtmlLabel(state.cell)) { return 'Type: '+state.cell.value.type; } else if (this.model.isEdge(state.cell)) { var source = this.model.getTerminal(state.cell, true); var parent = this.model.getParent(source); return parent.value.name+'.'+source.value.name; } return mxGraph.prototype.getTooltip.apply(this, arguments); // "supercall" }; // Creates a dynamic HTML label for column fields graph.getLabel = function(cell) { if (this.isHtmlLabel(cell)) { var label = ''; if (cell.value.primaryKey) { label += '<img title="Primary Key" src="images/key.png" width="16" height="16" align="top"> '; } else { label += '<img src="images/spacer.gif" width="16" height="1"> '; } if (cell.value.autoIncrement) { label += '<img title="Auto Increment" src="images/plus.png" width="16" height="16" align="top"> '; } else if (cell.value.unique) { label += '<img title="Unique" src="images/check.png" width="16" height="16" align="top"> '; } else { label += '<img src="images/spacer.gif" width="16" height="1"> '; } return label + mxUtils.htmlEntities(cell.value.name, false) + ': ' + mxUtils.htmlEntities(cell.value.type, false); } return mxGraph.prototype.getLabel.apply(this, arguments); // "supercall" }; // Removes the source vertex if edges are removed graph.addListener(mxEvent.REMOVE_CELLS, function(sender, evt) { var cells = evt.getProperty('cells'); for (var i = 0; i < cells.length; i++) { var cell = cells[i]; if (this.model.isEdge(cell)) { var terminal = this.model.getTerminal(cell, true); var parent = this.model.getParent(terminal); this.model.remove(terminal); } } }); // Disables drag-and-drop into non-swimlanes. graph.isValidDropTarget = function(cell, cells, evt) { return this.isSwimlane(cell); }; // Installs a popupmenu handler using local function (see below). graph.popupMenuHandler.factoryMethod = function(menu, cell, evt) { createPopupMenu(editor, graph, menu, cell, evt); }; // Adds all required styles to the graph (see below) configureStylesheet(graph); // Adds sidebar icon for the table object var tableObject = new Table('TABLENAME'); var table = new mxCell(tableObject, new mxGeometry(0, 0, 200, 28), 'table'); table.setVertex(true); addSidebarIcon(graph, sidebar, table, 'images/icons48/table.png'); // Adds sidebar icon for the column object var columnObject = new Column('COLUMNNAME'); var column = new mxCell(columnObject, new mxGeometry(0, 0, 0, 26)); column.setVertex(true); column.setConnectable(false); addSidebarIcon(graph, sidebar, column, 'images/icons48/column.png'); // Adds primary key field into table var firstColumn = column.clone(); firstColumn.value.name = 'TABLENAME_ID'; firstColumn.value.type = 'INTEGER'; firstColumn.value.primaryKey = true; firstColumn.value.autoIncrement = true; table.insert(firstColumn); // Adds child columns for new connections between tables graph.addEdge = function(edge, parent, source, target, index) { // Finds the primary key child of the target table var primaryKey = null; var childCount = this.model.getChildCount(target); for (var i=0; i < childCount; i++) { var child = this.model.getChildAt(target, i); if (child.value.primaryKey) { primaryKey = child; break; } } if (primaryKey == null) { mxUtils.alert('Target table must have a primary key'); return; } this.model.beginUpdate(); try { var col1 = this.model.cloneCell(column); col1.value.name = primaryKey.value.name; col1.value.type = primaryKey.value.type; this.addCell(col1, source); source = col1; target = primaryKey; return mxGraph.prototype.addEdge.apply(this, arguments); // "supercall" } finally { this.model.endUpdate(); } return null; }; // Displays useful hints in a small semi-transparent box. var hints = document.createElement('div'); hints.style.position = 'absolute'; hints.style.overflow = 'hidden'; hints.style.width = '230px'; hints.style.bottom = '56px'; hints.style.height = '86px'; hints.style.right = '20px'; hints.style.background = 'black'; hints.style.color = 'white'; hints.style.fontFamily = 'Arial'; hints.style.fontSize = '10px'; hints.style.padding = '4px'; mxUtils.setOpacity(hints, 50); mxUtils.writeln(hints, '- Drag an image from the sidebar to the graph'); mxUtils.writeln(hints, '- Doubleclick on a table or column to edit'); mxUtils.writeln(hints, '- Shift- or Rightclick and drag for panning'); mxUtils.writeln(hints, '- Move the mouse over a cell to see a tooltip'); mxUtils.writeln(hints, '- Click and drag a table to move and connect'); mxUtils.writeln(hints, '- Shift- or Rightclick to show a popup menu'); document.body.appendChild(hints); // Creates a new DIV that is used as a toolbar and adds // toolbar buttons. var spacer = document.createElement('div'); spacer.style.display = 'inline'; spacer.style.padding = '8px'; addToolbarButton(editor, toolbar, 'properties', 'Properties', 'editors/images/properties.gif'); // Defines a new export action editor.addAction('properties', function(editor, cell) { if (cell == null) { cell = graph.getSelectionCell(); } if (graph.isHtmlLabel(cell)) { showProperties(graph, cell); } }); addToolbarButton(editor, toolbar, 'delete', 'Delete', 'images/delete2.png'); toolbar.appendChild(spacer.cloneNode(true)); addToolbarButton(editor, toolbar, 'undo', '', 'images/undo.png'); addToolbarButton(editor, toolbar, 'redo', '', 'images/redo.png'); toolbar.appendChild(spacer.cloneNode(true)); addToolbarButton(editor, toolbar, 'show', 'Show', 'images/camera.png'); addToolbarButton(editor, toolbar, 'print', 'Print', 'images/printer.png'); toolbar.appendChild(spacer.cloneNode(true)); // Defines a create SQK action editor.addAction('showSql', function(editor, cell) { var sql = createSql(graph); if (sql.length > 0) { var textarea = document.createElement('textarea'); textarea.style.width = '400px'; textarea.style.height = '400px'; textarea.value = sql; showModalWindow('SQL', textarea, 410, 440); } else { mxUtils.alert('Schema is empty'); } }); addToolbarButton(editor, toolbar, 'showSql', 'Show SQL', 'images/export1.png'); // Defines export XML action editor.addAction('export', function(editor, cell) { var textarea = document.createElement('textarea'); textarea.style.width = '400px'; textarea.style.height = '400px'; var enc = new mxCodec(mxUtils.createXmlDocument()); var node = enc.encode(editor.graph.getModel()); textarea.value = mxUtils.getPrettyXml(node); showModalWindow('XML', textarea, 410, 440); }); addToolbarButton(editor, toolbar, 'export', 'Export XML', 'images/export1.png'); // Adds toolbar buttons into the status bar at the bottom // of the window. addToolbarButton(editor, status, 'collapseAll', 'Collapse All', 'images/navigate_minus.png', true); addToolbarButton(editor, status, 'expandAll', 'Expand All', 'images/navigate_plus.png', true); status.appendChild(spacer.cloneNode(true)); addToolbarButton(editor, status, 'zoomIn', '', 'images/zoom_in.png', true); addToolbarButton(editor, status, 'zoomOut', '', 'images/zoom_out.png', true); addToolbarButton(editor, status, 'actualSize', '', 'images/view_1_1.png', true); addToolbarButton(editor, status, 'fit', '', 'images/fit_to_size.png', true); // Creates the outline (navigator, overview) for moving // around the graph in the top, right corner of the window. var outln = new mxOutline(graph, outline); // Fades-out the splash screen after the UI has been loaded. var splash = document.getElementById('splash'); if (splash != null) { try { mxEvent.release(splash); mxEffects.fadeOut(splash, 100, true); } catch (e) { // mxUtils is not available (library not loaded) splash.parentNode.removeChild(splash); } } } } function addToolbarButton(editor, toolbar, action, label, image, isTransparent) { var button = document.createElement('button'); button.style.fontSize = '10'; if (image != null) { var img = document.createElement('img'); img.setAttribute('src', image); img.style.width = '16px'; img.style.height = '16px'; img.style.verticalAlign = 'middle'; img.style.marginRight = '2px'; button.appendChild(img); } if (isTransparent) { button.style.background = 'transparent'; button.style.color = '#FFFFFF'; button.style.border = 'none'; } mxEvent.addListener(button, 'click', function(evt) { editor.execute(action); }); mxUtils.write(button, label); toolbar.appendChild(button); }; function showModalWindow(title, content, width, height) { var background = document.createElement('div'); background.style.position = 'absolute'; background.style.left = '0px'; background.style.top = '0px'; background.style.right = '0px'; background.style.bottom = '0px'; background.style.background = 'black'; mxUtils.setOpacity(background, 50); document.body.appendChild(background); if (mxClient.IS_QUIRKS) { new mxDivResizer(background); } var x = Math.max(0, document.body.scrollWidth/2-width/2); var y = Math.max(10, (document.body.scrollHeight || document.documentElement.scrollHeight)/2-height*2/3); var wnd = new mxWindow(title, content, x, y, width, height, false, true); wnd.setClosable(true); // Fades the background out after after the window has been closed wnd.addListener(mxEvent.DESTROY, function(evt) { mxEffects.fadeOut(background, 50, true, 10, 30, true); }); wnd.setVisible(true); return wnd; }; function addSidebarIcon(graph, sidebar, prototype, image) { // Function that is executed when the image is dropped on // the graph. The cell argument points to the cell under // the mousepointer if there is one. var funct = function(graph, evt, cell) { graph.stopEditing(false); var pt = graph.getPointForEvent(evt); var parent = graph.getDefaultParent(); var model = graph.getModel(); var isTable = graph.isSwimlane(prototype); var name = null; if (!isTable) { parent = cell; var pstate = graph.getView().getState(parent); if (parent == null || pstate == null) { mxUtils.alert('Drop target must be a table'); return; } pt.x -= pstate.x; pt.y -= pstate.y; var columnCount = graph.model.getChildCount(parent)+1; name = mxUtils.prompt('Enter name for new column', 'COLUMN'+columnCount); } else { var tableCount = 0; var childCount = graph.model.getChildCount(parent); for (var i=0; i<childCount; i++) { if (!graph.model.isEdge(graph.model.getChildAt(parent, i))) { tableCount++; } } var name = mxUtils.prompt('Enter name for new table', 'TABLE'+(tableCount+1)); } if (name != null) { var v1 = model.cloneCell(prototype); model.beginUpdate(); try { v1.value.name = name; v1.geometry.x = pt.x; v1.geometry.y = pt.y; graph.addCell(v1, parent); if (isTable) { v1.geometry.alternateBounds = new mxRectangle(0, 0, v1.geometry.width, v1.geometry.height); v1.children[0].value.name = name + '_ID'; } } finally { model.endUpdate(); } graph.setSelectionCell(v1); } } // Creates the image which is used as the sidebar icon (drag source) var img = document.createElement('img'); img.setAttribute('src', image); img.style.width = '48px'; img.style.height = '48px'; img.title = 'Drag this to the diagram to create a new vertex'; sidebar.appendChild(img); // Creates the image which is used as the drag icon (preview) var dragImage = img.cloneNode(true); var ds = mxUtils.makeDraggable(img, graph, funct, dragImage); // Adds highlight of target tables for columns ds.highlightDropTargets = true; ds.getDropTarget = function(graph, x, y) { if (graph.isSwimlane(prototype)) { return null; } else { var cell = graph.getCellAt(x, y); if (graph.isSwimlane(cell)) { return cell; } else { var parent = graph.getModel().getParent(cell); if (graph.isSwimlane(parent)) { return parent; } } } }; }; function configureStylesheet(graph) { var style = new Object(); style[mxConstants.STYLE_SHAPE] = mxConstants.SHAPE_RECTANGLE; style[mxConstants.STYLE_PERIMETER] = mxPerimeter.RectanglePerimeter; style[mxConstants.STYLE_ALIGN] = mxConstants.ALIGN_LEFT; style[mxConstants.STYLE_VERTICAL_ALIGN] = mxConstants.ALIGN_MIDDLE; style[mxConstants.STYLE_FONTCOLOR] = '#000000'; style[mxConstants.STYLE_FONTSIZE] = '11'; style[mxConstants.STYLE_FONTSTYLE] = 0; style[mxConstants.STYLE_SPACING_LEFT] = '4'; style[mxConstants.STYLE_IMAGE_WIDTH] = '48'; style[mxConstants.STYLE_IMAGE_HEIGHT] = '48'; graph.getStylesheet().putDefaultVertexStyle(style); style = new Object(); style[mxConstants.STYLE_SHAPE] = mxConstants.SHAPE_SWIMLANE; style[mxConstants.STYLE_PERIMETER] = mxPerimeter.RectanglePerimeter; style[mxConstants.STYLE_ALIGN] = mxConstants.ALIGN_CENTER; style[mxConstants.STYLE_VERTICAL_ALIGN] = mxConstants.ALIGN_TOP; style[mxConstants.STYLE_GRADIENTCOLOR] = '#41B9F5'; style[mxConstants.STYLE_FILLCOLOR] = '#8CCDF5'; style[mxConstants.STYLE_SWIMLANE_FILLCOLOR] = '#ffffff'; style[mxConstants.STYLE_STROKECOLOR] = '#1B78C8'; style[mxConstants.STYLE_FONTCOLOR] = '#000000'; style[mxConstants.STYLE_STROKEWIDTH] = '2'; style[mxConstants.STYLE_STARTSIZE] = '28'; style[mxConstants.STYLE_VERTICAL_ALIGN] = 'middle'; style[mxConstants.STYLE_FONTSIZE] = '12'; style[mxConstants.STYLE_FONTSTYLE] = 1; style[mxConstants.STYLE_IMAGE] = 'images/icons48/table.png'; // Looks better without opacity if shadow is enabled //style[mxConstants.STYLE_OPACITY] = '80'; style[mxConstants.STYLE_SHADOW] = 1; graph.getStylesheet().putCellStyle('table', style); style = graph.stylesheet.getDefaultEdgeStyle(); style[mxConstants.STYLE_LABEL_BACKGROUNDCOLOR] = '#FFFFFF'; style[mxConstants.STYLE_STROKEWIDTH] = '2'; style[mxConstants.STYLE_ROUNDED] = true; style[mxConstants.STYLE_EDGE] = mxEdgeStyle.EntityRelation; }; // Function to create the entries in the popupmenu function createPopupMenu(editor, graph, menu, cell, evt) { if (cell != null) { if (graph.isHtmlLabel(cell)) { menu.addItem('Properties', 'editors/images/properties.gif', function() { editor.execute('properties', cell); }); menu.addSeparator(); } menu.addItem('Delete', 'images/delete2.png', function() { editor.execute('delete', cell); }); menu.addSeparator(); } menu.addItem('Undo', 'images/undo.png', function() { editor.execute('undo', cell); }); menu.addItem('Redo', 'images/redo.png', function() { editor.execute('redo', cell); }); menu.addSeparator(); menu.addItem('Show SQL', 'images/export1.png', function() { editor.execute('showSql', cell); }); }; function showProperties(graph, cell) { // Creates a form for the user object inside // the cell var form = new mxForm('properties'); // Adds a field for the columnname var nameField = form.addText('Name', cell.value.name); var typeField = form.addText('Type', cell.value.type); var primaryKeyField = form.addCheckbox('Primary Key', cell.value.primaryKey); var autoIncrementField = form.addCheckbox('Auto Increment', cell.value.autoIncrement); var notNullField = form.addCheckbox('Not Null', cell.value.notNull); var uniqueField = form.addCheckbox('Unique', cell.value.unique); var defaultField = form.addText('Default', cell.value.defaultValue || ''); var useDefaultField = form.addCheckbox('Use Default', (cell.value.defaultValue != null)); var wnd = null; // Defines the function to be executed when the // OK button is pressed in the dialog var okFunction = function() { var clone = cell.value.clone(); clone.name = nameField.value; clone.type = typeField.value; if (useDefaultField.checked) { clone.defaultValue = defaultField.value; } else { clone.defaultValue = null; } clone.primaryKey = primaryKeyField.checked; clone.autoIncrement = autoIncrementField.checked; clone.notNull = notNullField.checked; clone.unique = uniqueField.checked; graph.model.setValue(cell, clone); wnd.destroy(); } // Defines the function to be executed when the // Cancel button is pressed in the dialog var cancelFunction = function() { wnd.destroy(); } form.addButtons(okFunction, cancelFunction); var parent = graph.model.getParent(cell); var name = parent.value.name + '.' + cell.value.name; wnd = showModalWindow(name, form.table, 240, 240); }; function createSql(graph) { var sql = []; var parent = graph.getDefaultParent(); var childCount = graph.model.getChildCount(parent); for (var i=0; i<childCount; i++) { var child = graph.model.getChildAt(parent, i); if (!graph.model.isEdge(child)) { sql.push('CREATE TABLE IF NOT EXISTS '+child.value.name+' ('); var columnCount = graph.model.getChildCount(child); if (columnCount > 0) { for (var j=0; j<columnCount; j++) { var column = graph.model.getChildAt(child, j).value; sql.push('\n '+column.name+' '+column.type); if (column.notNull) { sql.push(' NOT NULL'); } if (column.primaryKey) { sql.push(' PRIMARY KEY'); } if (column.autoIncrement) { sql.push(' AUTOINCREMENT'); } if (column.unique) { sql.push(' UNIQUE'); } if (column.defaultValue != null) { sql.push(' DEFAULT '+column.defaultValue); } sql.push(','); } sql.splice(sql.length-1, 1); sql.push('\n);'); } sql.push('\n'); } } return sql.join(''); }; // Defines the column user object function Column(name) { this.name = name; }; Column.prototype.type = 'TEXT'; Column.prototype.defaultValue = null; Column.prototype.primaryKey = false; Column.prototype.autoIncrement = false; Column.prototype.notNull = false; Column.prototype.unique = false; Column.prototype.clone = function() { return mxUtils.clone(this); }; // Defines the table user object function Table(name) { this.name = name; }; Table.prototype.clone = function() { return mxUtils.clone(this); }; </script> </head> <!-- Page passes the container for the graph to the program --> <body onload="main(document.getElementById('graphContainer'), document.getElementById('outlineContainer'), document.getElementById('toolbarContainer'), document.getElementById('sidebarContainer'), document.getElementById('statusContainer'));"> <!-- Creates a container for the splash screen --> <div id="splash" style="position:absolute;top:0px;left:0px;width:100%;height:100%;background:white;z-index:1;"> <center id="splash" style="padding-top:230px;"> <img src="editors/images/loading.gif"> </center> </div> <!-- Creates a container for the sidebar --> <div id="toolbarContainer" style="position:absolute;white-space:nowrap;overflow:hidden;top:0px;left:0px;max-height:24px;height:36px;right:0px;padding:6px;background-image:url('images/toolbar_bg.gif');"> </div> <!-- Creates a container for the toolboox --> <div id="sidebarContainer" style="position:absolute;overflow:hidden;top:36px;left:0px;bottom:36px;max-width:52px;width:56px;padding-top:10px;padding-left:4px;background-image:url('images/sidebar_bg.gif');"> </div> <!-- Creates a container for the graph --> <div id="graphContainer" style="position:absolute;overflow:hidden;top:36px;left:60px;bottom:36px;right:0px;"> </div> <!-- Creates a container for the outline --> <div id="outlineContainer" style="position:absolute;overflow:hidden;top:36px;right:0px;width:200px;height:140px;background:transparent;border-style:solid;border-color:black;"> </div> <!-- Creates a container for the sidebar --> <div id="statusContainer" style="text-align:right;position:absolute;overflow:hidden;bottom:0px;left:0px;max-height:24px;height:36px;right:0px;color:white;padding:6px;background-image:url('images/toolbar_bg.gif');"> <div style="font-size:10pt;float:left;"> Created with <a href="http://www.jgraph.com" target="_blank">mxGraph</a> </div> </div> </body> </html>