Skip to content

Commit

Permalink
Merge pull request #445 from brmodeloweb/feature/select-multi-elements
Browse files Browse the repository at this point in the history
Feature: Select and move multi elements
  • Loading branch information
miltonbsn authored Aug 23, 2023
2 parents e4f688c + c8b6d47 commit 658f413
Show file tree
Hide file tree
Showing 4 changed files with 323 additions and 3 deletions.
5 changes: 4 additions & 1 deletion app/angular/conceptual/conceptual.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import "../editor/editorManager";
import "../editor/editorScroller";
import "../editor/editorActions";
import "../editor/elementActions";
import "../editor/elementSelector";

import shapes from "../../joint/shapes";
joint.shapes.erd = shapes;
Expand Down Expand Up @@ -347,7 +348,7 @@ const controller = function (ModelAPI, $stateParams, $rootScope, $timeout, $uibM
paper.on('blank:pointerdown', (evt) => {
ctrl.unselectAll();
if(!configs.keyboardController.spacePressed){

configs.elementSelector.start(evt);
} else {
configs.editorScroller.startPanning(evt);
}
Expand Down Expand Up @@ -484,6 +485,8 @@ const controller = function (ModelAPI, $stateParams, $rootScope, $timeout, $uibM

$(".elements-holder").append(enditorManager.render().el);

configs.elementSelector = new joint.ui.ElementSelector({ paper: configs.paper, graph: configs.graph, model: new Backbone.Collection });

enditorManager.loadElements([
ctrl.shapeFactory.createEntity({ position: { x: 25, y: 10 } }),
ctrl.shapeFactory.createIsa({ position: { x: 40, y: 70 } }),
Expand Down
279 changes: 279 additions & 0 deletions app/angular/editor/elementSelector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
import $ from "jquery";
import "backbone";
import * as joint from "jointjs/dist/joint";

joint.ui.ElementSelector = Backbone.View.extend({
options: {
paper: undefined,
graph: undefined,
boxContent: function(a) {},
useModelGeometry: false
},
className: "selection",
events: {
"mousedown .selection-box": "startVisualSelection",
"touchstart .selection-box": "startVisualSelection",
},
initialize(a) {
this.options = { ...this.options, ...a } || {};

this.options = {
...a.paper.model,
...this.options,
graph: a.paper.model,
};

this.start = this.start.bind(this);
this.stop = this.stop.bind(this);
this.adjust = this.adjust.bind(this);
this.pointerup = this.pointerup.bind(this);

$(document.body).on("mousemove.selectionView touchmove.selectionView", this.adjust);
$(document).on("mouseup.selectionView touchend.selectionView", this.pointerup);

this.listenTo(this.options.graph, "reset", this.cancel);
this.listenTo(this.options.graph, "remove change", (a, b) => b[`selectionView_${this.cid}`]);

this.options.paper.$el.append(this.$el);
this._boxCount = 0;
this.$selectionWrapper = this.createBoxWrapper();
},
startVisualSelection(event) {
event.stopPropagation();

const normalizedEvent = joint.util.normalizeEvent(event);
const { clientX, clientY } = normalizedEvent;

this._action = "translating";
this.options.graph.trigger("batch:start");

const snappedPoint = this.options.paper.snapToGrid(joint.g.point(clientX, clientY));
this._snappedClientX = snappedPoint.x;
this._snappedClientY = snappedPoint.y;

this.trigger("selection-box:pointerdown", normalizedEvent);
},
start: function(event) {
const normalizedEvent = joint.util.normalizeEvent(event);
this.cancel();
this._action = "selecting";
this._clientX = normalizedEvent.clientX;
this._clientY = normalizedEvent.clientY;
const targetElement = normalizedEvent.target.parentElement || normalizedEvent.target.parentNode;
const targetOffset = $(targetElement).offset();
const targetScrollLeft = targetElement.scrollLeft;
const targetScrollTop = targetElement.scrollTop;
this._offsetX = (normalizedEvent.offsetX === undefined)
? normalizedEvent.clientX - targetOffset.left + window.pageXOffset + targetScrollLeft
: normalizedEvent.offsetX;
this._offsetY = (normalizedEvent.offsetY === undefined)
? normalizedEvent.clientY - targetOffset.top + window.pageYOffset + targetScrollTop
: normalizedEvent.offsetY;
this.$el.css({
width: 1,
height: 1,
left: this._offsetX,
top: this._offsetY
}).show();
},
adjust: function(event) {
if (!this._action) return;
const normalizedEvent = joint.util.normalizeEvent(event);
const { clientX, clientY } = normalizedEvent;
let deltaX, deltaY;
switch (this._action) {
case "selecting":
deltaX = clientX - this._clientX;
deltaY = clientY - this._clientY;
const currentLeft = parseInt(this.$el.css("left"), 10);
const currentTop = parseInt(this.$el.css("top"), 10);
this.$el.css({
left: deltaX < 0 ? this._offsetX + deltaX : currentLeft,
top: deltaY < 0 ? this._offsetY + deltaY : currentTop,
width: Math.abs(deltaX),
height: Math.abs(deltaY)
});
break;
case "translating":
const snappedPoint = this.options.paper.snapToGrid(joint.g.point(normalizedEvent.clientX, normalizedEvent.clientY));
const snappedX = snappedPoint.x;
const snappedY = snappedPoint.y;
deltaX = snappedX - this._snappedClientX;
deltaY = snappedY - this._snappedClientY;
if (deltaX || deltaY) {
const translatedCells = new Set();
this.model.each(cell => {
if (!translatedCells.has(cell.id)) {
const translationOptions = { [`selectionView_${this.cid}`]: true };
cell.translate(deltaX, deltaY, translationOptions);
cell.getEmbeddedCells({ deep: true }).forEach(embeddedCell => {
translatedCells.add(embeddedCell.id);
});
const connectedLinks = this.options.graph.getConnectedLinks(cell);
connectedLinks.forEach(link => {
if (!translatedCells.has(link.id)) {
link.translate(deltaX, deltaY, translationOptions);
translatedCells.add(link.id);
}
});
}
}, this);
if (!this.boxesUpdated) {
const scale = joint.V(this.options.paper.viewport).scale();
this.$el.children(".selection-box").add(this.$selectionWrapper).css({
left: `+=${deltaX * scale.sx}`,
top: `+=${deltaY * scale.sy}`
});
}
this._snappedClientX = snappedX;
this._snappedClientY = snappedY;
}
this.trigger("selection-box:pointermove", normalizedEvent);
break;
default:
if (this._action) {
this.pointermove(normalizedEvent);
}
}
this.boxesUpdated = false;
},
stop: function(event) {
switch (this._action) {
case "selecting":
const $elOffset = this.$el.offset();
const $elWidth = this.$el.width();
const $elHeight = this.$el.height();
const paper = this.options.paper;
const localPoint = joint.V(paper.viewport).toLocalPoint($elOffset.left, $elOffset.top);
localPoint.x -= window.pageXOffset;
localPoint.y -= window.pageYOffset;
const scale = joint.V(paper.viewport).scale();
const scaledWidth = $elWidth / scale.sx;
const scaledHeight = $elHeight / scale.sy;
const selectionRect = joint.g.rect(localPoint.x, localPoint.y, scaledWidth, scaledHeight);
const selectedViews = this.options.useModelGeometry
? paper.model.findModelsInArea(selectionRect).map(model => paper.findViewByModel(model)).filter(Boolean)
: paper.findViewsInArea(selectionRect);
const filter = this.options.filter;
if (Array.isArray(filter)) {
selectedViews = selectedViews.filter(view => !filter.includes(view.model) && !filter.includes(view.model.get("type")));
} else if (typeof filter === "function") {
selectedViews = selectedViews.filter(view => !filter(view.model));
}
this.model.reset(selectedViews.map(view => view.model), { ui: true });
if (selectedViews.length) {
selectedViews.forEach(this.createBox, this);
this.$el.addClass("selected");
} else {
this.$el.hide();
}
break;
case "translating":
this.options.graph.trigger("batch:stop");
this.trigger("selection-box:pointerup", event);
break;
default:
if (!this._action) {
this.cancel();
}
}
delete this._action;
},
pointerup: function(a) {
this._action && (this.triggerAction(this._action, "pointerup", a), this.stop(), delete this._action)
},
cancel: function() {
this.destroyBoxes();
this.model.reset([], {
ui: !0
})
},
destroyBoxes: function() {
this.$el.hide().children(".selection-box").remove();
this.$el.removeClass("selected");
this._boxCount = 0;
this.updateBoxWrapper();
},
createBox(a) {
const bbox = a.getBBox({ useModelGeometry: this.options.useModelGeometry });
const c = $("<div/>", {
class: "selection-box",
"data-model": a.model.get("id")
});
c.css({
left: bbox.x,
top: bbox.y,
width: bbox.width,
height: bbox.height
});
this.$el.append(c);
this.$el.addClass("selected").show();
this._boxCount++;
this.updateBoxWrapper();
},
createBoxWrapper: function() {
const boxWrapper = $("<div/>", {
"class": "selection-wrapper"
});
const box = $("<div/>", {
"class": "box"
});
boxWrapper.append(box);
boxWrapper.attr("data-selection-length", this.model.length);
this.$el.prepend(boxWrapper);
return boxWrapper;
},
updateBoxWrapper() {
const initialCoords = { x: Infinity, y: Infinity };
const finalCoords = { x: 0, y: 0 };
this.model.each(c => {
const view = this.options.paper.findViewByModel(c);
if (view) {
const bbox = view.getBBox({ useModelGeometry: this.options.useModelGeometry });
initialCoords.x = Math.min(initialCoords.x, bbox.x);
initialCoords.y = Math.min(initialCoords.y, bbox.y);
finalCoords.x = Math.max(finalCoords.x, bbox.x + bbox.width);
finalCoords.y = Math.max(finalCoords.y, bbox.y + bbox.height);
}
}, this);
const { x: left, y: top } = initialCoords;
const width = finalCoords.x - left;
const height = finalCoords.y - top;
this.$selectionWrapper.css({
left,
top,
width,
height
}).attr("data-selection-length", this.model.length);
if (typeof this.options.boxContent === "function") {
const boxElement = this.$(".box");
const boxContent = this.options.boxContent.call(this, boxElement[0]);
if (boxContent) {
boxElement.html(boxContent);
}
}
},
pointermove(event) {
if (!this._action) return;

const { clientX, clientY } = event;
const currentSnap = this.options.paper.snapToGrid({ x: clientX, y: clientY });
const prevSnap = this.options.paper.snapToGrid({ x: this._clientX, y: this._clientY });

const d = currentSnap.x - prevSnap.x;
const e = currentSnap.y - prevSnap.y;

const deltaX = clientX - this._startClientX;
const deltaY = clientY - this._startClientY;

this.triggerAction(this._action, "pointermove", event, d, e, deltaX, deltaY);

this._clientX = clientX;
this._clientY = clientY;
},
triggerAction: function(a, b) {
var d = Array.prototype.slice.call(arguments, 2);
d.unshift("action:" + a + ":" + b);
this.trigger.apply(this, d);
}
});
5 changes: 4 additions & 1 deletion app/angular/service/logicService.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import "../editor/editorManager"
import "../editor/editorScroller"
import "../editor/editorActions"
import "../editor/elementActions";
import "../editor/elementSelector";

import KeyboardController, { types } from "../components/keyboardController";
import conversorService from "../service/conversorService"
Expand Down Expand Up @@ -56,6 +57,8 @@ const logicService = ($rootScope, ModelAPI, LogicFactory, LogicConversorService)

ls.toolsViewService = new ToolsViewService();

ls.elementSelector = new joint.ui.ElementSelector({ paper: ls.paper, graph: ls.graph, model: new Backbone.Collection });

ls.paper.on('link:options', function (link, evt, x, y) {

var source = ls.graph.getCell(link.model.get('source').id);
Expand Down Expand Up @@ -173,7 +176,7 @@ const logicService = ($rootScope, ModelAPI, LogicFactory, LogicConversorService)
ls.clearSelectedElement();

if(!ls.keyboardController.spacePressed){

ls.elementSelector.start(evt);
} else {
ls.editorScroller.startPanning(evt);
}
Expand Down
37 changes: 36 additions & 1 deletion app/sass/elementActions.scss
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,39 @@
.item.fork,
.item.rotate {cursor: move;}

.item.resize {cursor: se-resize;}
.item.resize {cursor: se-resize;}


////////////////////////////////////////////////////////////////////////////////
// select actions
////////////////////////////////////////////////////////////////////////////////

.selection {
position: absolute;
border: 2px solid #3d9970;
border-radius: 3px;
overflow: visible;
background-color: rgba(61, 153, 112, 0.2);
}

.selected .selection-wrapper {
position: absolute;
transform: scale(1.3);
border: 2px solid #3d9970;
border-radius: 3px;
}

.selection.selected {
position: static;
height: 0 !important;
border: none;
background-color: transparent;
}

.selection-box {
position: absolute;
border-radius: 2px;
border: 1px dashed #3d9970;
transform: scale(1.15);
cursor: move;
}

0 comments on commit 658f413

Please sign in to comment.