diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..e69de29 diff --git a/altium_sch.html b/altium_sch.html index 8050b8b..a4c2e7c 100644 --- a/altium_sch.html +++ b/altium_sch.html @@ -1,5 +1,6 @@ + @@ -12,14 +13,18 @@
-
-
+ +
+
+
+

+
- \ No newline at end of file + "|RECORD=255|".length) { @@ -426,6 +426,15 @@ class AltiumBus extends AltiumObject constructor(record) { super(record); + this.points = [] + let idx = 1 + while (this.attributes["x" + idx.toString()] != null) + { + let x = Number.parseInt(this.attributes["x" + idx.toString()], 10); + let y = Number.parseInt(this.attributes["y" + idx.toString()], 10); + this.points.push({ x: x, y: y }); + idx++; + } } } diff --git a/altium_sch_renderer.js b/altium_sch_renderer.js index c477ff9..3e20a22 100644 --- a/altium_sch_renderer.js +++ b/altium_sch_renderer.js @@ -54,63 +54,140 @@ class AltiumSchematicRenderer return parent.current_part_id == object.owner_part_id; } - render() - { - let canvas = this.canvas; + render() { + let graph = new mxGraph(document.getElementById('graphContainer')); + graph.setPanning(true); + graph.setConnectable(true); + graph.setConnectableEdges(true); + graph.setDisconnectOnMove(false); + graph.foldingEnabled = false; + + //Maximum size + graph.maximumGraphBounds = new mxRectangle(0, 0, 800, 600) + graph.border = 50; + var fontColor = '#FFFFFF' ; + var strokeColor = '#C0C0C0' ; + var fillColor = '#C0C0C0'; + // Panning handler consumed right click so this must be + // disabled if right click should stop connection handler. + graph.panningHandler.isPopupTrigger = function() { return false; }; + + // Enables return key to stop editing (use shift-enter for newlines) + graph.setEnterStopsCellEditing(true); + + // Adds rubberband selection + new mxRubberband(graph); + + // Alternative solution for implementing connection points without child cells. + // This can be extended as shown in portrefs.html example to allow for per-port + // incoming/outgoing direction. + graph.getAllConnectionConstraints = function(terminal) { + var geo = (terminal != null) ? this.getCellGeometry(terminal.cell) : null; + console.log("getAllConnectionConstraints ") + if ((geo != null ? !geo.relative : false) && + this.getModel().isVertex(terminal.cell) && + this.getModel().getChildCount(terminal.cell) == 0) + { + return [new mxConnectionConstraint(new mxPoint(0, 0.5), false), + new mxConnectionConstraint(new mxPoint(1, 0.5), false)]; + } + + return null; + }; + + // Makes sure non-relative cells can only be connected via constraints + graph.connectionHandler.isConnectableCell = function(cell) { + console.log("isConnectableCell",cell) + if (this.graph.getModel().isEdge(cell)) + { + return true; + } + else + { + var geo = (cell != null) ? this.graph.getCellGeometry(cell) : null; + + return (geo != null) ? geo.relative : false; + } + }; + var parent = graph.getDefaultParent(); + + graph.getModel().beginUpdate(); + try + { + var v1 = graph.insertVertex(parent, null, 'J1', 80, 40, 40, 80, + 'verticalLabelPosition=top;verticalAlign=bottom;shadow=1;fillColor=' + fillColor); + v1.setConnectable(false); + + var v11 = graph.insertVertex(v1, null, '1', 0, 0, 10, 16, + 'shape=line;align=left;verticalAlign=middle;fontSize=10;routingCenterX=-0.5;'+ + 'spacingLeft=12;fontColor=' + fontColor + ';strokeColor=' + strokeColor); + v11.geometry.relative = true; + v11.geometry.offset = new mxPoint(-v11.geometry.width, 2); + var v12 = v11.clone(); + v12.value = '2'; + v12.geometry.offset = new mxPoint(-v11.geometry.width, 22); + v1.insert(v12); + } + finally{ + graph.getModel().endUpdate(); + + } + + // let canvas = this.canvas; let doc = this.document; let sheetObject = doc.objects.find(o => o instanceof AltiumSheet); - canvas.style.width = sheetObject.width + "px"; - canvas.style.height = sheetObject.height + "px"; - canvas.width = sheetObject.width * window.devicePixelRatio; - canvas.height = sheetObject.height * window.devicePixelRatio; + // canvas.style.width = sheetObject.width + "px"; + // canvas.style.height = sheetObject.height + "px"; + // canvas.width = sheetObject.width * window.devicePixelRatio; + // canvas.height = sheetObject.height * window.devicePixelRatio; let areaColourInt = Number.parseInt(sheetObject.attributes.areacolor, 10); let areaColour = this.#altiumColourToHex(areaColourInt); - canvas.style.backgroundColor = areaColour; - let ctx = canvas.getContext('2d'); - ctx.scale(1, -1); - ctx.translate(0.5, 0.5); - ctx.translate(0, -canvas.height); - ctx.font = "7pt sans-serif"; - ctx.textRendering = "optimizeLegibility"; - ctx.imageSmoothingQuality = "high"; - ctx.textBaseline = "bottom"; - ctx.fillStyle = areaColour; - ctx.fillRect(0, 0, canvas.width, canvas.height); + // canvas.style.backgroundColor = areaColour; + // let ctx = canvas.getContext('2d'); + // ctx.scale(1, -1); + // ctx.translate(0.5, 0.5); + // ctx.translate(0, -canvas.height); + // ctx.font = "7pt sans-serif"; + // ctx.textRendering = "optimizeLegibility"; + // ctx.imageSmoothingQuality = "high"; + // ctx.textBaseline = "bottom"; + // ctx.fillStyle = areaColour; + // ctx.fillRect(0, 0, canvas.width, canvas.height); let results = document.getElementById("results"); let sheet = doc.objects.find((o) => o instanceof AltiumSheet); let gridLight = "#eeeeee"; let gridDark = "#cccccc"; - ctx.lineWidth = 1; - ctx.globalAlpha = 0.5; + // ctx.lineWidth = 1; + // ctx.globalAlpha = 0.5; if (sheet.show_grid) { - let n = 0; - for (let x = 0; x < canvas.width; x += sheet.grid_size) - { - ctx.strokeStyle = ((n % 10) == 0) ? gridDark : gridLight; - ctx.beginPath(); - ctx.moveTo(x, 0); - ctx.lineTo(x, canvas.height); - ctx.stroke(); - n++; - } - n = 0; - for (let y = 0; y < canvas.height; y += sheet.grid_size) - { - ctx.strokeStyle = ((n % 10) == 0) ? gridDark : gridLight; - ctx.beginPath(); - ctx.moveTo(0, y); - ctx.lineTo(canvas.width, y); - ctx.stroke(); - n++; - } - } - ctx.globalAlpha = 1; + let n = 0; + // for (let x = 0; x < canvas.width; x += sheet.grid_size) + // { + // ctx.strokeStyle = ((n % 10) == 0) ? gridDark : gridLight; + // ctx.beginPath(); + // ctx.moveTo(x, 0); + // ctx.lineTo(x, canvas.height); + // ctx.stroke(); + // n++; + // } + // n = 0; + // for (let y = 0; y < canvas.height; y += sheet.grid_size) + // { + // ctx.strokeStyle = ((n % 10) == 0) ? gridDark : gridLight; + // ctx.beginPath(); + // ctx.moveTo(0, y); + // ctx.lineTo(canvas.width, y); + // ctx.stroke(); + // n++; + // } + } + // ctx.globalAlpha = 1; /* @@ -152,608 +229,681 @@ class AltiumSchematicRenderer for (let obj of doc.objects.filter((o) => o instanceof AltiumWire)) { - if (!this.#shouldShow(obj)) continue; + // if (!this.#shouldShow(obj)) continue; - ctx.strokeStyle = this.#altiumColourToHex(obj.colour); - ctx.lineWidth = obj.width; - ctx.beginPath(); - ctx.moveTo(obj.points[0].x, obj.points[0].y); - for (let i = 1; i < obj.points.length; i++) - { - ctx.lineTo(obj.points[i].x, obj.points[i].y); - } - ctx.stroke(); + // ctx.strokeStyle = this.#altiumColourToHex(obj.colour); + // ctx.lineWidth = obj.width; + // ctx.beginPath(); + // ctx.moveTo(obj.points[0].x, obj.points[0].y); + // for (let i = 1; i < obj.points.length; i++) + // { + // ctx.lineTo(obj.points[i].x, obj.points[i].y); + // } + // ctx.stroke(); + } + for (let obj of doc.objects.filter((o) => o instanceof AltiumBus)) + { + // if (!this.#shouldShow(obj)) continue; + + // ctx.strokeStyle = this.#altiumColourToHex(obj.colour); + // ctx.lineWidth = obj.width; + // ctx.beginPath(); + // ctx.moveTo(obj.points[0].x, obj.points[0].y); + // for (let i = 1; i < obj.points.length; i++) + // { + // ctx.lineTo(obj.points[i].x, obj.points[i].y); + // } + // ctx.stroke(); } + for (let obj of doc.objects.filter((o) => o instanceof AltiumSheetSymbol)) + { + // if (!this.#shouldShow(obj)) continue; + // ctx.fillStyle = this.#altiumColourToHex(obj.attributes.areacolor); + + // ctx.fillRect(obj.attributes.location_x, + // obj.attributes.location_y- obj.attributes.ysize, + // obj.attributes.xsize, obj.attributes.ysize); + // ctx.stroke(); + // ctx.strokeStyle = this.#altiumColourToHex(obj.attributes.color); + // ctx.strokeRect(obj.attributes.location_x, + // obj.attributes.location_y- obj.attributes.ysize, + // obj.attributes.xsize, obj.attributes.ysize); + + + // ctx.strokeStyle = "#000080"; + // ctx.lineWidth = obj.width; + // ctx.beginPath(); + // ctx.moveTo(obj.attributes.location_x, obj.attributes.location_y); + // ctx.lineTo(obj.attributes.location_x + obj.attributes.xsize, + // obj.attributes.location_y + obj.attributes.ysize); + // ctx.stroke(); + + // ctx.strokeStyle = this.#altiumColourToHex(obj.colour); + // ctx.lineWidth = obj.width; + // ctx.beginPath(); + // ctx.moveTo(obj.points[0].x, obj.points[0].y); + // for (let i = 1; i < obj.points.length; i++) + // { + // ctx.lineTo(obj.points[i].x, obj.points[i].y); + // } + // ctx.stroke(); + } for (let obj of doc.objects.filter((o) => o instanceof AltiumRectangle)) { - if (!this.#shouldShow(obj)) continue; - - if (!obj.transparent) - { - ctx.fillStyle = this.#altiumColourToHex(obj.fill_colour); - ctx.fillRect(obj.left, obj.top, obj.right - obj.left, obj.bottom - obj.top); - } - ctx.strokeStyle = this.#altiumColourToHex(obj.line_colour); - ctx.strokeRect(obj.left, obj.top, obj.right - obj.left, obj.bottom - obj.top); + // if (!this.#shouldShow(obj)) + // continue; + // ctx.fillStyle = this.#altiumColourToHex(obj.attributes.areacolor); + + // ctx.fillRect(obj.attributes.location_x, + // obj.attributes.location_y, + // obj.attributes.corner_x - obj.attributes.location_x, obj.attributes.corner_y - obj.attributes.location_y); + // ctx.stroke(); + // ctx.strokeStyle = this.#altiumColourToHex(obj.attributes.color); + // ctx.strokeRect(obj.attributes.location_x, + // obj.attributes.location_y, + // obj.attributes.corner_x - obj.attributes.location_x, obj.attributes.corner_y - obj.attributes.location_y); + } - - for (let obj of doc.objects.filter((o) => o instanceof AltiumTextFrame)) + // todo undo + for (let obj of doc.objects.filter((o) => o instanceof AltiumSheetEntry)) { if (!this.#shouldShow(obj)) continue; - if (!obj.transparent) - { - ctx.fillStyle = this.#altiumColourToHex(obj.fill_colour); - ctx.fillRect(obj.left, obj.top, obj.right - obj.left, obj.bottom - obj.top); - } - if (obj.show_border) - { - ctx.strokeStyle = this.#altiumColourToHex(obj.border_colour); - ctx.strokeRect(obj.left, obj.top, obj.right - obj.left, obj.bottom - obj.top); - } + // ctx.strokeStyle = this.#altiumColourToHex(obj.colour); + // ctx.lineWidth = obj.width; + // ctx.beginPath(); + // ctx.moveTo(obj.points[0].x, obj.points[0].y); + // for (let i = 1; i < obj.points.length; i++) + // { + // ctx.lineTo(obj.points[i].x, obj.points[i].y); + // } + // ctx.stroke(); + } + + for (let obj of doc.objects.filter((o) => o instanceof AltiumTextFrame)) + { + // if (!this.#shouldShow(obj)) continue; + + // if (!obj.transparent) + // { + // ctx.fillStyle = this.#altiumColourToHex(obj.fill_colour); + // ctx.fillRect(obj.left, obj.top, obj.right - obj.left, + // obj.bottom - obj.top); + // } + // if (obj.show_border) + // { + // ctx.strokeStyle = this.#altiumColourToHex(obj.border_colour); + // ctx.strokeRect(obj.left, obj.top, obj.right - obj.left, + // obj.bottom - obj.top); + // } } for (let obj of doc.objects.filter((o) => o instanceof AltiumEllipse)) { - if (!this.#shouldShow(obj)) continue; + // if (!this.#shouldShow(obj)) continue; - if (!obj.transparent) - { - ctx.fillStyle = this.#altiumColourToHex(obj.fill_colour); - } - ctx.strokeStyle = this.#altiumColourToHex(obj.line_colour); - ctx.beginPath(); - ctx.ellipse(obj.x, obj.y, obj.radius_x, obj.radius_y, 0, 0, Math.PI*2); - ctx.stroke(); - if (!obj.transparent) - ctx.fill(); + // if (!obj.transparent) + // { + // ctx.fillStyle = this.#altiumColourToHex(obj.fill_colour); + // } + // ctx.strokeStyle = this.#altiumColourToHex(obj.line_colour); + // ctx.beginPath(); + // ctx.ellipse(obj.x, obj.y, obj.radius_x, obj.radius_y, 0, 0, Math.PI*2); + // ctx.stroke(); + // if (!obj.transparent) + // ctx.fill(); } for (let obj of doc.objects.filter((o) => o instanceof AltiumPin)) { - if (!this.#shouldShow(obj)) continue; + // if (!this.#shouldShow(obj)) continue; - ctx.strokeStyle = "#000000"; - ctx.beginPath(); - ctx.moveTo(obj.x, obj.y); - ctx.lineTo(obj.x + obj.angle_vec[0] * obj.length, obj.y + obj.angle_vec[1] * obj.length); - ctx.stroke(); + // ctx.strokeStyle = "#000000"; + // ctx.beginPath(); + // ctx.moveTo(obj.x, obj.y); + // ctx.lineTo(obj.x + obj.angle_vec[0] * obj.length, obj.y + obj.angle_vec[1] * obj.length); + // ctx.stroke(); + + // ctx.fillStyle = this.#altiumColourToHex(obj.colour); + // ctx.beginPath(); + // ctx.ellipse(obj.x + obj.angle_vec[0] * obj.length, obj.y + obj.angle_vec[1] * obj.length, 5, 4, 0, 0, 2*Math.PI); + // ctx.fill(); } for (let obj of doc.objects.filter((o) => o instanceof AltiumLine)) { - if (!this.#shouldShow(obj)) continue; + // if (!this.#shouldShow(obj)) continue; - ctx.strokeStyle = this.#altiumColourToHex(obj.colour); - ctx.beginPath(); - ctx.moveTo(obj.x1, obj.y1); - ctx.lineTo(obj.x2, obj.y2); - ctx.stroke(); + // ctx.strokeStyle = this.#altiumColourToHex(obj.colour); + // ctx.beginPath(); + // ctx.moveTo(obj.x1, obj.y1); + // ctx.lineTo(obj.x2, obj.y2); + // ctx.stroke(); } for (let obj of doc.objects.filter((o) => o instanceof AltiumArc)) { - if (!this.#shouldShow(obj)) continue; + // if (!this.#shouldShow(obj)) continue; - ctx.strokeStyle = this.#altiumColourToHex(obj.colour); - ctx.lineWidth = obj.width; - ctx.beginPath(); - ctx.arc(obj.x, obj.y, obj.radius, obj.start_angle * Math.PI/180, obj.end_angle * Math.PI/180); - ctx.stroke(); - ctx.lineWidth = 1; + // ctx.strokeStyle = this.#altiumColourToHex(obj.colour); + // ctx.lineWidth = obj.width; + // ctx.beginPath(); + // ctx.arc(obj.x, obj.y, obj.radius, obj.start_angle * Math.PI/180, obj.end_angle * Math.PI/180); + // ctx.stroke(); + // ctx.lineWidth = 1; } for (let obj of doc.objects.filter((o) => o instanceof AltiumPolyline)) { - if (!this.#shouldShow(obj)) continue; + // if (!this.#shouldShow(obj)) continue; - ctx.strokeStyle = this.#altiumColourToHex(obj.colour); - ctx.fillStyle = this.#altiumColourToHex(obj.colour); - ctx.lineWidth = obj.width; + // ctx.strokeStyle = this.#altiumColourToHex(obj.colour); + // ctx.fillStyle = this.#altiumColourToHex(obj.colour); + // ctx.lineWidth = obj.width; - switch (obj.line_style) - { - case 1: - ctx.setLineDash([4, 4]); - break; - case 2: - ctx.setLineDash([2, 2]); - break; - case 3: - ctx.setLineDash([4, 2, 2, 4]); - break; - } + // switch (obj.line_style) + // { + // case 1: + // ctx.setLineDash([4, 4]); + // break; + // case 2: + // ctx.setLineDash([2, 2]); + // break; + // case 3: + // ctx.setLineDash([4, 2, 2, 4]); + // break; + // } - ctx.beginPath(); - ctx.moveTo(obj.points[0].x, obj.points[0].y); - for (let i = 1; i < obj.points.length; i++) - { - ctx.lineTo(obj.points[i].x, obj.points[i].y); - } - ctx.stroke(); + // ctx.beginPath(); + // ctx.moveTo(obj.points[0].x, obj.points[0].y); + // for (let i = 1; i < obj.points.length; i++) + // { + // ctx.lineTo(obj.points[i].x, obj.points[i].y); + // } + // ctx.stroke(); - ctx.setLineDash([]); + // ctx.setLineDash([]); - let pa = null; - let pb = null; - let shapeSize = obj.shape_size + 1; - ctx.lineWidth = shapeSize; - if (obj.start_shape > 0) - { - let pa = obj.points[1]; - let pb = obj.points[0]; - let dx = pb.x - pa.x; - let dy = pb.y - pa.y; - let angle = Math.atan2(dy, dx); - const baseSize = 3 + shapeSize; - let tax = pb.x - Math.cos(angle - Math.PI/6) * baseSize; - let tay = pb.y - Math.sin(angle - Math.PI/6) * baseSize; - let tbx = pb.x - Math.cos(angle + Math.PI/6) * baseSize; - let tby = pb.y - Math.sin(angle + Math.PI/6) * baseSize; - ctx.beginPath(); - ctx.moveTo(tax, tay); - ctx.lineTo(pb.x + Math.cos(angle) * 0.5, pb.y + Math.sin(angle) * 0.5); - ctx.lineTo(tbx, tby); - ctx.stroke(); - if (obj.start_shape == 2 || obj.start_shape == 4) - ctx.fill(); - } - if (obj.end_shape > 0) - { - let pa = obj.points[obj.points.length - 2]; - let pb = obj.points[obj.points.length - 1]; - let dx = pb.x - pa.x; - let dy = pb.y - pa.y; - let angle = Math.atan2(dy, dx); - const baseSize = 3 + shapeSize; - let tax = pb.x - Math.cos(angle - Math.PI/6) * baseSize; - let tay = pb.y - Math.sin(angle - Math.PI/6) * baseSize; - let tbx = pb.x - Math.cos(angle + Math.PI/6) * baseSize; - let tby = pb.y - Math.sin(angle + Math.PI/6) * baseSize; - ctx.beginPath(); - ctx.moveTo(tax, tay); - ctx.lineTo(pb.x + Math.cos(angle) * 0.5, pb.y + Math.sin(angle) * 0.5); - ctx.lineTo(tbx, tby); - ctx.stroke(); - if (obj.end_shape == 2 || obj.end_shape == 4) - ctx.fill(); - } - ctx.lineWidth = 1; + // let pa = null; + // let pb = null; + // let shapeSize = obj.shape_size + 1; + // ctx.lineWidth = shapeSize; + // if (obj.start_shape > 0) + // { + // let pa = obj.points[1]; + // let pb = obj.points[0]; + // let dx = pb.x - pa.x; + // let dy = pb.y - pa.y; + // let angle = Math.atan2(dy, dx); + // const baseSize = 3 + shapeSize; + // let tax = pb.x - Math.cos(angle - Math.PI/6) * baseSize; + // let tay = pb.y - Math.sin(angle - Math.PI/6) * baseSize; + // let tbx = pb.x - Math.cos(angle + Math.PI/6) * baseSize; + // let tby = pb.y - Math.sin(angle + Math.PI/6) * baseSize; + // ctx.beginPath(); + // ctx.moveTo(tax, tay); + // ctx.lineTo(pb.x + Math.cos(angle) * 0.5, pb.y + Math.sin(angle) * 0.5); + // ctx.lineTo(tbx, tby); + // ctx.stroke(); + // if (obj.start_shape == 2 || obj.start_shape == 4) + // ctx.fill(); + // } + // if (obj.end_shape > 0) + // { + // let pa = obj.points[obj.points.length - 2]; + // let pb = obj.points[obj.points.length - 1]; + // let dx = pb.x - pa.x; + // let dy = pb.y - pa.y; + // let angle = Math.atan2(dy, dx); + // const baseSize = 3 + shapeSize; + // let tax = pb.x - Math.cos(angle - Math.PI/6) * baseSize; + // let tay = pb.y - Math.sin(angle - Math.PI/6) * baseSize; + // let tbx = pb.x - Math.cos(angle + Math.PI/6) * baseSize; + // let tby = pb.y - Math.sin(angle + Math.PI/6) * baseSize; + // ctx.beginPath(); + // ctx.moveTo(tax, tay); + // ctx.lineTo(pb.x + Math.cos(angle) * 0.5, pb.y + Math.sin(angle) * 0.5); + // ctx.lineTo(tbx, tby); + // ctx.stroke(); + // if (obj.end_shape == 2 || obj.end_shape == 4) + // ctx.fill(); + // } + // ctx.lineWidth = 1; } for (let obj of doc.objects.filter((o) => o instanceof AltiumPolygon)) { - if (!this.#shouldShow(obj)) continue; + // if (!this.#shouldShow(obj)) continue; - ctx.strokeStyle = this.#altiumColourToHex(obj.line_colour); - ctx.fillStyle = this.#altiumColourToHex(obj.fill_colour); - ctx.lineWidth = obj.width; - ctx.beginPath(); - ctx.moveTo(obj.points[0].x, obj.points[0].y); - for (let i = 1; i < obj.points.length; i++) - { - ctx.lineTo(obj.points[i].x, obj.points[i].y); - } - ctx.closePath(); - ctx.stroke(); - ctx.fill(); - ctx.lineWidth = 1; + // ctx.strokeStyle = this.#altiumColourToHex(obj.line_colour); + // ctx.fillStyle = this.#altiumColourToHex(obj.fill_colour); + // ctx.lineWidth = obj.width; + // ctx.beginPath(); + // ctx.moveTo(obj.points[0].x, obj.points[0].y); + // for (let i = 1; i < obj.points.length; i++) + // { + // ctx.lineTo(obj.points[i].x, obj.points[i].y); + // } + // ctx.closePath(); + // ctx.stroke(); + // ctx.fill(); + // ctx.lineWidth = 1; } for (let obj of doc.objects.filter((o) => o instanceof AltiumJunction)) { - if (!this.#shouldShow(obj)) continue; + // if (!this.#shouldShow(obj)) continue; - ctx.fillStyle = this.#altiumColourToHex(obj.colour); - ctx.beginPath(); - ctx.ellipse(obj.x, obj.y, 2, 2, 0, 0, 2*Math.PI); - ctx.fill(); + // ctx.fillStyle = this.#altiumColourToHex(obj.colour); + // ctx.beginPath(); + // ctx.ellipse(obj.x, obj.y, 5, 4, 0, 0, 2*Math.PI); + // ctx.fill(); } for (let obj of doc.objects.filter((o) => o instanceof AltiumPowerPort)) { - if (!this.#shouldShow(obj)) continue; + // if (!this.#shouldShow(obj)) continue; - ctx.strokeStyle = this.#altiumColourToHex(obj.colour); - ctx.fillStyle = this.#altiumColourToHex(obj.colour); - ctx.lineWidth = 1; - if (!obj.is_off_sheet_connector) - { - switch (obj.style) - { - case 2: - ctx.beginPath(); - ctx.moveTo(obj.x, obj.y); - ctx.lineTo(obj.x, obj.y + 10); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(obj.x - 5, obj.y + 10); - ctx.lineTo(obj.x + 5, obj.y + 10); - ctx.stroke(); - break; - case 4: - ctx.beginPath(); - ctx.moveTo(obj.x - 10, obj.y); - ctx.lineTo(obj.x + 10, obj.y); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(obj.x - 7.5, obj.y - 2); - ctx.lineTo(obj.x + 7.5, obj.y - 2); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(obj.x - 5, obj.y - 4); - ctx.lineTo(obj.x + 5, obj.y - 4); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(obj.x - 2.5, obj.y - 6); - ctx.lineTo(obj.x + 2.5, obj.y - 6); - ctx.stroke(); - break; - case 6: - ctx.beginPath(); - ctx.moveTo(obj.x, obj.y); - ctx.lineTo(obj.x, obj.y - 5); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(obj.x - 5, obj.y - 5); - ctx.lineTo(obj.x + 5, obj.y - 5); - ctx.stroke(); - for (let g = -1; g < 2; g++) - { - ctx.beginPath(); - ctx.moveTo(obj.x + (g * 5), obj.y - 5); - ctx.lineTo(obj.x + (g * 5) - 3, obj.y - 10); - ctx.stroke(); - } - break; - default: - ctx.fillRect(obj.x - 10, obj.y, 20, (obj.orientation == 1) ? 10 : -10); - break; - } - } - else - { - ctx.save(); - ctx.translate(obj.x, obj.y); - ctx.rotate((obj.orientation - 1) * Math.PI/2); + // ctx.strokeStyle = this.#altiumColourToHex(obj.colour); + // ctx.fillStyle = this.#altiumColourToHex(obj.colour); + // ctx.lineWidth = 1; + // if (!obj.is_off_sheet_connector) + // { + // switch (obj.style) + // { + // case 2: + // ctx.beginPath(); + // ctx.moveTo(obj.x, obj.y); + // ctx.lineTo(obj.x, obj.y + 10); + // ctx.stroke(); + // ctx.beginPath(); + // ctx.moveTo(obj.x - 5, obj.y + 10); + // ctx.lineTo(obj.x + 5, obj.y + 10); + // ctx.stroke(); + // break; + // case 4: + // ctx.beginPath(); + // ctx.moveTo(obj.x - 10, obj.y); + // ctx.lineTo(obj.x + 10, obj.y); + // ctx.stroke(); + // ctx.beginPath(); + // ctx.moveTo(obj.x - 7.5, obj.y - 2); + // ctx.lineTo(obj.x + 7.5, obj.y - 2); + // ctx.stroke(); + // ctx.beginPath(); + // ctx.moveTo(obj.x - 5, obj.y - 4); + // ctx.lineTo(obj.x + 5, obj.y - 4); + // ctx.stroke(); + // ctx.beginPath(); + // ctx.moveTo(obj.x - 2.5, obj.y - 6); + // ctx.lineTo(obj.x + 2.5, obj.y - 6); + // ctx.stroke(); + // break; + // case 6: + // ctx.beginPath(); + // ctx.moveTo(obj.x, obj.y); + // ctx.lineTo(obj.x, obj.y - 5); + // ctx.stroke(); + // ctx.beginPath(); + // ctx.moveTo(obj.x - 5, obj.y - 5); + // ctx.lineTo(obj.x + 5, obj.y - 5); + // ctx.stroke(); + // for (let g = -1; g < 2; g++) + // { + // ctx.beginPath(); + // ctx.moveTo(obj.x + (g * 5), obj.y - 5); + // ctx.lineTo(obj.x + (g * 5) - 3, obj.y - 10); + // ctx.stroke(); + // } + // break; + // default: + // ctx.fillRect(obj.x - 10, obj.y, 20, (obj.orientation == 1) ? 10 : -10); + // break; + // } + // } + // else + // { + // ctx.save(); + // ctx.translate(obj.x, obj.y); + // ctx.rotate((obj.orientation - 1) * Math.PI/2); - ctx.beginPath(); - ctx.moveTo(0, 0); - ctx.lineTo(-5, 5); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(0, 0); - ctx.lineTo(5, 5); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(0, 5); - ctx.lineTo(-5, 10); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(0, 5); - ctx.lineTo(5, 10); - ctx.stroke(); + // ctx.beginPath(); + // ctx.moveTo(0, 0); + // ctx.lineTo(-5, 5); + // ctx.stroke(); + // ctx.beginPath(); + // ctx.moveTo(0, 0); + // ctx.lineTo(5, 5); + // ctx.stroke(); + // ctx.beginPath(); + // ctx.moveTo(0, 5); + // ctx.lineTo(-5, 10); + // ctx.stroke(); + // ctx.beginPath(); + // ctx.moveTo(0, 5); + // ctx.lineTo(5, 10); + // ctx.stroke(); - ctx.restore(); - } + // ctx.restore(); + // } //ctx.fillText(obj.style.toString(), obj.x, obj.y); } // store the transform for recovery later - let savedTransform = ctx.getTransform(); - ctx.resetTransform(); + // let savedTransform = ctx.getTransform(); + // ctx.resetTransform(); for (let obj of doc.objects.filter((o) => o instanceof AltiumLabel)) { - if (!this.#shouldShow(obj)) continue; + // if (!this.#shouldShow(obj)) continue; - if (obj.hidden) - continue; - ctx.textAlign = ["left", "center", "right"][obj.justification]; - ctx.textBaseline = ["bottom", "bottom", "top", "top"][obj.orientation]; - ctx.fillStyle = this.#altiumColourToHex(obj.colour); - ctx.save(); - ctx.translate(obj.x, canvas.height - obj.y); - ctx.rotate(obj.orientation * -Math.PI/2); - ctx.fillText(obj.text, 0, 0); - ctx.restore(); + // if (obj.hidden) + // continue; + // ctx.textAlign = ["left", "center", "right"][obj.justification]; + // ctx.textBaseline = ["bottom", "bottom", "top", "top"][obj.orientation]; + // ctx.fillStyle = this.#altiumColourToHex(obj.colour); + // ctx.save(); + // ctx.translate(obj.x, canvas.height - obj.y); + // ctx.rotate(obj.orientation * -Math.PI/2); + // ctx.fillText(obj.text, 0, 0); + // ctx.restore(); } - ctx.textAlign = "left"; - ctx.textBaseline = "bottom"; + // ctx.textAlign = "left"; + // ctx.textBaseline = "bottom"; for (let obj of doc.objects.filter((o) => o instanceof AltiumDesignator)) { - if (!this.#shouldShow(obj)) continue; + // if (!this.#shouldShow(obj)) continue; - if (obj.hidden) - continue; - ctx.textAlign = ["left", "left", "right", "right"][obj.orientation]; - ctx.textBaseline = ["bottom", "bottom", "top", "top"][obj.orientation]; - ctx.fillStyle = this.#altiumColourToHex(obj.colour); - ctx.fillText(obj.full_designator, obj.x, canvas.height - obj.y); + // if (obj.hidden) + // continue; + // ctx.textAlign = ["left", "left", "right", "right"][obj.orientation]; + // ctx.textBaseline = ["bottom", "bottom", "top", "top"][obj.orientation]; + // ctx.fillStyle = this.#altiumColourToHex(obj.colour); + // ctx.fillText(obj.full_designator, obj.x, canvas.height - obj.y); } - ctx.textAlign = "left"; - ctx.textBaseline = "bottom"; + // ctx.textAlign = "left"; + // ctx.textBaseline = "bottom"; for (let obj of doc.objects.filter((o) => o instanceof AltiumParameter)) { - if (!this.#shouldShow(obj)) continue; + // if (!this.#shouldShow(obj)) continue; - if (obj.hidden || obj.is_implementation_parameter) - continue; + // if (obj.hidden || obj.is_implementation_parameter) + // continue; - ctx.textAlign = ["left", "left", "right", "right"][obj.orientation]; - ctx.textBaseline = ["bottom", "bottom", "top", "top"][obj.orientation]; - ctx.fillStyle = this.#altiumColourToHex(obj.colour); - if (obj.orientation == 1) - { - ctx.save(); - ctx.translate(obj.x, canvas.height - obj.y); - ctx.rotate(-Math.PI/2); - ctx.fillText(obj.text, 0, 0); - ctx.restore(); - } - else if (obj.orientation == 3) - { - ctx.save(); - ctx.translate(obj.x, canvas.height - obj.y); - ctx.rotate(Math.PI/2); - ctx.fillText(obj.text, 0, 0); - ctx.restore(); - } - else - { - ctx.fillText(obj.text, obj.x, canvas.height - obj.y); - } + // ctx.textAlign = ["left", "left", "right", "right"][obj.orientation]; + // ctx.textBaseline = ["bottom", "bottom", "top", "top"][obj.orientation]; + // ctx.fillStyle = this.#altiumColourToHex(obj.colour); + // if (obj.orientation == 1) + // { + // ctx.save(); + // ctx.translate(obj.x, canvas.height - obj.y); + // ctx.rotate(-Math.PI/2); + // ctx.fillText(obj.text, 0, 0); + // ctx.restore(); + // } + // else if (obj.orientation == 3) + // { + // ctx.save(); + // ctx.translate(obj.x, canvas.height - obj.y); + // ctx.rotate(Math.PI/2); + // ctx.fillText(obj.text, 0, 0); + // ctx.restore(); + // } + // else + // { + // ctx.fillText(obj.text, obj.x, canvas.height - obj.y); + // } } - ctx.textAlign = "left"; - ctx.textBaseline = "bottom"; + // ctx.textAlign = "left"; + // ctx.textBaseline = "bottom"; for (let obj of doc.objects.filter((o) => o instanceof AltiumNetLabel)) { - if (!this.#shouldShow(obj)) continue; + // if (!this.#shouldShow(obj)) continue; - if (obj.hidden) - continue; - ctx.textAlign = ["left", "center", "right"][obj.justification]; - ctx.textBaseline = ["bottom", "bottom", "top", "top"][obj.orientation]; - ctx.fillStyle = this.#altiumColourToHex(obj.colour); - if (obj.orientation == 1) - { - ctx.save(); - ctx.translate(obj.x, canvas.height - obj.y); - ctx.rotate(-Math.PI/2); - ctx.fillText(obj.text, 0, 0); - ctx.restore(); - } - else if (obj.orientation == 3) - { - ctx.save(); - ctx.translate(obj.x, canvas.height - obj.y); - ctx.rotate(Math.PI/2); - ctx.fillText(obj.text, 0, 0); - ctx.restore(); - } - else - { - ctx.fillText(obj.text, obj.x, canvas.height - obj.y); - } + // if (obj.hidden) + // continue; + // ctx.textAlign = ["left", "center", "right"][obj.justification]; + // ctx.textBaseline = ["bottom", "bottom", "top", "top"][obj.orientation]; + // ctx.fillStyle = this.#altiumColourToHex(obj.colour); + // if (obj.orientation == 1) + // { + // ctx.save(); + // ctx.translate(obj.x, canvas.height - obj.y); + // ctx.rotate(-Math.PI/2); + // ctx.fillText(obj.text, 0, 0); + // ctx.restore(); + // } + // else if (obj.orientation == 3) + // { + // ctx.save(); + // ctx.translate(obj.x, canvas.height - obj.y); + // ctx.rotate(Math.PI/2); + // ctx.fillText(obj.text, 0, 0); + // ctx.restore(); + // } + // else + // { + // ctx.fillText(obj.text, obj.x, canvas.height - obj.y); + // } } - ctx.textAlign = "left"; - ctx.textBaseline = "bottom"; + // ctx.textAlign = "left"; + // ctx.textBaseline = "bottom"; for (let obj of doc.objects.filter((o) => o instanceof AltiumPin)) { - if (!this.#shouldShow(obj)) continue; + // if (!this.#shouldShow(obj)) continue; - if (!obj.show_name) - continue; - ctx.textAlign = ["right", "right", "left", "right"][obj.orientation]; - ctx.textBaseline = "middle"; - let objName = obj.name; - let inverted = false; - if (obj.name.includes("\\")) - { - objName = obj.name.replaceAll("\\", ""); - inverted = true; - } - if (obj.name_orientation != 0) - { - ctx.textBaseline = ["middle", "top", "middle", "bottom"][obj.orientation]; - if (obj.name_orientation <= 3) - ctx.textAlign = ["left", "center", "right"][obj.name_orientation-1]; - else - ctx.textAlign = "center"; - } - let margin_x = [-1, 0, 1, 0][obj.orientation] * 2; - let margin_y = [0, -1, 0, 1][obj.orientation] * 2; - ctx.fillStyle = this.#altiumColourToHex(obj.colour); - ctx.strokeStyle = ctx.fillStyle; - ctx.lineWidth = 1; - if (obj.orientation == 1 && obj.name_orientation == 0) - { - ctx.save(); - ctx.translate(obj.x + margin_x, canvas.height - (obj.y + margin_y)); - ctx.rotate(-Math.PI/2); - ctx.fillText(objName, 0, 0); - if (inverted) - { - // todo: test this - let textSize = ctx.measureText(objName); - ctx.beginPath(); - ctx.moveTo(0, textSize.actualBoundingBoxAscent + 2); - ctx.lineTo(textSize.width, textSize.actualBoundingBoxAscent + 2); - ctx.stroke(); - } - ctx.restore(); - } - else if (obj.orientation == 3 && obj.name_orientation == 0) - { - ctx.save(); - ctx.translate(obj.x + margin_x, canvas.height - (obj.y + margin_y)); - ctx.rotate(Math.PI/2); - ctx.fillText(objName, 0, 0); - if (inverted) - { - // todo: test this - let textSize = ctx.measureText(objName); - ctx.beginPath(); - ctx.moveTo(0, textSize.actualBoundingBoxAscent + 2); - ctx.lineTo(textSize.width, textSize.actualBoundingBoxAscent + 2); - ctx.stroke(); - } - ctx.restore(); - } - else - { - ctx.fillText(objName, obj.x + margin_x, canvas.height - (obj.y + margin_y)); - if (inverted) - { - let textSize = ctx.measureText(objName); - let offset = 0; - switch (ctx.textAlign) - { - case "center": - offset = -(textSize.width/2); - break; - case "right": - offset = -textSize.width; - break; - case "left": - offset = 0; - break; - default: - offset = 0; - break; - } - ctx.beginPath(); - ctx.moveTo(obj.x + margin_x + offset, canvas.height - (obj.y + margin_y + textSize.actualBoundingBoxAscent + 2)); - ctx.lineTo(obj.x + margin_x + offset + textSize.width, canvas.height - (obj.y + margin_y + textSize.actualBoundingBoxAscent + 2)); - ctx.stroke(); - } - } + // if (!obj.show_name) + // continue; + // ctx.textAlign = ["right", "right", "left", "right"][obj.orientation]; + // ctx.textBaseline = "middle"; + // let objName = obj.name; + // let inverted = false; + // if (obj.name.includes("\\")) + // { + // objName = obj.name.replaceAll("\\", ""); + // inverted = true; + // } + // if (obj.name_orientation != 0) + // { + // ctx.textBaseline = ["middle", "top", "middle", "bottom"][obj.orientation]; + // if (obj.name_orientation <= 3) + // ctx.textAlign = ["left", "center", "right"][obj.name_orientation-1]; + // else + // ctx.textAlign = "center"; + // } + // let margin_x = [-1, 0, 1, 0][obj.orientation] * 2; + // let margin_y = [0, -1, 0, 1][obj.orientation] * 2; + // ctx.fillStyle = this.#altiumColourToHex(obj.colour); + // ctx.strokeStyle = ctx.fillStyle; + // ctx.lineWidth = 1; + // if (obj.orientation == 1 && obj.name_orientation == 0) + // { + // ctx.save(); + // ctx.translate(obj.x + margin_x, canvas.height - (obj.y + margin_y)); + // ctx.rotate(-Math.PI/2); + // ctx.fillText(objName, 0, 0); + // if (inverted) + // { + // // todo: test this + // let textSize = ctx.measureText(objName); + // ctx.beginPath(); + // ctx.moveTo(0, textSize.actualBoundingBoxAscent + 2); + // ctx.lineTo(textSize.width, textSize.actualBoundingBoxAscent + 2); + // ctx.stroke(); + // } + // ctx.restore(); + // } + // else if (obj.orientation == 3 && obj.name_orientation == 0) + // { + // ctx.save(); + // ctx.translate(obj.x + margin_x, canvas.height - (obj.y + margin_y)); + // ctx.rotate(Math.PI/2); + // ctx.fillText(objName, 0, 0); + // if (inverted) + // { + // // todo: test this + // let textSize = ctx.measureText(objName); + // ctx.beginPath(); + // ctx.moveTo(0, textSize.actualBoundingBoxAscent + 2); + // ctx.lineTo(textSize.width, textSize.actualBoundingBoxAscent + 2); + // ctx.stroke(); + // } + // ctx.restore(); + // } + // else + // { + // ctx.fillText(objName, obj.x + margin_x, canvas.height - (obj.y + margin_y)); + // if (inverted) + // { + // let textSize = ctx.measureText(objName); + // let offset = 0; + // switch (ctx.textAlign) + // { + // case "center": + // offset = -(textSize.width/2); + // break; + // case "right": + // offset = -textSize.width; + // break; + // case "left": + // offset = 0; + // break; + // default: + // offset = 0; + // break; + // } + // ctx.beginPath(); + // ctx.moveTo(obj.x + margin_x + offset, canvas.height - (obj.y + margin_y + textSize.actualBoundingBoxAscent + 2)); + // ctx.lineTo(obj.x + margin_x + offset + textSize.width, canvas.height - (obj.y + margin_y + textSize.actualBoundingBoxAscent + 2)); + // ctx.stroke(); + // } + // } - ctx.setLineDash([]); + // ctx.setLineDash([]); } - ctx.textAlign = "left"; - ctx.textBaseline = "bottom"; + // ctx.textAlign = "left"; + // ctx.textBaseline = "bottom"; for (let obj of doc.objects.filter((o) => o instanceof AltiumPowerPort)) { - if (!this.#shouldShow(obj)) continue; + // if (!this.#shouldShow(obj)) continue; - if (!obj.show_text) - continue; - ctx.fillStyle = this.#altiumColourToHex(obj.colour); - ctx.textBaseline = ["middle", "top", "middle", "bottom"][obj.orientation]; - ctx.textAlign = ["left", "center", "right", "center"][obj.orientation]; - let offset_x = [12, 0, -12, 0][obj.orientation]; - let offset_y = [0, 20, 0, -20][obj.orientation]; - ctx.fillText(obj.text, obj.x + offset_x, canvas.height - (obj.y + offset_y)); + // if (!obj.show_text) + // continue; + // ctx.fillStyle = this.#altiumColourToHex(obj.colour); + // ctx.textBaseline = ["middle", "top", "middle", "bottom"][obj.orientation]; + // ctx.textAlign = ["left", "center", "right", "center"][obj.orientation]; + // let offset_x = [12, 0, -12, 0][obj.orientation]; + // let offset_y = [0, 20, 0, -20][obj.orientation]; + // ctx.fillText(obj.text, obj.x + offset_x, canvas.height - (obj.y + offset_y)); } - ctx.textAlign = "left"; - ctx.textBaseline = "middle"; - let savedFont = ctx.font; + // ctx.textAlign = "left"; + // ctx.textBaseline = "middle"; + // let savedFont = ctx.font; for (let obj of doc.objects.filter((o) => o instanceof AltiumTextFrame)) { - if (!this.#shouldShow(obj)) continue; + // if (!this.#shouldShow(obj)) continue; - if (obj.font_id > 0 && doc.sheet.fonts[obj.font_id] != null) - { - const frameFont = doc.sheet.fonts[obj.font_id]; - const fontStr = (frameFont.size - 1).toString() + "px " + frameFont.name; - if (fontStr.includes(":") || fontStr.includes("/") || !document.fonts.check(fontStr)) - { - ctx.font = savedFont; - } - else - { - ctx.font = fontStr; - } - } + // if (obj.font_id > 0 && doc.sheet.fonts[obj.font_id] != null) + // { + // const frameFont = doc.sheet.fonts[obj.font_id]; + // const fontStr = (frameFont.size - 1).toString() + "px " + frameFont.name; + // if (fontStr.includes(":") || fontStr.includes("/") || !document.fonts.check(fontStr)) + // { + // ctx.font = savedFont; + // } + // else + // { + // ctx.font = fontStr; + // } + // } - ctx.fillStyle = this.#altiumColourToHex(obj.text_colour); - ctx.textAlign = ["center", "left", "right"][obj.alignment]; - let offset_x = [(obj.right-obj.left)/2, obj.text_margin, (obj.right-obj.left) - obj.text_margin][obj.alignment]; - if (!obj.word_wrap) - { - ctx.fillText(obj.text.replaceAll("~1", "\n"), obj.left + offset_x, canvas.height - (obj.top + (obj.bottom-obj.top)/2)); - } - else - { - // todo: refactor this so that an initial pass figures out all the line splits, then a second pass writes the text, so that vertical alignment can be supported. - const text = obj.text.replaceAll("~1", "\n"); - const lines = text.split("\n"); - let ypos = 0; - if (lines.length > 1) - { - // this is a total hack, but if there are multiple lines in the text then we can make a rough guess at how far up we need to shift the text to center it vertically - // this doesn't correct for line wraps (see todo above for refactoring approach) but it's at least something I guess! - const roughMeasure = ctx.measureText(text); - ypos = ((roughMeasure.fontBoundingBoxDescent + roughMeasure.fontBoundingBoxAscent) * -lines.length) / 2; - } - const maxWidth = (obj.right - obj.left) + (obj.text_margin * 2); - for (let line of lines) - { - const lineMeasure = ctx.measureText(line); - if (lineMeasure.width <= maxWidth) - { - ctx.fillText(line, obj.left + offset_x, (canvas.height - (obj.top + (obj.bottom-obj.top)/2)) + ypos); - ypos += lineMeasure.fontBoundingBoxDescent + lineMeasure.fontBoundingBoxAscent; - } - else - { - let words = line.split(" "); - while (words.length > 0) - { - if (words.length == 1) - { - // we only have one word, either because that's just how many we had or because the final word is super long - const lastWord = words[0]; - const lastWordMeasure = ctx.measureText(lastWord); - ctx.fillText(lastWord, obj.left + offset_x, (canvas.height - (obj.top + (obj.bottom-obj.top)/2)) + ypos); - ypos += lastWordMeasure.fontBoundingBoxDescent + lineMeasure.fontBoundingBoxAscent; - words = []; - break; - } - for (let wc = words.length; wc > 0; wc--) - { - const partialLine = words.slice(0, wc - 1).join(" "); - const partialMeasure = ctx.measureText(partialLine); - if (partialMeasure.width <= maxWidth || wc == 1) - { - ctx.fillText(partialLine, obj.left + offset_x, (canvas.height - (obj.top + (obj.bottom-obj.top)/2)) + ypos); - ypos += partialMeasure.fontBoundingBoxDescent + lineMeasure.fontBoundingBoxAscent; - words = words.slice(wc - 1); - break; - } - } - } - } - } - } + // ctx.fillStyle = this.#altiumColourToHex(obj.text_colour); + // ctx.textAlign = ["center", "left", "right"][obj.alignment]; + // let offset_x = [(obj.right-obj.left)/2, obj.text_margin, (obj.right-obj.left) - obj.text_margin][obj.alignment]; + // if (!obj.word_wrap) + // { + // ctx.fillText(obj.text.replaceAll("~1", "\n"), obj.left + offset_x, canvas.height - (obj.top + (obj.bottom-obj.top)/2)); + // } + // else + // { + // // todo: refactor this so that an initial pass figures out all the line splits, then a second pass writes the text, so that vertical alignment can be supported. + // const text = obj.text.replaceAll("~1", "\n"); + // const lines = text.split("\n"); + // let ypos = 0; + // if (lines.length > 1) + // { + // // this is a total hack, but if there are multiple lines in the text then we can make a rough guess at how far up we need to shift the text to center it vertically + // // this doesn't correct for line wraps (see todo above for refactoring approach) but it's at least something I guess! + // const roughMeasure = ctx.measureText(text); + // ypos = ((roughMeasure.fontBoundingBoxDescent + roughMeasure.fontBoundingBoxAscent) * -lines.length) / 2; + // } + // const maxWidth = (obj.right - obj.left) + (obj.text_margin * 2); + // for (let line of lines) + // { + // const lineMeasure = ctx.measureText(line); + // if (lineMeasure.width <= maxWidth) + // { + // ctx.fillText(line, obj.left + offset_x, (canvas.height - (obj.top + (obj.bottom-obj.top)/2)) + ypos); + // ypos += lineMeasure.fontBoundingBoxDescent + lineMeasure.fontBoundingBoxAscent; + // } + // else + // { + // let words = line.split(" "); + // while (words.length > 0) + // { + // if (words.length == 1) + // { + // // we only have one word, either because that's just how many we had or because the final word is super long + // const lastWord = words[0]; + // const lastWordMeasure = ctx.measureText(lastWord); + // ctx.fillText(lastWord, obj.left + offset_x, (canvas.height - (obj.top + (obj.bottom-obj.top)/2)) + ypos); + // ypos += lastWordMeasure.fontBoundingBoxDescent + lineMeasure.fontBoundingBoxAscent; + // words = []; + // break; + // } + // for (let wc = words.length; wc > 0; wc--) + // { + // const partialLine = words.slice(0, wc - 1).join(" "); + // const partialMeasure = ctx.measureText(partialLine); + // if (partialMeasure.width <= maxWidth || wc == 1) + // { + // ctx.fillText(partialLine, obj.left + offset_x, (canvas.height - (obj.top + (obj.bottom-obj.top)/2)) + ypos); + // ypos += partialMeasure.fontBoundingBoxDescent + lineMeasure.fontBoundingBoxAscent; + // words = words.slice(wc - 1); + // break; + // } + // } + // } + // } + // } + // } } - ctx.font = savedFont; + // ctx.font = savedFont; - ctx.textAlign = "left"; - ctx.textBaseline = "bottom"; + // ctx.textAlign = "left"; + // ctx.textBaseline = "bottom"; - ctx.setTransform(savedTransform); + // ctx.setTransform(savedTransform); - savedFont = ctx.font; - ctx.textAlign = "left"; - ctx.font = "bold 33px sans-serif"; - ctx.fillStyle = "#000000"; - ctx.globalAlpha = 0.2; - ctx.save(); - ctx.scale(1,-1); - ctx.fillText("Preview generated by altium.js", 10, -(canvas.height - 50)); - ctx.font = "bold 15px sans-serif"; - ctx.fillText("for reference purposes only. schematic accuracy not guaranteed.", 12, -(canvas.height - 75)); - ctx.restore(); - ctx.globalAlpha = 1; - ctx.font = savedFont; + // savedFont = ctx.font; + // ctx.textAlign = "left"; + // ctx.font = "bold 33px sans-serif"; + // ctx.fillStyle = "#000000"; + // ctx.globalAlpha = 0.2; + // ctx.save(); + // ctx.scale(1,-1); + // ctx.fillText("Preview generated by altium.js", 10, -(canvas.height - 50)); + // ctx.font = "bold 15px sans-serif"; + // ctx.fillText("for reference purposes only. schematic accuracy not guaranteed.", 12, -(canvas.height - 75)); + // ctx.restore(); + // ctx.globalAlpha = 1; + // ctx.font = savedFont; } } \ No newline at end of file diff --git a/mxclient/css/common.css b/mxclient/css/common.css new file mode 100644 index 0000000..21f3892 --- /dev/null +++ b/mxclient/css/common.css @@ -0,0 +1,166 @@ +div.mxRubberband { + position: absolute; + overflow: hidden; + border-style: solid; + border-width: 1px; + border-color: #0000FF; + background: #0077FF; +} +.mxCellEditor { + background: url(data:image/gif;base64,R0lGODlhMAAwAIAAAP///wAAACH5BAEAAAAALAAAAAAwADAAAAIxhI+py+0Po5y02ouz3rz7D4biSJbmiabqyrbuC8fyTNf2jef6zvf+DwwKh8Si8egpAAA7); + _background: url('../images/transparent.gif'); + border-color: transparent; + border-style: solid; + display: inline-block; + position: absolute; + overflow: visible; + word-wrap: normal; + border-width: 0; + min-width: 1px; + resize: none; + padding: 0px; + margin: 0px; +} +.mxPlainTextEditor * { + padding: 0px; + margin: 0px; +} +div.mxWindow { + -webkit-box-shadow: 3px 3px 12px #C0C0C0; + -moz-box-shadow: 3px 3px 12px #C0C0C0; + box-shadow: 3px 3px 12px #C0C0C0; + background: url(data:image/gif;base64,R0lGODlhGgAUAIAAAOzs7PDw8CH5BAAAAAAALAAAAAAaABQAAAIijI+py70Ao5y02lud3lzhD4ZUR5aPiKajyZbqq7YyB9dhAQA7); + _background: url('../images/window.gif'); + border:1px solid #c3c3c3; + position: absolute; + overflow: hidden; + z-index: 1; +} +table.mxWindow { + border-collapse: collapse; + table-layout: fixed; + font-family: Arial; + font-size: 8pt; +} +td.mxWindowTitle { + background: url(data:image/gif;base64,R0lGODlhFwAXAMQAANfX18rKyuHh4c7OzsDAwMHBwc/Pz+Li4uTk5NHR0dvb2+jo6O/v79/f3/n5+dnZ2dbW1uPj44yMjNPT0+Dg4N3d3ebm5szMzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAAAAAAALAAAAAAXABcAAAWQICESxWiW5Ck6bOu+MMvMdG3f86LvfO/rlqBwSCwaj8ikUohoOp/QaDNCrVqvWKpgezhsv+AwmEIum89ocmPNbrvf64p8Tq/b5Yq8fs/v5x+AgYKDhIAAh4iJiouHEI6PkJGSjhOVlpeYmZUJnJ2en6CcBqMDpaanqKgXq6ytrq+rAbKztLW2shK5uru8vbkhADs=) repeat-x; + _background: url('../images/window-title.gif') repeat-x; + text-overflow: ellipsis; + white-space: nowrap; + text-align: center; + font-weight: bold; + overflow: hidden; + height: 13px; + padding: 2px; + padding-top: 4px; + padding-bottom: 6px; + color: black; +} +td.mxWindowPane { + vertical-align: top; + padding: 0px; +} +div.mxWindowPane { + overflow: hidden; + position: relative; +} +td.mxWindowPane td { + font-family: Arial; + font-size: 8pt; +} +td.mxWindowPane input, td.mxWindowPane select, td.mxWindowPane textarea, td.mxWindowPane radio { + border-color: #8C8C8C; + border-style: solid; + border-width: 1px; + font-family: Arial; + font-size: 8pt; + padding: 1px; +} +td.mxWindowPane button { + background: url(data:image/gif;base64,R0lGODlhCgATALMAAP7+/t7e3vj4+Ojo6OXl5e/v7/n5+fb29vPz8/39/e3t7fHx8e7u7v///wAAAAAAACH5BAAAAAAALAAAAAAKABMAAAQ2MMlJhb0Y6c2X/2AhjiRjnqiirizqMkEsz0Rt30Ou7y8K/ouDcEg0GI9IgHLJbDif0Kh06owAADs=) repeat-x; + _background: url('../images/button.gif') repeat-x; + font-family: Arial; + font-size: 8pt; + padding: 2px; + float: left; +} +img.mxToolbarItem { + margin-right: 6px; + margin-bottom: 6px; + border-width: 1px; +} +select.mxToolbarCombo { + vertical-align: top; + border-style: inset; + border-width: 2px; +} +div.mxToolbarComboContainer { + padding: 2px; +} +img.mxToolbarMode { + margin: 2px; + margin-right: 4px; + margin-bottom: 4px; + border-width: 0px; +} +img.mxToolbarModeSelected { + margin: 0px; + margin-right: 2px; + margin-bottom: 2px; + border-width: 2px; + border-style: inset; +} +div.mxTooltip { + -webkit-box-shadow: 3px 3px 12px #C0C0C0; + -moz-box-shadow: 3px 3px 12px #C0C0C0; + box-shadow: 3px 3px 12px #C0C0C0; + background: #FFFFCC; + border-style: solid; + border-width: 1px; + border-color: black; + font-family: Arial; + font-size: 8pt; + position: absolute; + cursor: default; + padding: 4px; + color: black; +} +div.mxPopupMenu { + -webkit-box-shadow: 3px 3px 12px #C0C0C0; + -moz-box-shadow: 3px 3px 12px #C0C0C0; + box-shadow: 3px 3px 12px #C0C0C0; + background: url(data:image/gif;base64,R0lGODlhGgAUAIAAAOzs7PDw8CH5BAAAAAAALAAAAAAaABQAAAIijI+py70Ao5y02lud3lzhD4ZUR5aPiKajyZbqq7YyB9dhAQA7); + _background: url('../images/window.gif'); + position: absolute; + border-style: solid; + border-width: 1px; + border-color: black; +} +table.mxPopupMenu { + border-collapse: collapse; + margin-top: 1px; + margin-bottom: 1px; +} +tr.mxPopupMenuItem { + color: black; + cursor: pointer; +} +tr.mxPopupMenuItemHover { + background-color: #000066; + color: #FFFFFF; + cursor: pointer; +} +td.mxPopupMenuItem { + padding: 2px 30px 2px 10px; + white-space: nowrap; + font-family: Arial; + font-size: 8pt; +} +td.mxPopupMenuIcon { + background-color: #D0D0D0; + padding: 2px 4px 2px 4px; +} +.mxDisabled { + opacity: 0.2 !important; + cursor:default !important; +} diff --git a/mxclient/css/explorer.css b/mxclient/css/explorer.css new file mode 100644 index 0000000..50e704f --- /dev/null +++ b/mxclient/css/explorer.css @@ -0,0 +1,18 @@ +div.mxTooltip { + filter:progid:DXImageTransform.Microsoft.DropShadow(OffX=4, OffY=4, + Color='#A2A2A2', Positive='true'); +} +div.mxPopupMenu { + filter:progid:DXImageTransform.Microsoft.DropShadow(OffX=4, OffY=4, + Color='#C0C0C0', Positive='true'); +} +div.mxWindow { + _filter:progid:DXImageTransform.Microsoft.DropShadow(OffX=4, OffY=4, + Color='#C0C0C0', Positive='true'); +} +td.mxWindowTitle { + _height: 23px; +} +.mxDisabled { + filter:alpha(opacity=20) !important; +} diff --git a/mxclient/images/button.gif b/mxclient/images/button.gif new file mode 100644 index 0000000..ad55cab Binary files /dev/null and b/mxclient/images/button.gif differ diff --git a/mxclient/images/close.gif b/mxclient/images/close.gif new file mode 100644 index 0000000..1069e94 Binary files /dev/null and b/mxclient/images/close.gif differ diff --git a/mxclient/images/collapsed.gif b/mxclient/images/collapsed.gif new file mode 100644 index 0000000..0276444 Binary files /dev/null and b/mxclient/images/collapsed.gif differ diff --git a/mxclient/images/error.gif b/mxclient/images/error.gif new file mode 100644 index 0000000..14e1aee Binary files /dev/null and b/mxclient/images/error.gif differ diff --git a/mxclient/images/expanded.gif b/mxclient/images/expanded.gif new file mode 100644 index 0000000..3767b0b Binary files /dev/null and b/mxclient/images/expanded.gif differ diff --git a/mxclient/images/maximize.gif b/mxclient/images/maximize.gif new file mode 100644 index 0000000..e27cf3e Binary files /dev/null and b/mxclient/images/maximize.gif differ diff --git a/mxclient/images/minimize.gif b/mxclient/images/minimize.gif new file mode 100644 index 0000000..1e95e7c Binary files /dev/null and b/mxclient/images/minimize.gif differ diff --git a/mxclient/images/normalize.gif b/mxclient/images/normalize.gif new file mode 100644 index 0000000..34a8d30 Binary files /dev/null and b/mxclient/images/normalize.gif differ diff --git a/mxclient/images/point.gif b/mxclient/images/point.gif new file mode 100644 index 0000000..9074c39 Binary files /dev/null and b/mxclient/images/point.gif differ diff --git a/mxclient/images/resize.gif b/mxclient/images/resize.gif new file mode 100644 index 0000000..ff558db Binary files /dev/null and b/mxclient/images/resize.gif differ diff --git a/mxclient/images/separator.gif b/mxclient/images/separator.gif new file mode 100644 index 0000000..5c1b895 Binary files /dev/null and b/mxclient/images/separator.gif differ diff --git a/mxclient/images/submenu.gif b/mxclient/images/submenu.gif new file mode 100644 index 0000000..ffe7617 Binary files /dev/null and b/mxclient/images/submenu.gif differ diff --git a/mxclient/images/transparent.gif b/mxclient/images/transparent.gif new file mode 100644 index 0000000..76040f2 Binary files /dev/null and b/mxclient/images/transparent.gif differ diff --git a/mxclient/images/warning.gif b/mxclient/images/warning.gif new file mode 100644 index 0000000..705235f Binary files /dev/null and b/mxclient/images/warning.gif differ diff --git a/mxclient/images/warning.png b/mxclient/images/warning.png new file mode 100644 index 0000000..2f78789 Binary files /dev/null and b/mxclient/images/warning.png differ diff --git a/mxclient/images/window-title.gif b/mxclient/images/window-title.gif new file mode 100644 index 0000000..231def8 Binary files /dev/null and b/mxclient/images/window-title.gif differ diff --git a/mxclient/images/window.gif b/mxclient/images/window.gif new file mode 100644 index 0000000..6631c4f Binary files /dev/null and b/mxclient/images/window.gif differ diff --git a/mxclient/js/editor/mxDefaultKeyHandler.js b/mxclient/js/editor/mxDefaultKeyHandler.js new file mode 100644 index 0000000..237dea4 --- /dev/null +++ b/mxclient/js/editor/mxDefaultKeyHandler.js @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxDefaultKeyHandler + * + * Binds keycodes to actionnames in an editor. This aggregates an internal + * and extends the implementation of to not + * only cancel the editing, but also hide the properties dialog and fire an + * event via . An instance of this class is created + * by and stored in . + * + * 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 to read configuration + * data into an existing instance. See for a + * description of the configuration format. + * + * Keycodes: + * + * See . + * + * An event is fired via the editor if the escape key is + * pressed. + * + * Constructor: mxDefaultKeyHandler + * + * Constructs a new default key handler for the in the + * given . (The editor may be null if a prototypical instance for + * a is created.) + * + * Parameters: + * + * editor - Reference to the enclosing . + */ +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 . + */ +mxDefaultKeyHandler.prototype.editor = null; + +/** + * Variable: handler + * + * Holds the for key event handling. + */ +mxDefaultKeyHandler.prototype.handler = null; + +/** + * Function: bindAction + * + * Binds the specified keycode to the given action in . 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 . + * 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 associated with this object. This does normally + * not need to be called, the is destroyed automatically when the + * window unloads (in IE) by . + */ +mxDefaultKeyHandler.prototype.destroy = function () +{ + this.handler.destroy(); + this.handler = null; +}; diff --git a/mxclient/js/editor/mxDefaultPopupMenu.js b/mxclient/js/editor/mxDefaultPopupMenu.js new file mode 100644 index 0000000..2f2e6e7 --- /dev/null +++ b/mxclient/js/editor/mxDefaultPopupMenu.js @@ -0,0 +1,306 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * 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 + * , the configuration is applied to the context and + * the resulting menu items are added to the menu dynamically. See + * 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 + * each time the menu is displayed. + * + * Codec: + * + * This class uses the 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 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 to add items to the + * given menu based on . 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). + * enabled-if - Optional name of condition that specifies if the menu item + * should be enabled. + * 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) + * + * + * + * (end) + * + * To add a new item for a custom function: + * + * (code) + * + * + * + * (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 + * instance, the instance under the mouse, and the + * native mouse event. + * + * Custom Conditions: + * + * To add a new condition for popupmenu items: + * + * (code) + * + * (end) + * + * The new condition can then be used in any item as follows: + * + * (code) + * + * (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 instance. + * menu - that is used for adding items and separators. + * cell - Optional 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 instance. + * menu - that is used for adding items and separators. + * cell - Optional 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'); + var enabledCond = item.getAttribute('enabled-if'); + var enabled = enabledCond == null || conditions[enabledCond]; + + 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, enabled); + 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 - that is used for adding items and separators. + * editor - Enclosing 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 , the under the mouse and the + * mouse event that triggered the call. + * cell - Optional 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. + * enabled - Optional boolean that specifies if the menu item is enabled. + * Default is true. + */ +mxDefaultPopupMenu.prototype.addAction = function(menu, editor, lab, icon, funct, action, cell, parent, iconCls, enabled) +{ + 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, enabled); +}; + +/** + * 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 to read configuration + * data into an existing instance. See 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 + * is created. + * + * Parameters: + * + * container - DOM node that contains the toolbar. + * editor - Reference to the enclosing . + */ +function mxDefaultToolbar(container, editor) +{ + this.editor = editor; + + if (container != null && editor != null) + { + this.init(container); + } +}; + +/** + * Variable: editor + * + * Reference to the enclosing . + */ +mxDefaultToolbar.prototype.editor = null; + +/** + * Variable: toolbar + * + * Holds the internal . + */ +mxDefaultToolbar.prototype.toolbar = null; + +/** + * Variable: resetHandler + * + * Reference to the function used to reset the . + */ +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; + +/** + * Function: init + * + * Constructs the for the given container and installs a listener + * that updates the on if an item is + * selected in the toolbar. This assumes that 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 . 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 + '/separator.gif'. + */ +mxDefaultToolbar.prototype.addSeparator = function(icon) +{ + icon = icon || mxClient.imageBasePath + '/separator.gif'; + this.toolbar.addSeparator(icon); +}; + +/** + * Function: addCombo + * + * Helper method to invoke on and return the + * resulting DOM node. + */ +mxDefaultToolbar.prototype.addCombo = function() +{ + return this.toolbar.addCombo(); +}; + +/** + * Function: addActionCombo + * + * Helper method to invoke on 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 + * or . + * + * 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 . + */ +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 on 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 '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 + * . + * pressed - Optional URL of the icon that represents the pressed state. + * funct - Optional JavaScript function that takes the 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 '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 , new cell to be inserted, mouse + * event and optional 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 = mxUtils.bind(this, function() + { + if (typeof(ptype) == 'function') + { + return ptype(); + } + else if (ptype != null) + { + return this.editor.graph.cloneCell(ptype); + } + + 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 or + * depending on the given target cell. + * + * Parameters: + * + * vertex - to be inserted. + * evt - Mouse event that represents the drop. + * target - Optional 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 - to be inserted. + * evt - Mouse event that represents the drop. + * parent - Optional 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 - to be inserted. + * evt - Mouse event that represents the drop. + * source - Optional 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 associated with this object and removes all + * installed listeners. This does normally not need to be called, the + * is destroyed automatically when the window unloads (in IE) by + * . + */ +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/mxclient/js/editor/mxEditor.js b/mxclient/js/editor/mxEditor.js new file mode 100644 index 0000000..828ba14 --- /dev/null +++ b/mxclient/js/editor/mxEditor.js @@ -0,0 +1,3118 @@ +/** + * Copyright (c) 2006-2019, JGraph Ltd + * Copyright (c) 2006-2019, draw.io AG + */ +/** + * Class: mxEditor + * + * Extends to implement an application wrapper for a graph that + * adds , I/O using , auto-layout using , + * command history using , and standard dialogs and widgets, eg. + * properties, help, outline, toolbar, and popupmenu. It also adds + * to be used as cells in toolbars, auto-validation using the + * flag, attribute cycling using , higher-level events + * such as , and backend integration using and . + * + * Actions: + * + * Actions are functions stored in the array under their names. The + * functions take the as the first, and an optional as the + * second argument and are invoked using . Any additional arguments + * passed to execute are passed on to the action as-is. + * + * A list of built-in actions is available in the 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 method. + * + * To save diagrams in XML on a server, you can set the variable. + * This variable will be used in to construct a URL for the post + * request that is issued in the method. The post request contains the + * XML representation of the diagram as returned by 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 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 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) + * + * + * + * + * + * (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) + * + * (end) + * + * The Task node can have any tag name, attributes and child nodes. The + * 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 , the user object's + * attributes are put into a form for editing. Attributes are changed using + * the action in the model. The dialog can be replaced + * by overriding the hook or by replacing the showProperties + * action in . 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 + * 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) + * + * + * + * ... + * (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 , the format of the + * configuration is explained in . + * + * The toolbar is defined in the mxDefaultToolbar section. Items can be added + * and removed in this section. + * + * (code) + * + * + * + * + * ... + * (end) + * + * The format of the configuration is described in + * . + * + * Ids: + * + * For the IDs, there is an implicit behaviour in : 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 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 function. If there is a collision, a new + * Id will be created for the cell using . At encoding + * time, this new Id will replace the value previously stored under the id + * attribute in the Task node. + * + * See , and + * 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 + * to reset this cookie. This cookie + * only exists if is implemented. + * + * Event: mxEvent.OPEN + * + * Fires after a file was opened in . The filename property + * contains the filename that was used. The same value is also available in + * . + * + * Event: mxEvent.SAVE + * + * Fires after the current file was saved in . The url + * property contains the URL that was used for saving. + * + * Event: mxEvent.POST + * + * Fires if a successful response was received in . The + * request property contains the , the + * url and data 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.BEFORE_ADD_VERTEX + * + * Fires before a vertex is added in . The vertex + * property contains the new vertex and the parent property + * contains its parent. + * + * Event: mxEvent.ADD_VERTEX + * + * Fires between begin- and endUpdate in . The vertex + * property contains the vertex that is being inserted. + * + * Event: mxEvent.AFTER_ADD_VERTEX + * + * Fires after a vertex was inserted and selected in . The + * vertex 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 event property + * contains the key event. + * + * Constructor: mxEditor + * + * Constructs a new editor. This function invokes the 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; + + // Checks if the hook has been set + if (this.onInit != null) + { + // Invokes the 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'); +} +else +{ + mxClient.defaultBundles.push(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 'currentFile'. + */ +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 that contains the outline. The + * is stored in outline.outline. + */ +mxEditor.prototype.outline = null; + +/** + * Variable: graph + * + * Holds a for displaying the diagram. The graph + * is created in . + */ +mxEditor.prototype.graph = null; + +/** + * Variable: graphRenderHint + * + * Holds the render hint used for creating the + * graph in . See . + * Default is null. + */ +mxEditor.prototype.graphRenderHint = null; + +/** + * Variable: toolbar + * + * Holds a for displaying the toolbar. The + * toolbar is created in . + */ +mxEditor.prototype.toolbar = null; + +/** + * Variable: status + * + * DOM container that holds the statusbar. Default is null. + * Use to set this value. + */ +mxEditor.prototype.status = null; + +/** + * Variable: popupHandler + * + * Holds a for displaying + * popupmenus. + */ +mxEditor.prototype.popupHandler = null; + +/** + * Variable: undoManager + * + * Holds an for the command history. + */ +mxEditor.prototype.undoManager = null; + +/** + * Variable: keyHandler + * + * Holds a for handling keyboard events. + * The handler is created in . + */ +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 + * to add or replace an action and 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 + * 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 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 . + * Default is null. + */ +mxEditor.prototype.defaultEdgeStyle = null; + +/** + * Variable: defaultGroup + * + * Prototype group cell that is used for creating + * new groups. + */ +mxEditor.prototype.defaultGroup = null; + +/** + * Variable: groupBorderSize + * + * Default size for the border of new groups. If null, + * then then 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 . 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 . + */ +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; + +/** + * 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 . The spacing between the + * swimlanes is specified by . + * Default is false. + * + * If the top-level elements are swimlanes, then + * the intra-swimlane layout is activated by + * the switch. + */ +mxEditor.prototype.layoutDiagram = false; + +/** + * Variable: swimlaneSpacing + * + * Specifies the spacing between swimlanes if + * automatic layout is turned on in + * . 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 + * . 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 is true. + */ +mxEditor.prototype.maintainSwimlanes = false; + +/** + * Variable: layoutSwimlanes + * + * Specifies if the children of swimlanes should + * be layed out, either vertically or horizontally + * depending on . + * 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 + * at this index will be used as the value for + * . Default is 0. + */ +mxEditor.prototype.cycleAttributeIndex = 0; + +/** + * Variable: cycleAttributeName + * + * Name of the attribute to be assigned a + * when inserting new swimlanes. Default is 'fillColor'. + */ +mxEditor.prototype.cycleAttributeName = 'fillColor'; + +/** + * Group: Windows + */ + +/** + * Variable: tasks + * + * Holds the created in . + */ +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 created in . + */ +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: helpHeight + * + * Specifies the height 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 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 . + */ +mxEditor.prototype.isModified = function () +{ + return this.modified; +}; + +/** + * Function: setModified + * + * Sets 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 . + * 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 . + * 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. + * 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('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: 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 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 . + */ +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 for the editor. The graph is created with no + * container and is initialized from . + */ +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.popupMenuHandler.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 . + */ +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 . + */ +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 to invoke + * 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 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 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 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 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 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 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 . + * + * 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 - 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 + * . + */ +mxEditor.prototype.getRootTitle = function () +{ + var root = this.graph.getModel().getRoot(); + return this.graph.convertValueToString(root); +}; + +/** + * Function: undo + * + * Undo the last change in . + */ +mxEditor.prototype.undo = function () +{ + this.undoManager.undo(); +}; + +/** + * Function: redo + * + * Redo the last change in . + */ +mxEditor.prototype.redo = function () +{ + this.undoManager.redo(); +}; + +/** + * Function: groupCells + * + * Invokes to create a new group cell and the invokes + * , 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 to be used + * as a new group cell in . + */ +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 + * . It updates and fires an -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 to the given URL or the + * URL returned by . The actual posting is carried out by + * . If the URL is null then the resulting XML will be + * displayed using . 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 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 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 + * . + */ +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 . The default implementation returns , + * adding ?draft=true. + */ +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 . + */ +mxEditor.prototype.getUrlImage = function () +{ + return this.urlImage; +}; + +/** + * 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 + * . + */ +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].value; + 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. The + * default width of the window is 200 pixels, the y-coordinate of the location + * can be specifies in 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 . + */ +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 urlHelp key or 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 . + */ +mxEditor.prototype.showOutline = function () +{ + var create = this.outline == null; + + if (create) + { + var div = document.createElement('div'); + + div.style.overflow = 'hidden'; + div.style.position = 'relative'; + div.style.width = '100%'; + div.style.height = '100%'; + div.style.background = 'white'; + div.style.cursor = 'move'; + + if (document.documentMode == 8) + { + div.style.filter = 'progid:DXImageTransform.Microsoft.alpha(opacity=100)'; + } + + 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 . + * 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 to create the menu in the graph's + * panning handler. The redirection is setup in + * . + */ +mxEditor.prototype.createPopupMenu = function (menu, cell, evt) +{ + this.popupHandler.createMenu(this, menu, cell, evt); +}; + +/** + * Function: createEdge + * + * Uses 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 + * . + */ +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 when new edges + * are created in the graph. + */ +mxEditor.prototype.getEdgeStyle = function () +{ + return this.defaultEdgeStyle; +}; + +/** + * Function: consumeCycleAttribute + * + * Returns the next attribute in + * 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 + * as the value for the 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 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/mxclient/js/handler/mxCellHighlight.js b/mxclient/js/handler/mxCellHighlight.js new file mode 100644 index 0000000..262e2c0 --- /dev/null +++ b/mxclient/js/handler/mxCellHighlight.js @@ -0,0 +1,314 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * 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, dashed) +{ + if (graph != null) + { + this.graph = graph; + this.highlightColor = (highlightColor != null) ? highlightColor : mxConstants.DEFAULT_VALID_COLOR; + this.strokeWidth = (strokeWidth != null) ? strokeWidth : mxConstants.HIGHLIGHT_STROKEWIDTH; + this.dashed = (dashed != null) ? dashed : false; + this.opacity = mxConstants.HIGHLIGHT_OPACITY; + + // Updates the marker if the graph changes + this.repaintHandler = mxUtils.bind(this, function() + { + // Updates reference to state + if (this.state != null) + { + var tmp = this.graph.view.getState(this.state.cell); + + if (tmp == null) + { + this.hide(); + } + else + { + this.state = tmp; + 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 . + */ +mxCellHighlight.prototype.graph = null; + +/** + * Variable: state + * + * Reference to the . + */ +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) + { + this.shape.stroke = 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); + } +}; + +/** + * Function: createShape + * + * Creates and returns the highlight shape for the given state. + */ +mxCellHighlight.prototype.createShape = function() +{ + var shape = this.graph.cellRenderer.createShape(this.state); + + shape.svgStrokeTolerance = this.graph.tolerance; + shape.points = this.state.absolutePoints; + shape.apply(this.state); + shape.stroke = this.highlightColor; + shape.opacity = this.opacity; + shape.isDashed = this.dashed; + shape.isShadow = false; + + 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); + + if (this.graph.dialect != mxConstants.DIALECT_SVG) + { + shape.pointerEvents = false; + } + else + { + shape.svgPointerEvents = 'stroke'; + } + + return shape; +}; + +/** + * Function: getStrokeWidth + * + * Returns the stroke width. + */ +mxCellHighlight.prototype.getStrokeWidth = function(state) +{ + return this.strokeWidth; +}; + +/** + * Function: repaint + * + * Updates the highlight after a change of the model or view. + */ +mxCellHighlight.prototype.repaint = function() +{ + if (this.state != null && this.shape != null) + { + this.shape.scale = this.state.view.scale; + + if (this.graph.model.isEdge(this.state.cell)) + { + this.shape.strokewidth = this.getStrokeWidth(); + this.shape.points = this.state.absolutePoints; + this.shape.outline = false; + } + 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); + this.shape.rotation = Number(this.state.style[mxConstants.STYLE_ROTATION] || '0'); + this.shape.strokewidth = this.getStrokeWidth() / this.state.view.scale; + this.shape.outline = true; + } + + // Uses cursor from shape in highlight + if (this.state.shape != null) + { + this.shape.setCursor(this.state.shape.getCursor()); + } + + // Workaround for event transparency in VML with transparent color + // is to use a non-transparent color with near zero opacity + if (mxClient.IS_QUIRKS || document.documentMode == 8) + { + if (this.shape.stroke == 'transparent') + { + // KNOWN: Quirks mode does not seem to catch events if + // we do not force an update of the DOM via a change such + // as mxLog.debug. Since IE6 is EOL we do not add a fix. + this.shape.stroke = 'white'; + this.shape.opacity = 1; + } + else + { + this.shape.opacity = this.opacity; + } + } + + this.shape.redraw(); + } +}; + +/** + * Function: hide + * + * Resets the state of the cell marker. + */ +mxCellHighlight.prototype.hide = function() +{ + this.highlight(null); +}; + +/** + * Function: mark + * + * Marks the and fires a 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: isHighlightAt + * + * Returns true if this highlight is at the given position. + */ +mxCellHighlight.prototype.isHighlightAt = function(x, y) +{ + var hit = false; + + // Quirks mode is currently not supported as it used a different coordinate system + if (this.shape != null && document.elementFromPoint != null && !mxClient.IS_QUIRKS) + { + var elt = document.elementFromPoint(x, y); + + while (elt != null) + { + if (elt == this.shape.node) + { + hit = true; + break; + } + + elt = elt.parentNode; + } + } + + return hit; +}; + +/** + * Function: destroy + * + * Destroys the handler and all its resources and DOM nodes. + */ +mxCellHighlight.prototype.destroy = function() +{ + this.graph.getView().removeListener(this.resetHandler); + this.graph.getView().removeListener(this.repaintHandler); + this.graph.getModel().removeListener(this.repaintHandler); + + if (this.shape != null) + { + this.shape.destroy(); + this.shape = null; + } +}; diff --git a/mxclient/js/handler/mxCellMarker.js b/mxclient/js/handler/mxCellMarker.js new file mode 100644 index 0000000..569620f --- /dev/null +++ b/mxclient/js/handler/mxCellMarker.js @@ -0,0 +1,430 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * 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 state + * property contains the marked or null if no state is marked. + * + * Constructor: mxCellMarker + * + * Constructs a new cell marker. + * + * Parameters: + * + * graph - Reference to the enclosing . + * validColor - Optional marker color for valid states. Default is + * . + * invalidColor - Optional marker color for invalid states. Default is + * . + * hotspot - Portion of the width and hight where a state intersects a + * given coordinate pair. A value of 0 means always highlight. Default is + * . + */ +function mxCellMarker(graph, validColor, invalidColor, hotspot) +{ + mxEventSource.call(this); + + if (graph != null) + { + this.graph = graph; + this.validColor = (validColor != null) ? validColor : mxConstants.DEFAULT_VALID_COLOR; + this.invalidColor = (invalidColor != null) ? invalidColor : mxConstants.DEFAULT_INVALID_COLOR; + this.hotspot = (hotspot != null) ? hotspot : mxConstants.DEFAULT_HOTSPOT; + + this.highlight = new mxCellHighlight(graph); + } +}; + +/** + * Extends mxEventSource. + */ +mxUtils.extend(mxCellMarker, mxEventSource); + +/** + * Variable: graph + * + * Reference to the enclosing . + */ +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 if it is valid. + */ +mxCellMarker.prototype.validState = null; + +/** + * Variable: markedState + * + * Holds the marked . + */ +mxCellMarker.prototype.markedState = null; + +/** + * Function: setEnabled + * + * Enables or disables event handling. This implementation + * updates . + * + * 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 . + */ +mxCellMarker.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setHotspot + * + * Sets the . + */ +mxCellMarker.prototype.setHotspot = function(hotspot) +{ + this.hotspot = hotspot; +}; + +/** + * Function: getHotspot + * + * Returns the . + */ +mxCellMarker.prototype.getHotspot = function() +{ + return this.hotspot; +}; + +/** + * Function: setHotspotEnabled + * + * Specifies whether the hotspot should be used in . + */ +mxCellMarker.prototype.setHotspotEnabled = function(enabled) +{ + this.hotspotEnabled = enabled; +}; + +/** + * Function: isHotspotEnabled + * + * Returns true if hotspot is used in . + */ +mxCellMarker.prototype.isHotspotEnabled = function() +{ + return this.hotspotEnabled; +}; + +/** + * Function: hasValidState + * + * Returns true if is not null. + */ +mxCellMarker.prototype.hasValidState = function() +{ + return this.validState != null; +}; + +/** + * Function: getValidState + * + * Returns the . + */ +mxCellMarker.prototype.getValidState = function() +{ + return this.validState; +}; + +/** + * Function: getMarkedState + * + * Returns the . + */ +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 + * with the color returned by . If the + * markerColor is not null, then the state is stored in . If + * returns true, then the state is stored in + * 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); + this.setCurrentState(state, me); + } + + return state; +}; + +/** + * Function: setCurrentState + * + * Sets and marks the current valid state. + */ +mxCellMarker.prototype.setCurrentState = function(state, me, color) +{ + var isValid = (state != null) ? this.isValidState(state) : false; + color = (color != null) ? 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(); + } + } +}; + +/** + * Function: markCell + * + * Marks the given cell using the given color, or 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 and fires a 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 event. + */ +mxCellMarker.prototype.unmark = function() +{ + this.mark(); +}; + +/** + * Function: isValidState + * + * Returns true if the given is a valid state. If this + * returns true, then the state is stored in . The return value + * of this method is used as the argument for . + */ +mxCellMarker.prototype.isValidState = function(state) +{ + return true; +}; + +/** + * Function: getMarkerColor + * + * Returns the valid- or invalidColor depending on the value of isValid. + * The given is ignored by this implementation. + */ +mxCellMarker.prototype.getMarkerColor = function(evt, state, isValid) +{ + return (isValid) ? this.validColor : this.invalidColor; +}; + +/** + * Function: getState + * + * Uses , and to return the + * for the given . + */ +mxCellMarker.prototype.getState = function(me) +{ + var view = this.graph.getView(); + var cell = this.getCell(me); + var state = this.getStateToMark(view.getState(cell)); + + return (state != null && this.intersects(state, me)) ? state : null; +}; + +/** + * Function: getCell + * + * Returns the for the given event and cell. This returns the + * given cell. + */ +mxCellMarker.prototype.getCell = function(me) +{ + return me.getCell(); +}; + +/** + * Function: getStateToMark + * + * Returns the to be marked for the given 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 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/mxclient/js/handler/mxCellTracker.js b/mxclient/js/handler/mxCellTracker.js new file mode 100644 index 0000000..9f0c8bb --- /dev/null +++ b/mxclient/js/handler/mxCellTracker.js @@ -0,0 +1,145 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxCellTracker + * + * Event handler that highlights cells. Inherits from . + * + * 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 . + * color - Color of the highlight. Default is blue. + * funct - Optional JavaScript function that is used to override + * . + */ +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. + */ +mxUtils.extend(mxCellTracker, mxCellMarker); + +/** + * 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) { }; + +/** + * 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/mxclient/js/handler/mxConnectionHandler.js b/mxclient/js/handler/mxConnectionHandler.js new file mode 100644 index 0000000..32348c4 --- /dev/null +++ b/mxclient/js/handler/mxConnectionHandler.js @@ -0,0 +1,2249 @@ +/** + * Copyright (c) 2006-2016, JGraph Ltd + * Copyright (c) 2006-2016, Gaudenz Alder + */ +/** + * Class: mxConnectionHandler + * + * Graph event handler that creates new connections. Uses + * for finding and highlighting the source and target vertices and + * to create the edge instance. This handler is built-into + * and enabled using . + * + * 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 . + * + * (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 . The new edge is + * created using which in turn uses 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 other diagramming products. + * In order to allow both, moving and connecting cells at the same time, + * 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, + * 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 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 hook may be overridden to create more than one + * 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 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 state + * property contains the state of the source cell. + * + * Event: mxEvent.CONNECT + * + * Fires between begin- and endUpdate in . The cell + * property contains the inserted edge, the event and target + * properties contain the respective arguments that were passed to (where + * target corresponds to the dropTarget argument). Finally, the terminal + * property corresponds to the target argument in or the clone of the source + * terminal if is enabled. + * + * 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, can be used. To resolve + * the port IDs, use . + * + * (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 method is invoked. + * + * Constructor: mxConnectionHandler + * + * Constructs an event handler that connects vertices using the specified + * factory method to create the new edges. Modify + * 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 . + * factoryMethod - Optional function to create the edge. The function takes + * the source and target as the first and second argument and an + * optional cell style from the preview as the third argument. It returns + * the that represents the new edge. + */ +function mxConnectionHandler(graph, factoryMethod) +{ + mxEventSource.call(this); + + if (graph != null) + { + this.graph = graph; + this.factoryMethod = factoryMethod; + this.init(); + + // Handles escape keystrokes + this.escapeHandler = mxUtils.bind(this, function(sender, evt) + { + this.reset(); + }); + + this.graph.addListener(mxEvent.ESCAPE, this.escapeHandler); + } +}; + +/** + * Extends mxEventSource. + */ +mxUtils.extend(mxConnectionHandler, mxEventSource); + +/** + * Variable: graph + * + * Reference to the enclosing . + */ +mxConnectionHandler.prototype.graph = null; + +/** + * Variable: factoryMethod + * + * Function that is used for creating new edges. The function takes the + * source and target as the first and second argument and returns + * a new that represents the edge. This is used in . + */ +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 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 + * + * that is used to trigger the creation of a new connection. This + * is used in . 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 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 + * will be called before the connection is created between + * the source cell and the newly created vertex in , which + * can be overridden to create a new target. Default is false. + */ +mxConnectionHandler.prototype.createTarget = false; + +/** + * Variable: marker + * + * Holds the used for finding source and target cells. + */ +mxConnectionHandler.prototype.marker = null; + +/** + * Variable: constraintHandler + * + * Holds the 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: 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 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, ). + * 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 that represents the preview edge while the + * handler is active. This is created in . + */ +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 . + */ +mxConnectionHandler.prototype.movePreviewAway = mxClient.IS_VML; + +/** + * Variable: outlineConnect + * + * Specifies if connections to the outline of a highlighted target should be + * enabled. This will allow to place the connection point along the outline of + * the highlighted target. Default is false. + */ +mxConnectionHandler.prototype.outlineConnect = false; + +/** + * Variable: livePreview + * + * Specifies if the actual shape of the edge state should be used for the preview. + * Default is false. (Ignored if no edge state is created in .) + */ +mxConnectionHandler.prototype.livePreview = false; + +/** + * Variable: cursor + * + * Specifies the cursor to be used while the handler is active. Default is null. + */ +mxConnectionHandler.prototype.cursor = null; + +/** + * Variable: insertBeforeSource + * + * Specifies if new edges should be inserted before the source vertex in the + * cell hierarchy. Default is false for backwards compatibility. + */ +mxConnectionHandler.prototype.insertBeforeSource = false; + +/** + * Function: isEnabled + * + * Returns true if events are handled. This implementation + * returns . + */ +mxConnectionHandler.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Enables or disables event handling. This implementation + * updates . + * + * Parameters: + * + * enabled - Boolean that specifies the new enabled state. + */ +mxConnectionHandler.prototype.setEnabled = function(enabled) +{ + this.enabled = enabled; +}; + +/** + * Function: isInsertBefore + * + * Returns for non-loops and false for loops. + * + * Parameters: + * + * edge - that represents the edge to be inserted. + * source - that represents the source terminal. + * target - that represents the target terminal. + * evt - Mousedown event of the connect gesture. + * dropTarget - that represents the cell under the mouse when it was + * released. + */ +mxConnectionHandler.prototype.isInsertBefore = function(edge, source, target, evt, dropTarget) +{ + return this.insertBeforeSource && source != target; +}; + +/** + * Function: isCreateTarget + * + * Returns . + * + * Parameters: + * + * evt - Current active native pointer event. + */ +mxConnectionHandler.prototype.isCreateTarget = function(evt) +{ + return this.createTarget; +}; + +/** + * Function: setCreateTarget + * + * Sets . + */ +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 = (this.livePreview && this.edgeState != null) ? + this.graph.cellRenderer.createShape(this.edgeState) : + new mxPolyline([], mxConstants.INVALID_COLOR); + shape.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ? + mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG; + shape.scale = this.graph.view.scale; + shape.pointerEvents = false; + shape.isDashed = true; + shape.init(this.graph.getView().getOverlayPane()); + mxEvent.redirectMouseEvents(shape.node, this.graph, null); + + return shape; +}; + +/** + * Function: init + * + * Initializes the shapes required for this connection handler. This should + * be invoked if 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); + this.constraintHandler.reset(); + } + else if (this.previous != null && this.graph.view.getState(this.previous.cell) == null) + { + this.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.reset(); + }); + + 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 used in . + */ +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(me) + { + var cell = mxCellMarker.prototype.getCell.apply(marker, arguments); + this.error = null; + + // Checks for cell at preview point (with grid) + if (cell == null && this.currentPoint != null) + { + cell = this.graph.getCellAt(this.currentPoint.x, this.currentPoint.y); + } + + // Uses connectable parent vertex if one exists + if (cell != null && !this.graph.isCellConnectable(cell)) + { + var parent = this.graph.getModel().getParent(cell); + + if (this.graph.getModel().isVertex(parent) && this.graph.isCellConnectable(parent)) + { + cell = parent; + } + } + + if ((this.graph.isSwimlane(cell) && this.currentPoint != null && + this.graph.hitsSwimlaneContent(cell, this.currentPoint.x, this.currentPoint.y)) || + !this.isConnectableCell(cell)) + { + cell = 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(me.getEvent())) + { + this.error = null; + } + } + } + } + else if (!this.isValidSource(cell, me)) + { + cell = null; + } + } + else if (this.isConnecting() && !this.isCreateTarget(me.getEvent()) && + !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 for the given source terminal. + * + * Parameters: + * + * cell - that represents the source terminal. + * me - that is associated with this call. + */ +mxConnectionHandler.prototype.isValidSource = function(cell, me) +{ + return this.graph.isValidSource(cell); +}; + +/** + * Function: isValidTarget + * + * Returns true. The call to is implicit by calling + * in . This is an + * additional hook for disabling certain targets in this specific handler. + * + * Parameters: + * + * cell - 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 . + * + * Parameters: + * + * source - that represents the source terminal. + * target - 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 used for the connection icon of the given + * . This implementation returns . + * + * Parameters: + * + * state - 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 . + * + * Parameters: + * + * state - 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 that represent the connect icons for + * the given . + * + * Parameters: + * + * state - 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 . + * + * Parameters: + * + * icons - Optional array of 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: getIconPosition + * + * Returns the center position of the given icon. + * + * Parameters: + * + * icon - The connect icon of with the mouse. + * state - under the mouse. + */ +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; + + var alpha = mxUtils.toRadians(mxUtils.getValue(state.style, mxConstants.STYLE_ROTATION) || 0); + + if (alpha != 0) + { + var cos = Math.cos(alpha); + var sin = Math.sin(alpha); + var ct = new mxPoint(state.getCenterX(), state.getCenterY()); + var pt = mxUtils.getRotatedPoint(new mxPoint(cx, cy), cos, sin, ct); + cx = pt.x; + cy = pt.y; + } + } + + return new mxPoint(cx - icon.bounds.width / 2, + cy - icon.bounds.height / 2); +}; + +/** + * Function: destroyIcons + * + * Destroys the connect icons and resets the respective state. + */ +mxConnectionHandler.prototype.destroyIcons = function() +{ + if (this.icons != null) + { + for (var i = 0; i < this.icons.length; i++) + { + this.icons[i].destroy(); + } + + this.icons = null; + this.icon = null; + this.selectedIcon = null; + this.iconState = null; + } +}; + +/** + * 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 + * are not null, or and are not null and + * is null or and are not null. + */ +mxConnectionHandler.prototype.isStartEvent = function(me) +{ + return ((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(); + + if (this.edgeState != null) + { + this.shape.apply(this.edgeState); + } + } + + // 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(); + } + + this.selectedIcon = this.icon; + this.icon = null; +}; + +/** + * 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 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)); + * }; + * (end) + */ +mxConnectionHandler.prototype.createEdgeState = function(me) +{ + return null; +}; + +/** + * Function: isOutlineConnectEvent + * + * Returns true if is true and the source of the event is the outline shape + * or shift is pressed. + */ +mxConnectionHandler.prototype.isOutlineConnectEvent = function(me) +{ + var offset = mxUtils.getOffset(this.graph.container); + var evt = me.getEvent(); + + var clientX = mxEvent.getClientX(evt); + var clientY = mxEvent.getClientY(evt); + + var doc = document.documentElement; + var left = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0); + var top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); + + var gridX = this.currentPoint.x - this.graph.container.scrollLeft + offset.x - left; + var gridY = this.currentPoint.y - this.graph.container.scrollTop + offset.y - top; + + return this.outlineConnect && !mxEvent.isShiftDown(me.getEvent()) && + (me.isSource(this.marker.highlight.shape) || + (mxEvent.isAltDown(me.getEvent()) && me.getState() != null) || + this.marker.highlight.isHighlightAt(clientX, clientY) || + ((gridX != clientX || gridY != clientY) && me.getState() == null && + this.marker.highlight.isHighlightAt(gridX, gridY))); +}; + +/** + * Function: updateCurrentState + * + * Updates the current state for a given mouse move event by using + * the . + */ +mxConnectionHandler.prototype.updateCurrentState = function(me, point) +{ + this.constraintHandler.update(me, this.first == null, false, (this.first == null || + me.isSource(this.marker.highlight.shape)) ? null : point); + + if (this.constraintHandler.currentFocus != null && this.constraintHandler.currentConstraint != null) + { + // Handles special case where grid is large and connection point is at actual point in which + // case the outline is not followed as long as we're < gridSize / 2 away from that point + if (this.marker.highlight != null && this.marker.highlight.state != null && + this.marker.highlight.state.cell == this.constraintHandler.currentFocus.cell) + { + // Direct repaint needed if cell already highlighted + if (this.marker.highlight.shape.stroke != 'transparent') + { + this.marker.highlight.shape.stroke = 'transparent'; + this.marker.highlight.repaint(); + } + } + else + { + this.marker.markCell(this.constraintHandler.currentFocus.cell, 'transparent'); + } + + // Updates validation state + if (this.previous != null) + { + this.error = this.validateConnection(this.previous.cell, this.constraintHandler.currentFocus.cell); + + if (this.error == null) + { + this.currentState = this.constraintHandler.currentFocus; + } + + if (this.error != null || (this.currentState != null && + !this.isCellEnabled(this.currentState.cell))) + { + this.constraintHandler.reset(); + } + } + } + else + { + if (this.graph.isIgnoreTerminalEvent(me.getEvent())) + { + this.marker.reset(); + this.currentState = null; + } + else + { + this.marker.process(me); + this.currentState = this.marker.getValidState(); + } + + if (this.currentState != null && !this.isCellEnabled(this.currentState.cell)) + { + this.constraintHandler.reset(); + this.marker.reset(); + this.currentState = null; + } + + var outline = this.isOutlineConnectEvent(me); + + if (this.currentState != null && outline) + { + // Handles special case where mouse is on outline away from actual end point + // in which case the grid is ignored and mouse point is used instead + if (me.isSource(this.marker.highlight.shape)) + { + point = new mxPoint(me.getGraphX(), me.getGraphY()); + } + + var constraint = this.graph.getOutlineConstraint(point, this.currentState, me); + this.constraintHandler.setFocus(me, this.currentState, false); + this.constraintHandler.currentConstraint = constraint; + this.constraintHandler.currentPoint = point; + } + + if (this.outlineConnect) + { + if (this.marker.highlight != null && this.marker.highlight.shape != null) + { + var s = this.graph.view.scale; + + if (this.constraintHandler.currentConstraint != null && + this.constraintHandler.currentFocus != null) + { + this.marker.highlight.shape.stroke = mxConstants.OUTLINE_HIGHLIGHT_COLOR; + this.marker.highlight.shape.strokewidth = mxConstants.OUTLINE_HIGHLIGHT_STROKEWIDTH / s / s; + this.marker.highlight.repaint(); + } + else if (this.marker.hasValidState()) + { + // Handles special case where actual end point of edge and current mouse point + // are not equal (due to grid snapping) and there is no hit on shape or highlight + // but ignores cases where parent is used for non-connectable child cells + if (this.graph.isCellConnectable(me.getCell()) && + this.marker.getValidState() != me.getState()) + { + this.marker.highlight.shape.stroke = 'transparent'; + this.currentState = null; + } + else + { + this.marker.highlight.shape.stroke = mxConstants.DEFAULT_VALID_COLOR; + } + + this.marker.highlight.shape.strokewidth = mxConstants.HIGHLIGHT_STROKEWIDTH / s / s; + this.marker.highlight.repaint(); + } + } + } + } +}; + +/** + * Function: isCellEnabled + * + * Returns true if the given cell allows new connections to be created. This implementation + * always returns true. + */ +mxConnectionHandler.prototype.isCellEnabled = function(cell) +{ + return true; +}; + +/** + * 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: snapToPreview + * + * Called to snap the given point to the current preview. This snaps to the + * first point of the preview if alt is not pressed. + */ +mxConnectionHandler.prototype.snapToPreview = function(me, point) +{ + if (!mxEvent.isAltDown(me.getEvent()) && this.previous != null) + { + var tol = this.graph.gridSize * this.graph.view.scale / 2; + var tmp = (this.sourceConstraint != null) ? this.first : + new mxPoint(this.previous.getCenterX(), this.previous.getCenterY()); + + if (Math.abs(tmp.x - me.getGraphX()) < tol) + { + point.x = tmp.x; + } + + if (Math.abs(tmp.y - me.getGraphY()) < tol) + { + point.y = tmp.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 (!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.currentState = null; + } + + var view = this.graph.getView(); + var scale = view.scale; + var tr = view.translate; + var point = new mxPoint(me.getGraphX(), me.getGraphY()); + this.error = null; + + if (this.graph.isGridEnabledEvent(me.getEvent())) + { + point = new mxPoint((this.graph.snap(point.x / scale - tr.x) + tr.x) * scale, + (this.graph.snap(point.y / scale - tr.y) + tr.y) * scale); + } + + this.snapToPreview(me, point); + this.currentPoint = point; + + if ((this.first != null || (this.isEnabled() && this.graph.isEnabled())) && + (this.shape != null || this.first == null || + Math.abs(me.getGraphX() - this.first.x) > this.graph.tolerance || + Math.abs(me.getGraphY() - this.first.y) > this.graph.tolerance)) + { + this.updateCurrentState(me, point); + } + + if (this.first != null) + { + 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(); + } + else if (this.previous != null && !this.graph.isIgnoreTerminalEvent(me.getEvent()) && + mxEvent.isShiftDown(me.getEvent())) + { + if (Math.abs(this.previous.getCenterX() - point.x) < + Math.abs(this.previous.getCenterY() - point.y)) + { + point.x = this.previous.getCenterX(); + } + else + { + point.y = this.previous.getCenterY(); + } + } + + 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.updateEdgeState(current, constraint); + 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; + } + + // Stores old point to reuse when creating edge + this.originalPoint = current.clone(); + current.x -= dx * 4 / len; + current.y -= dy * 4 / len; + } + else + { + this.originalPoint = null; + } + + // Creates the preview shape (lazy) + if (this.shape == null) + { + var dx = Math.abs(me.getGraphX() - this.first.x); + var dy = Math.abs(me.getGraphY() - this.first.y); + + if (dx > this.graph.tolerance || dy > this.graph.tolerance) + { + this.shape = this.createShape(); + + if (this.edgeState != null) + { + this.shape.apply(this.edgeState); + } + + // Revalidates current connection + this.updateCurrentState(me, point); + } + } + + // 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(); + } + + // Makes sure endpoint of edge is visible during connect + if (this.cursor != null) + { + this.graph.container.style.cursor = this.cursor; + } + + 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(); + + // Sets the cursor on the current shape + if (this.currentState != null && this.error == null && this.constraintHandler.currentConstraint == 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.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: updateEdgeState + * + * Updates . + */ +mxConnectionHandler.prototype.updateEdgeState = function(current, constraint) +{ + // TODO: Use generic method for writing constraint to style + if (this.sourceConstraint != null && this.sourceConstraint.point != null) + { + this.edgeState.style[mxConstants.STYLE_EXIT_X] = this.sourceConstraint.point.x; + this.edgeState.style[mxConstants.STYLE_EXIT_Y] = this.sourceConstraint.point.y; + } + + if (constraint != null && constraint.point != null) + { + this.edgeState.style[mxConstants.STYLE_ENTRY_X] = constraint.point.x; + this.edgeState.style[mxConstants.STYLE_ENTRY_Y] = constraint.point.y; + } + else + { + delete this.edgeState.style[mxConstants.STYLE_ENTRY_X]; + delete this.edgeState.style[mxConstants.STYLE_ENTRY_Y]; + } + + 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); +}; + +/** + * Function: getTargetPerimeterPoint + * + * Returns the perimeter point for the given target state. + * + * Parameters: + * + * state - that represents the target cell state. + * me - 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 - that represents the target cell state. + * next - that represents the next point along the previewed edge. + * me - that represents the mouse move. + */ +mxConnectionHandler.prototype.getSourcePerimeterPoint = function(state, next, me) +{ + var result = null; + var view = state.view; + var sourcePerimeter = view.getPerimeterFunction(state); + var c = new mxPoint(state.getCenterX(), state.getCenterY()); + + if (sourcePerimeter != null) + { + var theta = mxUtils.getValue(state.style, mxConstants.STYLE_ROTATION, 0); + var rad = -theta * (Math.PI / 180); + + if (theta != 0) + { + next = mxUtils.getRotatedPoint(new mxPoint(next.x, next.y), Math.cos(rad), Math.sin(rad), c); + } + + var tmp = sourcePerimeter(view.getPerimeterBounds(state), state, next, false); + + if (tmp != null) + { + if (theta != 0) + { + tmp = mxUtils.getRotatedPoint(new mxPoint(tmp.x, tmp.y), Math.cos(-rad), Math.sin(-rad), c); + } + + result = tmp; + } + } + else + { + result = c; + } + + return result; +}; + + +/** + * Function: updateIcons + * + * Hook to update the icon position(s) based on a mouseOver event. This is + * an empty implementation. + * + * Parameters: + * + * state - under the mouse. + * icons - Array of currently displayed icons. + * me - 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 is null. Note that this is only + * called if 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 . + */ +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: checkConstraints + * + * Returns true if the connection for the given constraints is valid. This + * implementation returns true if the constraints are not pointing to the + * same fixed connection point. + */ +mxConnectionHandler.prototype.checkConstraints = function(c1, c2) +{ + return (c1 == null || c2 == null || c1.point == null || c2.point == null || + !c1.point.equals(c2.point) || c1.dx != c2.dx || c1.dy != c2.dy || + c1.perimeter != c2.perimeter); +}; + +/** + * 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; + } + + var c1 = this.sourceConstraint; + var c2 = this.constraintHandler.currentConstraint; + + 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.currentState != null) + { + target = this.currentState.cell; + } + + // Inserts the edge if no validation error exists and if constraints differ + if (this.error == null && (source == null || target == null || + source != target || this.checkConstraints(c1, c2))) + { + 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, me.getEvent()); + } + + // Displays the error message if it is not an empty string, + // for empty error messages, the event is silently dropped + if (this.error != null && this.error.length > 0) + { + this.graph.validationAlert(this.error); + } + } + + // Redraws the connect icons and resets the handler state + this.destroyIcons(); + me.consume(); + } + + if (this.first != null) + { + this.reset(); + } +}; + +/** + * Function: reset + * + * Resets the state of this handler. + */ +mxConnectionHandler.prototype.reset = function() +{ + if (this.shape != null) + { + this.shape.destroy(); + this.shape = null; + } + + // Resets the cursor on the container + if (this.cursor != null && this.graph.container != null) + { + this.graph.container.style.cursor = ''; + } + + this.destroyIcons(); + this.marker.reset(); + this.constraintHandler.reset(); + this.originalPoint = null; + this.currentPoint = null; + this.edgeState = null; + this.previous = null; + this.error = null; + this.sourceConstraint = null; + this.mouseDownCounter = 0; + this.first = null; + + this.fireEvent(new mxEventObject(mxEvent.RESET)); +}; + +/** + * Function: drawPreview + * + * Redraws the preview edge using the color and width returned by + * and . + */ +mxConnectionHandler.prototype.drawPreview = function() +{ + this.updatePreview(this.error == null); + this.shape.redraw(); +}; + +/** + * 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.updatePreview = function(valid) +{ + this.shape.strokewidth = this.getEdgeWidth(valid); + this.shape.stroke = this.getEdgeColor(valid); +}; + +/** + * 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 to create the edge. + * + * Parameters: + * + * source - that represents the source terminal. + * target - that represents the target terminal. + * evt - Mousedown event of the connect gesture. + * dropTarget - that represents the cell under the mouse when it was + * released. + */ +mxConnectionHandler.prototype.connect = function(source, target, evt, dropTarget) +{ + if (target != null || this.isCreateTarget(evt) || 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 terminalInserted = false; + var edge = null; + + model.beginUpdate(); + try + { + if (source != null && target == null && !this.graph.isIgnoreTerminalEvent(evt) && this.isCreateTarget(evt)) + { + target = this.createTargetVertex(evt, source); + + if (target != null) + { + dropTarget = this.graph.getDropTarget([target], evt, dropTarget); + terminalInserted = true; + + // 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); + } + + var parent = model.getParent(source); + + // Inserts edge before source + if (this.isInsertBefore(edge, source, target, evt, dropTarget)) + { + var index = null; + var tmp = source; + + while (tmp.parent != null && tmp.geometry != null && + tmp.geometry.relative && tmp.parent != edge.parent) + { + tmp = this.graph.model.getParent(tmp); + } + + if (tmp != null && tmp.parent != null && tmp.parent == edge.parent) + { + model.add(parent, edge, tmp.parent.getIndex(tmp)); + } + } + + // 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 t = this.graph.view.translate; + var s = this.graph.view.scale; + var pt = (this.originalPoint != null) ? + new mxPoint(this.originalPoint.x / s - t.x, this.originalPoint.y / s - t.y) : + new mxPoint(this.currentPoint.x / s - t.x, this.currentPoint.y / s - t.y); + 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, 'terminal', target, + 'event', evt, 'target', dropTarget, 'terminalInserted', terminalInserted)); + } + } + catch (e) + { + mxLog.show(); + mxLog.debug(e.message); + } + finally + { + model.endUpdate(); + } + + if (this.select) + { + this.selectCells(edge, (terminalInserted) ? target : null); + } + } +}; + +/** + * 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 if is defined, + * otherwise 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 is true and + * returns null. + * + * Parameters: + * + * evt - Mousedown event of the connect gesture. + * source - 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.cloneCell(source); + var geo = this.graph.getModel().getGeometry(clone); + + if (geo != null) + { + var t = this.graph.view.translate; + var s = this.graph.view.scale; + var point = new mxPoint(this.currentPoint.x / s - t.x, this.currentPoint.y / s - t.y); + geo.x = Math.round(point.x - geo.width / 2 - this.graph.panDx / s); + geo.y = Math.round(point.y - geo.height / 2 - this.graph.panDy / s); + + // Aligns with source if within certain tolerance + var tol = this.getAlignmentTolerance(); + + if (tol > 0) + { + var sourceState = this.graph.view.getState(source); + + if (sourceState != null) + { + var x = sourceState.x / s - t.x; + var y = sourceState.y / s - t.y; + + if (Math.abs(x - geo.x) <= tol) + { + geo.x = Math.round(x); + } + + if (Math.abs(y - geo.y) <= tol) + { + geo.y = Math.round(y); + } + } + } + } + + return clone; +}; + +/** + * Function: getAlignmentTolerance + * + * Returns the tolerance for aligning new targets to sources. This returns the grid size / 2. + */ +mxConnectionHandler.prototype.getAlignmentTolerance = function(evt) +{ + return (this.graph.isGridEnabled()) ? this.graph.gridSize / 2 : this.graph.tolerance; +}; + +/** + * Function: createEdge + * + * Creates and returns a new edge using 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 - that represents the source terminal. + * target - 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 . + */ +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; + } + + if (this.escapeHandler != null) + { + this.graph.removeListener(this.escapeHandler); + this.escapeHandler = null; + } +}; diff --git a/mxclient/js/handler/mxConstraintHandler.js b/mxclient/js/handler/mxConstraintHandler.js new file mode 100644 index 0000000..a4d2cb2 --- /dev/null +++ b/mxclient/js/handler/mxConstraintHandler.js @@ -0,0 +1,517 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * 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 . + * factoryMethod - Optional function to create the edge. The function takes + * the source and target as the first and second argument and + * returns the that represents the new edge. + */ +function mxConstraintHandler(graph) +{ + this.graph = graph; + + // Adds a graph model listener to update the current focus on changes + this.resetHandler = mxUtils.bind(this, function(sender, evt) + { + if (this.currentFocus != null && this.graph.view.getState(this.currentFocus.cell) == null) + { + this.reset(); + } + else + { + this.redraw(); + } + }); + + this.graph.model.addListener(mxEvent.CHANGE, this.resetHandler); + this.graph.view.addListener(mxEvent.SCALE_AND_TRANSLATE, this.resetHandler); + this.graph.view.addListener(mxEvent.TRANSLATE, this.resetHandler); + this.graph.view.addListener(mxEvent.SCALE, this.resetHandler); + this.graph.addListener(mxEvent.ROOT, this.resetHandler); +}; + +/** + * Variable: pointImage + * + * 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 . + */ +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 . + */ +mxConstraintHandler.prototype.highlightColor = mxConstants.DEFAULT_VALID_COLOR; + +/** + * Function: isEnabled + * + * Returns true if events are handled. This implementation + * returns . + */ +mxConstraintHandler.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Enables or disables event handling. This implementation + * updates . + * + * 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. This + * implementation returns . + * + * Parameters: + * + * me - whose tolerance should be returned. + */ +mxConstraintHandler.prototype.getTolerance = function(me) +{ + 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 should be ignored in . This + * implementation always returns false. + */ +mxConstraintHandler.prototype.isEventIgnored = function(me, source) +{ + return false; +}; + +/** + * Function: isStateIgnored + * + * Returns true if the given state should be ignored. This always returns false. + */ +mxConstraintHandler.prototype.isStateIgnored = function(state, source) +{ + return false; +}; + +/** + * Function: destroyIcons + * + * Destroys the if they exist. + */ +mxConstraintHandler.prototype.destroyIcons = function() +{ + if (this.focusIcons != null) + { + for (var i = 0; i < this.focusIcons.length; i++) + { + this.focusIcons[i].destroy(); + } + + this.focusIcons = null; + this.focusPoints = null; + } +}; + +/** + * Function: destroyFocusHighlight + * + * Destroys the if one exists. + */ +mxConstraintHandler.prototype.destroyFocusHighlight = function() +{ + if (this.focusHighlight != null) + { + this.focusHighlight.destroy(); + this.focusHighlight = null; + } +}; + +/** + * Function: isKeepFocusEvent + * + * Returns true if the current focused state should not be changed for the given event. + * This returns true if shift and alt are pressed. + */ +mxConstraintHandler.prototype.isKeepFocusEvent = function(me) +{ + return mxEvent.isShiftDown(me.getEvent()); +}; + +/** + * Function: getCellForEvent + * + * Returns the cell for the given event. + */ +mxConstraintHandler.prototype.getCellForEvent = function(me, point) +{ + var cell = me.getCell(); + + // Gets cell under actual point if different from event location + if (cell == null && point != null && (me.getGraphX() != point.x || me.getGraphY() != point.y)) + { + cell = this.graph.getCellAt(point.x, point.y); + } + + // Uses connectable parent vertex if one exists + if (cell != null && !this.graph.isCellConnectable(cell)) + { + var parent = this.graph.getModel().getParent(cell); + + if (this.graph.getModel().isVertex(parent) && this.graph.isCellConnectable(parent)) + { + cell = parent; + } + } + + return (this.graph.isCellLocked(cell)) ? null : cell; +}; + +/** + * Function: update + * + * Updates the state of this handler based on the given . + * Source is a boolean indicating if the cell is a source or target. + */ +mxConstraintHandler.prototype.update = function(me, source, existingEdge, point) +{ + if (this.isEnabled() && !this.isEventIgnored(me)) + { + // Lazy installation of mouseleave handler + if (this.mouseleaveHandler == null && this.graph.container != null) + { + this.mouseleaveHandler = mxUtils.bind(this, function() + { + this.reset(); + }); + + mxEvent.addListener(this.graph.container, 'mouseleave', this.resetHandler); + } + + var tol = this.getTolerance(me); + var x = (point != null) ? point.x : me.getGraphX(); + var y = (point != null) ? point.y : me.getGraphY(); + var grid = new mxRectangle(x - tol, y - tol, 2 * tol, 2 * tol); + var mouse = new mxRectangle(me.getGraphX() - tol, me.getGraphY() - tol, 2 * tol, 2 * tol); + var state = this.graph.view.getState(this.getCellForEvent(me, point)); + + // Keeps focus icons visible while over vertex bounds and no other cell under mouse or shift is pressed + if (!this.isKeepFocusEvent(me) && (this.currentFocusArea == null || this.currentFocus == null || + (state != null) || !this.graph.getModel().isVertex(this.currentFocus.cell) || + !mxUtils.intersects(this.currentFocusArea, mouse)) && (state != this.currentFocus)) + { + this.currentFocusArea = null; + this.currentFocus = null; + this.setFocus(me, state, source); + } + + this.currentConstraint = null; + this.currentPoint = null; + var minDistSq = null; + + if (this.focusIcons != null && this.constraints != null && + (state == null || this.currentFocus == state)) + { + var cx = mouse.getCenterX(); + var cy = mouse.getCenterY(); + + for (var i = 0; i < this.focusIcons.length; i++) + { + var dx = cx - this.focusIcons[i].bounds.getCenterX(); + var dy = cy - this.focusIcons[i].bounds.getCenterY(); + var tmp = dx * dx + dy * dy; + + if ((this.intersects(this.focusIcons[i], mouse, source, existingEdge) || (point != null && + this.intersects(this.focusIcons[i], grid, source, existingEdge))) && + (minDistSq == null || tmp < minDistSq)) + { + this.currentConstraint = this.constraints[i]; + this.currentPoint = this.focusPoints[i]; + minDistSq = tmp; + + var tmp = this.focusIcons[i].bounds.clone(); + tmp.grow(mxConstants.HIGHLIGHT_SIZE + 1); + tmp.width -= 1; + tmp.height -= 1; + + if (this.focusHighlight == null) + { + var hl = this.createHighlightShape(); + hl.dialect = (this.graph.dialect == mxConstants.DIALECT_SVG) ? + mxConstants.DIALECT_SVG : mxConstants.DIALECT_VML; + hl.pointerEvents = false; + + hl.init(this.graph.getView().getOverlayPane()); + this.focusHighlight = hl; + + var getState = mxUtils.bind(this, function() + { + return (this.currentFocus != null) ? this.currentFocus : state; + }); + + mxEvent.redirectMouseEvents(hl.node, this.graph, getState); + } + + this.focusHighlight.bounds = tmp; + this.focusHighlight.redraw(); + } + } + } + + if (this.currentConstraint == null) + { + this.destroyFocusHighlight(); + } + } + else + { + this.currentConstraint = null; + this.currentFocus = null; + this.currentPoint = null; + } +}; + +/** + * Function: redraw + * + * Transfers the focus to the given state as a source or target terminal. If + * the handler is not enabled then the outline is painted, but the constraints + * are ignored. + */ +mxConstraintHandler.prototype.redraw = function() +{ + if (this.currentFocus != null && this.constraints != null && this.focusIcons != null) + { + var state = this.graph.view.getState(this.currentFocus.cell); + this.currentFocus = state; + this.currentFocusArea = new mxRectangle(state.x, state.y, state.width, state.height); + + for (var i = 0; i < this.constraints.length; i++) + { + var cp = this.graph.getConnectionPoint(state, this.constraints[i]); + var img = this.getImageForConstraint(state, this.constraints[i], cp); + + var bounds = new mxRectangle(Math.round(cp.x - img.width / 2), + Math.round(cp.y - img.height / 2), img.width, img.height); + this.focusIcons[i].bounds = bounds; + this.focusIcons[i].redraw(); + this.currentFocusArea.add(this.focusIcons[i].bounds); + this.focusPoints[i] = cp; + } + } +}; + +/** + * Function: setFocus + * + * Transfers the focus to the given state as a source or target terminal. If + * the handler is not enabled then the outline is painted, but the constraints + * are ignored. + */ +mxConstraintHandler.prototype.setFocus = function(me, state, source) +{ + this.constraints = (state != null && !this.isStateIgnored(state, source) && + this.graph.isCellConnectable(state.cell)) ? ((this.isEnabled()) ? + (this.graph.getAllConnectionConstraints(state, source) || []) : []) : null; + + // Only uses cells which have constraints + if (this.constraints != null) + { + this.currentFocus = state; + this.currentFocusArea = new mxRectangle(state.x, state.y, state.width, state.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.focusPoints = []; + this.focusIcons = []; + + for (var i = 0; i < this.constraints.length; i++) + { + var cp = this.graph.getConnectionPoint(state, this.constraints[i]); + var img = this.getImageForConstraint(state, this.constraints[i], cp); + + var src = img.src; + var bounds = new mxRectangle(Math.round(cp.x - img.width / 2), + Math.round(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_MIXEDHTML : mxConstants.DIALECT_SVG; + icon.preserveImageAspect = false; + icon.init(this.graph.getView().getDecoratorPane()); + + // Fixes lost event tracking for images in quirks / IE8 standards + if (mxClient.IS_QUIRKS || document.documentMode == 8) + { + mxEvent.addListener(icon.node, 'dragstart', function(evt) + { + mxEvent.consume(evt); + + return false; + }); + } + + // 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 : state; + }); + + 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(this.getTolerance(me)); + } + else + { + this.destroyIcons(); + this.destroyFocusHighlight(); + } +}; + +/** + * Function: createHighlightShape + * + * Create the shape used to paint the highlight. + * + * Returns true if the given icon intersects the given point. + */ +mxConstraintHandler.prototype.createHighlightShape = function() +{ + var hl = new mxRectangleShape(null, this.highlightColor, this.highlightColor, mxConstants.HIGHLIGHT_STROKEWIDTH); + hl.opacity = mxConstants.HIGHLIGHT_OPACITY; + + return hl; +}; + +/** + * Function: intersects + * + * Returns true if the given icon intersects the given rectangle. + */ +mxConstraintHandler.prototype.intersects = function(icon, mouse, source, existingEdge) +{ + return mxUtils.intersects(icon.bounds, mouse); +}; + +/** + * Function: destroy + * + * Destroy this handler. + */ +mxConstraintHandler.prototype.destroy = function() +{ + this.reset(); + + if (this.resetHandler != null) + { + this.graph.model.removeListener(this.resetHandler); + this.graph.view.removeListener(this.resetHandler); + this.graph.removeListener(this.resetHandler); + this.resetHandler = null; + } + + if (this.mouseleaveHandler != null && this.graph.container != null) + { + mxEvent.removeListener(this.graph.container, 'mouseleave', this.mouseleaveHandler); + this.mouseleaveHandler = null; + } +}; diff --git a/mxclient/js/handler/mxEdgeHandler.js b/mxclient/js/handler/mxEdgeHandler.js new file mode 100644 index 0000000..d5827c8 --- /dev/null +++ b/mxclient/js/handler/mxEdgeHandler.js @@ -0,0 +1,2544 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxEdgeHandler + * + * Graph event handler that reconnects edges and modifies control points and + * the edge label location. Uses for finding and + * highlighting new source and target vertices. This handler is automatically + * created in 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 . + * + * Parameters: + * + * state - of the cell to be handled. + */ +function mxEdgeHandler(state) +{ + if (state != null && state.shape != null) + { + this.state = state; + this.init(); + + // Handles escape keystrokes + this.escapeHandler = mxUtils.bind(this, function(sender, evt) + { + var dirty = this.index != null; + this.reset(); + + if (dirty) + { + this.graph.cellRenderer.redraw(this.state, false, state.view.isRendering()); + } + }); + + this.state.view.graph.addListener(mxEvent.ESCAPE, this.escapeHandler); + } +}; + +/** + * Variable: graph + * + * Reference to the enclosing . + */ +mxEdgeHandler.prototype.graph = null; + +/** + * Variable: state + * + * Reference to the being modified. + */ +mxEdgeHandler.prototype.state = null; + +/** + * Variable: marker + * + * Holds the which is used for highlighting terminals. + */ +mxEdgeHandler.prototype.marker = null; + +/** + * Variable: constraintHandler + * + * Holds the 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 that represents the preview edge. + */ +mxEdgeHandler.prototype.shape = null; + +/** + * Variable: bends + * + * Holds the that represent the points. + */ +mxEdgeHandler.prototype.bends = null; + +/** + * Variable: labelShape + * + * Holds the 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: dblClickRemoveEnabled + * + * Specifies if removing bends by double click is enabled. Default is false. + */ +mxEdgeHandler.prototype.dblClickRemoveEnabled = false; + +/** + * Variable: mergeRemoveEnabled + * + * Specifies if removing bends by dropping them on other bends is enabled. + * Default is false. + */ +mxEdgeHandler.prototype.mergeRemoveEnabled = false; + +/** + * Variable: straightRemoveEnabled + * + * Specifies if removing bends by creating straight segments should be enabled. + * If enabled, this can be overridden by holding down the alt key while moving. + * Default is false. + */ +mxEdgeHandler.prototype.straightRemoveEnabled = false; + +/** + * Variable: virtualBendsEnabled + * + * Specifies if virtual bends should be added in the center of each + * segments. These bends can then be used to add new waypoints. + * Default is false. + */ +mxEdgeHandler.prototype.virtualBendsEnabled = false; + +/** + * Variable: virtualBendOpacity + * + * Opacity to be used for virtual bends (see ). + * Default is 20. + */ +mxEdgeHandler.prototype.virtualBendOpacity = 20; + +/** + * Variable: parentHighlightEnabled + * + * Specifies if the parent should be highlighted if a child cell is selected. + * Default is false. + */ +mxEdgeHandler.prototype.parentHighlightEnabled = false; + +/** + * Variable: preferHtml + * + * Specifies if bends should be added to the graph container. This is updated + * in 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: handleImage + * + * Optional to be used as handles. Default is null. + */ +mxEdgeHandler.prototype.handleImage = null; + +/** + * Variable: tolerance + * + * Optional tolerance for hit-detection in . Default is 0. + */ +mxEdgeHandler.prototype.tolerance = 0; + +/** + * Variable: outlineConnect + * + * Specifies if connections to the outline of a highlighted target should be + * enabled. This will allow to place the connection point along the outline of + * the highlighted target. Default is false. + */ +mxEdgeHandler.prototype.outlineConnect = false; + +/** + * Variable: manageLabelHandle + * + * Specifies if the label handle should be moved if it intersects with another + * handle. Uses for checking and moving. Default is false. + */ +mxEdgeHandler.prototype.manageLabelHandle = false; + +/** + * 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_MIXEDHTML : mxConstants.DIALECT_SVG; + this.shape.init(this.graph.getView().getOverlayPane()); + this.shape.pointerEvents = false; + this.shape.setCursor(mxConstants.CURSOR_MOVABLE_EDGE); + mxEvent.redirectMouseEvents(this.shape.node, this.graph, 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(); + + if (this.isVirtualBendsEnabled()) + { + this.virtualBends = this.createVirtualBends(); + } + } + + // Adds a rectangular handle for the label position + this.label = new mxPoint(this.state.absoluteOffset.x, this.state.absoluteOffset.y); + this.labelShape = this.createLabelHandleShape(); + this.initBend(this.labelShape); + this.labelShape.setCursor(mxConstants.CURSOR_LABEL_HANDLE); + + this.customHandles = this.createCustomHandles(); + + this.updateParentHighlight(); + this.redraw(); +}; + + +/** + * Function: isParentHighlightVisible + * + * Returns true if the parent highlight should be visible. This implementation + * always returns true. + */ +mxEdgeHandler.prototype.isParentHighlightVisible = mxVertexHandler.prototype.isParentHighlightVisible; + +/** + * Function: updateParentHighlight + * + * Updates the highlight of the parent if is true. + */ +mxEdgeHandler.prototype.updateParentHighlight = mxVertexHandler.prototype.updateParentHighlight; + +/** + * Function: createCustomHandles + * + * Returns an array of custom handles. This implementation returns null. + */ +mxEdgeHandler.prototype.createCustomHandles = function() +{ + return null; +}; + +/** + * Function: isVirtualBendsEnabled + * + * Returns true if virtual bends should be added. This returns true if + * is true and the current style allows and + * renders custom waypoints. + */ +mxEdgeHandler.prototype.isVirtualBendsEnabled = function(evt) +{ + return this.virtualBendsEnabled && (this.state.style[mxConstants.STYLE_EDGE] == null || + this.state.style[mxConstants.STYLE_EDGE] == mxConstants.NONE || + this.state.style[mxConstants.STYLE_NOEDGESTYLE] == 1) && + mxUtils.getValue(this.state.style, mxConstants.STYLE_SHAPE, null) != 'arrow'; +}; + +/** + * Function: isCellEnabled + * + * Returns true if the given cell allows new connections to be created. This implementation + * always returns true. + */ +mxEdgeHandler.prototype.isCellEnabled = function(cell) +{ + return true; +}; + +/** + * 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: createParentHighlightShape + * + * Creates the shape used to draw the selection border. + */ +mxEdgeHandler.prototype.createParentHighlightShape = function(bounds) +{ + var shape = new mxRectangleShape(mxRectangle.fromRectangle(bounds), + null, this.getSelectionColor()); + shape.strokewidth = this.getSelectionStrokeWidth(); + shape.isDashed = this.isSelectionDashed(); + + return shape; +}; + +/** + * Function: createSelectionShape + * + * Creates the shape used to draw the selection border. + */ +mxEdgeHandler.prototype.createSelectionShape = function(points) +{ + var shape = new this.state.shape.constructor(); + shape.outline = true; + shape.apply(this.state); + + shape.isDashed = this.isSelectionDashed(); + shape.stroke = this.getSelectionColor(); + shape.isShadow = false; + + return shape; +}; + +/** + * Function: getSelectionColor + * + * Returns . + */ +mxEdgeHandler.prototype.getSelectionColor = function() +{ + return mxConstants.EDGE_SELECTION_COLOR; +}; + +/** + * Function: getSelectionStrokeWidth + * + * Returns . + */ +mxEdgeHandler.prototype.getSelectionStrokeWidth = function() +{ + return mxConstants.EDGE_SELECTION_STROKEWIDTH; +}; + +/** + * Function: isSelectionDashed + * + * Returns . + */ +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: getCellAt + * + * Creates and returns the used in . + */ +mxEdgeHandler.prototype.getCellAt = function(x, y) +{ + return (!this.outlineConnect) ? this.graph.getCellAt(x, y) : null; +}; + +/** + * Function: createMarker + * + * Creates and returns the used in . + */ +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); + + // Checks for cell at preview point (with grid) + if ((cell == self.state.cell || cell == null) && self.currentPoint != null) + { + cell = self.graph.getCellAt(self.currentPoint.x, self.currentPoint.y); + } + + // Uses connectable parent vertex if one exists + if (cell != null && !this.graph.isCellConnectable(cell)) + { + var parent = this.graph.getModel().getParent(cell); + + if (this.graph.getModel().isVertex(parent) && this.graph.isCellConnectable(parent)) + { + cell = parent; + } + } + + var model = self.graph.getModel(); + + if ((this.graph.isSwimlane(cell) && self.currentPoint != null && + this.graph.hitsSwimlaneContent(cell, self.currentPoint.x, self.currentPoint.y)) || + (!self.isConnectableCell(cell)) || (cell == self.state.cell || + (cell != null && !self.graph.connectableEdges && model.isEdge(cell))) || + model.isAncestor(self.state.cell, cell)) + { + cell = null; + } + + if (!this.graph.isCellConnectable(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 . + * + * Parameters: + * + * source - that represents the source terminal. + * target - 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 . + */ + 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)) + { + (mxUtils.bind(this, function(index) + { + var bend = this.createHandleShape(index); + this.initBend(bend, mxUtils.bind(this, mxUtils.bind(this, function() + { + if (this.dblClickRemoveEnabled) + { + this.removePoint(this.state, index); + } + }))); + + if (this.isHandleEnabled(i)) + { + bend.setCursor((terminal) ? mxConstants.CURSOR_TERMINAL_HANDLE : mxConstants.CURSOR_BEND_HANDLE); + } + + bends.push(bend); + + if (!terminal) + { + this.points.push(new mxPoint(0,0)); + bend.node.style.visibility = 'hidden'; + } + }))(i); + } + } + } + + return bends; +}; + +/** + * Function: createVirtualBends + * + * Creates and returns the bends used for modifying the edge. This is + * typically an array of . + */ + mxEdgeHandler.prototype.createVirtualBends = function() + { + var cell = this.state.cell; + var last = this.abspoints[0]; + var bends = []; + + if (this.graph.isCellBendable(cell)) + { + for (var i = 1; i < this.abspoints.length; i++) + { + (mxUtils.bind(this, function(bend) + { + this.initBend(bend); + bend.setCursor(mxConstants.CURSOR_VIRTUAL_BEND_HANDLE); + bends.push(bend); + }))(this.createHandleShape()); + } + } + + 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) +{ + var source = this.state.getVisibleTerminalState(true); + var target = this.state.getVisibleTerminalState(false); + var geo = this.graph.getCellGeometry(this.state.cell); + var edgeStyle = (geo != null) ? this.graph.view.getEdgeStyle(this.state, geo.points, source, target) : null; + + return edgeStyle != mxEdgeStyle.EntityRelation || index == 0 || index == this.abspoints.length - 1; +}; + +/** + * 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 + * . Only images and rectangles should be + * returned if support for HTML labels with not foreign objects is required. + * Index if null for virtual handles. + */ +mxEdgeHandler.prototype.createHandleShape = function(index) +{ + if (this.handleImage != null) + { + var shape = new mxImageShape(new mxRectangle(0, 0, this.handleImage.width, this.handleImage.height), this.handleImage.src); + + // Allows HTML rendering of the images + shape.preserveImageAspect = false; + + return shape; + } + 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: createLabelHandleShape + * + * Creates the shape used to display the the label handle. + */ +mxEdgeHandler.prototype.createLabelHandleShape = function() +{ + if (this.labelHandleImage != null) + { + var shape = new mxImageShape(new mxRectangle(0, 0, this.labelHandleImage.width, this.labelHandleImage.height), this.labelHandleImage.src); + + // Allows HTML rendering of the images + shape.preserveImageAspect = false; + + return shape; + } + else + { + var s = mxConstants.LABEL_HANDLE_SIZE; + return new mxRectangleShape(new mxRectangle(0, 0, s, s), mxConstants.LABEL_HANDLE_FILLCOLOR, mxConstants.HANDLE_STROKECOLOR); + } +}; + +/** + * Function: initBend + * + * Helper method to initialize the given bend. + * + * Parameters: + * + * bend - that represents the bend to be initialized. + */ +mxEdgeHandler.prototype.initBend = function(bend, dblClick) +{ + if (this.preferHtml) + { + bend.dialect = mxConstants.DIALECT_STRICTHTML; + bend.init(this.graph.container); + } + else + { + bend.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ? + mxConstants.DIALECT_MIXEDHTML : mxConstants.DIALECT_SVG; + bend.init(this.graph.getView().getOverlayPane()); + } + + mxEvent.redirectMouseEvents(bend.node, this.graph, this.state, + null, null, null, dblClick); + + // Fixes lost event tracking for images in quirks / IE8 standards + if (mxClient.IS_QUIRKS || document.documentMode == 8) + { + mxEvent.addListener(bend.node, 'dragstart', function(evt) + { + mxEvent.consume(evt); + + return false; + }); + } + + if (mxClient.IS_TOUCH) + { + bend.node.setAttribute('pointer-events', 'none'); + } +}; + +/** + * Function: getHandleForEvent + * + * Returns the index of the handle for the given event. + */ +mxEdgeHandler.prototype.getHandleForEvent = function(me) +{ + var result = null; + + if (this.state != null) + { + // Connection highlight may consume events before they reach sizer handle + var tol = (!mxEvent.isMouseEvent(me.getEvent())) ? this.tolerance : 1; + var hit = (this.allowHandleBoundsCheck && (mxClient.IS_IE || tol > 0)) ? + new mxRectangle(me.getGraphX() - tol, me.getGraphY() - tol, 2 * tol, 2 * tol) : null; + var minDistSq = null; + + function checkShape(shape) + { + if (shape != null && shape.node != null && shape.node.style.display != 'none' && + shape.node.style.visibility != 'hidden' && + (me.isSource(shape) || (hit != null && mxUtils.intersects(shape.bounds, hit)))) + { + var dx = me.getGraphX() - shape.bounds.getCenterX(); + var dy = me.getGraphY() - shape.bounds.getCenterY(); + var tmp = dx * dx + dy * dy; + + if (minDistSq == null || tmp <= minDistSq) + { + minDistSq = tmp; + + return true; + } + } + + return false; + } + + if (this.customHandles != null && this.isCustomHandleEvent(me)) + { + // Inverse loop order to match display order + for (var i = this.customHandles.length - 1; i >= 0; i--) + { + if (checkShape(this.customHandles[i].shape)) + { + // LATER: Return reference to active shape + return mxEvent.CUSTOM_HANDLE - i; + } + } + } + + if (me.isSource(this.state.text) || checkShape(this.labelShape)) + { + result = mxEvent.LABEL_HANDLE; + } + + if (this.bends != null) + { + for (var i = 0; i < this.bends.length; i++) + { + if (checkShape(this.bends[i])) + { + result = i; + } + } + } + + if (this.virtualBends != null && this.isAddVirtualBendEvent(me)) + { + for (var i = 0; i < this.virtualBends.length; i++) + { + if (checkShape(this.virtualBends[i])) + { + result = mxEvent.VIRTUAL_HANDLE - i; + } + } + } + } + + return result; +}; + +/** + * Function: isAddVirtualBendEvent + * + * Returns true if the given event allows virtual bends to be added. This + * implementation returns true. + */ +mxEdgeHandler.prototype.isAddVirtualBendEvent = function(me) +{ + return true; +}; + +/** + * Function: isCustomHandleEvent + * + * Returns true if the given event allows custom handles to be changed. This + * implementation returns true. + */ +mxEdgeHandler.prototype.isCustomHandleEvent = function(me) +{ + return true; +}; + +/** + * 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 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 = this.getHandleForEvent(me); + + if (this.bends != null && this.bends[handle] != null) + { + var b = this.bends[handle].bounds; + this.snapPoint = new mxPoint(b.getCenterX(), b.getCenterY()); + } + + if (this.addEnabled && handle == null && this.isAddPointEvent(me.getEvent())) + { + this.addPoint(this.state, me.getEvent()); + me.consume(); + } + else if (handle != null && !me.isConsumed() && this.graph.isEnabled()) + { + if (this.removeEnabled && this.isRemovePointEvent(me.getEvent())) + { + this.removePoint(this.state, handle); + } + else if (handle != mxEvent.LABEL_HANDLE || this.graph.isLabelMovable(me.getCell())) + { + if (handle <= mxEvent.VIRTUAL_HANDLE) + { + mxUtils.setOpacity(this.virtualBends[mxEvent.VIRTUAL_HANDLE - handle].node, 100); + } + + 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; + } + + // Hides other custom handles + if (this.index <= mxEvent.CUSTOM_HANDLE && this.index > mxEvent.VIRTUAL_HANDLE) + { + if (this.customHandles != null) + { + for (var i = 0; i < this.customHandles.length; i++) + { + if (i != mxEvent.CUSTOM_HANDLE - this.index) + { + this.customHandles[i].setVisible(false); + } + } + } + } +}; + +/** + * 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: updateHint + * + * Hook for subclassers do show details while the handler is active. + */ +mxEdgeHandler.prototype.updateHint = function(me, point) { }; + +/** + * Function: removeHint + * + * Hooks for subclassers to hide details when the handler gets inactive. + */ +mxEdgeHandler.prototype.removeHint = function() { }; + +/** + * Function: roundLength + * + * Hook for rounding the unscaled width or height. This uses Math.round. + */ +mxEdgeHandler.prototype.roundLength = function(length) +{ + return Math.round(length); +}; + +/** + * Function: isSnapToTerminalsEvent + * + * Returns true if is true and if alt is not pressed. + */ +mxEdgeHandler.prototype.isSnapToTerminalsEvent = function(me) +{ + return this.snapToTerminals && !mxEvent.isAltDown(me.getEvent()); +}; + +/** + * Function: getPointForEvent + * + * Returns the point for the given event. + */ +mxEdgeHandler.prototype.getPointForEvent = function(me) +{ + var view = this.graph.getView(); + var scale = view.scale; + var point = new mxPoint(this.roundLength(me.getGraphX() / scale) * scale, + this.roundLength(me.getGraphY() / scale) * scale); + + var tt = this.getSnapToTerminalTolerance(); + var overrideX = false; + var overrideY = false; + + if (tt > 0 && this.isSnapToTerminalsEvent(me)) + { + 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.state.absolutePoints != null) + { + for (var i = 0; i < this.state.absolutePoints.length; i++) + { + snapToPoint.call(this, this.state.absolutePoints[i]); + } + } + } + + if (this.graph.isGridEnabledEvent(me.getEvent())) + { + 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, true, me.isSource(this.marker.highlight.shape) ? null : this.currentPoint); + + if (this.constraintHandler.currentFocus != null && this.constraintHandler.currentConstraint != null) + { + // Handles special case where grid is large and connection point is at actual point in which + // case the outline is not followed as long as we're < gridSize / 2 away from that point + if (this.marker.highlight != null && this.marker.highlight.state != null && + this.marker.highlight.state.cell == this.constraintHandler.currentFocus.cell) + { + // Direct repaint needed if cell already highlighted + if (this.marker.highlight.shape.stroke != 'transparent') + { + this.marker.highlight.shape.stroke = 'transparent'; + this.marker.highlight.repaint(); + } + } + else + { + this.marker.markCell(this.constraintHandler.currentFocus.cell, 'transparent'); + } + + var model = this.graph.getModel(); + var other = this.graph.view.getTerminalPort(this.state, + this.graph.view.getState(model.getTerminal(this.state.cell, + !this.isSource)), !this.isSource); + var otherCell = (other != null) ? other.cell : null; + var source = (this.isSource) ? this.constraintHandler.currentFocus.cell : otherCell; + var target = (this.isSource) ? otherCell : this.constraintHandler.currentFocus.cell; + + // Updates the error message of the handler + this.error = this.validateConnection(source, target); + var result = null; + + if (this.error == null) + { + result = this.constraintHandler.currentFocus; + } + + if (this.error != null || (result != null && + !this.isCellEnabled(result.cell))) + { + this.constraintHandler.reset(); + } + + return result; + } + else if (!this.graph.isIgnoreTerminalEvent(me.getEvent())) + { + this.marker.process(me); + var state = this.marker.getValidState(); + + if (state != null && !this.isCellEnabled(state.cell)) + { + this.constraintHandler.reset(); + this.marker.reset(); + } + + return this.marker.getValidState(); + } + else + { + this.marker.reset(); + + return null; + } +}; + +/** + * Function: getPreviewPoints + * + * Updates the given preview state taking into account the state of the constraint handler. + * + * Parameters: + * + * pt - that contains the current pointer position. + * me - Optional that contains the current event. + */ +mxEdgeHandler.prototype.getPreviewPoints = function(pt, me) +{ + var geometry = this.graph.getCellGeometry(this.state.cell); + var points = (geometry.points != null) ? geometry.points.slice() : null; + var point = new mxPoint(pt.x, pt.y); + var result = null; + + if (!this.isSource && !this.isTarget) + { + this.convertPoint(point, false); + + if (points == null) + { + points = [point]; + } + else + { + // Adds point from virtual bend + if (this.index <= mxEvent.VIRTUAL_HANDLE) + { + points.splice(mxEvent.VIRTUAL_HANDLE - this.index, 0, point); + } + + // Removes point if dragged on terminal point + if (!this.isSource && !this.isTarget) + { + for (var i = 0; i < this.bends.length; i++) + { + if (i != this.index) + { + var bend = this.bends[i]; + + if (bend != null && mxUtils.contains(bend.bounds, pt.x, pt.y)) + { + if (this.index <= mxEvent.VIRTUAL_HANDLE) + { + points.splice(mxEvent.VIRTUAL_HANDLE - this.index, 1); + } + else + { + points.splice(this.index - 1, 1); + } + + result = points; + } + } + } + + // Removes point if user tries to straighten a segment + if (result == null && this.straightRemoveEnabled && (me == null || !mxEvent.isAltDown(me.getEvent()))) + { + var tol = this.graph.tolerance * this.graph.tolerance; + var abs = this.state.absolutePoints.slice(); + abs[this.index] = pt; + + // Handes special case where removing waypoint affects tolerance (flickering) + var src = this.state.getVisibleTerminalState(true); + + if (src != null) + { + var c = this.graph.getConnectionConstraint(this.state, src, true); + + // Checks if point is not fixed + if (c == null || this.graph.getConnectionPoint(src, c) == null) + { + abs[0] = new mxPoint(src.view.getRoutingCenterX(src), src.view.getRoutingCenterY(src)); + } + } + + var trg = this.state.getVisibleTerminalState(false); + + if (trg != null) + { + var c = this.graph.getConnectionConstraint(this.state, trg, false); + + // Checks if point is not fixed + if (c == null || this.graph.getConnectionPoint(trg, c) == null) + { + abs[abs.length - 1] = new mxPoint(trg.view.getRoutingCenterX(trg), trg.view.getRoutingCenterY(trg)); + } + } + + function checkRemove(idx, tmp) + { + if (idx > 0 && idx < abs.length - 1 && + mxUtils.ptSegDistSq(abs[idx - 1].x, abs[idx - 1].y, + abs[idx + 1].x, abs[idx + 1].y, tmp.x, tmp.y) < tol) + { + points.splice(idx - 1, 1); + result = points; + } + }; + + // LATER: Check if other points can be removed if a segment is made straight + checkRemove(this.index, pt); + } + } + + // Updates existing point + if (result == null && this.index > mxEvent.VIRTUAL_HANDLE) + { + points[this.index - 1] = point; + } + } + } + else if (this.graph.resetEdgesOnConnect) + { + points = null; + } + + return (result != null) ? result : points; +}; + +/** + * Function: isOutlineConnectEvent + * + * Returns true if is true and the source of the event is the outline shape + * or shift is pressed. + */ +mxEdgeHandler.prototype.isOutlineConnectEvent = function(me) +{ + var offset = mxUtils.getOffset(this.graph.container); + var evt = me.getEvent(); + + var clientX = mxEvent.getClientX(evt); + var clientY = mxEvent.getClientY(evt); + + var doc = document.documentElement; + var left = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0); + var top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); + + var gridX = this.currentPoint.x - this.graph.container.scrollLeft + offset.x - left; + var gridY = this.currentPoint.y - this.graph.container.scrollTop + offset.y - top; + + return this.outlineConnect && !mxEvent.isShiftDown(me.getEvent()) && + (me.isSource(this.marker.highlight.shape) || + (mxEvent.isAltDown(me.getEvent()) && me.getState() != null) || + this.marker.highlight.isHighlightAt(clientX, clientY) || + ((gridX != clientX || gridY != clientY) && me.getState() == null && + this.marker.highlight.isHighlightAt(gridX, gridY))); +}; + +/** + * Function: updatePreviewState + * + * Updates the given preview state taking into account the state of the constraint handler. + */ +mxEdgeHandler.prototype.updatePreviewState = function(edge, point, terminalState, me, outline) +{ + // 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 && outline) + { + if (terminalState != null) + { + // Handles special case where mouse is on outline away from actual end point + // in which case the grid is ignored and mouse point is used instead + if (me.isSource(this.marker.highlight.shape)) + { + point = new mxPoint(me.getGraphX(), me.getGraphY()); + } + + constraint = this.graph.getOutlineConstraint(point, terminalState, me); + this.constraintHandler.setFocus(me, terminalState, this.isSource); + this.constraintHandler.currentConstraint = constraint; + this.constraintHandler.currentPoint = point; + } + else + { + constraint = new mxConnectionConstraint(); + } + } + + if (this.outlineConnect && this.marker.highlight != null && this.marker.highlight.shape != null) + { + var s = this.graph.view.scale; + + if (this.constraintHandler.currentConstraint != null && + this.constraintHandler.currentFocus != null) + { + this.marker.highlight.shape.stroke = (outline) ? mxConstants.OUTLINE_HIGHLIGHT_COLOR : 'transparent'; + this.marker.highlight.shape.strokewidth = mxConstants.OUTLINE_HIGHLIGHT_STROKEWIDTH / s / s; + this.marker.highlight.repaint(); + } + else if (this.marker.hasValidState()) + { + this.marker.highlight.shape.stroke = (this.graph.isCellConnectable(me.getCell()) && + this.marker.getValidState() != me.getState()) ? + 'transparent' : mxConstants.DEFAULT_VALID_COLOR; + this.marker.highlight.shape.strokewidth = mxConstants.HIGHLIGHT_STROKEWIDTH / s / s; + this.marker.highlight.repaint(); + } + } + + if (this.isSource) + { + sourceConstraint = constraint; + } + else if (this.isTarget) + { + targetConstraint = constraint; + } + + if (this.isSource || this.isTarget) + { + if (constraint != null && constraint.point != null) + { + edge.style[(this.isSource) ? mxConstants.STYLE_EXIT_X : mxConstants.STYLE_ENTRY_X] = constraint.point.x; + edge.style[(this.isSource) ? mxConstants.STYLE_EXIT_Y : mxConstants.STYLE_ENTRY_Y] = constraint.point.y; + } + else + { + delete edge.style[(this.isSource) ? mxConstants.STYLE_EXIT_X : mxConstants.STYLE_ENTRY_X]; + delete edge.style[(this.isSource) ? mxConstants.STYLE_EXIT_Y : mxConstants.STYLE_ENTRY_Y]; + } + } + + edge.setVisibleTerminalState(sourceState, true); + edge.setVisibleTerminalState(targetState, false); + + 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) + { + this.currentPoint = this.getPointForEvent(me); + this.error = null; + + // Uses the current point from the constraint handler if available + if (!this.graph.isIgnoreTerminalEvent(me.getEvent()) && mxEvent.isShiftDown(me.getEvent()) && this.snapPoint != null) + { + if (Math.abs(this.snapPoint.x - this.currentPoint.x) < Math.abs(this.snapPoint.y - this.currentPoint.y)) + { + this.currentPoint.x = this.snapPoint.x; + } + else + { + this.currentPoint.y = this.snapPoint.y; + } + } + + if (this.index <= mxEvent.CUSTOM_HANDLE && this.index > mxEvent.VIRTUAL_HANDLE) + { + if (this.customHandles != null) + { + this.customHandles[mxEvent.CUSTOM_HANDLE - this.index].processEvent(me); + this.customHandles[mxEvent.CUSTOM_HANDLE - this.index].positionChanged(); + + if (this.shape != null && this.shape.node != null) + { + this.shape.node.style.display = 'none'; + } + } + } + else if (this.isLabel) + { + this.label.x = this.currentPoint.x; + this.label.y = this.currentPoint.y; + } + else + { + this.points = this.getPreviewPoints(this.currentPoint, me); + var terminalState = (this.isSource || this.isTarget) ? this.getPreviewTerminalState(me) : null; + + if (this.constraintHandler.currentConstraint != null && + this.constraintHandler.currentFocus != null && + this.constraintHandler.currentPoint != null) + { + this.currentPoint = this.constraintHandler.currentPoint.clone(); + } + else if (this.outlineConnect) + { + // Need to check outline before cloning terminal state + var outline = (this.isSource || this.isTarget) ? this.isOutlineConnectEvent(me) : false + + if (outline) + { + terminalState = this.marker.highlight.state; + } + else if (terminalState != null && terminalState != me.getState() && + this.graph.isCellConnectable(me.getCell()) && + this.marker.highlight.shape != null) + { + this.marker.highlight.shape.stroke = 'transparent'; + this.marker.highlight.repaint(); + terminalState = null; + } + } + + if (terminalState != null && !this.isCellEnabled(terminalState.cell)) + { + terminalState = null; + this.marker.reset(); + } + + var clone = this.clonePreviewState(this.currentPoint, (terminalState != null) ? terminalState.cell : null); + this.updatePreviewState(clone, this.currentPoint, terminalState, me, outline); + + // 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.updateHint(me, this.currentPoint); + } + + // This should go before calling isOutlineConnectEvent above. As a workaround + // we add an offset of gridSize to the hint to avoid problem with hit detection + // in highlight.isHighlightAt (which uses comonentFromPoint) + 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 , or . + */ +mxEdgeHandler.prototype.mouseUp = function(sender, me) +{ + // Workaround for wrong event source in Webkit + if (this.index != null && this.marker != null) + { + if (this.shape != null && this.shape.node != null) + { + this.shape.node.style.display = ''; + } + + var edge = this.state.cell; + var index = this.index; + this.index = null; + + // Ignores event if mouse has not been moved + if (me.getX() != this.startX || me.getY() != this.startY) + { + var clone = !this.graph.isIgnoreTerminalEvent(me.getEvent()) && this.graph.isCloneEvent(me.getEvent()) && + this.cloneEnabled && this.graph.isCellsCloneable(); + + // 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 (index <= mxEvent.CUSTOM_HANDLE && index > mxEvent.VIRTUAL_HANDLE) + { + if (this.customHandles != null) + { + var model = this.graph.getModel(); + + model.beginUpdate(); + try + { + this.customHandles[mxEvent.CUSTOM_HANDLE - index].execute(me); + + if (this.shape != null && this.shape.node != null) + { + this.shape.apply(this.state); + this.shape.redraw(); + } + } + finally + { + model.endUpdate(); + } + } + } + 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() && this.marker.highlight != null && + this.marker.highlight.shape != null && + this.marker.highlight.shape.stroke != 'transparent' && + this.marker.highlight.shape.stroke != 'white') + { + terminal = this.marker.validState.cell; + } + + if (terminal != null) + { + var model = this.graph.getModel(); + var parent = model.getParent(edge); + + model.beginUpdate(); + try + { + // Clones and adds the cell + if (clone) + { + var geo = model.getGeometry(edge); + var clone = this.graph.cloneCell(edge); + model.add(parent, clone, model.getChildCount(parent)); + + if (geo != null) + { + geo = geo.clone(); + model.setGeometry(clone, geo); + } + + var other = model.getTerminal(edge, !this.isSource); + this.graph.connectCell(clone, other, !this.isSource); + + edge = clone; + } + + edge = this.connect(edge, terminal, this.isSource, clone, me); + } + finally + { + model.endUpdate(); + } + } + else if (this.graph.isAllowDanglingEdges()) + { + var pt = this.abspoints[(this.isSource) ? 0 : this.abspoints.length - 1]; + pt.x = this.roundLength(pt.x / this.graph.view.scale - this.graph.view.translate.x); + pt.y = this.roundLength(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 recreates this handler + edge = this.changeTerminalPoint(edge, pt, this.isSource, clone); + } + } + else if (this.active) + { + edge = this.changePoints(edge, this.points, clone); + } + else + { + this.graph.getView().invalidate(this.state.cell); + this.graph.getView().validate(this.state.cell); + } + } + else if (this.graph.isToggleEvent(me.getEvent())) + { + this.graph.selectCellForEvent(this.state.cell, me.getEvent()); + } + + // 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() +{ + if (this.active) + { + this.refresh(); + } + + this.error = null; + this.index = null; + this.label = null; + this.points = null; + this.snapPoint = null; + this.isLabel = false; + this.isSource = false; + this.isTarget = false; + this.active = false; + + if (this.livePreview && this.sizers != null) + { + for (var i = 0; i < this.sizers.length; i++) + { + if (this.sizers[i] != null) + { + this.sizers[i].node.style.display = ''; + } + } + } + + if (this.marker != null) + { + this.marker.reset(); + } + + if (this.constraintHandler != null) + { + this.constraintHandler.reset(); + } + + if (this.customHandles != null) + { + for (var i = 0; i < this.customHandles.length; i++) + { + this.customHandles[i].reset(); + } + } + + this.setPreviewColor(mxConstants.EDGE_SELECTION_COLOR); + this.removeHint(); + 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.stroke = 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 - 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 - 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) + { + var scale = this.graph.getView().scale; + geometry = geometry.clone(); + + if (geometry.relative) + { + // Resets the relative location stored inside the geometry + var pt = this.graph.getView().getRelativePoint(edgeState, x, y); + geometry.x = Math.round(pt.x * 10000) / 10000; + geometry.y = Math.round(pt.y); + + // Resets the offset inside the geometry to find the offset + // from the resulting point + geometry.offset = new mxPoint(0, 0); + var pt = this.graph.view.getPoint(edgeState, geometry); + geometry.offset = new mxPoint(Math.round((x - pt.x) / scale), Math.round((y - pt.y) / scale)); + } + else + { + var points = edgeState.absolutePoints; + var p0 = points[0]; + var pe = points[points.length - 1]; + + if (p0 != null && pe != null) + { + var cx = p0.x + (pe.x - p0.x) / 2; + var cy = p0.y + (pe.y - p0.y) / 2; + + geometry.offset = new mxPoint(Math.round((x - cx) / scale), Math.round((y - cy) / scale)); + geometry.x = 0; + geometry.y = 0; + } + } + + model.setGeometry(edgeState.cell, geometry); + } +}; + +/** + * Function: connect + * + * Changes the terminal or terminal point of the given edge in the graph + * model. + * + * Parameters: + * + * edge - that represents the edge to be reconnected. + * terminal - 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 - 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 + { + 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, clone) +{ + var model = this.graph.getModel(); + + model.beginUpdate(); + try + { + if (clone) + { + var parent = model.getParent(edge); + var terminal = model.getTerminal(edge, !isSource); + edge = this.graph.cloneCell(edge); + model.add(parent, edge, model.getChildCount(parent)); + model.setTerminal(edge, terminal, !isSource); + } + + var geo = model.getGeometry(edge); + + if (geo != null) + { + geo = geo.clone(); + geo.setTerminalPoint(point, isSource); + model.setGeometry(edge, geo); + this.graph.connectCell(edge, null, isSource, new mxConnectionConstraint()); + } + } + finally + { + model.endUpdate(); + } + + return edge; +}; + +/** + * Function: changePoints + * + * Changes the control points of the given edge in the graph model. + */ +mxEdgeHandler.prototype.changePoints = function(edge, points, clone) +{ + var model = this.graph.getModel(); + model.beginUpdate(); + try + { + if (clone) + { + var parent = model.getParent(edge); + var source = model.getTerminal(edge, true); + var target = model.getTerminal(edge, false); + edge = this.graph.cloneCell(edge); + model.add(parent, edge, model.getChildCount(parent)); + model.setTerminal(edge, source, true); + model.setTerminal(edge, target, false); + } + + var geo = model.getGeometry(edge); + + if (geo != null) + { + geo = geo.clone(); + geo.points = points; + + model.setGeometry(edge, geo); + } + } + finally + { + model.endUpdate(); + } + + return edge; +}; + +/** + * Function: addPoint + * + * Adds a control point for the given state and event. + */ +mxEdgeHandler.prototype.addPoint = function(state, evt) +{ + var pt = mxUtils.convertPoint(this.graph.container, mxEvent.getClientX(evt), + mxEvent.getClientY(evt)); + var gridEnabled = this.graph.isGridEnabledEvent(evt); + this.convertPoint(pt, gridEnabled); + this.addPointAt(state, pt.x, pt.y); + mxEvent.consume(evt); +}; + +/** + * Function: addPointAt + * + * Adds a control point at the given point. + */ +mxEdgeHandler.prototype.addPointAt = function(state, x, y) +{ + var geo = this.graph.getCellGeometry(state.cell); + var pt = new mxPoint(x, y); + + if (geo != null) + { + geo = geo.clone(); + var t = this.graph.view.translate; + var s = this.graph.view.scale; + var offset = new mxPoint(t.x * s, t.y * s); + + var parent = this.graph.model.getParent(this.state.cell); + + if (this.graph.model.isVertex(parent)) + { + var pState = this.graph.view.getState(parent); + offset = new mxPoint(pState.x, pState.y); + } + + var index = mxUtils.findNearestSegment(state, pt.x * s + offset.x, pt.y * s + offset.y); + + if (geo.points == null) + { + geo.points = [pt]; + } + else + { + geo.points.splice(index, 0, pt); + } + + this.graph.getModel().setGeometry(state.cell, geo); + this.refresh(); + this.redraw(); + } +}; + +/** + * 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.refresh(); + this.redraw(); + } + } +}; + +/** + * 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(ignoreHandles) +{ + if (this.state != null) + { + this.abspoints = this.state.absolutePoints.slice(); + var g = this.graph.getModel().getGeometry(this.state.cell); + + if (g != null) + { + var pts = g.points; + + if (this.bends != null && this.bends.length > 0) + { + if (pts != null) + { + if (this.points == null) + { + this.points = []; + } + + for (var i = 1; i < this.bends.length - 1; i++) + { + if (this.bends[i] != null && this.abspoints[i] != null) + { + this.points[i - 1] = pts[i - 1]; + } + } + } + } + } + + this.drawPreview(); + + if (!ignoreHandles) + { + this.redrawHandles(); + } + } +}; + +/** + * Function: redrawHandles + * + * Redraws the handles. + */ +mxEdgeHandler.prototype.redrawHandles = function() +{ + var cell = this.state.cell; + + // Updates the handle for the label position + var b = this.labelShape.bounds; + this.label = new mxPoint(this.state.absoluteOffset.x, this.state.absoluteOffset.y); + this.labelShape.bounds = new mxRectangle(Math.round(this.label.x - b.width / 2), + Math.round(this.label.y - b.height / 2), b.width, b.height); + + // Shows or hides the label handle depending on the label + var lab = this.graph.getLabel(cell); + this.labelShape.visible = (lab != null && lab.length > 0 && this.graph.isLabelMovable(cell)); + + if (this.bends != null && this.bends.length > 0) + { + var n = this.abspoints.length - 1; + + var p0 = this.abspoints[0]; + var x0 = p0.x; + var y0 = p0.y; + + b = this.bends[0].bounds; + this.bends[0].bounds = new mxRectangle(Math.floor(x0 - b.width / 2), + Math.floor(y0 - b.height / 2), b.width, b.height); + this.bends[0].fill = this.getHandleFillColor(0); + this.bends[0].redraw(); + + if (this.manageLabelHandle) + { + this.checkLabelHandle(this.bends[0].bounds); + } + + var pe = this.abspoints[n]; + var xn = pe.x; + var yn = pe.y; + + var bn = this.bends.length - 1; + b = this.bends[bn].bounds; + this.bends[bn].bounds = new mxRectangle(Math.floor(xn - b.width / 2), + Math.floor(yn - b.height / 2), b.width, b.height); + this.bends[bn].fill = this.getHandleFillColor(bn); + this.bends[bn].redraw(); + + if (this.manageLabelHandle) + { + this.checkLabelHandle(this.bends[bn].bounds); + } + + this.redrawInnerBends(p0, pe); + } + + if (this.abspoints != null && this.virtualBends != null && this.virtualBends.length > 0) + { + var last = this.abspoints[0]; + + for (var i = 0; i < this.virtualBends.length; i++) + { + if (this.virtualBends[i] != null && this.abspoints[i + 1] != null) + { + var pt = this.abspoints[i + 1]; + var b = this.virtualBends[i]; + var x = last.x + (pt.x - last.x) / 2; + var y = last.y + (pt.y - last.y) / 2; + b.bounds = new mxRectangle(Math.floor(x - b.bounds.width / 2), + Math.floor(y - b.bounds.height / 2), b.bounds.width, b.bounds.height); + b.redraw(); + mxUtils.setOpacity(b.node, this.virtualBendOpacity); + last = pt; + + if (this.manageLabelHandle) + { + this.checkLabelHandle(b.bounds); + } + } + } + } + + if (this.labelShape != null) + { + this.labelShape.redraw(); + } + + if (this.customHandles != null) + { + for (var i = 0; i < this.customHandles.length; i++) + { + var temp = this.customHandles[i].shape.node.style.display; + this.customHandles[i].redraw(); + this.customHandles[i].shape.node.style.display = temp; + + // Hides custom handles during text editing + this.customHandles[i].shape.node.style.visibility = + (this.isCustomHandleVisible(this.customHandles[i])) ? + '' : 'hidden'; + } + } +}; + +/** + * Function: isCustomHandleVisible + * + * Returns true if the given custom handle is visible. + */ +mxEdgeHandler.prototype.isCustomHandleVisible = function(handle) +{ + return !this.graph.isEditing() && this.state.view.graph.getSelectionCount() == 1; +}; + +/** + * Function: hideHandles + * + * Shortcut to . + */ +mxEdgeHandler.prototype.setHandlesVisible = function(visible) +{ + if (this.bends != null) + { + for (var i = 0; i < this.bends.length; i++) + { + this.bends[i].node.style.display = (visible) ? '' : 'none'; + } + } + + if (this.virtualBends != null) + { + for (var i = 0; i < this.virtualBends.length; i++) + { + this.virtualBends[i].node.style.display = (visible) ? '' : 'none'; + } + } + + if (this.labelShape != null) + { + this.labelShape.node.style.display = (visible) ? '' : 'none'; + } + + if (this.customHandles != null) + { + for (var i = 0; i < this.customHandles.length; i++) + { + this.customHandles[i].setVisible(visible); + } + } +}; + +/** + * Function: redrawInnerBends + * + * Updates and redraws the inner bends. + * + * Parameters: + * + * p0 - that represents the location of the first point. + * pe - that represents the location of the last point. + */ +mxEdgeHandler.prototype.redrawInnerBends = function(p0, pe) +{ + 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(Math.round(x - b.width / 2), + Math.round(y - b.height / 2), b.width, b.height); + + if (this.manageLabelHandle) + { + this.checkLabelHandle(this.bends[i].bounds); + } + else if (this.handleImage == null && this.labelShape.visible && mxUtils.intersects(this.bends[i].bounds, this.labelShape.bounds)) + { + w = mxConstants.HANDLE_SIZE + 3; + h = mxConstants.HANDLE_SIZE + 3; + this.bends[i].bounds = new mxRectangle(Math.round(x - w / 2), Math.round(y - h / 2), w, h); + } + + this.bends[i].redraw(); + } + else + { + this.bends[i].destroy(); + this.bends[i] = null; + } + } + } +}; + +/** + * Function: checkLabelHandle + * + * Checks if the label handle intersects the given bounds and moves it if it + * intersects. + */ +mxEdgeHandler.prototype.checkLabelHandle = function(b) +{ + if (this.labelShape != null) + { + var b2 = this.labelShape.bounds; + + if (mxUtils.intersects(b, b2)) + { + if (b.getCenterY() < b2.getCenterY()) + { + b2.y = b.y + b.height; + } + else + { + b2.y = b.y - b2.height; + } + } + } +}; + +/** + * Function: drawPreview + * + * Redraws the preview. + */ +mxEdgeHandler.prototype.drawPreview = function() +{ + try + { + if (this.isLabel) + { + var b = this.labelShape.bounds; + var bounds = new mxRectangle(Math.round(this.label.x - b.width / 2), + Math.round(this.label.y - b.height / 2), b.width, b.height); + + if (!this.labelShape.bounds.equals(bounds)) + { + this.labelShape.bounds = bounds; + this.labelShape.redraw(); + } + } + + if (this.shape != null && !mxUtils.equalPoints(this.shape.points, this.abspoints)) + { + this.shape.apply(this.state); + this.shape.points = this.abspoints.slice(); + this.shape.scale = this.state.view.scale; + this.shape.isDashed = this.isSelectionDashed(); + this.shape.stroke = this.getSelectionColor(); + this.shape.strokewidth = this.getSelectionStrokeWidth() / this.shape.scale / this.shape.scale; + this.shape.isShadow = false; + this.shape.redraw(); + } + + this.updateParentHighlight(); + } + catch (e) + { + // ignore + } +}; + +/** + * Function: refresh + * + * Refreshes the bends of this handler. + */ +mxEdgeHandler.prototype.refresh = function() +{ + if (this.state != null) + { + this.abspoints = this.getSelectionPoints(this.state); + this.points = []; + + if (this.bends != null) + { + this.destroyBends(this.bends); + this.bends = this.createBends(); + } + + if (this.virtualBends != null) + { + this.destroyBends(this.virtualBends); + this.virtualBends = this.createVirtualBends(); + } + + if (this.customHandles != null) + { + this.destroyBends(this.customHandles); + this.customHandles = this.createCustomHandles(); + } + + // Puts label node on top of bends + if (this.labelShape != null && this.labelShape.node != null && this.labelShape.node.parentNode != null) + { + this.labelShape.node.parentNode.appendChild(this.labelShape.node); + } + } +}; + +/** + * Function: isDestroyed + * + * Returns true if was called. + */ +mxEdgeHandler.prototype.isDestroyed = function() +{ + return this.shape == null; +}; + +/** + * Function: destroyBends + * + * Destroys all elements in . + */ +mxEdgeHandler.prototype.destroyBends = function(bends) +{ + if (bends != null) + { + for (var i = 0; i < bends.length; i++) + { + if (bends[i] != null) + { + bends[i].destroy(); + } + } + } +}; + +/** + * 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.escapeHandler != null) + { + this.state.view.graph.removeListener(this.escapeHandler); + this.escapeHandler = null; + } + + if (this.marker != null) + { + this.marker.destroy(); + this.marker = null; + } + + if (this.shape != null) + { + this.shape.destroy(); + this.shape = null; + } + + if (this.parentHighlight != null) + { + var parent = this.graph.model.getParent(this.state.cell); + var pstate = this.graph.view.getState(parent); + + if (pstate != null && pstate.parentHighlight == this.parentHighlight) + { + pstate.parentHighlight = null; + } + + this.parentHighlight.destroy(); + this.parentHighlight = null; + } + + if (this.labelShape != null) + { + this.labelShape.destroy(); + this.labelShape = null; + } + + if (this.constraintHandler != null) + { + this.constraintHandler.destroy(); + this.constraintHandler = null; + } + + this.destroyBends(this.virtualBends); + this.virtualBends = null; + + this.destroyBends(this.customHandles); + this.customHandles = null; + + this.destroyBends(this.bends); + this.bends = null; + + this.removeHint(); +}; diff --git a/mxclient/js/handler/mxEdgeSegmentHandler.js b/mxclient/js/handler/mxEdgeSegmentHandler.js new file mode 100644 index 0000000..3d55c96 --- /dev/null +++ b/mxclient/js/handler/mxEdgeSegmentHandler.js @@ -0,0 +1,413 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +function mxEdgeSegmentHandler(state) +{ + mxEdgeHandler.call(this, state); +}; + +/** + * Extends mxEdgeHandler. + */ +mxUtils.extend(mxEdgeSegmentHandler, mxElbowEdgeHandler); + +/** + * Function: getCurrentPoints + * + * Returns the current absolute points. + */ +mxEdgeSegmentHandler.prototype.getCurrentPoints = function() +{ + var pts = this.state.absolutePoints; + + if (pts != null) + { + // Special case for straight edges where we add a virtual middle handle for moving the edge + var tol = Math.max(1, this.graph.view.scale); + + if (pts.length == 2 || (pts.length == 3 && + (Math.abs(pts[0].x - pts[1].x) < tol && Math.abs(pts[1].x - pts[2].x) < tol || + Math.abs(pts[0].y - pts[1].y) < tol && Math.abs(pts[1].y - pts[2].y) < tol))) + { + var cx = pts[0].x + (pts[pts.length - 1].x - pts[0].x) / 2; + var cy = pts[0].y + (pts[pts.length - 1].y - pts[0].y) / 2; + + pts = [pts[0], new mxPoint(cx, cy), new mxPoint(cx, cy), pts[pts.length - 1]]; + } + } + + return pts; +}; + +/** + * 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 + { + var pts = this.getCurrentPoints(); + var last = this.convertPoint(pts[0].clone(), false); + point = this.convertPoint(point.clone(), false); + var result = []; + + for (var i = 1; i < pts.length; i++) + { + var pt = this.convertPoint(pts[i].clone(), false); + + if (i == this.index) + { + if (Math.round(last.x - pt.x) == 0) + { + last.x = point.x; + pt.x = point.x; + } + + if (Math.round(last.y - pt.y) == 0) + { + last.y = point.y; + pt.y = point.y; + } + } + + if (i < pts.length - 1) + { + result.push(pt); + } + + last = pt; + } + + // Replaces single point that intersects with source or target + if (result.length == 1) + { + var source = this.state.getVisibleTerminalState(true); + var target = this.state.getVisibleTerminalState(false); + var scale = this.state.view.getScale(); + var tr = this.state.view.getTranslate(); + + var x = result[0].x * scale + tr.x; + var y = result[0].y * scale + tr.y; + + if ((source != null && mxUtils.contains(source, x, y)) || + (target != null && mxUtils.contains(target, x, y))) + { + result = [point, point]; + } + } + + return result; + } +}; + +/** + * Function: updatePreviewState + * + * Overridden to perform optimization of the edge style result. + */ +mxEdgeSegmentHandler.prototype.updatePreviewState = function(edge, point, terminalState, me) +{ + mxEdgeHandler.prototype.updatePreviewState.apply(this, arguments); + + // Checks and corrects preview by running edge style again + if (!this.isSource && !this.isTarget) + { + point = this.convertPoint(point.clone(), false); + var pts = edge.absolutePoints; + var pt0 = pts[0]; + var pt1 = pts[1]; + + var result = []; + + for (var i = 2; i < pts.length; i++) + { + var pt2 = pts[i]; + + // Merges adjacent segments only if more than 2 to allow for straight edges + if ((Math.round(pt0.x - pt1.x) != 0 || Math.round(pt1.x - pt2.x) != 0) && + (Math.round(pt0.y - pt1.y) != 0 || Math.round(pt1.y - pt2.y) != 0)) + { + result.push(this.convertPoint(pt1.clone(), false)); + } + + pt0 = pt1; + pt1 = pt2; + } + + var source = this.state.getVisibleTerminalState(true); + var target = this.state.getVisibleTerminalState(false); + var rpts = this.state.absolutePoints; + + // A straight line is represented by 3 handles + if (result.length == 0 && (Math.round(pts[0].x - pts[pts.length - 1].x) == 0 || + Math.round(pts[0].y - pts[pts.length - 1].y) == 0)) + { + result = [point, point]; + } + // Handles special case of transitions from straight vertical to routed + else if (pts.length == 5 && result.length == 2 && source != null && target != null && + rpts != null && Math.round(rpts[0].x - rpts[rpts.length - 1].x) == 0) + { + var view = this.graph.getView(); + var scale = view.getScale(); + var tr = view.getTranslate(); + + var y0 = view.getRoutingCenterY(source) / scale - tr.y; + + // Use fixed connection point y-coordinate if one exists + var sc = this.graph.getConnectionConstraint(edge, source, true); + + if (sc != null) + { + var pt = this.graph.getConnectionPoint(source, sc); + + if (pt != null) + { + this.convertPoint(pt, false); + y0 = pt.y; + } + } + + var ye = view.getRoutingCenterY(target) / scale - tr.y; + + // Use fixed connection point y-coordinate if one exists + var tc = this.graph.getConnectionConstraint(edge, target, false); + + if (tc) + { + var pt = this.graph.getConnectionPoint(target, tc); + + if (pt != null) + { + this.convertPoint(pt, false); + ye = pt.y; + } + } + + result = [new mxPoint(point.x, y0), new mxPoint(point.x, ye)]; + } + + this.points = result; + + // LATER: Check if points and result are different + edge.view.updateFixedTerminalPoints(edge, source, target); + edge.view.updatePoints(edge, this.points, source, target); + edge.view.updateFloatingTerminalPoints(edge, source, target); + } +}; + +/** + * Overriden to merge edge segments. + */ +mxEdgeSegmentHandler.prototype.connect = function(edge, terminal, isSource, isClone, me) +{ + var model = this.graph.getModel(); + var geo = model.getGeometry(edge); + var result = null; + + // Merges adjacent edge segments + if (geo != null && geo.points != null && geo.points.length > 0) + { + var pts = this.abspoints; + var pt0 = pts[0]; + var pt1 = pts[1]; + result = []; + + for (var i = 2; i < pts.length; i++) + { + var pt2 = pts[i]; + + // Merges adjacent segments only if more than 2 to allow for straight edges + if ((Math.round(pt0.x - pt1.x) != 0 || Math.round(pt1.x - pt2.x) != 0) && + (Math.round(pt0.y - pt1.y) != 0 || Math.round(pt1.y - pt2.y) != 0)) + { + result.push(this.convertPoint(pt1.clone(), false)); + } + + pt0 = pt1; + pt1 = pt2; + } + } + + model.beginUpdate(); + try + { + if (result != null) + { + var geo = model.getGeometry(edge); + + if (geo != null) + { + geo = geo.clone(); + geo.points = result; + + model.setGeometry(edge, geo); + } + } + + edge = mxEdgeHandler.prototype.connect.apply(this, arguments); + } + finally + { + model.endUpdate(); + } + + return edge; +}; + +/** + * Function: getTooltipForNode + * + * Returns no tooltips. + */ +mxEdgeSegmentHandler.prototype.getTooltipForNode = function(node) +{ + return null; +}; + +/** + * Function: start + * + * Starts the handling of the mouse gesture. + */ +mxEdgeSegmentHandler.prototype.start = function(x, y, index) +{ + mxEdgeHandler.prototype.start.apply(this, arguments); + + if (this.bends != null && this.bends[index] != null && + !this.isSource && !this.isTarget) + { + mxUtils.setOpacity(this.bends[index].node, 100); + } +}; + +/** + * 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.setCursor(mxConstants.CURSOR_TERMINAL_HANDLE); + bends.push(bend); + + var pts = this.getCurrentPoints(); + + // 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++) + { + bend = this.createVirtualBend(); + bends.push(bend); + var horizontal = Math.round(pts[i].x - pts[i + 1].x) == 0; + + // Special case where dy is 0 as well + if (Math.round(pts[i].y - pts[i + 1].y) == 0 && i < pts.length - 2) + { + horizontal = Math.round(pts[i].x - pts[i + 2].x) == 0; + } + + bend.setCursor((horizontal) ? 'col-resize' : 'row-resize'); + this.points.push(new mxPoint(0,0)); + } + } + + // Target + var bend = this.createHandleShape(pts.length); + this.initBend(bend); + bend.setCursor(mxConstants.CURSOR_TERMINAL_HANDLE); + bends.push(bend); + + return bends; +}; + +/** + * Function: redraw + * + * Overridden to invoke before the redraw. + */ +mxEdgeSegmentHandler.prototype.redraw = function() +{ + this.refresh(); + mxEdgeHandler.prototype.redraw.apply(this, arguments); +}; + +/** + * Function: redrawInnerBends + * + * Updates the position of the custom bends. + */ +mxEdgeSegmentHandler.prototype.redrawInnerBends = function(p0, pe) +{ + if (this.graph.isCellBendable(this.state.cell)) + { + var pts = this.getCurrentPoints(); + + if (pts != null && pts.length > 1) + { + var straight = false; + + // Puts handle in the center of straight edges + if (pts.length == 4 && Math.round(pts[1].x - pts[2].x) == 0 && Math.round(pts[1].y - pts[2].y) == 0) + { + straight = true; + + if (Math.round(pts[0].y - pts[pts.length - 1].y) == 0) + { + var cx = pts[0].x + (pts[pts.length - 1].x - pts[0].x) / 2; + pts[1] = new mxPoint(cx, pts[1].y); + pts[2] = new mxPoint(cx, pts[2].y); + } + else + { + var cy = pts[0].y + (pts[pts.length - 1].y - pts[0].y) / 2; + pts[1] = new mxPoint(pts[1].x, cy); + pts[2] = new mxPoint(pts[2].x, cy); + } + } + + for (var i = 0; i < pts.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); + var b = this.bends[i + 1].bounds; + this.bends[i + 1].bounds = new mxRectangle(Math.floor(pt.x - b.width / 2), + Math.floor(pt.y - b.height / 2), b.width, b.height); + this.bends[i + 1].redraw(); + + if (this.manageLabelHandle) + { + this.checkLabelHandle(this.bends[i + 1].bounds); + } + } + } + + if (straight) + { + mxUtils.setOpacity(this.bends[1].node, this.virtualBendOpacity); + mxUtils.setOpacity(this.bends[3].node, this.virtualBendOpacity); + } + } + } +}; diff --git a/mxclient/js/handler/mxElbowEdgeHandler.js b/mxclient/js/handler/mxElbowEdgeHandler.js new file mode 100644 index 0000000..a018017 --- /dev/null +++ b/mxclient/js/handler/mxElbowEdgeHandler.js @@ -0,0 +1,230 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxElbowEdgeHandler + * + * Graph event handler that reconnects edges and modifies control points and + * the edge label location. Uses for finding and + * highlighting new source and target vertices. This handler is automatically + * created in . It extends . + * + * Constructor: mxEdgeHandler + * + * Constructs an edge handler for the specified . + * + * Parameters: + * + * state - of the cell to be modified. + */ +function mxElbowEdgeHandler(state) +{ + mxEdgeHandler.call(this, state); +}; + +/** + * Extends mxEdgeHandler. + */ +mxUtils.extend(mxElbowEdgeHandler, mxEdgeHandler); + +/** + * Specifies if a double click on the middle handle should call + * . 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 to create custom bends. + */ + mxElbowEdgeHandler.prototype.createBends = function() + { + var bends = []; + + // Source + var bend = this.createHandleShape(0); + this.initBend(bend); + bend.setCursor(mxConstants.CURSOR_TERMINAL_HANDLE); + bends.push(bend); + + // Virtual + bends.push(this.createVirtualBend(mxUtils.bind(this, function(evt) + { + if (!mxEvent.isConsumed(evt) && this.flipEnabled) + { + this.graph.flipEdge(this.state.cell, evt); + mxEvent.consume(evt); + } + }))); + + this.points.push(new mxPoint(0,0)); + + // Target + bend = this.createHandleShape(2); + this.initBend(bend); + bend.setCursor(mxConstants.CURSOR_TERMINAL_HANDLE); + bends.push(bend); + + return bends; + }; + +/** + * Function: createVirtualBend + * + * Creates a virtual bend that supports double clicking and calls + * . + */ +mxElbowEdgeHandler.prototype.createVirtualBend = function(dblClickHandler) +{ + var bend = this.createHandleShape(); + this.initBend(bend, dblClickHandler); + + bend.setCursor(this.getCursorForBend()); + + if (!this.graph.isCellBendable(this.state.cell)) + { + bend.node.style.display = 'none'; + } + + 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 - 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); + + return point; +}; + +/** + * Function: redrawInnerBends + * + * Updates and redraws the inner bends. + * + * Parameters: + * + * p0 - that represents the location of the first point. + * pe - 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 = this.state.absolutePoints; + var pt = null; + + // Keeps the virtual bend on the edge shape + if (pts.length > 1) + { + p0 = pts[1]; + pe = pts[pts.length - 2]; + } + else if (g.points != null && g.points.length > 0) + { + pt = pts[0]; + } + + 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; + var bounds = new mxRectangle(Math.round(pt.x - w / 2), Math.round(pt.y - h / 2), w, h); + + if (this.manageLabelHandle) + { + this.checkLabelHandle(bounds); + } + else if (this.handleImage == null && this.labelShape.visible && mxUtils.intersects(bounds, this.labelShape.bounds)) + { + w = mxConstants.HANDLE_SIZE + 3; + h = mxConstants.HANDLE_SIZE + 3; + bounds = new mxRectangle(Math.floor(pt.x - w / 2), Math.floor(pt.y - h / 2), w, h); + } + + this.bends[1].bounds = bounds; + this.bends[1].redraw(); + + if (this.manageLabelHandle) + { + this.checkLabelHandle(this.bends[1].bounds); + } +}; diff --git a/mxclient/js/handler/mxGraphHandler.js b/mxclient/js/handler/mxGraphHandler.js new file mode 100644 index 0000000..327518c --- /dev/null +++ b/mxclient/js/handler/mxGraphHandler.js @@ -0,0 +1,1865 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxGraphHandler + * + * Graph event handler that handles selection. Individual cells are handled + * separately using or one of the edge handlers. These + * handlers are created using in + * . + * + * To avoid the container to scroll a moved cell into view, set + * to false. + * + * Constructor: mxGraphHandler + * + * Constructs an event handler that creates handles for the + * selection cells. + * + * Parameters: + * + * graph - Reference to the enclosing . + */ +function mxGraphHandler(graph) +{ + this.graph = graph; + this.graph.addMouseListener(this); + + // Repaints the handler after autoscroll + this.panHandler = mxUtils.bind(this, function() + { + if (!this.suspended) + { + this.updatePreview(); + this.updateHint(); + } + }); + + this.graph.addListener(mxEvent.PAN, this.panHandler); + + // Handles escape keystrokes + this.escapeHandler = mxUtils.bind(this, function(sender, evt) + { + this.reset(); + }); + + this.graph.addListener(mxEvent.ESCAPE, this.escapeHandler); + + // Updates the preview box for remote changes + this.refreshHandler = mxUtils.bind(this, function(sender, evt) + { + // Merges multiple pending calls + if (this.refreshThread) + { + window.clearTimeout(this.refreshThread); + } + + // Waits for the states and handlers to be updated + this.refreshThread = window.setTimeout(mxUtils.bind(this, function() + { + this.refreshThread = null; + + if (this.first != null && !this.suspended) + { + // Updates preview with no translate to compute bounding box + var dx = this.currentDx; + var dy = this.currentDy; + this.currentDx = 0; + this.currentDy = 0; + this.updatePreview(); + this.bounds = this.graph.getView().getBounds(this.cells); + this.pBounds = this.getPreviewBounds(this.cells); + + if (this.pBounds == null && !this.livePreviewUsed) + { + this.reset(); + } + else + { + // Restores translate and updates preview + this.currentDx = dx; + this.currentDy = dy; + this.updatePreview(); + this.updateHint(); + + if (this.livePreviewUsed) + { + // Forces update to ignore last visible state + this.setHandlesVisibleForCells( + this.graph.selectionCellsHandler. + getHandledSelectionCells(), false, true); + this.updatePreview(); + } + } + } + }), 0); + }); + + this.graph.getModel().addListener(mxEvent.CHANGE, this.refreshHandler); + this.graph.addListener(mxEvent.REFRESH, this.refreshHandler); + + this.keyHandler = mxUtils.bind(this, function(e) + { + if (this.graph.container != null && this.graph.container.style.visibility != 'hidden' && + this.first != null && !this.suspended) + { + var clone = this.graph.isCloneEvent(e) && + this.graph.isCellsCloneable() && + this.isCloneEnabled(); + + if (clone != this.cloning) + { + this.cloning = clone; + this.checkPreview(); + this.updatePreview(); + } + } + }); + + mxEvent.addListener(document, 'keydown', this.keyHandler); + mxEvent.addListener(document, 'keyup', this.keyHandler); +}; + +/** + * Variable: graph + * + * Reference to the enclosing . + */ +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: handlesVisible + * + * Whether the handles of the selection are currently visible. + */ +mxGraphHandler.prototype.handlesVisible = true; + +/** + * Variable: guide + * + * Holds the 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 over 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: removeEmptyParents + * + * If empty parents should be removed from the model after all child cells + * have been moved out. Default is true. + */ +mxGraphHandler.prototype.removeEmptyParents = false; + +/** + * 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 because + * the HTML preview does not "let events through". Default is false. + */ +mxGraphHandler.prototype.htmlPreview = false; + +/** + * Variable: shape + * + * Reference to the 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: rotationEnabled + * + * Specifies if the bounding box should allow for rotation. Default is true. + */ +mxGraphHandler.prototype.rotationEnabled = true; + +/** + * Variable: maxLivePreview + * + * Maximum number of cells for which live preview should be used. Default is 0 + * which means no live preview. + */ +mxGraphHandler.prototype.maxLivePreview = 0; + +/** + * Variable: allowLivePreview + * + * If live preview is allowed on this system. Default is true for systems with + * SVG support. + */ +mxGraphHandler.prototype.allowLivePreview = mxClient.IS_SVG; + +/** + * Function: isEnabled + * + * Returns . + */ +mxGraphHandler.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Sets . + */ +mxGraphHandler.prototype.setEnabled = function(value) +{ + this.enabled = value; +}; + +/** + * Function: isCloneEnabled + * + * Returns . + */ +mxGraphHandler.prototype.isCloneEnabled = function() +{ + return this.cloneEnabled; +}; + +/** + * Function: setCloneEnabled + * + * Sets . + * + * Parameters: + * + * value - Boolean that specifies the new clone enabled state. + */ +mxGraphHandler.prototype.setCloneEnabled = function(value) +{ + this.cloneEnabled = value; +}; + +/** + * Function: isMoveEnabled + * + * Returns . + */ +mxGraphHandler.prototype.isMoveEnabled = function() +{ + return this.moveEnabled; +}; + +/** + * Function: setMoveEnabled + * + * Sets . + */ +mxGraphHandler.prototype.setMoveEnabled = function(value) +{ + this.moveEnabled = value; +}; + +/** + * Function: isSelectEnabled + * + * Returns . + */ +mxGraphHandler.prototype.isSelectEnabled = function() +{ + return this.selectEnabled; +}; + +/** + * Function: setSelectEnabled + * + * Sets . + */ +mxGraphHandler.prototype.setSelectEnabled = function(value) +{ + this.selectEnabled = value; +}; + +/** + * Function: isRemoveCellsFromParent + * + * Returns . + */ +mxGraphHandler.prototype.isRemoveCellsFromParent = function() +{ + return this.removeCellsFromParent; +}; + +/** + * Function: setRemoveCellsFromParent + * + * Sets . + */ +mxGraphHandler.prototype.setRemoveCellsFromParent = function(value) +{ + this.removeCellsFromParent = value; +}; + +/** + * Function: isPropagateSelectionCell + * + * Returns true if the given cell and parent should propagate + * selection state to the parent. + */ +mxGraphHandler.prototype.isPropagateSelectionCell = function(cell, immediate, me) +{ + var parent = this.graph.model.getParent(cell); + + if (immediate) + { + var geo = (this.graph.model.isEdge(cell)) ? null : + this.graph.getCellGeometry(cell); + + return !this.graph.isSiblingSelected(cell) && + ((geo != null && geo.relative) || + !this.graph.isSwimlane(parent)); + } + else + { + return (!this.graph.isToggleEvent(me.getEvent()) || + (!this.graph.isSiblingSelected(cell) && + !this.graph.isCellSelected(cell) && + (!this.graph.isSwimlane(parent)) || + this.graph.isCellSelected(parent))) && + (this.graph.isToggleEvent(me.getEvent()) || + !this.graph.isCellSelected(parent)); + } +}; + +/** + * Function: getInitialCellForEvent + * + * Hook to return initial cell for the given event. This returns + * the topmost cell that is not a swimlane or is selected. + */ +mxGraphHandler.prototype.getInitialCellForEvent = function(me) +{ + var state = me.getState(); + + if ((!this.graph.isToggleEvent(me.getEvent()) || !mxEvent.isAltDown(me.getEvent())) && + state != null && !this.graph.isCellSelected(state.cell)) + { + var model = this.graph.model; + var next = this.graph.view.getState(model.getParent(state.cell)); + + while (next != null && !this.graph.isCellSelected(next.cell) && + (model.isVertex(next.cell) || model.isEdge(next.cell)) && + this.isPropagateSelectionCell(state.cell, true, me)) + { + state = next; + next = this.graph.view.getState(this.graph.getModel().getParent(state.cell)); + } + } + + return (state != null) ? state.cell : null; +}; + +/** + * Function: isDelayedSelection + * + * Returns true if the cell or one of its ancestors is selected. + */ +mxGraphHandler.prototype.isDelayedSelection = function(cell, me) +{ + if (!this.graph.isToggleEvent(me.getEvent()) || !mxEvent.isAltDown(me.getEvent())) + { + while (cell != null) + { + if (this.graph.selectionCellsHandler.isHandled(cell)) + { + return this.graph.cellEditor.getEditingCell() != cell; + } + + cell = this.graph.model.getParent(cell); + } + } + + return this.graph.isToggleEvent(me.getEvent()) && !mxEvent.isAltDown(me.getEvent()); +}; + +/** + * Function: selectDelayed + * + * Implements the delayed selection for the given mouse event. + */ +mxGraphHandler.prototype.selectDelayed = function(me) +{ + if (!this.graph.popupMenuHandler.isPopupTrigger(me)) + { + var cell = me.getCell(); + + if (cell == null) + { + cell = this.cell; + } + + this.selectCellForEvent(cell, me); + } +}; + +/** + * Function: selectCellForEvent + * + * Selects the given cell for the given . + */ +mxGraphHandler.prototype.selectCellForEvent = function(cell, me) +{ + var state = this.graph.view.getState(cell); + + if (state != null) + { + if (me.isSource(state.control)) + { + this.graph.selectCellForEvent(cell, me.getEvent()); + } + else + { + if (!this.graph.isToggleEvent(me.getEvent()) || + !mxEvent.isAltDown(me.getEvent())) + { + var model = this.graph.getModel(); + var parent = model.getParent(cell); + + while (this.graph.view.getState(parent) != null && + (model.isVertex(parent) || model.isEdge(parent)) && + this.isPropagateSelectionCell(cell, false, me)) + { + cell = parent; + parent = model.getParent(cell); + } + } + + this.graph.selectCellForEvent(cell, me.getEvent()); + } + } + + return cell; +}; + +/** + * Function: consumeMouseEvent + * + * Consumes the given mouse event. NOTE: This may be used to enable click + * events for links in labels on iOS as follows as consuming the initial + * touchStart disables firing the subsequent click event on the link. + * + * + * mxGraphHandler.prototype.consumeMouseEvent = function(evtName, me) + * { + * var source = mxEvent.getSource(me.getEvent()); + * + * if (!mxEvent.isTouchEvent(me.getEvent()) || source.nodeName != 'A') + * { + * me.consume(); + * } + * } + * + */ +mxGraphHandler.prototype.consumeMouseEvent = function(evtName, me) +{ + me.consume(); +}; + +/** + * 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() && + me.getState() != null && !mxEvent.isMultiTouchEvent(me.getEvent())) + { + var cell = this.getInitialCellForEvent(me); + this.delayedSelection = this.isDelayedSelection(cell, me); + this.cell = null; + + 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()); + } + else if (this.delayedSelection) + { + this.cell = cell; + } + + this.cellWasClicked = true; + this.consumeMouseEvent(mxEvent.MOUSE_DOWN, me); + } + } +}; + +/** + * 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 - 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 used as the preview bounds for + * moving the given cells. + */ +mxGraphHandler.prototype.getPreviewBounds = function(cells) +{ + var bounds = this.getBoundingBox(cells); + + if (bounds != null) + { + // Corrects width and height + bounds.width = Math.max(0, bounds.width - 1); + bounds.height = Math.max(0, bounds.height - 1); + + if (bounds.width < this.minimumSize) + { + var dx = this.minimumSize - bounds.width; + bounds.x -= dx / 2; + bounds.width = this.minimumSize; + } + else + { + bounds.x = Math.round(bounds.x); + bounds.width = Math.ceil(bounds.width); + } + + var tr = this.graph.view.translate; + var s = this.graph.view.scale; + + if (bounds.height < this.minimumSize) + { + var dy = this.minimumSize - bounds.height; + bounds.y -= dy / 2; + bounds.height = this.minimumSize; + } + else + { + bounds.y = Math.round(bounds.y); + bounds.height = Math.ceil(bounds.height); + } + } + + return bounds; +}; + +/** + * Function: getBoundingBox + * + * Returns the union of the for the given array of . + * For vertices, this method uses the bounding box of the corresponding shape + * if one exists. The bounding box of the corresponding text label and all + * controls and overlays are ignored. See also: and + * . + * + * Parameters: + * + * cells - Array of whose bounding box should be returned. + */ +mxGraphHandler.prototype.getBoundingBox = 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.graph.view.getState(cells[i]); + + if (state != null) + { + var bbox = state; + + if (model.isVertex(cells[i]) && state.shape != null && state.shape.boundingBox != null) + { + bbox = state.shape.boundingBox; + } + + if (result == null) + { + result = mxRectangle.fromRectangle(bbox); + } + else + { + result.add(bbox); + } + } + } + } + } + + return result; +}; + +/** + * 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; + + 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()); + shape.pointerEvents = false; + + // Workaround for artifacts on iOS + if (mxClient.IS_IOS) + { + shape.getSvgScreenOffset = function() + { + return 0; + }; + } + } + + return shape; +}; + +/** + * Function: start + * + * Starts the handling of the mouse gesture. + */ +mxGraphHandler.prototype.start = function(cell, x, y, cells) +{ + this.cell = cell; + this.first = mxUtils.convertPoint(this.graph.container, x, y); + this.cells = (cells != null) ? cells : this.getCells(this.cell); + this.bounds = this.graph.getView().getBounds(this.cells); + this.pBounds = this.getPreviewBounds(this.cells); + this.allCells = new mxDictionary(); + this.cloning = false; + this.cellCount = 0; + + for (var i = 0; i < this.cells.length; i++) + { + this.cellCount += this.addStates(this.cells[i], this.allCells); + } + + if (this.guidesEnabled) + { + this.guide = new mxGuide(this.graph, this.getGuideStates()); + var parent = this.graph.model.getParent(cell); + var ignore = this.graph.model.getChildCount(parent) < 2; + + // Uses connected states as guides + var connected = new mxDictionary(); + var opps = this.graph.getOpposites(this.graph.getEdges(this.cell), this.cell); + + for (var i = 0; i < opps.length; i++) + { + var state = this.graph.view.getState(opps[i]); + + if (state != null && !connected.get(state)) + { + connected.put(state, true); + } + } + + this.guide.isStateIgnored = mxUtils.bind(this, function(state) + { + var p = this.graph.model.getParent(state.cell); + + return state.cell != null && ((!this.cloning && + this.isCellMoving(state.cell)) || + (state.cell != (this.target || parent) && !ignore && + !connected.get(state) && + (this.target == null || this.graph.model.getChildCount( + this.target) >= 2) && p != (this.target || parent))); + }); + } +}; + +/** + * Function: addStates + * + * Adds the states for the given cell recursively to the given dictionary. + */ +mxGraphHandler.prototype.addStates = function(cell, dict) +{ + var state = this.graph.view.getState(cell); + var count = 0; + + if (state != null && dict.get(cell) == null) + { + dict.put(cell, state); + count++; + + var childCount = this.graph.model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + count += this.addStates(this.graph.model.getChildAt(cell, i), dict); + } + } + + return count; +}; + +/** + * Function: isCellMoving + * + * Returns true if the given cell is currently being moved. + */ +mxGraphHandler.prototype.isCellMoving = function(cell) +{ + return this.allCells.get(cell) != null; +}; + +/** + * Function: useGuidesForEvent + * + * Returns true if the guides should be used for the given . + * This implementation returns . + */ +mxGraphHandler.prototype.useGuidesForEvent = function(me) +{ + return (this.guide != null) ? this.guide.isEnabledForEvent(me.getEvent()) && + !this.graph.isConstrainedEvent(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: getDelta + * + * Returns an that represents the vector for moving the cells + * for the given . + */ +mxGraphHandler.prototype.getDelta = function(me) +{ + var point = mxUtils.convertPoint(this.graph.container, me.getX(), me.getY()); + + return new mxPoint(point.x - this.first.x - this.graph.panDx, + point.y - this.first.y - this.graph.panDy); +}; + +/** + * Function: updateHint + * + * Hook for subclassers do show details while the handler is active. + */ +mxGraphHandler.prototype.updateHint = function(me) { }; + +/** + * Function: removeHint + * + * Hooks for subclassers to hide details when the handler gets inactive. + */ +mxGraphHandler.prototype.removeHint = function() { }; + +/** + * Function: roundLength + * + * Hook for rounding the unscaled vector. Allows for half steps in the raster so + * numbers coming in should be rounded if no half steps are allowed (ie for non + * aligned standard moving where pixel steps should be preferred). + */ +mxGraphHandler.prototype.roundLength = function(length) +{ + return Math.round(length * 100) / 100; +}; + +/** + * Function: isValidDropTarget + * + * Returns true if the given cell is a valid drop target. + */ +mxGraphHandler.prototype.isValidDropTarget = function(target, me) +{ + return this.graph.model.getParent(this.cell) != target; +}; + +/** + * Function: checkPreview + * + * Updates the preview if cloning state has changed. + */ +mxGraphHandler.prototype.checkPreview = function() +{ + if (this.livePreviewActive && this.cloning) + { + this.resetLivePreview(); + this.livePreviewActive = false; + } + else if (this.maxLivePreview >= this.cellCount && !this.livePreviewActive && this.allowLivePreview) + { + if (!this.cloning || !this.livePreviewActive) + { + this.livePreviewActive = true; + this.livePreviewUsed = true; + } + } + else if (!this.livePreviewUsed && this.shape == null) + { + this.shape = this.createPreviewShape(this.bounds); + } +}; + +/** + * 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 && !this.suspended) + { + // Stops moving if a multi touch event is received + if (mxEvent.isMultiTouchEvent(me.getEvent())) + { + this.reset(); + return; + } + + var delta = this.getDelta(me); + var tol = graph.tolerance; + + if (this.shape != null || this.livePreviewActive || Math.abs(delta.x) > tol || Math.abs(delta.y) > tol) + { + // Highlight is used for highlighting drop targets + if (this.highlight == null) + { + this.highlight = new mxCellHighlight(this.graph, + mxConstants.DROP_TARGET_COLOR, 3); + } + + var clone = graph.isCloneEvent(me.getEvent()) && graph.isCellsCloneable() && this.isCloneEnabled(); + var gridEnabled = graph.isGridEnabledEvent(me.getEvent()); + var cell = me.getCell(); + var hideGuide = true; + var target = null; + this.cloning = clone; + + 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, clone); + } + + var state = graph.getView().getState(target); + var highlight = false; + + if (state != null && (clone || this.isValidDropTarget(target, me))) + { + 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(); + } + + if (this.guide != null && this.useGuidesForEvent(me)) + { + delta = this.guide.move(this.bounds, delta, gridEnabled, clone); + hideGuide = false; + } + else + { + delta = this.graph.snapDelta(delta, this.bounds, !gridEnabled, false, false); + } + + if (this.guide != null && hideGuide) + { + this.guide.hide(); + } + + // Constrained movement if shift key is pressed + if (graph.isConstrainedEvent(me.getEvent())) + { + if (Math.abs(delta.x) > Math.abs(delta.y)) + { + delta.y = 0; + } + else + { + delta.x = 0; + } + } + + this.checkPreview(); + + if (this.currentDx != delta.x || this.currentDy != delta.y) + { + this.currentDx = delta.x; + this.currentDy = delta.y; + this.updatePreview(); + } + } + + this.updateHint(me); + this.consumeMouseEvent(mxEvent.MOUSE_MOVE, me); + + // 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 || me.sourceState != null) && !graph.isMouseDown) + { + var cursor = graph.getCursorForMouseEvent(me); + + 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; + } + } + + // Sets the cursor on the original source state under the mouse + // instead of the event source state which can be the parent + if (cursor != null && me.sourceState != null) + { + me.sourceState.setCursor(cursor); + } + } +}; + +/** + * Function: updatePreview + * + * Updates the bounds of the preview shape. + */ +mxGraphHandler.prototype.updatePreview = function(remote) +{ + if (this.livePreviewUsed && !remote) + { + if (this.cells != null) + { + this.setHandlesVisibleForCells( + this.graph.selectionCellsHandler. + getHandledSelectionCells(), false); + this.updateLivePreview(this.currentDx, this.currentDy); + } + } + else + { + this.updatePreviewShape(); + } +}; + +/** + * Function: updatePreviewShape + * + * Updates the bounds of the preview shape. + */ +mxGraphHandler.prototype.updatePreviewShape = function() +{ + if (this.shape != null && this.pBounds != null) + { + this.shape.bounds = new mxRectangle(Math.round(this.pBounds.x + this.currentDx), + Math.round(this.pBounds.y + this.currentDy), this.pBounds.width, this.pBounds.height); + this.shape.redraw(); + } +}; + +/** + * Function: updateLivePreview + * + * Updates the bounds of the preview shape. + */ +mxGraphHandler.prototype.updateLivePreview = function(dx, dy) +{ + if (!this.suspended) + { + var states = []; + + if (this.allCells != null) + { + this.allCells.visit(mxUtils.bind(this, function(key, state) + { + var realState = this.graph.view.getState(state.cell); + + // Checks if cell was removed or replaced + if (realState != state) + { + state.destroy(); + + if (realState != null) + { + this.allCells.put(state.cell, realState); + } + else + { + this.allCells.remove(state.cell); + } + + state = realState; + } + + if (state != null) + { + // Saves current state + var tempState = state.clone(); + states.push([state, tempState]); + + // Makes transparent for events to detect drop targets + if (state.shape != null) + { + if (state.shape.originalPointerEvents == null) + { + state.shape.originalPointerEvents = state.shape.pointerEvents; + } + + state.shape.pointerEvents = false; + + if (state.text != null) + { + if (state.text.originalPointerEvents == null) + { + state.text.originalPointerEvents = state.text.pointerEvents; + } + + state.text.pointerEvents = false; + } + } + + // Temporarily changes position + if (this.graph.model.isVertex(state.cell)) + { + state.x += dx; + state.y += dy; + + // Draws the live preview + if (!this.cloning) + { + state.view.graph.cellRenderer.redraw(state, true); + + // Forces redraw of connected edges after all states + // have been updated but avoids update of state + state.view.invalidate(state.cell); + state.invalid = false; + + // Hides folding icon + if (state.control != null && state.control.node != null) + { + state.control.node.style.visibility = 'hidden'; + } + } + // Clone live preview may use text bounds + else if (state.text != null) + { + state.text.updateBoundingBox(); + + // Fixes preview box for edge labels + if (state.text.boundingBox != null) + { + state.text.boundingBox.x += dx; + state.text.boundingBox.y += dy; + } + + if (state.text.unrotatedBoundingBox != null) + { + state.text.unrotatedBoundingBox.x += dx; + state.text.unrotatedBoundingBox.y += dy; + } + } + } + } + })); + } + + // Resets the handler if everything was removed + if (states.length == 0) + { + this.reset(); + } + else + { + // Redraws connected edges + var s = this.graph.view.scale; + + for (var i = 0; i < states.length; i++) + { + var state = states[i][0]; + + if (this.graph.model.isEdge(state.cell)) + { + var geometry = this.graph.getCellGeometry(state.cell); + var points = []; + + if (geometry != null && geometry.points != null) + { + for (var j = 0; j < geometry.points.length; j++) + { + if (geometry.points[j] != null) + { + points.push(new mxPoint( + geometry.points[j].x + dx / s, + geometry.points[j].y + dy / s)); + } + } + } + + var source = state.visibleSourceState; + var target = state.visibleTargetState; + var pts = states[i][1].absolutePoints; + + if (source == null || !this.isCellMoving(source.cell)) + { + var pt0 = pts[0]; + state.setAbsoluteTerminalPoint(new mxPoint(pt0.x + dx, pt0.y + dy), true); + source = null; + } + else + { + state.view.updateFixedTerminalPoint(state, source, true, + this.graph.getConnectionConstraint(state, source, true)); + } + + if (target == null || !this.isCellMoving(target.cell)) + { + var ptn = pts[pts.length - 1]; + state.setAbsoluteTerminalPoint(new mxPoint(ptn.x + dx, ptn.y + dy), false); + target = null; + } + else + { + state.view.updateFixedTerminalPoint(state, target, false, + this.graph.getConnectionConstraint(state, target, false)); + } + + state.view.updatePoints(state, points, source, target); + state.view.updateFloatingTerminalPoints(state, source, target); + state.view.updateEdgeLabelOffset(state); + state.invalid = false; + + // Draws the live preview but avoids update of state + if (!this.cloning) + { + state.view.graph.cellRenderer.redraw(state, true); + } + } + } + + this.graph.view.validate(); + this.redrawHandles(states); + this.resetPreviewStates(states); + } + } +}; + +/** + * Function: redrawHandles + * + * Redraws the preview shape for the given states array. + */ +mxGraphHandler.prototype.redrawHandles = function(states) +{ + for (var i = 0; i < states.length; i++) + { + var handler = this.graph.selectionCellsHandler.getHandler(states[i][0].cell); + + if (handler != null) + { + handler.redraw(true); + } + } +}; + +/** + * Function: resetPreviewStates + * + * Resets the given preview states array. + */ +mxGraphHandler.prototype.resetPreviewStates = function(states) +{ + for (var i = 0; i < states.length; i++) + { + states[i][0].setState(states[i][1]); + } +}; + +/** + * Function: suspend + * + * Suspends the livew preview. + */ +mxGraphHandler.prototype.suspend = function() +{ + if (!this.suspended) + { + if (this.livePreviewUsed) + { + this.updateLivePreview(0, 0); + } + + if (this.shape != null) + { + this.shape.node.style.visibility = 'hidden'; + } + + if (this.guide != null) + { + this.guide.setVisible(false); + } + + this.suspended = true; + } +}; + +/** + * Function: resume + * + * Suspends the livew preview. + */ +mxGraphHandler.prototype.resume = function() +{ + if (this.suspended) + { + this.suspended = null; + + if (this.livePreviewUsed) + { + this.livePreviewActive = true; + } + + if (this.shape != null) + { + this.shape.node.style.visibility = 'visible'; + } + + if (this.guide != null) + { + this.guide.setVisible(true); + } + } +}; + +/** + * Function: resetLivePreview + * + * Resets the livew preview. + */ +mxGraphHandler.prototype.resetLivePreview = function() +{ + if (this.allCells != null) + { + this.allCells.visit(mxUtils.bind(this, function(key, state) + { + // Restores event handling + if (state.shape != null && state.shape.originalPointerEvents != null) + { + state.shape.pointerEvents = state.shape.originalPointerEvents; + state.shape.originalPointerEvents = null; + + // Forces repaint even if not moved to update pointer events + state.shape.bounds = null; + + if (state.text != null) + { + state.text.pointerEvents = state.text.originalPointerEvents; + state.text.originalPointerEvents = null; + } + } + + // Shows folding icon + if (state.control != null && state.control.node != null && + state.control.node.style.visibility == 'hidden') + { + state.control.node.style.visibility = ''; + } + + // Fixes preview box for edge labels + if (!this.cloning) + { + if (state.text != null) + { + state.text.updateBoundingBox(); + } + } + + // Forces repaint of connected edges + state.view.invalidate(state.cell); + })); + + // Repaints all invalid states + this.graph.view.validate(); + } +}; + +/** + * Function: setHandlesVisibleForCells + * + * Sets wether the handles attached to the given cells are visible. + * + * Parameters: + * + * cells - Array of . + * visible - Boolean that specifies if the handles should be visible. + * force - Forces an update of the handler regardless of the last used value. + */ +mxGraphHandler.prototype.setHandlesVisibleForCells = function(cells, visible, force) +{ + if (force || this.handlesVisible != visible) + { + this.handlesVisible = visible; + + for (var i = 0; i < cells.length; i++) + { + var handler = this.graph.selectionCellsHandler.getHandler(cells[i]); + + if (handler != null) + { + handler.setHandlesVisible(visible); + + if (visible) + { + handler.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()) + { + if (this.livePreviewUsed) + { + this.resetLivePreview(); + } + + if (this.cell != null && this.first != null && (this.shape != null || this.livePreviewUsed) && + this.currentDx != null && this.currentDy != null) + { + var graph = this.graph; + 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 clone = graph.isCloneEvent(me.getEvent()) && graph.isCellsCloneable() && this.isCloneEnabled(); + var scale = graph.getView().scale; + var dx = this.roundLength(this.currentDx / scale); + var dy = this.roundLength(this.currentDy / scale); + var target = this.target; + + if (graph.isSplitEnabled() && graph.isSplitTarget(target, this.cells, me.getEvent())) + { + graph.splitEdge(target, this.cells, null, dx, dy, + me.getGraphX(), me.getGraphY()); + } + 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) + { + this.consumeMouseEvent(mxEvent.MOUSE_UP, me); + } + + this.reset(); +}; + +/** + * Function: reset + * + * Resets the state of this handler. + */ +mxGraphHandler.prototype.reset = function() +{ + if (this.livePreviewUsed) + { + this.resetLivePreview(); + this.setHandlesVisibleForCells( + this.graph.selectionCellsHandler. + getHandledSelectionCells(), true); + } + + this.destroyShapes(); + this.removeHint(); + + this.delayedSelection = false; + this.livePreviewActive = null; + this.livePreviewUsed = null; + this.cellWasClicked = false; + this.suspended = null; + this.currentDx = null; + this.currentDy = null; + this.cellCount = null; + this.cloning = false; + this.allCells = null; + this.pBounds = null; + this.guides = null; + this.target = null; + this.first = null; + this.cells = null; + this.cell = 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); + + if (pState != null) + { + var pt = mxUtils.convertPoint(this.graph.container, + mxEvent.getClientX(evt), mxEvent.getClientY(evt)); + var alpha = mxUtils.toRadians(mxUtils.getValue(pState.style, mxConstants.STYLE_ROTATION) || 0); + + if (alpha != 0) + { + var cos = Math.cos(-alpha); + var sin = Math.sin(-alpha); + var cx = new mxPoint(pState.getCenterX(), pState.getCenterY()); + pt = mxUtils.getRotatedPoint(pt, cos, sin, cx); + } + + return !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 + var parent = this.graph.getModel().getParent(this.cell); + + if (target == null && this.isRemoveCellsFromParent() && + this.shouldRemoveCellsFromParent(parent, cells, evt)) + { + target = this.graph.getDefaultParent(); + } + + // Cloning into locked cells is not allowed + clone = clone && !this.graph.isCellLocked(target || this.graph.getDefaultParent()); + + this.graph.getModel().beginUpdate(); + try + { + var parents = []; + + // Removes parent if all child cells are removed + if (!clone && target != null && this.removeEmptyParents) + { + // Collects all non-selected parents + var dict = new mxDictionary(); + + for (var i = 0; i < cells.length; i++) + { + dict.put(cells[i], true); + } + + // LATER: Recurse up the cell hierarchy + for (var i = 0; i < cells.length; i++) + { + var par = this.graph.model.getParent(cells[i]); + + if (par != null && !dict.get(par)) + { + dict.put(par, true); + parents.push(par); + } + } + } + + // 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, dy, clone, target, evt); + + // Removes parent if all child cells are removed + var temp = []; + + for (var i = 0; i < parents.length; i++) + { + if (this.shouldRemoveParent(parents[i])) + { + temp.push(parents[i]); + } + } + + this.graph.removeCells(temp, false); + } + finally + { + this.graph.getModel().endUpdate(); + } + + // Selects the new cells if cells have been cloned + if (clone) + { + this.graph.setSelectionCells(cells); + } + + if (this.isSelectEnabled() && this.scrollOnMove) + { + this.graph.scrollCellToVisible(cells[0]); + } +}; + +/** + * Function: shouldRemoveParent + * + * Returns true if the given parent should be removed after removal of child cells. + */ +mxGraphHandler.prototype.shouldRemoveParent = function(parent) +{ + var state = this.graph.view.getState(parent); + + return state != null && (this.graph.model.isEdge(state.cell) || this.graph.model.isVertex(state.cell)) && + this.graph.isCellDeletable(state.cell) && this.graph.model.getChildCount(state.cell) == 0 && + this.graph.isTransparentState(state); +}; + +/** + * 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); + + if (this.escapeHandler != null) + { + this.graph.removeListener(this.escapeHandler); + this.escapeHandler = null; + } + + if (this.refreshHandler != null) + { + this.graph.getModel().removeListener(this.refreshHandler); + this.graph.removeListener(this.refreshHandler); + this.refreshHandler = null; + } + + mxEvent.removeListener(document, 'keydown', this.keyHandler); + mxEvent.removeListener(document, 'keyup', this.keyHandler); + + this.destroyShapes(); + this.removeHint(); +}; diff --git a/mxclient/js/handler/mxHandle.js b/mxclient/js/handler/mxHandle.js new file mode 100644 index 0000000..dac27c6 --- /dev/null +++ b/mxclient/js/handler/mxHandle.js @@ -0,0 +1,352 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxHandle + * + * Implements a single custom handle for vertices. + * + * Constructor: mxHandle + * + * Constructs a new handle for the given state. + * + * Parameters: + * + * state - of the cell to be handled. + */ +function mxHandle(state, cursor, image, shape) +{ + this.graph = state.view.graph; + this.state = state; + this.cursor = (cursor != null) ? cursor : this.cursor; + this.image = (image != null) ? image : this.image; + this.shape = (shape != null) ? shape : null; + this.init(); +}; + +/** + * Variable: cursor + * + * Specifies the cursor to be used for this handle. Default is 'default'. + */ +mxHandle.prototype.cursor = 'default'; + +/** + * Variable: image + * + * Specifies the to be used to render the handle. Default is null. + */ +mxHandle.prototype.image = null; + +/** + * Variable: ignoreGrid + * + * Default is false. + */ +mxHandle.prototype.ignoreGrid = false; + +/** + * Function: getPosition + * + * Hook for subclassers to return the current position of the handle. + */ +mxHandle.prototype.getPosition = function(bounds) { }; + +/** + * Function: setPosition + * + * Hooks for subclassers to update the style in the . + */ +mxHandle.prototype.setPosition = function(bounds, pt, me) { }; + +/** + * Function: execute + * + * Hook for subclassers to execute the handle. + */ +mxHandle.prototype.execute = function(me) { }; + +/** + * Function: copyStyle + * + * Sets the cell style with the given name to the corresponding value in . + */ +mxHandle.prototype.copyStyle = function(key) +{ + this.graph.setCellStyles(key, this.state.style[key], [this.state.cell]); +}; + +/** + * Function: processEvent + * + * Processes the given and invokes . + */ +mxHandle.prototype.processEvent = function(me) +{ + var scale = this.graph.view.scale; + var tr = this.graph.view.translate; + var pt = new mxPoint(me.getGraphX() / scale - tr.x, me.getGraphY() / scale - tr.y); + + // Center shape on mouse cursor + if (this.shape != null && this.shape.bounds != null) + { + pt.x -= this.shape.bounds.width / scale / 4; + pt.y -= this.shape.bounds.height / scale / 4; + } + + // Snaps to grid for the rotated position then applies the rotation for the direction after that + var alpha1 = -mxUtils.toRadians(this.getRotation()); + var alpha2 = -mxUtils.toRadians(this.getTotalRotation()) - alpha1; + pt = this.flipPoint(this.rotatePoint(this.snapPoint(this.rotatePoint(pt, alpha1), + this.ignoreGrid || !this.graph.isGridEnabledEvent(me.getEvent())), alpha2)); + this.setPosition(this.state.getPaintBounds(), pt, me); + this.redraw(); +}; + +/** + * Function: positionChanged + * + * Should be called after in . + * This repaints the state using . + */ +mxHandle.prototype.positionChanged = function() +{ + if (this.state.text != null) + { + this.state.text.apply(this.state); + } + + if (this.state.shape != null) + { + this.state.shape.apply(this.state); + } + + this.graph.cellRenderer.redraw(this.state, true); +}; + +/** + * Function: getRotation + * + * Returns the rotation defined in the style of the cell. + */ +mxHandle.prototype.getRotation = function() +{ + if (this.state.shape != null) + { + return this.state.shape.getRotation(); + } + + return 0; +}; + +/** + * Function: getTotalRotation + * + * Returns the rotation from the style and the rotation from the direction of + * the cell. + */ +mxHandle.prototype.getTotalRotation = function() +{ + if (this.state.shape != null) + { + return this.state.shape.getShapeRotation(); + } + + return 0; +}; + +/** + * Function: init + * + * Creates and initializes the shapes required for this handle. + */ +mxHandle.prototype.init = function() +{ + var html = this.isHtmlRequired(); + + if (this.image != null) + { + this.shape = new mxImageShape(new mxRectangle(0, 0, this.image.width, this.image.height), this.image.src); + this.shape.preserveImageAspect = false; + } + else if (this.shape == null) + { + this.shape = this.createShape(html); + } + + this.initShape(html); +}; + +/** + * Function: createShape + * + * Creates and returns the shape for this handle. + */ +mxHandle.prototype.createShape = function(html) +{ + var bounds = new mxRectangle(0, 0, mxConstants.HANDLE_SIZE, mxConstants.HANDLE_SIZE); + + return new mxRectangleShape(bounds, mxConstants.HANDLE_FILLCOLOR, mxConstants.HANDLE_STROKECOLOR); +}; + +/** + * Function: initShape + * + * Initializes and sets its cursor. + */ +mxHandle.prototype.initShape = function(html) +{ + if (html && this.shape.isHtmlAllowed()) + { + this.shape.dialect = mxConstants.DIALECT_STRICTHTML; + this.shape.init(this.graph.container); + } + else + { + this.shape.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ? + mxConstants.DIALECT_MIXEDHTML : mxConstants.DIALECT_SVG; + + if (this.cursor != null) + { + this.shape.init(this.graph.getView().getOverlayPane()); + } + } + + mxEvent.redirectMouseEvents(this.shape.node, this.graph, this.state); + this.shape.node.style.cursor = this.cursor; +}; + +/** + * Function: redraw + * + * Renders the shape for this handle. + */ +mxHandle.prototype.redraw = function() +{ + if (this.shape != null && this.state.shape != null) + { + var pt = this.getPosition(this.state.getPaintBounds()); + + if (pt != null) + { + var alpha = mxUtils.toRadians(this.getTotalRotation()); + pt = this.rotatePoint(this.flipPoint(pt), alpha); + + var scale = this.graph.view.scale; + var tr = this.graph.view.translate; + this.shape.bounds.x = Math.floor((pt.x + tr.x) * scale - this.shape.bounds.width / 2); + this.shape.bounds.y = Math.floor((pt.y + tr.y) * scale - this.shape.bounds.height / 2); + + // Needed to force update of text bounds + this.shape.redraw(); + } + } +}; + +/** + * Function: isHtmlRequired + * + * Returns true if this handle should be rendered in HTML. This returns true if + * the text node is in the graph container. + */ +mxHandle.prototype.isHtmlRequired = function() +{ + return this.state.text != null && this.state.text.node.parentNode == this.graph.container; +}; + +/** + * Function: rotatePoint + * + * Rotates the point by the given angle. + */ +mxHandle.prototype.rotatePoint = function(pt, alpha) +{ + var bounds = this.state.getCellBounds(); + var cx = new mxPoint(bounds.getCenterX(), bounds.getCenterY()); + var cos = Math.cos(alpha); + var sin = Math.sin(alpha); + + return mxUtils.getRotatedPoint(pt, cos, sin, cx); +}; + +/** + * Function: flipPoint + * + * Flips the given point vertically and/or horizontally. + */ +mxHandle.prototype.flipPoint = function(pt) +{ + if (this.state.shape != null) + { + var bounds = this.state.getCellBounds(); + + if (this.state.shape.flipH) + { + pt.x = 2 * bounds.x + bounds.width - pt.x; + } + + if (this.state.shape.flipV) + { + pt.y = 2 * bounds.y + bounds.height - pt.y; + } + } + + return pt; +}; + +/** + * Function: snapPoint + * + * Snaps the given point to the grid if ignore is false. This modifies + * the given point in-place and also returns it. + */ +mxHandle.prototype.snapPoint = function(pt, ignore) +{ + if (!ignore) + { + pt.x = this.graph.snap(pt.x); + pt.y = this.graph.snap(pt.y); + } + + return pt; +}; + +/** + * Function: setVisible + * + * Shows or hides this handle. + */ +mxHandle.prototype.setVisible = function(visible) +{ + if (this.shape != null && this.shape.node != null) + { + this.shape.node.style.display = (visible) ? '' : 'none'; + } +}; + +/** + * Function: reset + * + * Resets the state of this handle by setting its visibility to true. + */ +mxHandle.prototype.reset = function() +{ + this.setVisible(true); + this.state.style = this.graph.getCellStyle(this.state.cell); + this.positionChanged(); +}; + +/** + * Function: destroy + * + * Destroys this handle. + */ +mxHandle.prototype.destroy = function() +{ + if (this.shape != null) + { + this.shape.destroy(); + this.shape = null; + } +}; diff --git a/mxclient/js/handler/mxKeyHandler.js b/mxclient/js/handler/mxKeyHandler.js new file mode 100644 index 0000000..6a391f0 --- /dev/null +++ b/mxclient/js/handler/mxKeyHandler.js @@ -0,0 +1,428 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * 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 + * 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 . + * + * 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 . + * 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 = []; + + this.keydownHandler = mxUtils.bind(this, function(evt) + { + this.keyDown(evt); + }); + + // Installs the keystroke listener in the target + mxEvent.addListener(this.target, 'keydown', this.keydownHandler); + + // Automatically deallocates memory in IE + if (mxClient.IS_IE) + { + mxEvent.addListener(window, 'unload', + mxUtils.bind(this, function() + { + this.destroy(); + }) + ); + } + } +}; + +/** + * Variable: graph + * + * Reference to the 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 + * . + */ +mxKeyHandler.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Enables or disables event handling by updating . + * + * 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 . + * + * 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 && !mxEvent.isAltDown(evt)) + { + 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 , or the of the + * . + * + * 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 && this.graph.cellEditor.isEventSource(evt))) + { + return true; + } + + // Accepts events from inside the container + return mxUtils.isAncestorNode(this.graph.container, source); +}; + +/** + * Function: keyDown + * + * Handles the event by invoking the function bound to the respective keystroke + * if returns true for the given event and if + * returns false, except for escape for which + * is not invoked. + * + * Parameters: + * + * evt - Key event that represents the keystroke. + */ +mxKeyHandler.prototype.keyDown = function(evt) +{ + if (this.isEnabledForEvent(evt)) + { + // Cancels the editing if escape is pressed + if (evt.keyCode == 27 /* Escape */) + { + this.escape(evt); + } + + // Invokes the function for the keystroke + else if (!this.isEventIgnored(evt)) + { + var boundFunction = this.getFunction(evt); + + if (boundFunction != null) + { + boundFunction(evt); + mxEvent.consume(evt); + } + } + } +}; + +/** + * Function: isEnabledForEvent + * + * Returns true if the given event should be handled. is + * called later if the event is not an escape key stroke, in which case + * is called. This implementation returns true if + * returns true for both, this handler and , if the event is not + * consumed and if returns true. + * + * Parameters: + * + * evt - Key event that represents the keystroke. + */ +mxKeyHandler.prototype.isEnabledForEvent = function(evt) +{ + return (this.graph.isEnabled() && !mxEvent.isConsumed(evt) && + this.isGraphEvent(evt) && this.isEnabled()); +}; + +/** + * Function: isEventIgnored + * + * Returns true if the given keystroke should be ignored. This returns + * graph.isEditing(). + * + * Parameters: + * + * evt - Key event that represents the keystroke. + */ +mxKeyHandler.prototype.isEventIgnored = function(evt) +{ + return this.graph.isEditing(); +}; + +/** + * Function: escape + * + * Hook to process ESCAPE keystrokes. This implementation invokes + * 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() +{ + if (this.target != null && this.keydownHandler != null) + { + mxEvent.removeListener(this.target, 'keydown', this.keydownHandler); + this.keydownHandler = null; + } + + this.target = null; +}; diff --git a/mxclient/js/handler/mxPanningHandler.js b/mxclient/js/handler/mxPanningHandler.js new file mode 100644 index 0000000..382f40c --- /dev/null +++ b/mxclient/js/handler/mxPanningHandler.js @@ -0,0 +1,494 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxPanningHandler + * + * Event handler that pans and creates popupmenus. To use the left + * mousebutton for panning without interfering with cell moving and + * resizing, use and . For grid size + * steps while panning, use . This handler is built-into + * and enabled using . + * + * Constructor: mxPanningHandler + * + * Constructs an event handler that creates a + * and pans the graph. + * + * Event: mxEvent.PAN_START + * + * Fires when the panning handler changes its state to true. The + * event property contains the corresponding . + * + * Event: mxEvent.PAN + * + * Fires while handle is processing events. The event property contains + * the corresponding . + * + * Event: mxEvent.PAN_END + * + * Fires when the panning handler changes its state to false. The + * event property contains the corresponding . + */ +function mxPanningHandler(graph) +{ + if (graph != null) + { + this.graph = graph; + this.graph.addMouseListener(this); + + // Handles force panning event + this.forcePanningHandler = mxUtils.bind(this, function(sender, evt) + { + var evtName = evt.getProperty('eventName'); + var me = evt.getProperty('event'); + + if (evtName == mxEvent.MOUSE_DOWN && this.isForcePanningEvent(me)) + { + this.start(me); + this.active = true; + this.fireEvent(new mxEventObject(mxEvent.PAN_START, 'event', me)); + me.consume(); + } + }); + + this.graph.addListener(mxEvent.FIRE_MOUSE_EVENT, this.forcePanningHandler); + + // Handles pinch gestures + this.gestureHandler = mxUtils.bind(this, function(sender, eo) + { + if (this.isPinchEnabled()) + { + var evt = eo.getProperty('event'); + + if (!mxEvent.isConsumed(evt) && evt.type == 'gesturestart') + { + this.initialScale = this.graph.view.scale; + + // Forces start of panning when pinch gesture starts + if (!this.active && this.mouseDownEvent != null) + { + this.start(this.mouseDownEvent); + this.mouseDownEvent = null; + } + } + else if (evt.type == 'gestureend' && this.initialScale != null) + { + this.initialScale = null; + } + + if (this.initialScale != null) + { + this.zoomGraph(evt); + } + } + }); + + this.graph.addListener(mxEvent.GESTURE, this.gestureHandler); + + this.mouseUpListener = mxUtils.bind(this, function() + { + if (this.active) + { + this.reset(); + } + }); + + // Stops scrolling on every mouseup anywhere in the document + mxEvent.addListener(document, 'mouseup', this.mouseUpListener); + } +}; + +/** + * Extends mxEventSource. + */ +mxPanningHandler.prototype = new mxEventSource(); +mxPanningHandler.prototype.constructor = mxPanningHandler; + +/** + * Variable: graph + * + * Reference to the enclosing . + */ +mxPanningHandler.prototype.graph = null; + +/** + * Variable: useLeftButtonForPanning + * + * Specifies if panning should be active for the left mouse button. + * Setting this to true may conflict with . Default is false. + */ +mxPanningHandler.prototype.useLeftButtonForPanning = false; + +/** + * Variable: usePopupTrigger + * + * Specifies if should also be used for panning. + */ +mxPanningHandler.prototype.usePopupTrigger = 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; + +/** + * Variable: pinchEnabled + * + * Specifies if pinch gestures should be handled as zoom. Default is true. + */ +mxPanningHandler.prototype.pinchEnabled = true; + +/** + * Variable: maxScale + * + * Specifies the maximum scale. Default is 8. + */ +mxPanningHandler.prototype.maxScale = 8; + +/** + * Variable: minScale + * + * Specifies the minimum scale. Default is 0.01. + */ +mxPanningHandler.prototype.minScale = 0.01; + +/** + * Variable: dx + * + * Holds the current horizontal offset. + */ +mxPanningHandler.prototype.dx = null; + +/** + * Variable: dy + * + * Holds the current vertical offset. + */ +mxPanningHandler.prototype.dy = null; + +/** + * Variable: startX + * + * Holds the x-coordinate of the start point. + */ +mxPanningHandler.prototype.startX = 0; + +/** + * Variable: startY + * + * Holds the y-coordinate of the start point. + */ +mxPanningHandler.prototype.startY = 0; + +/** + * Function: isActive + * + * Returns true if the handler is currently active. + */ +mxPanningHandler.prototype.isActive = function() +{ + return this.active || this.initialScale != null; +}; + +/** + * Function: isPanningEnabled + * + * Returns . + */ +mxPanningHandler.prototype.isPanningEnabled = function() +{ + return this.panningEnabled; +}; + +/** + * Function: setPanningEnabled + * + * Sets . + */ +mxPanningHandler.prototype.setPanningEnabled = function(value) +{ + this.panningEnabled = value; +}; + +/** + * Function: isPinchEnabled + * + * Returns . + */ +mxPanningHandler.prototype.isPinchEnabled = function() +{ + return this.pinchEnabled; +}; + +/** + * Function: setPinchEnabled + * + * Sets . + */ +mxPanningHandler.prototype.setPinchEnabled = function(value) +{ + this.pinchEnabled = value; +}; + +/** + * 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 + * is true and the event is a popup trigger. + */ +mxPanningHandler.prototype.isPanningTrigger = function(me) +{ + var evt = me.getEvent(); + + return (this.useLeftButtonForPanning && me.getState() == null && + mxEvent.isLeftMouseButton(evt)) || (mxEvent.isControlDown(evt) && + mxEvent.isShiftDown(evt)) || (this.usePopupTrigger && mxEvent.isPopupTrigger(evt)); +}; + +/** + * Function: isForcePanningEvent + * + * Returns true if the given should start panning. This + * implementation always returns true if is true or for + * multi touch events. + */ +mxPanningHandler.prototype.isForcePanningEvent = function(me) +{ + return this.ignoreCell || mxEvent.isMultiTouchEvent(me.getEvent()); +}; + +/** + * 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) +{ + this.mouseDownEvent = me; + + if (!me.isConsumed() && this.isPanningEnabled() && !this.active && this.isPanningTrigger(me)) + { + this.start(me); + this.consumePanningTrigger(me); + } +}; + +/** + * Function: start + * + * Starts panning at the given event. + */ +mxPanningHandler.prototype.start = function(me) +{ + this.dx0 = -this.graph.container.scrollLeft; + this.dy0 = -this.graph.container.scrollTop; + + // Stores the location of the trigger event + this.startX = me.getX(); + this.startY = me.getY(); + this.dx = null; + this.dy = null; + + this.panningTrigger = true; +}; + +/** + * Function: consumePanningTrigger + * + * Consumes the given if it was a panning trigger in + * . The default is to invoke . 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) +{ + this.dx = me.getX() - this.startX; + this.dy = me.getY() - this.startY; + + if (this.active) + { + if (this.previewEnabled) + { + // Applies the grid to the panning steps + if (this.useGrid) + { + this.dx = this.graph.snap(this.dx); + this.dy = this.graph.snap(this.dy); + } + + this.graph.panGraph(this.dx + this.dx0, this.dy + this.dy0); + } + + this.fireEvent(new mxEventObject(mxEvent.PAN, 'event', me)); + } + 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(this.dx) > this.graph.tolerance || Math.abs(this.dy) > this.graph.tolerance; + + if (!tmp && this.active) + { + this.fireEvent(new mxEventObject(mxEvent.PAN_START, 'event', me)); + } + } + + if (this.active || this.panningTrigger) + { + me.consume(); + } +}; + +/** + * Function: mouseUp + * + * Handles the event by setting the translation on the view or showing the + * popupmenu. + */ +mxPanningHandler.prototype.mouseUp = function(sender, me) +{ + if (this.active) + { + if (this.dx != null && this.dy != null) + { + // Ignores if scrollbars have been used for panning + if (!this.graph.useScrollbarsForPanning || !mxUtils.hasScrollbars(this.graph.container)) + { + var scale = this.graph.getView().scale; + var t = this.graph.getView().translate; + this.graph.panGraph(0, 0); + this.panGraph(t.x + this.dx / scale, t.y + this.dy / scale); + } + + me.consume(); + } + + this.fireEvent(new mxEventObject(mxEvent.PAN_END, 'event', me)); + } + + this.reset(); +}; + +/** + * Function: zoomGraph + * + * Zooms the graph to the given value and consumed the event if needed. + */ +mxPanningHandler.prototype.zoomGraph = function(evt) +{ + var value = Math.round(this.initialScale * evt.scale * 100) / 100; + + if (this.minScale != null) + { + value = Math.max(this.minScale, value); + } + + if (this.maxScale != null) + { + value = Math.min(this.maxScale, value); + } + + if (this.graph.view.scale != value) + { + this.graph.zoomTo(value); + mxEvent.consume(evt); + } +}; + +/** + * Function: reset + * + * Resets the state of this handler. + */ +mxPanningHandler.prototype.reset = function() +{ + this.panningTrigger = false; + this.mouseDownEvent = null; + this.active = false; + this.dx = null; + this.dy = null; +}; + +/** + * Function: panGraph + * + * Pans 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); + this.graph.removeListener(this.forcePanningHandler); + this.graph.removeListener(this.gestureHandler); + mxEvent.removeListener(document, 'mouseup', this.mouseUpListener); +}; diff --git a/mxclient/js/handler/mxPopupMenuHandler.js b/mxclient/js/handler/mxPopupMenuHandler.js new file mode 100644 index 0000000..2388319 --- /dev/null +++ b/mxclient/js/handler/mxPopupMenuHandler.js @@ -0,0 +1,218 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxPopupMenuHandler + * + * Event handler that creates popupmenus. + * + * Constructor: mxPopupMenuHandler + * + * Constructs an event handler that creates a . + */ +function mxPopupMenuHandler(graph, factoryMethod) +{ + if (graph != null) + { + this.graph = graph; + this.factoryMethod = factoryMethod; + this.graph.addMouseListener(this); + + // Does not show menu if any touch gestures take place after the trigger + this.gestureHandler = mxUtils.bind(this, function(sender, eo) + { + this.inTolerance = false; + }); + + this.graph.addListener(mxEvent.GESTURE, this.gestureHandler); + + this.init(); + } +}; + +/** + * Extends mxPopupMenu. + */ +mxPopupMenuHandler.prototype = new mxPopupMenu(); +mxPopupMenuHandler.prototype.constructor = mxPopupMenuHandler; + +/** + * Variable: graph + * + * Reference to the enclosing . + */ +mxPopupMenuHandler.prototype.graph = null; + +/** + * Variable: selectOnPopup + * + * Specifies if cells should be selected if a popupmenu is displayed for + * them. Default is true. + */ +mxPopupMenuHandler.prototype.selectOnPopup = true; + +/** + * Variable: clearSelectionOnBackground + * + * Specifies if cells should be deselected if a popupmenu is displayed for + * the diagram background. Default is true. + */ +mxPopupMenuHandler.prototype.clearSelectionOnBackground = true; + +/** + * Variable: triggerX + * + * X-coordinate of the mouse down event. + */ +mxPopupMenuHandler.prototype.triggerX = null; + +/** + * Variable: triggerY + * + * Y-coordinate of the mouse down event. + */ +mxPopupMenuHandler.prototype.triggerY = null; + +/** + * Variable: screenX + * + * Screen X-coordinate of the mouse down event. + */ +mxPopupMenuHandler.prototype.screenX = null; + +/** + * Variable: screenY + * + * Screen Y-coordinate of the mouse down event. + */ +mxPopupMenuHandler.prototype.screenY = null; + +/** + * Function: init + * + * Initializes the shapes required for this vertex handler. + */ +mxPopupMenuHandler.prototype.init = function() +{ + // Supercall + mxPopupMenu.prototype.init.apply(this); + + // Hides the tooltip if the mouse is over + // the context menu + mxEvent.addGestureListeners(this.div, mxUtils.bind(this, function(evt) + { + this.graph.tooltipHandler.hide(); + })); +}; + +/** + * Function: isSelectOnPopup + * + * Hook for returning if a cell should be selected for a given . + * This implementation returns . + */ +mxPopupMenuHandler.prototype.isSelectOnPopup = function(me) +{ + return this.selectOnPopup; +}; + +/** + * Function: mouseDown + * + * Handles the event by initiating the panning. By consuming the event all + * subsequent events of the gesture are redirected to this handler. + */ +mxPopupMenuHandler.prototype.mouseDown = function(sender, me) +{ + if (this.isEnabled() && !mxEvent.isMultiTouchEvent(me.getEvent())) + { + // Hides the popupmenu if is is being displayed + this.hideMenu(); + this.triggerX = me.getGraphX(); + this.triggerY = me.getGraphY(); + this.screenX = mxEvent.getMainEvent(me.getEvent()).screenX; + this.screenY = mxEvent.getMainEvent(me.getEvent()).screenY; + this.popupTrigger = this.isPopupTrigger(me); + this.inTolerance = true; + } +}; + +/** + * Function: mouseMove + * + * Handles the event by updating the panning on the graph. + */ +mxPopupMenuHandler.prototype.mouseMove = function(sender, me) +{ + // Popup trigger may change on mouseUp so ignore it + if (this.inTolerance && this.screenX != null && this.screenY != null) + { + if (Math.abs(mxEvent.getMainEvent(me.getEvent()).screenX - this.screenX) > this.graph.tolerance || + Math.abs(mxEvent.getMainEvent(me.getEvent()).screenY - this.screenY) > this.graph.tolerance) + { + this.inTolerance = false; + } + } +}; + +/** + * Function: mouseUp + * + * Handles the event by setting the translation on the view or showing the + * popupmenu. + */ +mxPopupMenuHandler.prototype.mouseUp = function(sender, me) +{ + if (this.popupTrigger && this.inTolerance && this.triggerX != null && this.triggerY != null) + { + var cell = this.getCellForPopupEvent(me); + + // Selects the cell for which the context menu is being displayed + if (this.graph.isEnabled() && this.isSelectOnPopup(me) && + 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(); + + // Menu is shifted by 1 pixel so that the mouse up event + // is routed via the underlying shape instead of the DIV + var origin = mxUtils.getScrollOrigin(); + this.popup(me.getX() + origin.x + 1, me.getY() + origin.y + 1, cell, me.getEvent()); + me.consume(); + } + + this.popupTrigger = false; + this.inTolerance = false; +}; + +/** + * Function: getCellForPopupEvent + * + * Hook to return the cell for the mouse up popup trigger handling. + */ +mxPopupMenuHandler.prototype.getCellForPopupEvent = function(me) +{ + return me.getCell(); +}; + +/** + * Function: destroy + * + * Destroys the handler and all its resources and DOM nodes. + */ +mxPopupMenuHandler.prototype.destroy = function() +{ + this.graph.removeMouseListener(this); + this.graph.removeListener(this.gestureHandler); + + // Supercall + mxPopupMenu.prototype.destroy.apply(this); +}; diff --git a/mxclient/js/handler/mxRubberband.js b/mxclient/js/handler/mxRubberband.js new file mode 100644 index 0000000..37f68f0 --- /dev/null +++ b/mxclient/js/handler/mxRubberband.js @@ -0,0 +1,429 @@ +/** + * Copyright (c) 2006-2016, JGraph Ltd + * Copyright (c) 2006-2016, Gaudenz Alder + */ +/** + * Class: mxRubberband + * + * Event handler that selects rectangular regions. This is not built-into + * . 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); + + // Handles force rubberband event + this.forceRubberbandHandler = mxUtils.bind(this, function(sender, evt) + { + var evtName = evt.getProperty('eventName'); + var me = evt.getProperty('event'); + + if (evtName == mxEvent.MOUSE_DOWN && this.isForceRubberbandEvent(me)) + { + 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); + me.consume(false); + } + }); + + this.graph.addListener(mxEvent.FIRE_MOUSE_EVENT, this.forceRubberbandHandler); + + // Repaints the marquee after autoscroll + this.panHandler = mxUtils.bind(this, function() + { + this.repaint(); + }); + + this.graph.addListener(mxEvent.PAN, this.panHandler); + + // Does not show menu if any touch gestures take place after the trigger + this.gestureHandler = mxUtils.bind(this, function(sender, eo) + { + if (this.first != null) + { + this.reset(); + } + }); + + this.graph.addListener(mxEvent.GESTURE, this.gestureHandler); + + // 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 . + */ +mxRubberband.prototype.currentX = 0; + +/** + * Variable: currentY + * + * Holds the value of the y argument in the last call to . + */ +mxRubberband.prototype.currentY = 0; + +/** + * Variable: fadeOut + * + * Optional fade out effect. Default is false. + */ +mxRubberband.prototype.fadeOut = false; + +/** + * Function: isEnabled + * + * Returns true if events are handled. This implementation returns + * . + */ +mxRubberband.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Enables or disables event handling. This implementation updates + * . + */ +mxRubberband.prototype.setEnabled = function(enabled) +{ + this.enabled = enabled; +}; + +/** + * Function: isForceRubberbandEvent + * + * Returns true if the given should start rubberband selection. + * This implementation returns true if the alt key is pressed. + */ +mxRubberband.prototype.isForceRubberbandEvent = function(me) +{ + return mxEvent.isAltDown(me.getEvent()); +}; + +/** + * 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() && + me.getState() == null && !mxEvent.isMultiTouchEvent(me.getEvent())) + { + 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); + + // 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); + + 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)); + }); + + // Workaround for rubberband stopping if the mouse leaves the container in Firefox + if (mxClient.IS_FF) + { + mxEvent.addGestureListeners(document, null, this.dragHandler, this.dropHandler); + } +}; + +/** + * 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); + var result = this.sharedDiv; + + if (mxClient.IS_SVG && (!mxClient.IS_IE || document.documentMode >= 10) && this.fadeOut) + { + this.sharedDiv = null; + } + + return result; +}; + +/** + * Function: isActive + * + * Returns true if this handler is active. + */ +mxRubberband.prototype.isActive = function(sender, me) +{ + return this.div != null && this.div.style.display != 'none'; +}; + +/** + * Function: mouseUp + * + * Handles the event by selecting the region of the rubberband using + * . + */ +mxRubberband.prototype.mouseUp = function(sender, me) +{ + var active = this.isActive(); + this.reset(); + + if (active) + { + this.execute(me.getEvent()); + me.consume(); + } +}; + +/** + * Function: execute + * + * Resets the state of this handler and selects the current region + * for the given event. + */ +mxRubberband.prototype.execute = function(evt) +{ + var rect = new mxRectangle(this.x, this.y, this.width, this.height); + this.graph.selectRegion(rect, evt); +}; + +/** + * Function: reset + * + * Resets the state of the rubberband selection. + */ +mxRubberband.prototype.reset = function() +{ + if (this.div != null) + { + if (mxClient.IS_SVG && (!mxClient.IS_IE || document.documentMode >= 10) && this.fadeOut) + { + var temp = this.div; + mxUtils.setPrefixedStyle(temp.style, 'transition', 'all 0.2s linear'); + temp.style.pointerEvents = 'none'; + temp.style.opacity = 0; + + window.setTimeout(function() + { + temp.parentNode.removeChild(temp); + }, 200); + } + else + { + this.div.parentNode.removeChild(this.div); + } + } + + mxEvent.removeGestureListeners(document, null, this.dragHandler, this.dropHandler); + this.dragHandler = null; + this.dropHandler = null; + + this.currentX = 0; + this.currentY = 0; + this.first = null; + this.div = null; +}; + +/** + * Function: update + * + * Sets and and calls . + */ +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
. + */ +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.forceRubberbandHandler); + this.graph.removeListener(this.panHandler); + this.reset(); + + if (this.sharedDiv != null) + { + this.sharedDiv = null; + } + } +}; diff --git a/mxclient/js/handler/mxSelectionCellsHandler.js b/mxclient/js/handler/mxSelectionCellsHandler.js new file mode 100644 index 0000000..0027b0e --- /dev/null +++ b/mxclient/js/handler/mxSelectionCellsHandler.js @@ -0,0 +1,344 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * 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 state + * property contains the that has been added. + * + * Event: mxEvent.REMOVE + * + * Fires if a cell has been remove from the selection. The state + * property contains the that has been removed. + * + * Parameters: + * + * graph - Reference to the enclosing . + */ +function mxSelectionCellsHandler(graph) +{ + mxEventSource.call(this); + + 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. + */ +mxUtils.extend(mxSelectionCellsHandler, mxEventSource); + +/** + * Variable: graph + * + * Reference to the enclosing . + */ +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 + * + * that maps from cells to handlers. + */ +mxSelectionCellsHandler.prototype.handlers = null; + +/** + * Function: isEnabled + * + * Returns . + */ +mxSelectionCellsHandler.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Sets . + */ +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: isHandled + * + * Returns true if the given cell has a handler. + */ +mxSelectionCellsHandler.prototype.isHandled = function(cell) +{ + return this.getHandler(cell) != null; +}; + +/** + * Function: reset + * + * Resets all handlers. + */ +mxSelectionCellsHandler.prototype.reset = function() +{ + this.handlers.visit(function(key, handler) + { + handler.reset.apply(handler); + }); +}; + +/** + * Function: getHandledSelectionCells + * + * Reloads or updates all handlers. + */ +mxSelectionCellsHandler.prototype.getHandledSelectionCells = function() +{ + return this.graph.getSelectionCells(); +}; + +/** + * 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 = mxUtils.sortCells(this.getHandledSelectionCells(), false); + + // Destroys or updates old handlers + 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 if (!this.isHandlerActive(handler)) + { + if (handler.refresh != null) + { + handler.refresh(); + } + + handler.redraw(); + } + } + + if (handler != null) + { + this.handlers.put(tmp[i], handler); + } + } + } + + // Destroys unused handlers + oldHandlers.visit(mxUtils.bind(this, function(key, handler) + { + this.fireEvent(new mxEventObject(mxEvent.REMOVE, 'state', handler.state)); + handler.destroy(); + })); + + // Creates new handlers and updates parent highlight on existing handlers + for (var i = 0; i < tmp.length; i++) + { + var state = this.graph.view.getState(tmp[i]); + + if (state != null) + { + var handler = this.handlers.get(tmp[i]); + + if (handler == null) + { + handler = this.graph.createHandler(state); + this.fireEvent(new mxEventObject(mxEvent.ADD, 'state', state)); + this.handlers.put(tmp[i], handler); + } + else + { + handler.updateParentHighlight(); + } + } + } +}; + +/** + * Function: isHandlerActive + * + * Returns true if the given handler is active and should not be redrawn. + */ +mxSelectionCellsHandler.prototype.isHandlerActive = function(handler) +{ + return handler.index != null; +}; + +/** + * Function: updateHandler + * + * Updates the handler for the given shape if one exists. + */ +mxSelectionCellsHandler.prototype.updateHandler = function(state) +{ + var handler = this.handlers.remove(state.cell); + + if (handler != null) + { + // Transfers the current state to the new handler + var index = handler.index; + var x = handler.startX; + var y = handler.startY; + + handler.destroy(); + handler = this.graph.createHandler(state); + + if (handler != null) + { + this.handlers.put(state.cell, handler); + + if (index != null && x != null && y != null) + { + handler.start(x, y, index); + } + } + } +}; + +/** + * 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/mxclient/js/handler/mxTooltipHandler.js b/mxclient/js/handler/mxTooltipHandler.js new file mode 100644 index 0000000..85462a5 --- /dev/null +++ b/mxclient/js/handler/mxTooltipHandler.js @@ -0,0 +1,353 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxTooltipHandler + * + * Graph event handler that displays tooltips. is used to + * get the tooltip for a cell or handle. This handler is built-into + * and enabled using . + * + * 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 . + * 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 . + */ +mxTooltipHandler.prototype.graph = null; + +/** + * Variable: delay + * + * Delay to show the tooltip in milliseconds. Default is 500. + */ +mxTooltipHandler.prototype.delay = null; + +/** + * Variable: ignoreTouchEvents + * + * Specifies if touch and pen events should be ignored. Default is true. + */ +mxTooltipHandler.prototype.ignoreTouchEvents = true; + +/** + * 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: destroyed + * + * True if this handler was destroyed using . + */ +mxTooltipHandler.prototype.destroyed = 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 . + */ +mxTooltipHandler.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Enables or disables event handling. This implementation + * updates . + */ +mxTooltipHandler.prototype.setEnabled = function(enabled) +{ + this.enabled = enabled; +}; + +/** + * Function: isHideOnHover + * + * Returns . + */ +mxTooltipHandler.prototype.isHideOnHover = function() +{ + return this.hideOnHover; +}; + +/** + * Function: setHideOnHover + * + * Sets . + */ +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'; + + document.body.appendChild(this.div); + + mxEvent.addGestureListeners(this.div, mxUtils.bind(this, function(evt) + { + var source = mxEvent.getSource(evt); + + if (source.nodeName != 'A') + { + this.hideTooltip(); + } + })); + } +}; + +/** + * Function: getStateForEvent + * + * Returns the to be used for showing a tooltip for this event. + */ +mxTooltipHandler.prototype.getStateForEvent = function(me) +{ + return me.getState(); +}; + +/** + * 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); + var state = this.getStateForEvent(me); + + if (this.isHideOnHover() || state != this.state || (me.getSource() != this.node && + (!this.stateSource || (state != null && this.stateSource == + (me.isSource(state.shape) || !me.isSource(state.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, state) +{ + if (!this.ignoreTouchEvents || mxEvent.isMouseEvent(me.getEvent())) + { + this.resetTimer(); + state = (state != null) ? state : this.getStateForEvent(me); + + if (restart && this.isEnabled() && state != null && (this.div == null || + this.div.style.visibility == 'hidden')) + { + 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.popupMenuHandler.isMenuShowing() && !this.graph.isMouseDown) + { + // 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'; + this.div.innerHTML = ''; + } +}; + +/** + * 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 (!this.destroyed && tip != null && tip.length > 0) + { + // Initializes the DOM nodes if required + if (this.div == null) + { + this.init(); + } + + var origin = mxUtils.getScrollOrigin(); + + this.div.style.zIndex = this.zIndex; + 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, '
'); + } + 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() +{ + if (!this.destroyed) + { + this.graph.removeMouseListener(this); + mxEvent.release(this.div); + + if (this.div != null && this.div.parentNode != null) + { + this.div.parentNode.removeChild(this.div); + } + + this.destroyed = true; + this.div = null; + } +}; diff --git a/mxclient/js/handler/mxVertexHandler.js b/mxclient/js/handler/mxVertexHandler.js new file mode 100644 index 0000000..64fe1bf --- /dev/null +++ b/mxclient/js/handler/mxVertexHandler.js @@ -0,0 +1,2251 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxVertexHandler + * + * Event handler for resizing cells. This handler is automatically created in + * . + * + * Constructor: mxVertexHandler + * + * Constructs an event handler that allows to resize vertices + * and groups. + * + * Parameters: + * + * state - of the cell to be resized. + */ +function mxVertexHandler(state) +{ + if (state != null) + { + this.state = state; + this.init(); + + // Handles escape keystrokes + this.escapeHandler = mxUtils.bind(this, function(sender, evt) + { + if (this.livePreview && this.index != null) + { + // Redraws the live preview + this.state.view.graph.cellRenderer.redraw(this.state, true); + + // Redraws connected edges + this.state.view.invalidate(this.state.cell); + this.state.invalid = false; + this.state.view.validate(); + } + + this.reset(); + }); + + this.state.view.graph.addListener(mxEvent.ESCAPE, this.escapeHandler); + } +}; + +/** + * Variable: graph + * + * Reference to the enclosing . + */ +mxVertexHandler.prototype.graph = null; + +/** + * Variable: state + * + * Reference to the 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 or + * if > 0. Default is true. + */ +mxVertexHandler.prototype.allowHandleBoundsCheck = true; + +/** + * Variable: handleImage + * + * Optional to be used as handles. Default is null. + */ +mxVertexHandler.prototype.handleImage = null; + +/** + * Variable: handlesVisible + * + * If handles are currently visible. + */ +mxVertexHandler.prototype.handlesVisible = true; + +/** + * Variable: tolerance + * + * Optional tolerance for hit-detection in . Default is 0. + */ +mxVertexHandler.prototype.tolerance = 0; + +/** + * Variable: rotationEnabled + * + * Specifies if a rotation handle should be visible. Default is false. + */ +mxVertexHandler.prototype.rotationEnabled = false; + +/** + * Variable: parentHighlightEnabled + * + * Specifies if the parent should be highlighted if a child cell is selected. + * Default is false. + */ +mxVertexHandler.prototype.parentHighlightEnabled = false; + +/** + * Variable: rotationRaster + * + * Specifies if rotation steps should be "rasterized" depening on the distance + * to the handle. Default is true. + */ +mxVertexHandler.prototype.rotationRaster = true; + +/** + * Variable: rotationCursor + * + * Specifies the cursor for the rotation handle. Default is 'crosshair'. + */ +mxVertexHandler.prototype.rotationCursor = 'crosshair'; + +/** + * Variable: livePreview + * + * Specifies if resize should change the cell in-place. This is an experimental + * feature for non-touch devices. Default is false. + */ +mxVertexHandler.prototype.livePreview = false; + +/** + * Variable: movePreviewToFront + * + * Specifies if the live preview should be moved to the front. + */ +mxVertexHandler.prototype.movePreviewToFront = false; + +/** + * Variable: manageSizers + * + * Specifies if sizers should be hidden and spaced if the vertex is small. + * Default is false. + */ +mxVertexHandler.prototype.manageSizers = false; + +/** + * Variable: constrainGroupByChildren + * + * Specifies if the size of groups should be constrained by the children. + * Default is false. + */ +mxVertexHandler.prototype.constrainGroupByChildren = false; + +/** + * Variable: rotationHandleVSpacing + * + * Vertical spacing for rotation icon. Default is -16. + */ +mxVertexHandler.prototype.rotationHandleVSpacing = -16; + +/** + * Variable: horizontalOffset + * + * The horizontal offset for the handles. This is updated in + * if is true and the sizers are offset horizontally. + */ +mxVertexHandler.prototype.horizontalOffset = 0; + +/** + * Variable: verticalOffset + * + * The horizontal offset for the handles. This is updated in + * if is true and the sizers are offset vertically. + */ +mxVertexHandler.prototype.verticalOffset = 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); + // VML dialect required here for event transparency in IE + this.selectionBorder.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ? mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG; + this.selectionBorder.pointerEvents = false; + this.selectionBorder.rotation = Number(this.state.style[mxConstants.STYLE_ROTATION] || '0'); + this.selectionBorder.init(this.graph.getView().getOverlayPane()); + mxEvent.redirectMouseEvents(this.selectionBorder.node, this.graph, this.state); + + if (this.graph.isCellMovable(this.state.cell)) + { + this.selectionBorder.setCursor(mxConstants.CURSOR_MOVABLE_VERTEX); + } + + // 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, + mxEvent.LABEL_HANDLE, null, mxConstants.LABEL_HANDLE_FILLCOLOR); + this.sizers.push(this.labelShape); + } + } + + // Adds the rotation handler + if (this.isRotationHandleVisible()) + { + this.rotationShape = this.createSizer(this.rotationCursor, mxEvent.ROTATION_HANDLE, + mxConstants.HANDLE_SIZE + 3, mxConstants.HANDLE_FILLCOLOR); + this.sizers.push(this.rotationShape); + } + + this.customHandles = this.createCustomHandles(); + this.redraw(); + + if (this.constrainGroupByChildren) + { + this.updateMinBounds(); + } +}; + +/** + * Function: isRotationHandleVisible + * + * Returns true if the rotation handle should be showing. + */ +mxVertexHandler.prototype.isRotationHandleVisible = function() +{ + return this.graph.isEnabled() && this.rotationEnabled && this.graph.isCellRotatable(this.state.cell) && + (mxGraphHandler.prototype.maxCells <= 0 || this.graph.getSelectionCount() < mxGraphHandler.prototype.maxCells); +}; + +/** + * Function: isConstrainedEvent + * + * Returns true if the aspect ratio if the cell should be maintained. + */ +mxVertexHandler.prototype.isConstrainedEvent = function(me) +{ + return mxEvent.isShiftDown(me.getEvent()) || this.state.style[mxConstants.STYLE_ASPECT] == 'fixed'; +}; + +/** + * Function: isCenteredEvent + * + * Returns true if the center of the vertex should be maintained during the resize. + */ +mxVertexHandler.prototype.isCenteredEvent = function(state, me) +{ + return false; +}; + +/** + * Function: createCustomHandles + * + * Returns an array of custom handles. This implementation returns null. + */ +mxVertexHandler.prototype.createCustomHandles = function() +{ + return null; +}; + +/** + * Function: updateMinBounds + * + * Initializes the shapes required for this vertex handler. + */ +mxVertexHandler.prototype.updateMinBounds = function() +{ + var children = this.graph.getChildCells(this.state.cell); + + if (children.length > 0) + { + this.minBounds = this.graph.view.getBounds(children); + + if (this.minBounds != null) + { + var s = this.state.view.scale; + var t = this.state.view.translate; + + this.minBounds.x -= this.state.x; + this.minBounds.y -= this.state.y; + this.minBounds.x /= s; + this.minBounds.y /= s; + this.minBounds.width /= s; + this.minBounds.height /= s; + this.x0 = this.state.x / s - t.x; + this.y0 = this.state.y / s - t.y; + } + } +}; + +/** + * Function: getSelectionBounds + * + * Returns the mxRectangle that defines the bounds of the selection + * border. + */ +mxVertexHandler.prototype.getSelectionBounds = function(state) +{ + return new mxRectangle(Math.round(state.x), Math.round(state.y), Math.round(state.width), Math.round(state.height)); +}; + +/** + * Function: createParentHighlightShape + * + * Creates the shape used to draw the selection border. + */ +mxVertexHandler.prototype.createParentHighlightShape = function(bounds) +{ + return this.createSelectionShape(bounds); +}; + +/** + * Function: createSelectionShape + * + * Creates the shape used to draw the selection border. + */ +mxVertexHandler.prototype.createSelectionShape = function(bounds) +{ + var shape = new mxRectangleShape( + mxRectangle.fromRectangle(bounds), + null, this.getSelectionColor()); + shape.strokewidth = this.getSelectionStrokeWidth(); + shape.isDashed = this.isSelectionDashed(); + + return shape; +}; + +/** + * Function: getSelectionColor + * + * Returns . + */ +mxVertexHandler.prototype.getSelectionColor = function() +{ + return mxConstants.VERTEX_SELECTION_COLOR; +}; + +/** + * Function: getSelectionStrokeWidth + * + * Returns . + */ +mxVertexHandler.prototype.getSelectionStrokeWidth = function() +{ + return mxConstants.VERTEX_SELECTION_STROKEWIDTH; +}; + +/** + * Function: isSelectionDashed + * + * Returns . + */ +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 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 (sizer.isHtmlAllowed() && 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_MIXEDHTML : mxConstants.DIALECT_SVG; + sizer.init(this.graph.getView().getOverlayPane()); + } + + mxEvent.redirectMouseEvents(sizer.node, this.graph, this.state); + + if (this.graph.isEnabled()) + { + sizer.setCursor(cursor); + } + + if (!this.isSizerVisible(index)) + { + sizer.visible = false; + } + + 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 an + * index. Only images and rectangles should be returned if support for HTML + * labels with not foreign objects is required. + */ +mxVertexHandler.prototype.createSizerShape = function(bounds, index, fillColor) +{ + if (this.handleImage != null) + { + bounds = new mxRectangle(bounds.x, bounds.y, this.handleImage.width, this.handleImage.height); + var shape = new mxImageShape(bounds, this.handleImage.src); + + // Allows HTML rendering of the images + shape.preserveImageAspect = false; + + return shape; + } + else if (index == mxEvent.ROTATION_HANDLE) + { + return new mxEllipse(bounds, fillColor || mxConstants.HANDLE_FILLCOLOR, mxConstants.HANDLE_STROKECOLOR); + } + else + { + return new mxRectangleShape(bounds, fillColor || mxConstants.HANDLE_FILLCOLOR, mxConstants.HANDLE_STROKECOLOR); + } +}; + +/** + * Function: createBounds + * + * Helper method to create an 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 = Math.floor(x - shape.bounds.width / 2); + shape.bounds.y = Math.floor(y - shape.bounds.height / 2); + + // Fixes visible inactive handles in VML + if (shape.node != null && shape.node.style.display != 'none') + { + 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 . + */ +mxVertexHandler.prototype.getHandleForEvent = function(me) +{ + // Connection highlight may consume events before they reach sizer handle + var tol = (!mxEvent.isMouseEvent(me.getEvent())) ? this.tolerance : 1; + var hit = (this.allowHandleBoundsCheck && (mxClient.IS_IE || tol > 0)) ? + new mxRectangle(me.getGraphX() - tol, me.getGraphY() - tol, 2 * tol, 2 * tol) : null; + + var checkShape = mxUtils.bind(this, function(shape) + { + var st = (shape != null && shape.constructor != mxImageShape && + this.allowHandleBoundsCheck) ? shape.strokewidth + shape.svgStrokeTolerance : null; + var real = (st != null) ? new mxRectangle(me.getGraphX() - Math.floor(st / 2), + me.getGraphY() - Math.floor(st / 2), st, st) : hit; + + return shape != null && (me.isSource(shape) || (real != null && mxUtils.intersects(shape.bounds, real) && + shape.node.style.display != 'none' && shape.node.style.visibility != 'hidden')); + }); + + if (checkShape(this.rotationShape)) + { + return mxEvent.ROTATION_HANDLE; + } + else if (checkShape(this.labelShape)) + { + return mxEvent.LABEL_HANDLE; + } + + if (this.sizers != null) + { + for (var i = 0; i < this.sizers.length; i++) + { + if (checkShape(this.sizers[i])) + { + return i; + } + } + } + + if (this.customHandles != null && this.isCustomHandleEvent(me)) + { + // Inverse loop order to match display order + for (var i = this.customHandles.length - 1; i >= 0; i--) + { + if (checkShape(this.customHandles[i].shape)) + { + // LATER: Return reference to active shape + return mxEvent.CUSTOM_HANDLE - i; + } + } + } + + return null; +}; + +/** + * Function: isCustomHandleEvent + * + * Returns true if the given event allows custom handles to be changed. This + * implementation returns true. + */ +mxVertexHandler.prototype.isCustomHandleEvent = function(me) +{ + return true; +}; + +/** + * 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()) + { + var handle = this.getHandleForEvent(me); + + if (handle != null) + { + this.start(me.getGraphX(), me.getGraphY(), handle); + me.consume(); + } + } +}; + +/** + * Function: isLivePreviewBorder + * + * Called if is enabled to check if a border should be painted. + * This implementation returns true if the shape is transparent. + */ +mxVertexHandler.prototype.isLivePreviewBorder = function() +{ + return this.state.shape != null && this.state.shape.fill == null && this.state.shape.stroke == null; +}; + +/** + * Function: start + * + * Starts the handling of the mouse gesture. + */ +mxVertexHandler.prototype.start = function(x, y, index) +{ + if (this.selectionBorder != null) + { + this.livePreviewActive = this.livePreview && this.graph.model.getChildCount(this.state.cell) == 0; + this.inTolerance = true; + this.childOffsetX = 0; + this.childOffsetY = 0; + this.index = index; + this.startX = x; + this.startY = y; + + if (this.index <= mxEvent.CUSTOM_HANDLE && this.isGhostPreview()) + { + this.ghostPreview = this.createGhostPreview(); + } + else + { + // Saves reference to parent state + var model = this.state.view.graph.model; + var parent = model.getParent(this.state.cell); + + if (this.state.view.currentRoot != parent && (model.isVertex(parent) || model.isEdge(parent))) + { + this.parentState = this.state.view.graph.view.getState(parent); + } + + // Creates a preview that can be on top of any HTML label + this.selectionBorder.node.style.display = (index == mxEvent.ROTATION_HANDLE) ? 'inline' : 'none'; + + // Creates the border that represents the new bounds + if (!this.livePreviewActive || this.isLivePreviewBorder()) + { + this.preview = this.createSelectionShape(this.bounds); + + if (!(mxClient.IS_SVG && Number(this.state.style[mxConstants.STYLE_ROTATION] || '0') != 0) && + 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()); + } + } + + if (index == mxEvent.ROTATION_HANDLE) + { + // With the rotation handle in a corner, need the angle and distance + var pos = this.getRotationHandlePosition(); + + var dx = pos.x - this.state.getCenterX(); + var dy = pos.y - this.state.getCenterY(); + + this.startAngle = (dx != 0) ? Math.atan(dy / dx) * 180 / Math.PI + 90 : 0; + this.startDist = Math.sqrt(dx * dx + dy * dy); + } + + // Prepares the handles for live preview + if (this.livePreviewActive) + { + this.hideSizers(); + + if (index == mxEvent.ROTATION_HANDLE) + { + this.rotationShape.node.style.display = ''; + } + else if (index == mxEvent.LABEL_HANDLE) + { + this.labelShape.node.style.display = ''; + } + else if (this.sizers != null && this.sizers[index] != null) + { + this.sizers[index].node.style.display = ''; + } + else if (index <= mxEvent.CUSTOM_HANDLE && this.customHandles != null) + { + this.customHandles[mxEvent.CUSTOM_HANDLE - index].setVisible(true); + } + + // Gets the array of connected edge handlers for redrawing + var edges = this.graph.getEdges(this.state.cell); + this.edgeHandlers = []; + + for (var i = 0; i < edges.length; i++) + { + var handler = this.graph.selectionCellsHandler.getHandler(edges[i]); + + if (handler != null) + { + this.edgeHandlers.push(handler); + } + } + } + } + } +}; + +/** + * Function: createGhostPreview + * + * Starts the handling of the mouse gesture. + */ +mxVertexHandler.prototype.createGhostPreview = function() +{ + var shape = this.graph.cellRenderer.createShape(this.state); + shape.init(this.graph.view.getOverlayPane()); + shape.scale = this.state.view.scale; + shape.bounds = this.bounds; + shape.outline = true; + + return shape; +}; + +/** + * Function: hideHandles + * + * Shortcut to . + */ +mxVertexHandler.prototype.setHandlesVisible = function(visible) +{ + this.handlesVisible = visible; + + if (this.sizers != null) + { + for (var i = 0; i < this.sizers.length; i++) + { + this.sizers[i].node.style.display = (visible) ? '' : 'none'; + } + } + + if (this.customHandles != null) + { + for (var i = 0; i < this.customHandles.length; i++) + { + this.customHandles[i].setVisible(visible); + } + } +}; + +/** + * Function: hideSizers + * + * Hides all sizers except. + * + * Starts the handling of the mouse gesture. + */ +mxVertexHandler.prototype.hideSizers = function() +{ + this.setHandlesVisible(false); +}; + +/** + * Function: checkTolerance + * + * Checks if the coordinates for the given event are within the + * . If the event is a mouse event then the tolerance is + * ignored. + */ +mxVertexHandler.prototype.checkTolerance = function(me) +{ + if (this.inTolerance && this.startX != null && this.startY != null) + { + if (mxEvent.isMouseEvent(me.getEvent()) || + Math.abs(me.getGraphX() - this.startX) > this.graph.tolerance || + Math.abs(me.getGraphY() - this.startY) > this.graph.tolerance) + { + this.inTolerance = false; + } + } +}; + +/** + * Function: updateHint + * + * Hook for subclassers do show details while the handler is active. + */ +mxVertexHandler.prototype.updateHint = function(me) { }; + +/** + * Function: removeHint + * + * Hooks for subclassers to hide details when the handler gets inactive. + */ +mxVertexHandler.prototype.removeHint = function() { }; + +/** + * Function: roundAngle + * + * Hook for rounding the angle. This uses Math.round. + */ +mxVertexHandler.prototype.roundAngle = function(angle) +{ + return Math.round(angle * 10) / 10; +}; + +/** + * Function: roundLength + * + * Hook for rounding the unscaled width or height. This uses Math.round. + */ +mxVertexHandler.prototype.roundLength = function(length) +{ + return Math.round(length * 100) / 100; +}; + +/** + * Function: mouseMove + * + * Handles the event by updating the preview. + */ +mxVertexHandler.prototype.mouseMove = function(sender, me) +{ + if (!me.isConsumed() && this.index != null) + { + // Checks tolerance for ignoring single clicks + this.checkTolerance(me); + + if (!this.inTolerance) + { + if (this.index <= mxEvent.CUSTOM_HANDLE) + { + if (this.customHandles != null) + { + this.customHandles[mxEvent.CUSTOM_HANDLE - this.index].processEvent(me); + this.customHandles[mxEvent.CUSTOM_HANDLE - this.index].active = true; + + if (this.ghostPreview != null) + { + this.ghostPreview.apply(this.state); + this.ghostPreview.strokewidth = this.getSelectionStrokeWidth() / + this.ghostPreview.scale / this.ghostPreview.scale; + this.ghostPreview.isDashed = this.isSelectionDashed(); + this.ghostPreview.stroke = this.getSelectionColor(); + this.ghostPreview.redraw(); + + if (this.selectionBounds != null) + { + this.selectionBorder.node.style.display = 'none'; + } + } + else + { + if (this.movePreviewToFront) + { + this.moveToFront(); + } + + this.customHandles[mxEvent.CUSTOM_HANDLE - this.index].positionChanged(); + } + } + } + else if (this.index == mxEvent.LABEL_HANDLE) + { + this.moveLabel(me); + } + else + { + if (this.index == mxEvent.ROTATION_HANDLE) + { + this.rotateVertex(me); + } + else + { + this.resizeVertex(me); + } + + this.updateHint(me); + } + } + + me.consume(); + } + // Workaround for disabling the connect highlight when over handle + else if (!this.graph.isMouseDown && this.getHandleForEvent(me) != null) + { + me.consume(false); + } +}; + +/** + * Function: isGhostPreview + * + * Returns true if a ghost preview should be used for custom handles. + */ +mxVertexHandler.prototype.isGhostPreview = function() +{ + return this.state.view.graph.model.getChildCount(this.state.cell) > 0; +}; + +/** + * Function: moveLabel + * + * Moves the label. + */ +mxVertexHandler.prototype.moveLabel = function(me) +{ + var point = new mxPoint(me.getGraphX(), me.getGraphY()); + var tr = this.graph.view.translate; + var scale = this.graph.view.scale; + + if (this.graph.isGridEnabledEvent(me.getEvent())) + { + point.x = (this.graph.snap(point.x / scale - tr.x) + tr.x) * scale; + point.y = (this.graph.snap(point.y / scale - tr.y) + tr.y) * scale; + } + + var index = (this.rotationShape != null) ? this.sizers.length - 2 : this.sizers.length - 1; + this.moveSizerTo(this.sizers[index], point.x, point.y); +}; + +/** + * Function: rotateVertex + * + * Rotates the vertex. + */ +mxVertexHandler.prototype.rotateVertex = function(me) +{ + var point = new mxPoint(me.getGraphX(), me.getGraphY()); + var dx = this.state.x + this.state.width / 2 - point.x; + var dy = this.state.y + this.state.height / 2 - point.y; + this.currentAlpha = (dx != 0) ? Math.atan(dy / dx) * 180 / Math.PI + 90 : ((dy < 0) ? 180 : 0); + + if (dx > 0) + { + this.currentAlpha -= 180; + } + + this.currentAlpha -= this.startAngle; + + // Rotation raster + if (this.rotationRaster && this.graph.isGridEnabledEvent(me.getEvent())) + { + var dx = point.x - this.state.getCenterX(); + var dy = point.y - this.state.getCenterY(); + var dist = Math.sqrt(dx * dx + dy * dy); + + if (dist - this.startDist < 2) + { + raster = 15; + } + else if (dist - this.startDist < 25) + { + raster = 5; + } + else + { + raster = 1; + } + + this.currentAlpha = Math.round(this.currentAlpha / raster) * raster; + } + else + { + this.currentAlpha = this.roundAngle(this.currentAlpha); + } + + this.selectionBorder.rotation = this.currentAlpha; + this.selectionBorder.redraw(); + + if (this.livePreviewActive) + { + this.redrawHandles(); + } +}; + +/** + * Function: resizeVertex + * + * Risizes the vertex. + */ +mxVertexHandler.prototype.resizeVertex = function(me) +{ + var ct = new mxPoint(this.state.getCenterX(), this.state.getCenterY()); + var alpha = mxUtils.toRadians(this.state.style[mxConstants.STYLE_ROTATION] || '0'); + var point = new mxPoint(me.getGraphX(), me.getGraphY()); + var tr = this.graph.view.translate; + var scale = this.graph.view.scale; + var cos = Math.cos(-alpha); + var sin = Math.sin(-alpha); + + var dx = point.x - this.startX; + var dy = point.y - this.startY; + + // Rotates vector for mouse gesture + var tx = cos * dx - sin * dy; + var ty = sin * dx + cos * dy; + + dx = tx; + dy = ty; + + var geo = this.graph.getCellGeometry(this.state.cell); + this.unscaledBounds = this.union(geo, dx / scale, dy / scale, this.index, + this.graph.isGridEnabledEvent(me.getEvent()), 1, + new mxPoint(0, 0), this.isConstrainedEvent(me), + this.isCenteredEvent(this.state, me)); + + // Keeps vertex within maximum graph or parent bounds + if (!geo.relative) + { + var max = this.graph.getMaximumGraphBounds(); + + // Handles child cells + if (max != null && this.parentState != null) + { + max = mxRectangle.fromRectangle(max); + + max.x -= (this.parentState.x - tr.x * scale) / scale; + max.y -= (this.parentState.y - tr.y * scale) / scale; + } + + if (this.graph.isConstrainChild(this.state.cell)) + { + var tmp = this.graph.getCellContainmentArea(this.state.cell); + + if (tmp != null) + { + var overlap = this.graph.getOverlap(this.state.cell); + + if (overlap > 0) + { + tmp = mxRectangle.fromRectangle(tmp); + + tmp.x -= tmp.width * overlap; + tmp.y -= tmp.height * overlap; + tmp.width += 2 * tmp.width * overlap; + tmp.height += 2 * tmp.height * overlap; + } + + if (max == null) + { + max = tmp; + } + else + { + max = mxRectangle.fromRectangle(max); + max.intersect(tmp); + } + } + } + + if (max != null) + { + if (this.unscaledBounds.x < max.x) + { + this.unscaledBounds.width -= max.x - this.unscaledBounds.x; + this.unscaledBounds.x = max.x; + } + + if (this.unscaledBounds.y < max.y) + { + this.unscaledBounds.height -= max.y - this.unscaledBounds.y; + this.unscaledBounds.y = max.y; + } + + if (this.unscaledBounds.x + this.unscaledBounds.width > max.x + max.width) + { + this.unscaledBounds.width -= this.unscaledBounds.x + + this.unscaledBounds.width - max.x - max.width; + } + + if (this.unscaledBounds.y + this.unscaledBounds.height > max.y + max.height) + { + this.unscaledBounds.height -= this.unscaledBounds.y + + this.unscaledBounds.height - max.y - max.height; + } + } + } + + var old = this.bounds; + this.bounds = new mxRectangle(((this.parentState != null) ? this.parentState.x : tr.x * scale) + + (this.unscaledBounds.x) * scale, ((this.parentState != null) ? this.parentState.y : tr.y * scale) + + (this.unscaledBounds.y) * scale, this.unscaledBounds.width * scale, this.unscaledBounds.height * scale); + + if (geo.relative && this.parentState != null) + { + this.bounds.x += this.state.x - this.parentState.x; + this.bounds.y += this.state.y - this.parentState.y; + } + + cos = Math.cos(alpha); + sin = Math.sin(alpha); + + var c2 = new mxPoint(this.bounds.getCenterX(), this.bounds.getCenterY()); + + var dx = c2.x - ct.x; + var dy = c2.y - ct.y; + + var dx2 = cos * dx - sin * dy; + var dy2 = sin * dx + cos * dy; + + var dx3 = dx2 - dx; + var dy3 = dy2 - dy; + + var dx4 = this.bounds.x - this.state.x; + var dy4 = this.bounds.y - this.state.y; + + var dx5 = cos * dx4 - sin * dy4; + var dy5 = sin * dx4 + cos * dy4; + + this.bounds.x += dx3; + this.bounds.y += dy3; + + // Rounds unscaled bounds to int + this.unscaledBounds.x = this.roundLength(this.unscaledBounds.x + dx3 / scale); + this.unscaledBounds.y = this.roundLength(this.unscaledBounds.y + dy3 / scale); + this.unscaledBounds.width = this.roundLength(this.unscaledBounds.width); + this.unscaledBounds.height = this.roundLength(this.unscaledBounds.height); + + // Shifts the children according to parent offset + if (!this.graph.isCellCollapsed(this.state.cell) && (dx3 != 0 || dy3 != 0)) + { + this.childOffsetX = this.state.x - this.bounds.x + dx5; + this.childOffsetY = this.state.y - this.bounds.y + dy5; + } + else + { + this.childOffsetX = 0; + this.childOffsetY = 0; + } + + if (!old.equals(this.bounds)) + { + if (this.livePreviewActive) + { + this.updateLivePreview(me); + } + + if (this.preview != null) + { + this.drawPreview(); + } + else + { + this.updateParentHighlight(); + } + } +}; + +/** + * Function: updateLivePreview + * + * Repaints the live preview. + */ +mxVertexHandler.prototype.updateLivePreview = function(me) +{ + // TODO: Apply child offset to children in live preview + var scale = this.graph.view.scale; + var tr = this.graph.view.translate; + + // Saves current state + var tempState = this.state.clone(); + + // Temporarily changes size and origin + this.state.x = this.bounds.x; + this.state.y = this.bounds.y; + this.state.origin = new mxPoint(this.state.x / scale - tr.x, this.state.y / scale - tr.y); + this.state.width = this.bounds.width; + this.state.height = this.bounds.height; + + // Redraws cell and handles + var off = this.state.absoluteOffset; + off = new mxPoint(off.x, off.y); + + // Required to store and reset absolute offset for updating label position + this.state.absoluteOffset.x = 0; + this.state.absoluteOffset.y = 0; + var geo = this.graph.getCellGeometry(this.state.cell); + + if (geo != null) + { + var offset = geo.offset || this.EMPTY_POINT; + + if (offset != null && !geo.relative) + { + this.state.absoluteOffset.x = this.state.view.scale * offset.x; + this.state.absoluteOffset.y = this.state.view.scale * offset.y; + } + + this.state.view.updateVertexLabelOffset(this.state); + } + + // Draws the live preview + this.state.view.graph.cellRenderer.redraw(this.state, true); + + // Redraws connected edges TODO: Include child edges + this.state.view.invalidate(this.state.cell); + this.state.invalid = false; + this.state.view.validate(); + this.redrawHandles(); + + // Moves live preview to front + if (this.movePreviewToFront) + { + this.moveToFront(); + } + + // Hides folding icon + if (this.state.control != null && this.state.control.node != null) + { + this.state.control.node.style.visibility = 'hidden'; + } + + // Restores current state + this.state.setState(tempState); +}; + +/** + * Function: moveToFront + * + * Handles the event by applying the changes to the geometry. + */ +mxVertexHandler.prototype.moveToFront = function() +{ + if ((this.state.text != null && this.state.text.node != null && + this.state.text.node.nextSibling != null) || + (this.state.shape != null && this.state.shape.node != null && + this.state.shape.node.nextSibling != null && (this.state.text == null || + this.state.shape.node.nextSibling != this.state.text.node))) + { + if (this.state.shape != null && this.state.shape.node != null) + { + this.state.shape.node.parentNode.appendChild(this.state.shape.node); + } + + if (this.state.text != null && this.state.text.node != null) + { + this.state.text.node.parentNode.appendChild(this.state.text.node); + } + } +}; + +/** + * Function: mouseUp + * + * Handles the event by applying the changes to the geometry. + */ +mxVertexHandler.prototype.mouseUp = function(sender, me) +{ + if (this.index != null && this.state != null) + { + var point = new mxPoint(me.getGraphX(), me.getGraphY()); + var index = this.index; + this.index = null; + + if (this.ghostPreview == null) + { + // Required to restore order in case of no change + this.state.view.invalidate(this.state.cell, false, false); + this.state.view.validate(); + } + + this.graph.getModel().beginUpdate(); + try + { + if (index <= mxEvent.CUSTOM_HANDLE) + { + if (this.customHandles != null) + { + // Creates style before changing cell state + var style = this.state.view.graph.getCellStyle(this.state.cell); + + this.customHandles[mxEvent.CUSTOM_HANDLE - index].active = false; + this.customHandles[mxEvent.CUSTOM_HANDLE - index].execute(me); + + // Sets style and apply on shape to force repaint and + // check if execute has removed custom handles + if (this.customHandles != null && + this.customHandles[mxEvent.CUSTOM_HANDLE - index] != null) + { + this.state.style = style; + this.customHandles[mxEvent.CUSTOM_HANDLE - index].positionChanged(); + } + } + } + else if (index == mxEvent.ROTATION_HANDLE) + { + if (this.currentAlpha != null) + { + var delta = this.currentAlpha - (this.state.style[mxConstants.STYLE_ROTATION] || 0); + + if (delta != 0) + { + this.rotateCell(this.state.cell, delta); + } + } + else + { + this.rotateClick(); + } + } + else + { + var gridEnabled = this.graph.isGridEnabledEvent(me.getEvent()); + var alpha = mxUtils.toRadians(this.state.style[mxConstants.STYLE_ROTATION] || '0'); + var cos = Math.cos(-alpha); + var sin = Math.sin(-alpha); + + var dx = point.x - this.startX; + var dy = point.y - this.startY; + + // Rotates vector for mouse gesture + var tx = cos * dx - sin * dy; + var ty = sin * dx + cos * dy; + + dx = tx; + dy = ty; + + var s = this.graph.view.scale; + var recurse = this.isRecursiveResize(this.state, me); + this.resizeCell(this.state.cell, this.roundLength(dx / s), this.roundLength(dy / s), + index, gridEnabled, this.isConstrainedEvent(me), recurse); + } + } + finally + { + this.graph.getModel().endUpdate(); + } + + me.consume(); + this.reset(); + this.redrawHandles(); + } +}; + +/** + * Function: isRecursiveResize + * + * Returns the recursiveResize of the give state. + * + * Parameters: + * + * state - the given . This implementation takes + * the value of this state. + * me - the mouse event. + */ +mxVertexHandler.prototype.isRecursiveResize = function(state, me) +{ + return this.graph.isRecursiveResize(this.state); +}; + +/** + * Function: rotateClick + * + * Hook for subclassers to implement a single click on the rotation handle. + * This code is executed as part of the model transaction. This implementation + * is empty. + */ +mxVertexHandler.prototype.rotateClick = function() { }; + +/** + * Function: rotateCell + * + * Rotates the given cell and its children by the given angle in degrees. + * + * Parameters: + * + * cell - to be rotated. + * angle - Angle in degrees. + */ +mxVertexHandler.prototype.rotateCell = function(cell, angle, parent) +{ + if (angle != 0) + { + var model = this.graph.getModel(); + + if (model.isVertex(cell) || model.isEdge(cell)) + { + if (!model.isEdge(cell)) + { + var style = this.graph.getCurrentCellStyle(cell); + var total = (style[mxConstants.STYLE_ROTATION] || 0) + angle; + this.graph.setCellStyles(mxConstants.STYLE_ROTATION, total, [cell]); + } + + var geo = this.graph.getCellGeometry(cell); + + if (geo != null) + { + var pgeo = this.graph.getCellGeometry(parent); + + if (pgeo != null && !model.isEdge(parent)) + { + geo = geo.clone(); + geo.rotate(angle, new mxPoint(pgeo.width / 2, pgeo.height / 2)); + model.setGeometry(cell, geo); + } + + if ((model.isVertex(cell) && !geo.relative) || model.isEdge(cell)) + { + // Recursive rotation + var childCount = model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + this.rotateCell(model.getChildAt(cell, i), angle, cell); + } + } + } + } + } +}; + +/** + * Function: reset + * + * Resets the state of this handler. + */ +mxVertexHandler.prototype.reset = function() +{ + if (this.sizers != null && this.index != null && this.sizers[this.index] != null && + this.sizers[this.index].node.style.display == 'none') + { + this.sizers[this.index].node.style.display = ''; + } + + this.currentAlpha = null; + this.inTolerance = null; + this.index = null; + + // TODO: Reset and redraw cell states for live preview + if (this.preview != null) + { + this.preview.destroy(); + this.preview = null; + } + + if (this.ghostPreview != null) + { + this.ghostPreview.destroy(); + this.ghostPreview = null; + } + + if (this.livePreviewActive && this.sizers != null) + { + for (var i = 0; i < this.sizers.length; i++) + { + if (this.sizers[i] != null) + { + this.sizers[i].node.style.display = ''; + } + } + + // Shows folding icon + if (this.state.control != null && this.state.control.node != null) + { + this.state.control.node.style.visibility = ''; + } + } + + if (this.customHandles != null) + { + for (var i = 0; i < this.customHandles.length; i++) + { + if (this.customHandles[i].active) + { + this.customHandles[i].active = false; + this.customHandles[i].reset(); + } + else + { + this.customHandles[i].setVisible(true); + } + } + } + + // Checks if handler has been destroyed + if (this.selectionBorder != null) + { + this.selectionBorder.node.style.display = 'inline'; + this.selectionBounds = this.getSelectionBounds(this.state); + this.bounds = new mxRectangle(this.selectionBounds.x, this.selectionBounds.y, + this.selectionBounds.width, this.selectionBounds.height); + this.drawPreview(); + } + + this.removeHint(); + this.redrawHandles(); + this.edgeHandlers = null; + this.handlesVisible = true; + this.unscaledBounds = null; + this.livePreviewActive = null; +}; + +/** + * Function: resizeCell + * + * Uses the given vector to change the bounds of the given cell + * in the graph using . + */ +mxVertexHandler.prototype.resizeCell = function(cell, dx, dy, index, gridEnabled, constrained, recurse) +{ + var geo = this.graph.model.getGeometry(cell); + + if (geo != null) + { + if (index == mxEvent.LABEL_HANDLE) + { + var alpha = -mxUtils.toRadians(this.state.style[mxConstants.STYLE_ROTATION] || '0'); + var cos = Math.cos(alpha); + var sin = Math.sin(alpha); + var scale = this.graph.view.scale; + var pt = mxUtils.getRotatedPoint(new mxPoint( + Math.round((this.labelShape.bounds.getCenterX() - this.startX) / scale), + Math.round((this.labelShape.bounds.getCenterY() - this.startY) / scale)), + cos, sin); + + geo = geo.clone(); + + if (geo.offset == null) + { + geo.offset = pt; + } + else + { + geo.offset.x += pt.x; + geo.offset.y += pt.y; + } + + this.graph.model.setGeometry(cell, geo); + } + else if (this.unscaledBounds != null) + { + var scale = this.graph.view.scale; + + if (this.childOffsetX != 0 || this.childOffsetY != 0) + { + this.moveChildren(cell, Math.round(this.childOffsetX / scale), Math.round(this.childOffsetY / scale)); + } + + this.graph.resizeCell(cell, this.unscaledBounds, recurse); + } + } +}; + +/** + * Function: moveChildren + * + * Moves the children of the given cell by the given vector. + */ +mxVertexHandler.prototype.moveChildren = function(cell, dx, dy) +{ + var model = this.graph.getModel(); + var childCount = model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + var child = model.getChildAt(cell, i); + var geo = this.graph.getCellGeometry(child); + + if (geo != null) + { + geo = geo.clone(); + geo.translate(dx, dy); + model.setGeometry(child, geo); + } + } +}; +/** + * 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, constrained) + * { + * 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) + * + * To override this to update the height for a wrapped text if the width of a vertex is + * changed, the following can be used. + * + * (code) + * var mxVertexHandlerUnion = mxVertexHandler.prototype.union; + * mxVertexHandler.prototype.union = function(bounds, dx, dy, index, gridEnabled, scale, tr, constrained) + * { + * var result = mxVertexHandlerUnion.apply(this, arguments); + * var s = this.state; + * + * if (this.graph.isHtmlLabel(s.cell) && (index == 3 || index == 4) && + * s.text != null && s.style[mxConstants.STYLE_WHITE_SPACE] == 'wrap') + * { + * var label = this.graph.getLabel(s.cell); + * var fontSize = mxUtils.getNumber(s.style, mxConstants.STYLE_FONTSIZE, mxConstants.DEFAULT_FONTSIZE); + * var ww = result.width / s.view.scale - s.text.spacingRight - s.text.spacingLeft + * + * result.height = mxUtils.getSizeForString(label, fontSize, s.style[mxConstants.STYLE_FONTFAMILY], ww).height; + * } + * + * return result; + * }; + * (end) + */ +mxVertexHandler.prototype.union = function(bounds, dx, dy, index, gridEnabled, scale, tr, constrained, centered) +{ + gridEnabled = (gridEnabled != null) ? gridEnabled && this.graph.gridEnabled : this.graph.gridEnabled; + + 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 w0 = bounds.width; + var h0 = bounds.height; + var left = bounds.x - tr.x * scale; + var right = left + w0; + var top = bounds.y - tr.y * scale; + var bottom = top + h0; + + var cx = left + w0 / 2; + var cy = top + h0 / 2; + + if (index > 4 /* Bottom Row */) + { + bottom = bottom + dy; + + if (gridEnabled) + { + bottom = this.graph.snap(bottom / scale) * scale; + } + else + { + bottom = Math.round(bottom / scale) * scale; + } + } + else if (index < 3 /* Top Row */) + { + top = top + dy; + + if (gridEnabled) + { + top = this.graph.snap(top / scale) * scale; + } + else + { + top = Math.round(top / scale) * scale; + } + } + + if (index == 0 || index == 3 || index == 5 /* Left */) + { + left += dx; + + if (gridEnabled) + { + left = this.graph.snap(left / scale) * scale; + } + else + { + left = Math.round(left / scale) * scale; + } + } + else if (index == 2 || index == 4 || index == 7 /* Right */) + { + right += dx; + + if (gridEnabled) + { + right = this.graph.snap(right / scale) * scale; + } + else + { + right = Math.round(right / scale) * scale; + } + } + + var width = right - left; + var height = bottom - top; + + if (constrained) + { + var geo = this.graph.getCellGeometry(this.state.cell); + + if (geo != null) + { + var aspect = geo.width / geo.height; + + if (index== 1 || index== 2 || index == 7 || index == 6) + { + width = height * aspect; + } + else + { + height = width / aspect; + } + + if (index == 0) + { + left = right - width; + top = bottom - height; + } + } + } + + if (centered) + { + width += (width - w0); + height += (height - h0); + + var cdx = cx - (left + width / 2); + var cdy = cy - (top + height / 2); + + left += cdx; + top += cdy; + right += cdx; + bottom += cdy; + } + + // 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); + } + + var result = new mxRectangle(left + tr.x * scale, top + tr.y * scale, width, height); + + if (this.minBounds != null) + { + result.width = Math.max(result.width, this.minBounds.x * scale + this.minBounds.width * scale + + Math.max(0, this.x0 * scale - result.x)); + result.height = Math.max(result.height, this.minBounds.y * scale + this.minBounds.height * scale + + Math.max(0, this.y0 * scale - result.y)); + } + + return result; + } +}; + +/** + * Function: redraw + * + * Redraws the handles and the preview. + */ +mxVertexHandler.prototype.redraw = function(ignoreHandles) +{ + this.selectionBounds = this.getSelectionBounds(this.state); + this.bounds = new mxRectangle(this.selectionBounds.x, this.selectionBounds.y, + this.selectionBounds.width, this.selectionBounds.height); + this.drawPreview(); + + if (!ignoreHandles) + { + this.redrawHandles(); + } +}; + +/** + * Returns the padding to be used for drawing handles for the current . + */ +mxVertexHandler.prototype.getHandlePadding = function() +{ + // KNOWN: Tolerance depends on event type (eg. 0 for mouse events) + var result = new mxPoint(0, 0); + var tol = this.tolerance; + + if (this.sizers != null && this.sizers.length > 0 && this.sizers[0] != null && + (this.bounds.width < 2 * this.sizers[0].bounds.width + 2 * tol || + this.bounds.height < 2 * this.sizers[0].bounds.height + 2 * tol)) + { + tol /= 2; + + result.x = this.sizers[0].bounds.width + tol; + result.y = this.sizers[0].bounds.height + tol; + } + + return result; +}; + +/** + * Function: getSizerBounds + * + * Returns the bounds used to paint the resize handles. + */ +mxVertexHandler.prototype.getSizerBounds = function() +{ + return this.bounds; +}; + +/** + * Function: redrawHandles + * + * Redraws the handles. To hide certain handles the following code can be used. + * + * (code) + * mxVertexHandler.prototype.redrawHandles = function() + * { + * mxVertexHandlerRedrawHandles.apply(this, arguments); + * + * if (this.sizers != null && this.sizers.length > 7) + * { + * this.sizers[1].node.style.display = 'none'; + * this.sizers[6].node.style.display = 'none'; + * } + * }; + * (end) + */ +mxVertexHandler.prototype.redrawHandles = function() +{ + var s = this.getSizerBounds(); + var tol = this.tolerance; + this.horizontalOffset = 0; + this.verticalOffset = 0; + + if (this.customHandles != null) + { + for (var i = 0; i < this.customHandles.length; i++) + { + var temp = this.customHandles[i].shape.node.style.display; + this.customHandles[i].redraw(); + this.customHandles[i].shape.node.style.display = temp; + + // Hides custom handles during text editing + this.customHandles[i].shape.node.style.visibility = + (this.handlesVisible && this.isCustomHandleVisible( + this.customHandles[i])) ? '' : 'hidden'; + } + } + + if (this.sizers != null && this.sizers.length > 0 && this.sizers[0] != null) + { + if (this.index == null && this.manageSizers && this.sizers.length >= 8) + { + // KNOWN: Tolerance depends on event type (eg. 0 for mouse events) + var padding = this.getHandlePadding(); + this.horizontalOffset = padding.x; + this.verticalOffset = padding.y; + + if (this.horizontalOffset != 0 || this.verticalOffset != 0) + { + s = new mxRectangle(s.x, s.y, s.width, s.height); + + s.x -= this.horizontalOffset / 2; + s.width += this.horizontalOffset; + s.y -= this.verticalOffset / 2; + s.height += this.verticalOffset; + } + + if (this.sizers.length >= 8) + { + if ((s.width < 2 * this.sizers[0].bounds.width + 2 * tol) || + (s.height < 2 * this.sizers[0].bounds.height + 2 * tol)) + { + this.sizers[0].node.style.display = 'none'; + this.sizers[2].node.style.display = 'none'; + this.sizers[5].node.style.display = 'none'; + this.sizers[7].node.style.display = 'none'; + } + else if (this.handlesVisible) + { + this.sizers[0].node.style.display = ''; + this.sizers[2].node.style.display = ''; + this.sizers[5].node.style.display = ''; + this.sizers[7].node.style.display = ''; + } + } + } + + 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 >= 8) + { + var crs = ['nw-resize', 'n-resize', 'ne-resize', 'e-resize', 'se-resize', 's-resize', 'sw-resize', 'w-resize']; + + var alpha = mxUtils.toRadians(this.state.style[mxConstants.STYLE_ROTATION] || '0'); + var cos = Math.cos(alpha); + var sin = Math.sin(alpha); + + var da = Math.round(alpha * 4 / Math.PI); + + var ct = new mxPoint(s.getCenterX(), s.getCenterY()); + var pt = mxUtils.getRotatedPoint(new mxPoint(s.x, s.y), cos, sin, ct); + + this.moveSizerTo(this.sizers[0], pt.x, pt.y); + this.sizers[0].setCursor(crs[mxUtils.mod(0 + da, crs.length)]); + + pt.x = cx; + pt.y = s.y; + pt = mxUtils.getRotatedPoint(pt, cos, sin, ct); + + this.moveSizerTo(this.sizers[1], pt.x, pt.y); + this.sizers[1].setCursor(crs[mxUtils.mod(1 + da, crs.length)]); + + pt.x = r; + pt.y = s.y; + pt = mxUtils.getRotatedPoint(pt, cos, sin, ct); + + this.moveSizerTo(this.sizers[2], pt.x, pt.y); + this.sizers[2].setCursor(crs[mxUtils.mod(2 + da, crs.length)]); + + pt.x = s.x; + pt.y = cy; + pt = mxUtils.getRotatedPoint(pt, cos, sin, ct); + + this.moveSizerTo(this.sizers[3], pt.x, pt.y); + this.sizers[3].setCursor(crs[mxUtils.mod(7 + da, crs.length)]); + + pt.x = r; + pt.y = cy; + pt = mxUtils.getRotatedPoint(pt, cos, sin, ct); + + this.moveSizerTo(this.sizers[4], pt.x, pt.y); + this.sizers[4].setCursor(crs[mxUtils.mod(3 + da, crs.length)]); + + pt.x = s.x; + pt.y = b; + pt = mxUtils.getRotatedPoint(pt, cos, sin, ct); + + this.moveSizerTo(this.sizers[5], pt.x, pt.y); + this.sizers[5].setCursor(crs[mxUtils.mod(6 + da, crs.length)]); + + pt.x = cx; + pt.y = b; + pt = mxUtils.getRotatedPoint(pt, cos, sin, ct); + + this.moveSizerTo(this.sizers[6], pt.x, pt.y); + this.sizers[6].setCursor(crs[mxUtils.mod(5 + da, crs.length)]); + + pt.x = r; + pt.y = b; + pt = mxUtils.getRotatedPoint(pt, cos, sin, ct); + + this.moveSizerTo(this.sizers[7], pt.x, pt.y); + this.sizers[7].setCursor(crs[mxUtils.mod(4 + da, crs.length)]); + + pt.x = cx + this.state.absoluteOffset.x; + pt.y = cy + this.state.absoluteOffset.y; + pt = mxUtils.getRotatedPoint(pt, cos, sin, ct); + this.moveSizerTo(this.sizers[8], pt.x, pt.y); + } + else if (this.state.width >= 2 && this.state.height >= 2) + { + this.moveSizerTo(this.sizers[0], cx + this.state.absoluteOffset.x, cy + this.state.absoluteOffset.y); + } + else + { + this.moveSizerTo(this.sizers[0], this.state.x, this.state.y); + } + } + } + + if (this.rotationShape != null) + { + var alpha = mxUtils.toRadians((this.currentAlpha != null) ? this.currentAlpha : this.state.style[mxConstants.STYLE_ROTATION] || '0'); + var cos = Math.cos(alpha); + var sin = Math.sin(alpha); + + var ct = new mxPoint(this.state.getCenterX(), this.state.getCenterY()); + var pt = mxUtils.getRotatedPoint(this.getRotationHandlePosition(), cos, sin, ct); + + if (this.rotationShape.node != null) + { + this.moveSizerTo(this.rotationShape, pt.x, pt.y); + + // Hides rotation handle during text editing + this.rotationShape.node.style.visibility = (this.state.view.graph.isEditing() || + !this.handlesVisible) ? 'hidden' : ''; + } + } + + if (this.selectionBorder != null) + { + this.selectionBorder.rotation = Number(this.state.style[mxConstants.STYLE_ROTATION] || '0'); + } + + if (this.edgeHandlers != null) + { + for (var i = 0; i < this.edgeHandlers.length; i++) + { + this.edgeHandlers[i].redraw(); + } + } +}; + +/** + * Function: isCustomHandleVisible + * + * Returns true if the given custom handle is visible. + */ +mxVertexHandler.prototype.isCustomHandleVisible = function(handle) +{ + return !this.graph.isEditing() && this.state.view.graph.getSelectionCount() == 1; +}; + +/** + * Function: getRotationHandlePosition + * + * Returns an that defines the rotation handle position. + */ +mxVertexHandler.prototype.getRotationHandlePosition = function() +{ + return new mxPoint(this.bounds.x + this.bounds.width / 2, this.bounds.y + this.rotationHandleVSpacing) +}; + +/** + * Function: isParentHighlightVisible + * + * Returns true if the parent highlight should be visible. This implementation + * always returns true. + */ +mxVertexHandler.prototype.isParentHighlightVisible = function() +{ + return !this.graph.isCellSelected(this.graph.model.getParent(this.state.cell)); +}; + +/** + * Function: updateParentHighlight + * + * Updates the highlight of the parent if is true. + */ +mxVertexHandler.prototype.updateParentHighlight = function() +{ + if (!this.isDestroyed()) + { + var visible = this.isParentHighlightVisible(); + var parent = this.graph.model.getParent(this.state.cell); + var pstate = this.graph.view.getState(parent); + + if (this.parentHighlight != null) + { + if (this.graph.model.isVertex(parent) && visible) + { + var b = this.parentHighlight.bounds; + + if (pstate != null && (b.x != pstate.x || b.y != pstate.y || + b.width != pstate.width || b.height != pstate.height)) + { + this.parentHighlight.bounds = mxRectangle.fromRectangle(pstate); + this.parentHighlight.redraw(); + } + } + else + { + if (pstate != null && pstate.parentHighlight == this.parentHighlight) + { + pstate.parentHighlight = null; + } + + this.parentHighlight.destroy(); + this.parentHighlight = null; + } + } + else if (this.parentHighlightEnabled && visible) + { + if (this.graph.model.isVertex(parent) && pstate != null && + pstate.parentHighlight == null) + { + this.parentHighlight = this.createParentHighlightShape(pstate); + // VML dialect required here for event transparency in IE + this.parentHighlight.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ? mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG; + this.parentHighlight.pointerEvents = false; + this.parentHighlight.rotation = Number(pstate.style[mxConstants.STYLE_ROTATION] || '0'); + this.parentHighlight.init(this.graph.getView().getOverlayPane()); + this.parentHighlight.redraw(); + + // Shows highlight once per parent + pstate.parentHighlight = this.parentHighlight; + } + } + } +}; + +/** + * 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.rotation = Number(this.state.style[mxConstants.STYLE_ROTATION] || '0'); + this.preview.redraw(); + } + + this.selectionBorder.bounds = this.getSelectionBorderBounds(); + this.selectionBorder.redraw(); + this.updateParentHighlight(); +}; + +/** + * Function: getSelectionBorderBounds + * + * Returns the bounds for the selection border. + */ +mxVertexHandler.prototype.getSelectionBorderBounds = function() +{ + return this.bounds; +}; + +/** + * Function: isDestroyed + * + * Returns true if this handler was destroyed or not initialized. + */ +mxVertexHandler.prototype.isDestroyed = function() +{ + return this.selectionBorder == null; +}; + +/** + * Function: destroy + * + * Destroys the handler and all its resources and DOM nodes. + */ +mxVertexHandler.prototype.destroy = function() +{ + if (this.escapeHandler != null) + { + this.state.view.graph.removeListener(this.escapeHandler); + this.escapeHandler = null; + } + + if (this.preview != null) + { + this.preview.destroy(); + this.preview = null; + } + + if (this.parentHighlight != null) + { + var parent = this.graph.model.getParent(this.state.cell); + var pstate = this.graph.view.getState(parent); + + if (pstate != null && pstate.parentHighlight == this.parentHighlight) + { + pstate.parentHighlight = null; + } + + this.parentHighlight.destroy(); + this.parentHighlight = null; + } + + if (this.ghostPreview != null) + { + this.ghostPreview.destroy(); + this.ghostPreview = null; + } + + if (this.selectionBorder != null) + { + this.selectionBorder.destroy(); + this.selectionBorder = null; + } + + this.labelShape = null; + this.removeHint(); + + if (this.sizers != null) + { + for (var i = 0; i < this.sizers.length; i++) + { + this.sizers[i].destroy(); + } + + this.sizers = null; + } + + if (this.customHandles != null) + { + for (var i = 0; i < this.customHandles.length; i++) + { + this.customHandles[i].destroy(); + } + + this.customHandles = null; + } +}; diff --git a/mxclient/js/index.txt b/mxclient/js/index.txt new file mode 100644 index 0000000..f3631d6 --- /dev/null +++ b/mxclient/js/index.txt @@ -0,0 +1,316 @@ +Document: API Specification + +Overview: + + This JavaScript library is divided into 8 packages. The top-level + class includes (or dynamically imports) everything else. The current version + is stored in . + + The *editor* package provides the classes required to implement a diagram + editor. The main class in this package is . + + The *view* and *model* packages implement the graph component, represented + by . It refers to a which contains s and + caches the state of the cells in a . The cells are painted + using a based on the appearance defined in . + Undo history is implemented in . To display an icon on the + graph, may be used. Validation rules are defined with + . + + The *handler*, *layout* and *shape* packages contain event listeners, + layout algorithms and shapes, respectively. The graph event listeners + include for rubberband selection, + for tooltips and for basic cell modifications. + implements a tree layout algorithm, and the + shape package provides various shapes, which are subclasses of + . + + The *util* package provides utility classes including for + copy-paste, for drag-and-drop, for keys and + values of stylesheets, and for cross-browser + event-handling and general purpose functions, for + internationalization and for console output. + + The *io* package implements a generic for turning + JavaScript objects into XML. The main class is . + is the global registry for custom codecs. + +Events: + + There are three different types of events, namely native DOM events, + which are fired in an , and + which are fired in . + + Some helper methods for handling native events are provided in . 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 . Its + listeners are functions that take a sender and . Additionally, + the class fires special which are handled using + mouse listeners, which are objects that provide a mousedown, mousemove and + mouseup method. + + Events in are fired using . + Listeners are added and removed using and + . in are fired using + . Listeners are added and removed using + and , 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 . + - mxImageBasePath: Specifies the path in . + - mxLanguage: Specifies the language for resources in . + - mxDefaultLanguage: Specifies the default language in . + - 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 + - *window._mxDynamicCode*: Temporarily used to load code in Safari and Chrome + (see ). + - *_mxJavaScriptExpression*: Global variable that is temporarily used to + evaluate code in Safari, Opera, Firefox 3 and IE (see ). + +Files: + + The library contains these relative filenames. All filenames are relative + to . + +Built-in Images: + + All images are loaded from the , + 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 + is defined in . + +Resources: + + The and classes add the following resources to + 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) + + (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, 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 . 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 its + constructor using mxUtils.getFunctionName(obj.constructor). + +Constructor: + + For subclassing in mxGraph, the same scheme should be applied. For example, + for subclassing the 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/mxclient/js/io/mxCellCodec.js b/mxclient/js/io/mxCellCodec.js new file mode 100644 index 0000000..329fc67 --- /dev/null +++ b/mxclient/js/io/mxCellCodec.js @@ -0,0 +1,189 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +mxCodecRegistry.register(function() +{ + /** + * Class: mxCellCodec + * + * Codec for s. This class is created and registered + * dynamically at load time and used implicitly via + * and the . + * + * 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'); + * + * To subclass , replace the template and add an alias as + * follows. + * + * (code) + * function CustomCell(value, geometry, style) + * { + * mxCell.apply(this, arguments); + * } + * + * mxUtils.extend(CustomCell, mxCell); + * + * mxCodecRegistry.getCodec(mxCell).template = new CustomCell(); + * mxCodecRegistry.addAlias('CustomCell', 'mxCell'); + * (end) + */ + 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; + }; + + /** + * Overidden to disable conversion of value to number. + */ + codec.isNumericAttribute = function(dec, attr, obj) + { + return attr.nodeName !== 'value' && mxObjectCodec.prototype.isNumericAttribute.apply(this, arguments); + }; + + /** + * 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 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 = mxUtils.importNode(enc.document, 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 and uses the enclosing XML node as + * the user object for the cell (inversion). + */ + codec.beforeDecode = function(dec, node, obj) + { + var inner = node.cloneNode(true); + 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/mxclient/js/io/mxChildChangeCodec.js b/mxclient/js/io/mxChildChangeCodec.js new file mode 100644 index 0000000..5cff378 --- /dev/null +++ b/mxclient/js/io/mxChildChangeCodec.js @@ -0,0 +1,168 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +mxCodecRegistry.register(function() +{ + /** + * Class: mxChildChangeCodec + * + * Codec for s. This class is created and registered + * dynamically at load time and used implicitly via and + * the . + * + * 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' && (!isWrite || obj.model.contains(obj.previous))) + { + return true; + } + + return mxUtils.indexOf(this.idrefs, attr) >= 0; + }; + + /** + * Function: isExcluded + * + * Excludes references to parent or previous if not in the model. + */ + codec.isExcluded = function(obj, attr, value, write) + { + return mxObjectCodec.prototype.isExcluded.apply(this, arguments) || + (write && value != null && (attr == 'previous' || + attr == 'parent') && !obj.model.contains(value)); + }; + + /** + * 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 decoded 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. + if (obj.child != null) + { + if (obj.child.parent != null && obj.previous != null && + obj.child.parent != obj.previous) + { + obj.previous = obj.child.parent; + } + + 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/mxclient/js/io/mxCodec.js b/mxclient/js/io/mxCodec.js new file mode 100644 index 0000000..52ea282 --- /dev/null +++ b/mxclient/js/io/mxCodec.js @@ -0,0 +1,621 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxCodec + * + * XML codec for JavaScript object graphs. See for a + * description of the general encoding/decoding scheme. This class uses the + * codecs registered in 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 code below, an XML document is decoded into an existing model. The + * document may be obtained using one of the functions in mxUtils for loading + * an XML file, eg. , or using for parsing an + * XML string. + * + * (code) + * var doc = mxUtils.parseXml(xmlString); + * var codec = new mxCodec(doc); + * codec.decode(doc.documentElement, graph.getModel()); + * (end) + * + * Example: + * + * This example demonstrates parsing a list of isolated cells into an existing + * graph model. Note that the cells do not have a parent reference so they can + * be added anywhere in the cell hierarchy after parsing. + * + * (code) + * var xml = ''; + * var doc = mxUtils.parseXml(xml); + * var codec = new mxCodec(doc); + * var elt = doc.documentElement.firstChild; + * var cells = []; + * + * while (elt != null) + * { + * cells.push(codec.decode(elt)); + * elt = elt.nextSibling; + * } + * + * graph.addCells(cells); + * (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 converted to
, in which case a '
' argument + * must be passed to as the second argument. + * + * 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) + * + * Note that the I/O system adds object codecs for new object automatically. For + * decoding those objects, the constructor should be written as follows: + * + * (code) + * var MyObj = function(name) + * { + * // ... + * }; + * (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 . + */ +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: elements + * + * Lookup table for resolving IDs to elements. + */ +mxCodec.prototype.elements = 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 + * . If the object is not known then is used to find an + * object. If no object is found, then the element with the respective ID + * from the document is parsed using . + */ +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 . + * + * Parameters: + * + * id - String that contains the ID. + */ +mxCodec.prototype.getElementById = function(id) +{ + this.updateElements(); + + return this.elements[id]; +}; + +/** + * Function: updateElements + * + * Returns the element with the given ID from . + * + * Parameters: + * + * id - String that contains the ID. + */ +mxCodec.prototype.updateElements = function() +{ + if (this.elements == null) + { + this.elements = new Object(); + + if (this.document.documentElement != null) + { + this.addElement(this.document.documentElement); + } + } +}; + +/** + * Function: addElement + * + * Adds the given element to if it has an ID. + */ +mxCodec.prototype.addElement = function(node) +{ + if (node.nodeType == mxConstants.NODETYPE_ELEMENT) + { + var id = node.getAttribute('id'); + + if (id != null) + { + if (this.elements[id] == null) + { + this.elements[id] = node; + } + else if (this.elements[id] != node) + { + throw new Error(id + ': Duplicate ID'); + } + } + } + + node = node.firstChild; + + while (node != null) + { + this.addElement(node); + node = node.nextSibling; + } +}; + +/** + * Function: getId + * + * Returns the ID of the specified object. This implementation + * calls first and if that returns null handles + * the object as an by returning their IDs using + * . If no ID exists for the given cell, then + * an on-the-fly ID is generated using . + * + * 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 = mxUtils.importNode(this.document, 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) +{ + this.updateElements(); + var obj = null; + + if (node != null && node.nodeType == mxConstants.NODETYPE_ELEMENT) + { + var ctor = null; + + try + { + ctor = window[node.nodeName]; + } + catch (err) + { + // ignore + } + + var dec = mxCodecRegistry.getCodec(ctor); + + if (dec != null) + { + obj = dec.decode(this, node, into); + } + else + { + obj = node.cloneNode(true); + obj.removeAttribute('as'); + } + } + + 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. , + * and ). 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 - 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 + * 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 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) + { + if (parent == cell) + { + throw new Error(parent.id + ': Self Reference'); + } + else + { + 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/mxclient/js/io/mxCodecRegistry.js b/mxclient/js/io/mxCodecRegistry.js new file mode 100644 index 0000000..42ebcd7 --- /dev/null +++ b/mxclient/js/io/mxCodecRegistry.js @@ -0,0 +1,137 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +var mxCodecRegistry = +{ + /** + * Class: mxCodecRegistry + * + * Singleton class that acts as a global registry for codecs. + * + * Adding an : + * + * 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 . + * + * (code) + * mxCodecRegistry.register(codec); + * (end) + * + * 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 - 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/mxclient/js/io/mxDefaultKeyHandlerCodec.js b/mxclient/js/io/mxDefaultKeyHandlerCodec.js new file mode 100644 index 0000000..e24e88f --- /dev/null +++ b/mxclient/js/io/mxDefaultKeyHandlerCodec.js @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +mxCodecRegistry.register(function() +{ + /** + * Class: mxDefaultKeyHandlerCodec + * + * Custom codec for configuring s. This class is created + * and registered dynamically at load time and used implicitly via + * and the . 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) + * + * + * + * + * + * (end) + * + * The keycodes are for the x, c and v keys. + * + * See also: , + * 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/mxclient/js/io/mxDefaultPopupMenuCodec.js b/mxclient/js/io/mxDefaultPopupMenuCodec.js new file mode 100644 index 0000000..b6fa1c0 --- /dev/null +++ b/mxclient/js/io/mxDefaultPopupMenuCodec.js @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +mxCodecRegistry.register(function() +{ + /** + * Class: mxDefaultPopupMenuCodec + * + * Custom codec for configuring s. This class is created + * and registered dynamically at load time and used implicitly via + * and the . 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 + * . + */ + 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 . + */ + 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/mxclient/js/io/mxDefaultToolbarCodec.js b/mxclient/js/io/mxDefaultToolbarCodec.js new file mode 100644 index 0000000..5a66fec --- /dev/null +++ b/mxclient/js/io/mxDefaultToolbarCodec.js @@ -0,0 +1,312 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxDefaultToolbarCodec + * + * Custom codec for configuring s. This class is created + * and registered dynamically at load time and used implicitly via + * and the . This codec only reads configuration + * data for existing toolbars handlers, it does not encode or create toolbars. + */ +var mxDefaultToolbarCodec = mxCodecRegistry.register(function() +{ + 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) + * + * (end) + * + * In the above function, editor is the enclosing 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 . + * + * 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) + * + * (end) + * + * Both functions require to be set to true. + * + * 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) + * + * + *

+ * + * + *
+ * (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 = (mxDefaultToolbarCodec.allowEval) ? mxUtils.eval(text) : null; + 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 = editor.graph.cloneCell(cell); + cell.setStyle(style); + } + + var insertFunction = null; + + if (text != null && text.length > 0 && mxDefaultToolbarCodec.allowEval) + { + 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 0) + { + elt.setAttribute('id', id); + } + } + } + } + } + + node = node.nextSibling; + } + } + + return into; + }; + + // Returns the codec into the registry + return codec; + +}()); + +/** + * Variable: allowEval + * + * Static global switch that specifies if the use of eval is allowed for + * evaluating text content. Default is true. Set this to false if stylesheets + * may contain user input + */ +mxDefaultToolbarCodec.allowEval = true; diff --git a/mxclient/js/io/mxEditorCodec.js b/mxclient/js/io/mxEditorCodec.js new file mode 100644 index 0000000..1c9d718 --- /dev/null +++ b/mxclient/js/io/mxEditorCodec.js @@ -0,0 +1,245 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +mxCodecRegistry.register(function() +{ + /** + * Class: mxEditorCodec + * + * Codec for s. This class is created and registered + * dynamically at load time and used implicitly via + * and the . + * + * 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 ). + * + * The x, y, width and height attributes are used to create a new + * 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 ). + * title - Title element (see ). + * toolbar - Toolbar element (see ). + * status - Status bar element (see ). + * + * Example: + * + * (code) + * + * + * + * + * + * + * + * (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: decodeUi + * + * Decodes the ui elements from the given node. + */ + 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; js, s, s, + * s and s. This class is created + * and registered dynamically at load time and used implicitly + * via and the . + * + * Transient Fields: + * + * - model + * - previous + * + * Reference Fields: + * + * - cell + * + * Constructor: mxGenericChangeCodec + * + * Factory function that creates a 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/mxclient/js/io/mxGraphCodec.js b/mxclient/js/io/mxGraphCodec.js new file mode 100644 index 0000000..f7f9a15 --- /dev/null +++ b/mxclient/js/io/mxGraphCodec.js @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +mxCodecRegistry.register(function() +{ + /** + * Class: mxGraphCodec + * + * Codec for s. This class is created and registered + * dynamically at load time and used implicitly via + * and the . + * + * Transient Fields: + * + * - graphListeners + * - eventListeners + * - view + * - container + * - cellRenderer + * - editor + * - selection + */ + return new mxObjectCodec(new mxGraph(), + ['graphListeners', 'eventListeners', 'view', 'container', + 'cellRenderer', 'editor', 'selection']); + +}()); diff --git a/mxclient/js/io/mxGraphViewCodec.js b/mxclient/js/io/mxGraphViewCodec.js new file mode 100644 index 0000000..5343ae0 --- /dev/null +++ b/mxclient/js/io/mxGraphViewCodec.js @@ -0,0 +1,197 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +mxCodecRegistry.register(function() +{ + /** + * Class: mxGraphViewCodec + * + * Custom encoder for s. This class is created + * and registered dynamically at load time and used implicitly via + * and the . 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 using + * 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 + * returns true for the cell, + * then edge is used for the nodename, else if + * returns true for the cell, + * then vertex is used for the nodename. + * + * 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; is. This class is created and registered + * dynamically at load time and used implicitly via + * and the . + */ + var codec = new mxObjectCodec(new mxGraphModel()); + + /** + * Function: encodeObject + * + * Encodes the given by writing a (flat) XML sequence of + * cell nodes as produced by the . 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/mxclient/js/io/mxObjectCodec.js b/mxclient/js/io/mxObjectCodec.js new file mode 100644 index 0000000..0d8f8b1 --- /dev/null +++ b/mxclient/js/io/mxObjectCodec.js @@ -0,0 +1,1097 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * 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 as follows. + * + * (code) + * mxLog.show(); + * mxLog.debug(mxUtils.getPrettyXml(node)); + * (end) + * + * Finally, the result of the encoding looks as follows. + * + * (code) + * + * (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) + * + * + * + * (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) + * + * + * + * + * + * (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 . The + * 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 , an array of strings that is used to configure + * the . + * + * 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 , 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 . 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, 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 . + * + * (code) + * + * mxConstants.ALIGN_LEFT + * + * (end) + * + * The resulting object has a field called foo with the value "left". Its XML + * representation looks as follows. + * + * (code) + * + * (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. + * + * Expressions are only evaluated if is true. + * + * 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: allowEval + * + * Static global switch that specifies if expressions in arrays are allowed. + * Default is false. NOTE: Enabling this carries a possible security risk. + */ +mxObjectCodec.allowEval = false; + +/** + * 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 + * and . + */ +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 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 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 or + * if the fieldname equals . + * + * 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 . + * + * 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 + * after creating the node and 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 then it is ignored. + * - If the variable name is in then + * is used to replace the object with its ID. + * - The variable name is mapped using . + * - 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 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 or if an object + * cannot be encoded, a warning is issued using . + * + * Returns the resulting XML node that represents the given + * object. + * + * Parameters: + * + * enc - 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 + * . + * + * Parameters: + * + * enc - 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.isInteger(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 + * to write the attribute into the given node. + * + * Parameters: + * + * enc - 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 + * or depending on the type of the value. + */ +mxObjectCodec.prototype.writeAttribute = function(enc, obj, name, value, node) +{ + if (typeof(value) != 'object' /* primitive type */) + { + this.writePrimitiveAttribute(enc, obj, name, value, node); + } + else /* complex type */ + { + this.writeComplexAttribute(enc, obj, name, value, node); + } +}; + +/** + * Function: writePrimitiveAttribute + * + * Writes the given value as an attribute of the given node. + */ +mxObjectCodec.prototype.writePrimitiveAttribute = function(enc, obj, name, value, node) +{ + value = this.convertAttributeToXml(enc, obj, name, value, node); + + if (name == 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, name, value); + } +}; + +/** + * Function: writeComplexAttribute + * + * Writes the given value as a child node of the given node. + */ +mxObjectCodec.prototype.writeComplexAttribute = function(enc, obj, name, value, node) +{ + var child = enc.encode(value); + + if (child != null) + { + if (name != null) + { + child.setAttribute('as', name); + } + + node.appendChild(child); + } + else + { + mxLog.warn('mxObjectCodec.encode: No node for ' + this.getName() + '.' + name + ': ' + value); + } +}; + +/** + * Function: convertAttributeToXml + * + * Converts true to "1" and false to "0" is returns true. + * All other values are not converted. + * + * Parameters: + * + * enc - that controls the encoding process. + * obj - Objec to convert the attribute for. + * name - Name of the attribute to be converted. + * value - Value to be converted. + */ +mxObjectCodec.prototype.convertAttributeToXml = function(enc, obj, name, value) +{ + // Makes sure to encode boolean values as numeric values + if (this.isBooleanAttribute(enc, obj, name, value)) + { + // 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: isBooleanAttribute + * + * Returns true if the given object attribute is a boolean value. + * + * Parameters: + * + * enc - that controls the encoding process. + * obj - Objec to convert the attribute for. + * name - Name of the attribute to be converted. + * value - Value of the attribute to be converted. + */ +mxObjectCodec.prototype.isBooleanAttribute = function(enc, obj, name, value) +{ + return (typeof(value.length) == 'undefined' && (value == true || value == false)); +}; + +/** + * Function: convertAttributeFromXml + * + * Converts booleans and numeric values to the respective types. Values are + * numeric if returns true. + * + * Parameters: + * + * dec - that controls the decoding process. + * attr - XML attribute to be converted. + * obj - Objec to convert the attribute for. + */ +mxObjectCodec.prototype.convertAttributeFromXml = function(dec, attr, obj) +{ + var value = attr.value; + + if (this.isNumericAttribute(dec, attr, obj)) + { + value = parseFloat(value); + + if (isNaN(value) || !isFinite(value)) + { + value = 0; + } + } + + return value; +}; + +/** + * Function: isNumericAttribute + * + * Returns true if the given XML attribute is or should be a numeric value. + * + * Parameters: + * + * dec - that controls the decoding process. + * attr - XML attribute to be converted. + * obj - Objec to convert the attribute for. + */ +mxObjectCodec.prototype.isNumericAttribute = function(dec, attr, obj) +{ + // Handles known numeric attributes for generic objects + var result = (obj.constructor == mxGeometry && + (attr.name == 'x' || attr.name == 'y' || + attr.name == 'width' || attr.name == 'height')) || + (obj.constructor == mxPoint && + (attr.name == 'x' || attr.name == 'y')) || + mxUtils.isNumeric(attr.value); + + return result; +}; + +/** + * 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 to perform + * the default encoding into the given node. + * + * Parameters: + * + * enc - 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 . + * + * Parameters: + * + * enc - 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