From dd905f56fe5388bc08a680a508be266dc0c98797 Mon Sep 17 00:00:00 2001 From: Carlos Venegas Date: Sat, 9 Nov 2024 09:41:31 +0100 Subject: [PATCH] Fix weird selection issues when zoom --- app/scripts/graphics/joint.selection.js | 781 ++++++++++++------------ 1 file changed, 407 insertions(+), 374 deletions(-) diff --git a/app/scripts/graphics/joint.selection.js b/app/scripts/graphics/joint.selection.js index f49899f88..4d2f6657d 100644 --- a/app/scripts/graphics/joint.selection.js +++ b/app/scripts/graphics/joint.selection.js @@ -6,426 +6,459 @@ Copyright (c) 2013 client IO "use strict"; joint.ui.SelectionView = Backbone.View.extend({ - className: "selection", - - events: { - "click .selection-box": "click", - dblclick: "dblclick", - "mousedown .selection-box": "startTranslatingSelection", - mouseover: "mouseover", - mouseout: "mouseout", - mouseup: "mouseup", - mousedown: "mousedown", - }, - - showtooltip: true, - $selectionArea: null, - - initialize: function (options) { - _.bindAll( - this, - "click", - "startSelecting", - "stopSelecting", - "adjustSelection" - ); - - $(document.body).on( - "mouseup touchend", - function (evt) { + className: "selection", + + events: { + "click .selection-box": "click", + dblclick: "dblclick", + "mousedown .selection-box": "startTranslatingSelection", + mouseover: "mouseover", + mouseout: "mouseout", + mouseup: "mouseup", + mousedown: "mousedown", + }, + + showtooltip: true, + $selectionArea: null, + + initialize: function (options) { + _.bindAll( + this, + "click", + "startSelecting", + "stopSelecting", + "adjustSelection" + ); + + $(document.body).on( + "mouseup touchend", + function (evt) { + if (evt.which === 1) { + // Mouse left button + this.stopSelecting(evt); + } + }.bind(this) + ); + $(document.body).on("mousemove touchmove", this.adjustSelection); + + this.options = options; + + this.options.paper.$el.append(this.$el); + this.$el.addClass("selected").show(); + }, + + click: function (evt) { if (evt.which === 1) { - // Mouse left button - this.stopSelecting(evt); + // Mouse left button + this.trigger("selection-box:pointerclick", evt); + } + }, + + dblclick: function (evt) { + var id = evt.target.getAttribute("data-model"); + if (id) { + var view = this.options.paper.findViewByModel(id); + if (view) { + // Trigger dblclick in selection to the Cell View + view.notify("cell:pointerdblclick", evt); + } } - }.bind(this) - ); - $(document.body).on("mousemove touchmove", this.adjustSelection); - - this.options = options; - - this.options.paper.$el.append(this.$el); - this.$el.addClass("selected").show(); - }, - - click: function (evt) { - if (evt.which === 1) { - // Mouse left button - this.trigger("selection-box:pointerclick", evt); - } - }, - - dblclick: function (evt) { - var id = evt.target.getAttribute("data-model"); - if (id) { - var view = this.options.paper.findViewByModel(id); - if (view) { - // Trigger dblclick in selection to the Cell View - view.notify("cell:pointerdblclick", evt); - } - } - }, - - mouseover: function (evt) { - this.mouseManager(evt, "mouseovercard"); - }, - - mouseout: function (evt) { - this.mouseManager(evt, "mouseoutcard"); - }, - - mouseup: function (evt) { - this.mouseManager(evt, "mouseupcard"); - }, - - mousedown: function (evt) { - if (!this.showtooltip && evt.which === 1) { - // Mouse left button: block fixed - this.showtooltip = true; - } - - this.mouseManager(evt, "mousedowncard"); - }, - - mouseManager: function (evt, fnc) { - evt.preventDefault(); - - if (this.showtooltip) { - var id = evt.target.getAttribute("data-model"); - if (id) { - var view = this.options.paper.findViewByModel(id); - if (view && view[fnc]) { - view[fnc].apply(view, [evt]); + }, + + mouseover: function (evt) { + this.mouseManager(evt, "mouseovercard"); + }, + + mouseout: function (evt) { + this.mouseManager(evt, "mouseoutcard"); + }, + + mouseup: function (evt) { + this.mouseManager(evt, "mouseupcard"); + }, + + mousedown: function (evt) { + if (!this.showtooltip && evt.which === 1) { + // Mouse left button: block fixed + this.showtooltip = true; + } + + this.mouseManager(evt, "mousedowncard"); + }, + + mouseManager: function (evt, fnc) { + evt.preventDefault(); + + if (this.showtooltip) { + var id = evt.target.getAttribute("data-model"); + if (id) { + var view = this.options.paper.findViewByModel(id); + if (view && view[fnc]) { + view[fnc].apply(view, [evt]); + } + } } - } - } - }, + }, + + startTranslatingSelection: function (evt) { + if (this._action !== "adding" && evt.which === 1) { + // Mouse left button + + if (!evt.shiftKey) { + this._action = "translating"; - startTranslatingSelection: function (evt) { - if (this._action !== "adding" && evt.which === 1) { - // Mouse left button + this.options.graph.trigger("batch:stop"); + this.options.graph.trigger("batch:start"); - if (!evt.shiftKey) { - this._action = "translating"; + var snappedClientCoords = this.options.paper.snapToGrid( + g.point(evt.clientX, evt.clientY) + ); + this._snappedClientX = snappedClientCoords.x; + this._snappedClientY = snappedClientCoords.y; - this.options.graph.trigger("batch:stop"); - this.options.graph.trigger("batch:start"); + this.trigger("selection-box:pointerdown", evt); + } + } + }, + + startAddingSelection: function (evt) { + this._action = "adding"; var snappedClientCoords = this.options.paper.snapToGrid( - g.point(evt.clientX, evt.clientY) + g.point(evt.clientX, evt.clientY) ); this._snappedClientX = snappedClientCoords.x; this._snappedClientY = snappedClientCoords.y; this.trigger("selection-box:pointerdown", evt); - } - } - }, - - startAddingSelection: function (evt) { - this._action = "adding"; - - var snappedClientCoords = this.options.paper.snapToGrid( - g.point(evt.clientX, evt.clientY) - ); - this._snappedClientX = snappedClientCoords.x; - this._snappedClientY = snappedClientCoords.y; - - this.trigger("selection-box:pointerdown", evt); - }, - - startSelecting: function (evt /*, x, y*/) { - /* jshint laxbreak: true */ - - this.createSelectionArea(); - - this._action = "selecting"; - - this._clientX = evt.clientX; - this._clientY = evt.clientY; - - // Normalize `evt.offsetX`/`evt.offsetY` for browsers that don't support it (Firefox). - var paperElement = evt.target.parentElement || evt.target.parentNode; - var paperOffset = $(paperElement).offset(); - var paperScrollLeft = paperElement.scrollLeft; - var paperScrollTop = paperElement.scrollTop; - - this._offsetX = - evt.offsetX === undefined - ? evt.clientX - paperOffset.left + window.pageXOffset + paperScrollLeft - : evt.offsetX; - this._offsetY = - evt.offsetY === undefined - ? evt.clientY - paperOffset.top + window.pageYOffset + paperScrollTop - : evt.offsetY; - - this.$selectionArea.css({ - width: 1, - height: 1, - left: this._offsetX, - top: this._offsetY, - }); - }, - - adjustSelection: function (evt) { - var dx; - var dy; - - switch (this._action) { - case "selecting": - dx = evt.clientX - this._clientX; - dy = evt.clientY - this._clientY; - - var left = parseInt(this.$selectionArea.css("left"), 10); - var top = parseInt(this.$selectionArea.css("top"), 10); + }, + + startSelecting: function (evt /*, x, y*/) { + /* jshint laxbreak: true */ + + this.createSelectionArea(); + + this._action = "selecting"; + + this._clientX = evt.clientX; + this._clientY = evt.clientY; + + // Normalize `evt.offsetX`/`evt.offsetY` for browsers that don't support it (Firefox). + var paperElement = evt.target.parentElement || evt.target.parentNode; + var paperOffset = $(paperElement).offset(); + var paperScrollLeft = paperElement.scrollLeft; + var paperScrollTop = paperElement.scrollTop; + + this._offsetX = + evt.offsetX === undefined + ? evt.clientX - paperOffset.left + window.pageXOffset + paperScrollLeft + : evt.offsetX; + this._offsetY = + evt.offsetY === undefined + ? evt.clientY - paperOffset.top + window.pageYOffset + paperScrollTop + : evt.offsetY; this.$selectionArea.css({ - left: dx < 0 ? this._offsetX + dx : left, - top: dy < 0 ? this._offsetY + dy : top, - width: Math.abs(dx), - height: Math.abs(dy), + width: 1, + height: 1, + left: this._offsetX, + top: this._offsetY, }); - break; + /*-- unComment for debugging purpouses + */ + console.table({'client':{x:this._clientX,y:this._clientY}, + 'scroll':{x:paperScrollLeft,y:paperScrollTop}, + 'offsetP':{x:paperOffset.left,y:paperOffset.top}, + 'offset':{x:this._offsetX,y:this._offsetY} + }); + + }, - case "adding": - case "translating": - var snappedClientCoords = this.options.paper.snapToGrid( - g.point(evt.clientX, evt.clientY) - ); - var snappedClientX = snappedClientCoords.x; - var snappedClientY = snappedClientCoords.y; - dx = snappedClientX - this._snappedClientX; - dy = snappedClientY - this._snappedClientY; - // This hash of flags makes sure we're not adjusting vertices of one link twice. - // This could happen as one link can be an inbound link of one element in the selection - // and outbound link of another at the same time. - var processedLinks = {}; - this.model.each(function (element) { - // Translate the element itself. - element.translate(dx, dy); + adjustSelection: function (evt) { + var dx; + var dy; - // Translate also the `selection-box` of the element. - this.updateBox(element); + switch (this._action) { + case "selecting": + dx = evt.clientX - this._clientX; + dy = evt.clientY - this._clientY; - // Translate link vertices as well. - var connectedLinks = this.options.graph.getConnectedLinks(element); - _.each(connectedLinks, function (link) { - if (processedLinks[link.id]) { - return; - } + var left = parseInt(this.$selectionArea.css("left"), 10); + var top = parseInt(this.$selectionArea.css("top"), 10); - var vertices = link.get("vertices"); - if (vertices && vertices.length) { - var newVertices = []; - _.each(vertices, function (vertex) { - newVertices.push({ x: vertex.x + dx, y: vertex.y + dy }); - }); + this.$selectionArea.css({ + left: dx < 0 ? this._offsetX + dx : left, + top: dy < 0 ? this._offsetY + dy : top, + width: Math.abs(dx), + height: Math.abs(dy), + }); - link.set("vertices", newVertices); - } - processedLinks[link.id] = true; - }); - }, this); + break; - if (dx || dy) { - this._snappedClientX = snappedClientX; - this._snappedClientY = snappedClientY; - } + case "adding": + case "translating": + + var snappedClientCoords = this.options.paper.snapToGrid( + g.point(evt.clientX, evt.clientY) + ); + var snappedClientX = snappedClientCoords.x; + var snappedClientY = snappedClientCoords.y; + + dx = snappedClientX - this._snappedClientX; + dy = snappedClientY - this._snappedClientY; + + // This hash of flags makes sure we're not adjusting vertices of one link twice. + // This could happen as one link can be an inbound link of one element in the selection + // and outbound link of another at the same time. + var processedLinks = {}; + + this.model.each(function (element) { + // Translate the element itself. + element.translate(dx, dy); + + // Translate also the `selection-box` of the element. + this.updateBox(element); - this.trigger("selection-box:pointermove", evt); + // Translate link vertices as well. + var connectedLinks = this.options.graph.getConnectedLinks(element); - break; - } - }, + _.each(connectedLinks, function (link) { + if (processedLinks[link.id]) { + return; + } - stopSelecting: function (evt) { - switch (this._action) { - case "selecting": - if (!evt.shiftKey) { - // Reset previous selection - this.cancelSelection(); + var vertices = link.get("vertices"); + if (vertices && vertices.length) { + var newVertices = []; + _.each(vertices, function (vertex) { + newVertices.push({ x: vertex.x + dx, y: vertex.y + dy }); + }); + + link.set("vertices", newVertices); + } + + processedLinks[link.id] = true; + }); + }, this); + + if (dx || dy) { + this._snappedClientX = snappedClientX; + this._snappedClientY = snappedClientY; + } + + this.trigger("selection-box:pointermove", evt); + + break; } + }, - var offset = this.$selectionArea.offset(); - var width = this.$selectionArea.width(); - var height = this.$selectionArea.height(); - // Convert offset coordinates to the local point of the root element. - var localPoint = V(this.options.paper.svg).toLocalPoint( - offset.left, - offset.top - ); + stopSelecting: function (evt) { + switch (this._action) { + case "selecting": + if (!evt.shiftKey) { + // Reset previous selection + this.cancelSelection(); + } - // Take page scroll into consideration. - localPoint.x -= window.pageXOffset; - localPoint.y -= window.pageYOffset; + let offset = this.$selectionArea.offset(); + let width = this.$selectionArea.width(); + let height = this.$selectionArea.height(); - var elementViews = this.findBlocksInArea( - g.rect(localPoint.x, localPoint.y, width, height), - { strict: false } - ); + // Convert offset coordinates to the local point of the root element. + var localPoint = V(this.options.paper.svg).toLocalPoint( + offset.left, + offset.top + ); - this.model.add(_.pluck(elementViews, "model")); + // Take page scroll into consideration. + localPoint.x -= window.pageXOffset; + localPoint.y -= window.pageYOffset; - _.each(this.model.models, this.createSelectionBox, this); + let elementViews = this.findBlocksInArea( + g.rect(localPoint.x, localPoint.y, width, height), + { strict: false } + ); - this.destroySelectionArea(); + this.model.add(_.pluck(elementViews, "model")); - break; + _.each(this.model.models, this.createSelectionBox, this); - case "translating": - this.options.graph.trigger("batch:stop"); - // Everything else is done during the translation. - break; + this.destroySelectionArea(); - case "adding": - break; + break; - case "cherry-picking": - // noop; All is done in the `createSelectionBox()` function. - // This is here to avoid removing selection boxes as a reaction on mouseup event and - // propagating to the `default` branch in this switch. - break; + case "translating": + this.options.graph.trigger("batch:stop"); + // Everything else is done during the translation. + break; - default: - break; - } + case "adding": + break; - delete this._action; - }, + case "cherry-picking": + // noop; All is done in the `createSelectionBox()` function. + // This is here to avoid removing selection boxes as a reaction on mouseup event and + // propagating to the `default` branch in this switch. + break; - findBlocksInArea: function (rect, opt) { - opt = _.defaults(opt || {}, { strict: false }); - rect = g.rect(rect); + default: + break; + } + + delete this._action; + }, + + findBlocksInArea: function (rect, opt) { + opt = _.defaults(opt || {}, { strict: false }); + rect = g.rect(rect); + const zoom = this.options.state.zoom; + const paper = this.options.paper; + let views = _.map(paper.model.getElements(), paper.findViewByModel, paper); + let method = opt.strict ? "containsRect" : "intersect"; + /*-- unComment for debugging purpouses + + let table=[]; + */ + let search= _.filter( + views, + function (view) { + var $box = $(view.$box[0]); + var position = $box.position(); + var rbox = g.rect( + position.left, + position.top, + $box.width()*zoom, + $box.height()*zoom + ); + + /*-- unComment for debugging purpouses + + table.push(rbox); + */ + return view && rect[method](rbox); + }, + this + ); + /*-- unComment for debugging puropouses + + console.table(rect); + console.table(table); + */ + return search; + + }, + + + cancelSelection: function () { + this.$(".selection-box").remove(); + this.model.reset([]); + }, + + destroySelectionArea: function () { + this.$selectionArea.remove(); + this.$selectionArea = this.$(".selection-area"); + this.$el.addClass("selected"); + }, + + createSelectionArea: function () { + let $selectionArea = $("
", { + class: "selection-area", + }); + this.$el.append($selectionArea); + this.$selectionArea = this.$(".selection-area"); + this.$el.removeClass("selected"); + }, + + destroySelectionBox: function (element) { + this.$('[data-model="' + element.get("id") + '"]').remove(); + domCache['div[data-model="' + element.get("id") + '"]'] = []; + }, + + createSelectionBox: function (element, opt) { + opt = opt || {}; + + if (!element.isLink()) { + let $selectionBox = $("
", { + class: "selection-box", + "data-model": element.get("id"), + }); + if (this.$('[data-model="' + element.get("id") + '"]').length === 0) { + this.$el.append($selectionBox); + domCache['div[data-model="' + element.get("id") + '"]'] = $selectionBox; + } + this.showtooltip = opt.initooltip !== undefined ? opt.initooltip : true; + $selectionBox.css({ opacity: opt.transparent ? 0 : 1 }); + + this.updateBox(element); + + this._action = "cherry-picking"; + } + }, - var paper = this.options.paper; - var views = _.map(paper.model.getElements(), paper.findViewByModel, paper); - var method = opt.strict ? "containsRect" : "intersect"; + updateBox: function (element) { + let sels = domCache['div[data-model="' + element.get("id") + '"]']; + if (!sels || sels.length === 0) { + return; + } + let bbox = element.getBBox(), + state = this.options.state, + i, + pendingTasks = []; - return _.filter( - views, - function (view) { - var $box = $(view.$box[0]); - var position = $box.position(); - var rbox = g.rect( - position.left, - position.top, - $box.width(), - $box.height() + let bx = Math.round( + bbox.x * state.zoom + state.pan.x + (bbox.width / 2.0) * (state.zoom - 1) ); - return view && rect[method](rbox); - }, - this - ); - }, - - cancelSelection: function () { - this.$(".selection-box").remove(); - this.model.reset([]); - }, - - destroySelectionArea: function () { - this.$selectionArea.remove(); - this.$selectionArea = this.$(".selection-area"); - this.$el.addClass("selected"); - }, - - createSelectionArea: function () { - var $selectionArea = $("
", { - class: "selection-area", - }); - this.$el.append($selectionArea); - this.$selectionArea = this.$(".selection-area"); - this.$el.removeClass("selected"); - }, - - destroySelectionBox: function (element) { - this.$('[data-model="' + element.get("id") + '"]').remove(); - domCache['div[data-model="' + element.get("id") + '"]'] = []; - }, - - createSelectionBox: function (element, opt) { - opt = opt || {}; - - if (!element.isLink()) { - var $selectionBox = $("
", { - class: "selection-box", - "data-model": element.get("id"), - }); - if (this.$('[data-model="' + element.get("id") + '"]').length === 0) { - this.$el.append($selectionBox); - domCache['div[data-model="' + element.get("id") + '"]'] = $selectionBox; - } - this.showtooltip = opt.initooltip !== undefined ? opt.initooltip : true; - $selectionBox.css({ opacity: opt.transparent ? 0 : 1 }); - - this.updateBox(element); - - this._action = "cherry-picking"; - } - }, - - updateBox: function (element) { - let sels = domCache['div[data-model="' + element.get("id") + '"]']; - if (!sels || sels.length === 0) { - return; - } - let bbox = element.getBBox(), - state = this.options.state, - i, - pendingTasks = []; - - let bx = Math.round( - bbox.x * state.zoom + state.pan.x + (bbox.width / 2.0) * (state.zoom - 1) - ); - let by = Math.round( - bbox.y * state.zoom + state.pan.y + (bbox.height / 2.0) * (state.zoom - 1) - ); - let bw = bbox.width; - let bh = bbox.height; - - for (i = 0; i < sels.length; i++) { - pendingTasks.push({ - e: sels[i], - property: "left", - value: 0, - }); - - pendingTasks.push({ - e: sels[i], - property: "top", - value: 0, - }); - - pendingTasks.push({ - e: sels[i], - property: "width", - value: bw + "px", - }); - - pendingTasks.push({ - e: sels[i], - property: "height", - value: bh + "px", - }); - - pendingTasks.push({ - e: sels[i], - property: "transform", - value: `translate3d(${bx}px,${by}px,0) scale(${state.zoom})`, - }); - } - - i = pendingTasks.length; - for (i = 0; i < pendingTasks.length; i++) { - if (pendingTasks[i].e !== null) { - pendingTasks[i].e.style[pendingTasks[i].property] = - pendingTasks[i].value; - } - } - }, + let by = Math.round( + bbox.y * state.zoom + state.pan.y + (bbox.height / 2.0) * (state.zoom - 1) + ); + let bw = bbox.width; + let bh = bbox.height; + + for (i = 0; i < sels.length; i++) { + pendingTasks.push({ + e: sels[i], + property: "left", + value: 0, + }); + + pendingTasks.push({ + e: sels[i], + property: "top", + value: 0, + }); + + pendingTasks.push({ + e: sels[i], + property: "width", + value: bw + "px", + }); + + pendingTasks.push({ + e: sels[i], + property: "height", + value: bh + "px", + }); + + pendingTasks.push({ + e: sels[i], + property: "transform", + //ZOOM + value: `translate3d(${bx}px,${by}px,0) scale(${state.zoom})`, + }); + } + + i = pendingTasks.length; + for (i = 0; i < pendingTasks.length; i++) { + if (pendingTasks[i].e !== null) { + pendingTasks[i].e.style[pendingTasks[i].property] = + pendingTasks[i].value; + } + } + }, });