diff --git a/README.md b/README.md index 9275f4e2..d56d8c05 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ## Web application for database modeling and teaching -![Hero shot](https://raw.githubusercontent.com/brmodeloweb/brmodelo-site/master/img/hero-shot.png) +![Hero shot](https://raw.githubusercontent.com/brmodeloweb/brmodelo-site/master/img/hero-shot-en.png) > Released under the [Apache License 2.0](https://choosealicense.com/licenses/apache-2.0/) ## Dependencies diff --git a/app/angular/conceptual/conceptual.html b/app/angular/conceptual/conceptual.html index 9c7cc9d3..7677d6a4 100644 --- a/app/angular/conceptual/conceptual.html +++ b/app/angular/conceptual/conceptual.html @@ -23,8 +23,8 @@
-
- +
+
diff --git a/app/angular/conceptual/conceptual.js b/app/angular/conceptual/conceptual.js index 457968ec..c083bb98 100644 --- a/app/angular/conceptual/conceptual.js +++ b/app/angular/conceptual/conceptual.js @@ -3,14 +3,11 @@ import $ from "jquery"; import * as joint from "jointjs/dist/joint"; -import "../../joint/joint.ui.stencil"; -import "../../joint/joint.ui.stencil.css"; -import "../../joint/joint.ui.selectionView"; -import "../../joint/joint.ui.selectionView.css"; -import "../../joint/joint.ui.halo.css"; -import "../../joint/joint.ui.halo"; -import "../../joint/br-scroller"; -import "../../joint/joint.dia.command"; +import "../editor/editorManager"; +import "../editor/editorScroller"; +import "../editor/editorActions"; +import "../editor/elementActions"; + import shapes from "../../joint/shapes"; joint.shapes.erd = shapes; @@ -48,12 +45,11 @@ const controller = function (ModelAPI, $stateParams, $rootScope, $timeout, $uibM user: $rootScope.loggeduser } ctrl.selectedElement = {}; - ctrl.selectedHalo = {}; + ctrl.selectedElementActions = {}; const configs = { graph: {}, paper: {}, - paperScroller: {}, - commandManager: {}, + editorActions: {}, keyboardController: null, }; @@ -90,23 +86,23 @@ const controller = function (ModelAPI, $stateParams, $rootScope, $timeout, $uibM } ctrl.undoModel = () => { - configs.commandManager.undo(); + configs.editorActions.undo(); } ctrl.redoModel = () => { - configs.commandManager.redo(); + configs.editorActions.redo(); } ctrl.zoomIn = () => { - configs.paperScroller.zoom(0.1, { max: 2 }); + configs.editorScroller.zoom(0.1, { max: 2 }); } ctrl.zoomOut = () => { - configs.paperScroller.zoom(-0.1, { min: 0.2 }); + configs.editorScroller.zoom(-0.1, { min: 0.2 }); } ctrl.zoomNone = () => { - configs.paperScroller.zoom(); + configs.editorScroller.zoom(); } ctrl.duplicateModel = (model) => { @@ -152,10 +148,9 @@ const controller = function (ModelAPI, $stateParams, $rootScope, $timeout, $uibM ctrl.unselectAll = () => { ctrl.showFeedback(false, ""); ctrl.onSelectElement(null); - configs.selectionView.cancelSelection(); - if(configs.selectedHalo) { - configs.selectedHalo.remove(); - configs.selectedHalo = null; + if(configs.selectedElementActions) { + configs.selectedElementActions.remove(); + configs.selectedElementActions = null; } } @@ -352,9 +347,9 @@ const controller = function (ModelAPI, $stateParams, $rootScope, $timeout, $uibM paper.on('blank:pointerdown', (evt) => { ctrl.unselectAll(); if(!configs.keyboardController.spacePressed){ - configs.selectionView.startSelecting(evt); + } else { - configs.paperScroller.startPanning(evt); + configs.editorScroller.startPanning(evt); } }); @@ -364,29 +359,22 @@ const controller = function (ModelAPI, $stateParams, $rootScope, $timeout, $uibM paper.on('element:pointerup', (cellView, evt, x, y) => { ctrl.onSelectElement(cellView); - // if(x != null && y != null){ - // $scope.conectElements(cellView, x, y) - // } - configs.selectionView.cancelSelection(); - const halo = new joint.ui.Halo({ + const elementActions = new joint.ui.ElementActions({ cellView: cellView, boxContent: false }); - configs.selectedHalo = halo; - halo.on('action:link:add', function (link) { + configs.selectedElementActions = elementActions; + elementActions.on('action:link:add', function (link) { ctrl.shapeLinker.onLink(link); }); if (ctrl.shapeValidator.isAttribute(cellView.model) || ctrl.shapeValidator.isExtension(cellView.model)) { - halo.removeHandle('resize'); + elementActions.removeAction('resize'); } - halo.removeHandle('clone'); - halo.removeHandle('fork'); - halo.removeHandle('rotate'); - halo.render(); + elementActions.render(); }); configs.paper.on('link:mouseenter', (linkView) => { @@ -463,8 +451,6 @@ const controller = function (ModelAPI, $stateParams, $rootScope, $timeout, $uibM registerGraphEvents(configs.graph); - configs.commandManager = new joint.dia.CommandManager({ graph: configs.graph }) - const content = $("#content"); configs.paper = new joint.dia.Paper({ @@ -481,23 +467,23 @@ const controller = function (ModelAPI, $stateParams, $rootScope, $timeout, $uibM registerPaperEvents(configs.paper); - configs.selectionView = new joint.ui.SelectionView({ paper: configs.paper, graph: configs.graph, model: new Backbone.Collection }); - - configs.paperScroller = new joint.ui.PaperScroller({ + configs.editorScroller = new joint.ui.EditorScroller({ paper: configs.paper, cursor: "grabbing", autoResizePaper: true, }); - content.append(configs.paperScroller.render().el); + content.append(configs.editorScroller.render().el); - const stencil = new joint.ui.Stencil({ + const enditorManager = new joint.ui.EditorManager({ graph: configs.graph, paper: configs.paper, }); - $("#stencil-holder").append(stencil.render().el); + configs.editorActions = new joint.ui.EditorActions({ graph: configs.graph }); + + $(".elements-holder").append(enditorManager.render().el); - stencil.load([ + enditorManager.loadElements([ ctrl.shapeFactory.createEntity({ position: { x: 25, y: 10 } }), ctrl.shapeFactory.createIsa({ position: { x: 40, y: 70 } }), ctrl.shapeFactory.createRelationship({ position: { x: 25, y: 130 } }), @@ -543,8 +529,6 @@ const controller = function (ModelAPI, $stateParams, $rootScope, $timeout, $uibM ctrl.entityExtensor = null; configs.graph = null; configs.paper = null; - configs.paperScroller = null; - configs.commandManager = null; configs.keyboardController.unbindAll(); configs.keyboardController = null; preventExitService.cleanup(ctrl)() diff --git a/app/angular/editor/editorActions.js b/app/angular/editor/editorActions.js new file mode 100644 index 00000000..0622f810 --- /dev/null +++ b/app/angular/editor/editorActions.js @@ -0,0 +1,215 @@ +import * as joint from "jointjs/dist/joint"; + +joint.ui.EditorActions = Backbone.Model.extend({ + defaults: { + cmdBeforeAdd: null, + cmdNameRegex: /^(?:add|remove|change:\w+)$/ + }, + PREFIX_LENGTH: 7, + actions: { + ADD: "add", + REMOVE: "remove" + }, + initialize: function (configs) { + _.bindAll(this, "initCommands", "storeCommands"); + this.graph = configs.graph; + this.undoStack = []; + this.redoStack = []; + this.listen(); + }, + listen: function () { + this.listenTo(this.graph, "all", this.listenCommand, this); + this.listenTo(this.graph, "batch:start", this.initCommands, this); + this.listenTo(this.graph, "batch:stop", this.storeCommands, this); + }, + newCommand: function (param) { + return { + action: param.action, + data: param.data || { + id: null, + type: null, + previous: {}, + next: {} + }, + batch: param && param.batch, + options: param.options + }; + }, + saveCommand: function (event) { + this.redoStack = []; + if(event.batch) { + this.lastCmdIndex = Math.max(this.lastCmdIndex, 0); + this.trigger("batch", event); + } else { + this.undoStack.push(event); + this.trigger(this.actions.ADD, event); + } + }, + listenCommand: function (commandAction, cellView, c, d) { + const commandDescription = commandAction.substr(this.PREFIX_LENGTH); + if (!(d && d.dry || !this.get("cmdNameRegex").test(commandAction) || "function" == typeof this.get("cmdBeforeAdd") && !this.get("cmdBeforeAdd").apply(this, arguments))) { + let runningCommand = null; + if (this.batchCommand) { + runningCommand = this.batchCommand[Math.max(this.lastCmdIndex, 0)]; + if (this.lastCmdIndex >= 0 && (runningCommand.data.id !== cellView.id || runningCommand.action !== commandAction)) { + const currentCommandIndex = this.batchCommand.findIndex(element => { + return element.data.id === cellView.id && element.action === commandAction; + }); + if(currentCommandIndex < 0 || this.actions.ADD === commandAction || this.actions.REMOVE === commandAction) { + runningCommand = this.newCommand({ + batch: true + }); + } else { + runningCommand = this.batchCommand[currentCommandIndex]; + this.batchCommand.splice(currentCommandIndex, 1); + } + this.lastCmdIndex = this.batchCommand.push(runningCommand) - 1; + } + } else runningCommand = this.newCommand({ + batch: false + }); + if (this.actions.ADD === commandAction || this.actions.REMOVE === commandAction) { + runningCommand.action = commandAction; + runningCommand.data.id = cellView.id; + runningCommand.data.type = cellView.attributes.type; + runningCommand.data.attributes = _.merge({}, cellView.toJSON()); + runningCommand.options = d || {}; + runningCommand.data.view = cellView; + this.saveCommand(runningCommand); + return runningCommand; + } + if(!(runningCommand.batch && runningCommand.action)) { + runningCommand.action = commandAction; + runningCommand.data.id = cellView.id; + runningCommand.data.type = cellView.attributes.type; + runningCommand.data.previous[commandDescription] = _.clone(cellView.previous(commandDescription)); + runningCommand.options = d || {}; + } + runningCommand.data.next[commandDescription] = _.clone(cellView.get(commandDescription)); + this.saveCommand(runningCommand); + } + }, + initCommands: function () { + if(this.batchCommand) { + this.batchLevel++; + } else { + const newCommand = this.newCommand({ + action: null, + batch: true + }); + this.batchCommand = [newCommand]; + this.lastCmdIndex = -1; + this.batchLevel = 0; + } + }, + storeCommands: function () { + if (this.batchCommand && this.batchLevel <= 0) { + const batchCommand = this.filterCommands(this.batchCommand); + if(batchCommand.length > 0) { + this.redoStack = []; + this.undoStack.push(batchCommand); + this.trigger(this.actions.ADD, batchCommand); + } + delete this.batchCommand; + delete this.lastCmdIndex; + delete this.batchLevel; + } else if (this.batchCommand && this.batchLevel > 0) { + this.batchLevel--; + } + }, + filterCommands: function (commandEvent) { + const filteredBatch = []; + for (let batchCommand = commandEvent.slice(); batchCommand.length > 0;) { + const command = batchCommand.shift(); + const elementId = command.data.id; + if (command.action != null && elementId != null) { + switch(command.action) { + case this.actions.ADD: + const commandAddIndex = batchCommand.findIndex(element => { + return element.action === this.actions.REMOVE && element.data.id === elementId; + }); + if (commandAddIndex >= 0) { + batchCommand = batchCommand.filter((element, index) => { + return !(commandAddIndex >= index && element.data.id === elementId); + }); + continue; + } + break; + case this.actions.REMOVE: + const commandRemoveIndex = batchCommand.findIndex(element => { + return element.action === this.actions.ADD && element.data.id === elementId; + }); + if (commandRemoveIndex >= 0) { + batchCommand.splice(commandRemoveIndex, 1); + continue; + } + break; + default: + if (command.action.startsWith("change") && _.isEqual(command.data.previous, command.data.next)) { + continue + } + } + filteredBatch.push(command); + } + } + return filteredBatch; + }, + undoCommand: function (commandEvent) { + this.stopListening(); + const commandId = { + commandManager: this.id || this.cid + }; + const commandEventArr = _.isArray(commandEvent) ? commandEvent : [commandEvent]; + commandEventArr.reverse().forEach(command => { + const cellView = this.graph.getCell(command.data.id); + switch (command.action) { + case this.actions.ADD: + cellView.remove(commandId); + break; + case this.actions.REMOVE: + this.graph.addCell(command.data.view, commandId); + break; + default: + const action = command.action.substr(this.PREFIX_LENGTH); + cellView.set(action, command.data.previous[action], commandId); + } + }); + this.listen(); + }, + redoCommand: function (commandEvent) { + this.stopListening(); + const commandId = { + commandManager: this.id || this.cid + }; + const commandEventArr = _.isArray(commandEvent) ? commandEvent : [commandEvent]; + commandEventArr.forEach(command => { + const cellView = this.graph.getCell(command.data.id); + switch (command.action) { + case this.actions.ADD: + this.graph.addCell(command.data.view, commandId); + break; + case this.actions.REMOVE: + cellView.remove(commandId); + break; + default: + const action = command.action.substr(this.PREFIX_LENGTH); + cellView.set(action, command.data.next[action], commandId); + } + }); + this.listen(); + }, + undo: function () { + const redoAction = this.undoStack.pop(); + if(redoAction) { + this.undoCommand(redoAction); + this.redoStack.push(redoAction); + } + }, + redo: function () { + const redoAction = this.redoStack.pop(); + if(redoAction) { + this.redoCommand(redoAction); + this.undoStack.push(redoAction); + } + }, +}); \ No newline at end of file diff --git a/app/angular/editor/editorManager.js b/app/angular/editor/editorManager.js new file mode 100644 index 00000000..4bdbb21b --- /dev/null +++ b/app/angular/editor/editorManager.js @@ -0,0 +1,157 @@ +import $ from "jquery"; +import _ from "underscore"; +import "backbone"; +import * as joint from "jointjs/dist/joint"; + +const Handlebars = { + template: (content) => { + return () => { + return content(); + } + } +}; + +joint.templates = joint.templates || {}; +joint.templates.draggable = joint.templates.draggable || {}; +joint.templates.draggable["elements.html"] = Handlebars.template(() => { + return '
\n'; +}); +joint.templates.draggable["draggable-paper.html"] = Handlebars.template(() => { + return '
\n
\n\n' +}); + +joint.ui.EditorManager = Backbone.View.extend({ + className: "elements-list", + options: { + width: 126, + height: 500 + }, + initialize: function (configs) { + this.options = _.extend({}, _.result(this, "options"), configs || {}); + this.graphs = {}; + this.papers = {}; + _.bindAll(this, "onDrag", "onDragEnd"); + $(document.body).on({ + "mousemove.elements-holder touchmove.elements-holder": this.onDrag, + "mouseup.elements-holder touchend.elements-holder": this.onDragEnd + }); + }, + render: function () { + this.$el.html(joint.templates.draggable["draggable-paper.html"](this.template)); + this.$content = this.$(".content"); + this.$content.append($(joint.templates.draggable["elements.html"]())); + this.graphs.originalGraph = new joint.dia.Graph; + + const paperConfig = { + width: this.options.width, + height: this.options.height, + interactive: false + }; + + this.papers.originalGraph = new joint.dia.Paper(_.extend(paperConfig, { + el: this.$(".elements"), + model: this.graphs.originalGraph + })); + + this.listenTo(this.papers.originalGraph, "cell:pointerdown", this.onDragStart); + this.draggableGraph = new joint.dia.Graph; + this.draggablePaper = new joint.dia.Paper({ + el: this.$(".draggable-paper"), + width: 1, + height: 1, + model: this.draggableGraph + }); + + return this; + }, + loadElements: function (elements) { + this.graphs["originalGraph"].resetCells(elements); + }, + onDragStart: function (element, mouseEvent) { + this.$el.addClass("dragging"); + this.draggablePaper.$el.addClass("dragging"); + $(document.body).append(this.draggablePaper.$el); + this.modelCopy = element.model.clone(); + this.elementCopyBox = element.getBBox(); + const offset = 5; + const xPoint = this.elementCopyBox.x - this.modelCopy.get("position").x; + const yPoint = this.elementCopyBox.y - this.modelCopy.get("position").y; + const elementPoint = joint.g.point(xPoint, yPoint); + + this.modelCopy.set("position", { + x: -elementPoint.x + offset, + y: -elementPoint.y + offset + }); + + this.draggableGraph.addCell(this.modelCopy); + this.draggablePaper.setDimensions(this.elementCopyBox.width + 2 * offset, this.elementCopyBox.height + 2 * offset); + const scrollTopPosition = document.body.scrollTop || document.documentElement.scrollTop; + + this.draggablePaper.$el.offset({ + left: mouseEvent.clientX - this.elementCopyBox.width / 2, + top: mouseEvent.clientY + scrollTopPosition - this.elementCopyBox.height / 2 + }); + }, + onDrag: function (mouseEvent) { + const normalizedEvent = joint.util.normalizeEvent(mouseEvent); + + if (this.modelCopy) { + const scrollTopPosition = document.body.scrollTop || document.documentElement.scrollTop; + this.draggablePaper.$el.offset({ + left: normalizedEvent.clientX - this.elementCopyBox.width / 2, + top: normalizedEvent.clientY + scrollTopPosition - this.elementCopyBox.height / 2 + }); + } + }, + onDragEnd: function (mouseEvent) { + const mouseUpEvent = joint.util.normalizeEvent(mouseEvent); + if (this.modelCopy && this.elementCopyBox) { + this.drop(mouseUpEvent, this.modelCopy.clone(), this.elementCopyBox); + this.$el.append(this.draggablePaper.$el); + this.$el.removeClass("dragging"); + this.draggablePaper.$el.removeClass("dragging"); + this.modelCopy.remove(); + this.modelCopy = false; + } + }, + drop: function (mouseUpEvent, droppedModel, modelDimensions) { + const paper = this.options.paper; + const scrollTopPos = document.body.scrollTop || document.documentElement.scrollTop; + const scrollLeftPos = document.body.scrollLeft || document.documentElement.scrollLeft; + const rectX = paper.$el.offset().left + parseInt(paper.$el.css("border-left-width"), 10) - scrollLeftPos; + const rectY = paper.$el.offset().top + parseInt(paper.$el.css("border-top-width"), 10) - scrollTopPos; + const rectWidth = paper.$el.innerWidth(); + const rectHeight = paper.$el.innerHeight(); + const rect = joint.g.rect(rectX, rectY, rectWidth, rectHeight); + const paperTargetPoint = paper.svg.createSVGPoint(); + paperTargetPoint.x = mouseUpEvent.clientX; + paperTargetPoint.y = mouseUpEvent.clientY; + + if (rect.containsPoint(paperTargetPoint)) { + const fakeElement = joint.V("rect", { + width: paper.options.width, + height: paper.options.height, + x: 0, + y: 0, + opacity: 0 + }); + joint.V(paper.svg).prepend(fakeElement); + const paperOffset = $(paper.svg).offset(); + fakeElement.remove(); + paperTargetPoint.x += scrollLeftPos - paperOffset.left; + paperTargetPoint.y += scrollTopPos - paperOffset.top; + const targetPos = paperTargetPoint.matrixTransform(paper.viewport.getCTM().inverse()); + const droppedModelBox = droppedModel.getBBox(); + targetPos.x += droppedModelBox.x - modelDimensions.width / 2, + targetPos.y += droppedModelBox.y - modelDimensions.height / 2, + droppedModel.set("position", { + x: joint.g.snapToGrid(targetPos.x, paper.options.gridSize), + y: joint.g.snapToGrid(targetPos.y, paper.options.gridSize) + }); + droppedModel.unset("z"); + this.options.graph.addCell(droppedModel, { + xxx: this.cid + }); + } + } +}).bind(this); \ No newline at end of file diff --git a/app/angular/editor/editorScroller.js b/app/angular/editor/editorScroller.js new file mode 100644 index 00000000..edafb194 --- /dev/null +++ b/app/angular/editor/editorScroller.js @@ -0,0 +1,231 @@ +import $ from "jquery"; +import * as joint from "jointjs/dist/joint"; + +joint.ui.EditorScroller = Backbone.View.extend({ + className: "editor-scroller", + events: { + mousedown: "pointerdown", + mousemove: "pointermove", + touchmove: "pointermove" + }, + options: { + paper: undefined, + padding: 0, + autoResizePaper: false, + baseWidth: undefined, + baseHeight: undefined, + contentOptions: undefined + }, + initialize: function(configs) { + _.bindAll(this, "startPanning", "stopPanning", "pan"), + this.options = _.extend({}, _.result(this, "options"), configs || {}); + const paper = this.options.paper; + const scaledPaper = joint.V(paper.viewport).scale(); + this._sx = scaledPaper.sx; + this._sy = scaledPaper.sy; + this.$el.append(paper.el), this.addPadding(); + this.listenTo(paper, "scale", this.onScale); + this.listenTo(paper, "resize", this.onResize); + if(_.isUndefined(this.options.baseWidth)) { + this.options.baseWidth = paper.options.width; + } + if(_.isUndefined(this.options.baseHeight)) { + this.options.baseHeight = paper.options.height; + } + if (this.options.autoResizePaper) { + this.listenTo(paper.model, "change add remove reset", this.adjustPaper); + } + }, + onResize: function() { + if(this._center) { + this.center(this._center.x, this._center.y); + } + }, + onScale: function(zoomWidth, zoomHeight, offsetWidth, offsetHeight) { + this.adjustScale(zoomWidth, zoomHeight); + this._sx = zoomWidth; + this._sy = zoomHeight; + if(offsetWidth || offsetHeight) { + this.center(offsetWidth, offsetHeight); + } + }, + beforePaperManipulation: function() { + this.$el.css("visibility", "hidden"); + }, + afterPaperManipulation: function() { + this.$el.css("visibility", "visible"); + }, + toLocalPoint: function(clientWidth, clientHeight) { + const viewportCTM = this.options.paper.viewport.getCTM(); + clientWidth = (clientWidth + (this.el.scrollLeft - this.padding.paddingLeft - viewportCTM.e)); + clientWidth = clientWidth / viewportCTM.a; + clientWidth = clientWidth + viewportCTM.d; + clientHeight = clientHeight + (this.el.scrollTop - this.padding.paddingTop - viewportCTM.f); + return joint.g.point(clientWidth, clientHeight); + }, + adjustPaper: function() { + this._center = this.toLocalPoint(this.el.clientWidth / 2, this.el.clientHeight / 2); + const newContentOptions = _.extend({ + gridWidth: this.options.baseWidth, + gridHeight: this.options.baseHeight, + allowNewOrigin: "negative" + }, this.options.contentOptions); + this.options.paper.fitToContent(this.transformContentOptions(newContentOptions)); + return this; + }, + adjustScale: function(zoomWidth, zoomHeight) { + const paperOptions = this.options.paper.options; + const newScaleX = zoomWidth / this._sx; + const newScaleY = zoomHeight / this._sy; + this.options.paper.setOrigin(paperOptions.origin.x * newScaleX, paperOptions.origin.y * newScaleY); + this.options.paper.setDimensions(paperOptions.width * newScaleX, paperOptions.height * newScaleY); + }, + transformContentOptions: function(gridConfigs) { + const zoomWidth = this._sx; + const zoomHeight = this._sy; + if(gridConfigs.gridWidth) { + gridConfigs.gridWidth *= zoomWidth; + } + if(gridConfigs.gridHeight) { + gridConfigs.gridHeight *= zoomHeight; + } + if(gridConfigs.minWidth) { + gridConfigs.minWidth *= zoomWidth; + } + if(gridConfigs.minHeight) { + gridConfigs.minHeight *= zoomHeight; + } + if(_.isObject(gridConfigs.padding)) { + gridConfigs.padding = { + left: (gridConfigs.padding.left || 0) * zoomWidth, + right: (gridConfigs.padding.right || 0) * zoomWidth, + top: (gridConfigs.padding.top || 0) * zoomHeight, + bottom: (gridConfigs.padding.bottom || 0) * zoomHeight + } + } else if (_.isNumber(gridConfigs.padding)) { + gridConfigs.padding = gridConfigs.padding * zoomWidth; + } + return gridConfigs; + }, + center: function(halfWidth, halfHeight) { + const viewportCTM = this.options.paper.viewport.getCTM(); + const svgMatrixE = -viewportCTM.e; + const e = -viewportCTM.f; + const paperWidth = svgMatrixE + this.options.paper.options.width; + const paperHeight = e + this.options.paper.options.height; + if(_.isUndefined(halfWidth) || _.isUndefined(halfHeight)) { + halfWidth = (svgMatrixE + paperWidth) / 2; + halfHeight = (e + paperHeight) / 2; + } else { + halfWidth *= viewportCTM.a; + halfHeight *= viewportCTM.d; + } + const paddingReference = this.options.padding; + const clientWidth = this.el.clientWidth / 2; + const clientHeight = this.el.clientHeight / 2; + const left = clientWidth - paddingReference - halfWidth + svgMatrixE; + const right = clientWidth - paddingReference + halfWidth - paperWidth; + const top = clientHeight - paddingReference - halfHeight + e; + const bottom = clientHeight - paddingReference + halfHeight - paperHeight; + this.addPadding(Math.max(left, 0), Math.max(right, 0), Math.max(top, 0), Math.max(bottom, 0)); + this.el.scrollLeft = halfWidth - clientWidth + viewportCTM.e + this.padding.paddingLeft; + this.el.scrollTop = halfHeight - clientHeight + viewportCTM.f + this.padding.paddingTop; + return this; + }, + centerContent: function() { + const paperBox = joint.V(this.options.paper.viewport) + .bbox(!0, this.options.paper.svg); + this.center(paperBox.x + paperBox.width / 2, paperBox.y + paperBox.height / 2); + return this; + }, + addPadding: function(left, right, top, bottom) { + const originalPadding = this.options.padding; + this.padding = { + paddingLeft: Math.round(originalPadding + (left || 0)), + paddingTop: Math.round(originalPadding + (top || 0)) + }; + const newPadding = this.padding; + const newMargin = { + marginBottom: Math.round(originalPadding + (bottom || 0)), + marginRight: Math.round(originalPadding + (right || 0)) + }; + newPadding.paddingLeft = Math.min(newPadding.paddingLeft, .9 * this.el.clientWidth); + newPadding.paddingTop = Math.min(newPadding.paddingTop, .9 * this.el.clientHeight); + this.$el.css(newPadding); + this.options.paper.$el.css(newMargin); + return this; + }, + zoom: function(zoomOffset, zoomConfigs) { + zoomConfigs = zoomConfigs || {}; + const point = this.toLocalPoint(this.el.clientWidth / 2, this.el.clientHeight / 2); + let zoomOffsetX = zoomOffset; + let zoomOffsetY = zoomOffset; + let pointX; + let pointY; + if (!zoomConfigs.absolute) { + zoomOffsetX += this._sx; + zoomOffsetY += this._sy; + if(zoomConfigs.grid) { + zoomOffsetX = Math.round(f / zoomConfigs.grid) * zoomConfigs.grid; + zoomOffsetY = Math.round(zoomOffsetY / zoomConfigs.grid) * zoomConfigs.grid; + } + if(zoomConfigs.max) { + zoomOffsetX = Math.min(zoomConfigs.max, zoomOffsetX); + zoomOffsetY = Math.min(zoomConfigs.max, zoomOffsetY); + } + if(zoomConfigs.min) { + zoomOffsetX = Math.max(zoomConfigs.min, zoomOffsetX); + zoomOffsetY = Math.max(zoomConfigs.min, zoomOffsetY); + } + if (_.isUndefined(zoomConfigs.ox) || _.isUndefined(zoomConfigs.oy)) { + pointX = point.x, + pointY = point.y; + } + } + else { + pointX = zoomConfigs.ox - (zoomConfigs.ox - point.x) / (zoomOffsetX / this._sx); + pointY = zoomConfigs.oy - (zoomConfigs.oy - point.y) / (zoomOffsetY / this._sy); + } + zoomOffsetX = zoomOffsetX || 1; + zoomOffsetY = zoomOffsetY || 1; + this.beforePaperManipulation(); + this.options.paper.scale(zoomOffsetX, zoomOffsetY); + this.center(pointX, pointY); + this.afterPaperManipulation(); + return this; + }, + startPanning: function(mouseEvent) { + $('.editor-scroller').css('cursor', 'grab'); + const normalizedEvent = joint.util.normalizeEvent(mouseEvent); + this._clientX = normalizedEvent.clientX, + this._clientY = normalizedEvent.clientY, + $(document.body).on({ + "mousemove.panning touchmove.panning": this.pan, + "mouseup.panning touchend.panning": this.stopPanning + }) + }, + pan: function (mouseEvent) { + $('.editor-scroller').css('cursor', 'grabbing'); + const normalizedEvent = joint.util.normalizeEvent(mouseEvent); + const neededMargeLeft = normalizedEvent.clientX - this._clientX; + const neededMargeTop = normalizedEvent.clientY - this._clientY; + this.el.scrollTop -= neededMargeTop; + this.el.scrollLeft -= neededMargeLeft; + this._clientX = normalizedEvent.clientX; + this._clientY = normalizedEvent.clientY; + }, + stopPanning: function() { + $('.editor-scroller').css('cursor', 'default'); + $(document.body).off(".panning"); + }, + pointerdown: function(event) { + if(event.target == this.el) { + this.options.paper.pointerdown.apply(this.options.paper, arguments) + } + }, + pointermove: function(event) { + if(event.target == this.el) { + this.options.paper.pointermove.apply(this.options.paper, arguments); + } + } +}); \ No newline at end of file diff --git a/app/angular/editor/elementActions.js b/app/angular/editor/elementActions.js new file mode 100644 index 00000000..777688f8 --- /dev/null +++ b/app/angular/editor/elementActions.js @@ -0,0 +1,245 @@ +import $ from "jquery"; +import _ from "underscore"; +import "backbone"; +import * as joint from "jointjs/dist/joint"; + +joint.ui.ElementActions = Backbone.View.extend({ + className: "element-action", + events: { + "mousedown .item": "onActionPointerDown", + "touchstart .item": "onActionPointerDown", + }, + options: { + useModelGeometry: !1, + actions: [{ + name: "resize", + position: "se", + events: { + pointerdown: "startResizing", + pointermove: "doResize", + pointerup: "stopBatch" + }, + icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAABmJLR0QA/wD/AP+gvaeTAAABMUlEQVRoge3ZwUrDQBRA0YMV9OO60m7c+091KW7Fr7NQUUEXtRDTJpk0SfsmzIVZFDrwDsmQlFIqlUoTdIs3fOJnwLp4j4YBGiFXEw8+227wig+Z31qp3Ws/R1nUhcgCssKX/0PXP4eHHLsS33iQEaQNQSaQLgQZQFIQBIekIggM6YMgKKQvgoCQUxAEgzQ97FYJezeVPe9TDZjSEAQ8VfatpxgwpaEIWGD5txZjD5jSqWciVAURpVCIa7vDeKff4RrjYI/aS2WQ58Q94RCwrQyzTfh+SAT9XglCnYl6qZDQCNIg4RF0Q7JA0A7JBkEzJCsExyHZITiEZIngEBLyYZdSHZLdldg3CwTtV6Rrbex+Y1/kJ2m9IZD9Wp5z4Cn/Q+x62TxLQ2+ttSC3VqlUmlm/31h44m9dArgAAAAASUVORK5CYII=" + }, { + name: "remove", + position: "nw", + events: { + pointerdown: "removeElement" + }, + icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAAAjklEQVRIie2Vyw2AIBQER3uQaIlarhwsRy+Y4AfCPuTmnEx0dwg+FH4MzIAz5FzIZlmAHfCixIXMHjqSDMAaHtyAqaD8nhnVQE4ilysSc3mJpLo8J/ms/CSeEH+7tozzK/GqpZX3FdKuInuh6Ra9vVDLYSwuT92TJSWjaJYocy5LLIdIkjT/XEPjH87PgwNng1K28QMLlAAAAABJRU5ErkJggg==" + }, { + name: "link", + position: "e", + events: { + pointerdown: "startLinking", + pointermove: "doLink", + pointerup: "stopLinking" + }, + icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAABmJLR0QA/wD/AP+gvaeTAAAA7klEQVRIie3WMUpDQRAG4I+AWNkJgpWFB7DQ0iqVrTZ6B5NLGA9hpVfIARJsAgl4BkHBSuwkIBFj8RT2LSkE2XkJ5Ictl49dZmeHdZYsQ8x/1gRbUfBDAs/RRysC3sNrhvciYDjGRwJ/4SIK76ifeoqjKPwmw1+wGwFv4D7DR9iMwLfxmOF3ETAc4D3DO1H4qaq6f+FPnEThPfVTv2E/Am6pOlmKT/6yaWVzrYGrPlMvrhnapdFFz+myNLqD5wy9LY021jLzT+JJdQNF083QKQ5Lo21V1aaDwHlpdNHoc1UapcFhb5CgY4Hj7Tr/zjfcW2a3eoiKgwAAAABJRU5ErkJggg==" + }], + type: "toolbar", + linkAttributes: {} + }, + initialize: function (configs) { + this.options = Object.assign({}, _.result(this, "options"), configs || {}); + _.defaults(this.options, { + paper: this.options.cellView.paper, + graph: this.options.cellView.paper.model + }); + _.bindAll(this, "pointermove", "pointerup", "render", "updateElement", "remove"); + joint.ui.ElementActions.clear(this.options.paper); + this.listenTo(this.options.graph, "reset", this.remove); + this.listenTo(this.options.graph, "all", this.updateElement); + this.listenTo(this.options.paper, "blank:pointerdown element-actions:activate", this.remove); + this.listenTo(this.options.paper, "scale translate", this.updateElement); + this.listenTo(this.options.cellView.model, "remove", this.remove); + $(document.body).on("mousemove touchmove", this.pointermove); + $(document).on("mouseup touchend", this.pointerup); + this.options.paper.$el.append(this.$el); + this.actions = []; + _.each(this.options.actions, this.addAction, this); + }, + render: function () { + const options = this.options; + this.$el.empty(); + this.$actions = $("
").addClass("holder").appendTo(this.el); + this.$box = $("