/*! * Copyright (c) 2023, 2025, Oracle and/or its affiliates. */ /** * @file * * Extended joint.ui.Selection with additional functionality and/or overrides. */ const { ui, g, util } = joint; const CLS_LASSO_SELECTING = "is-lasso-selecting"; const ConnectedLinksTranslation = { NONE: "none", SUBGRAPH: "subgraph", ALL: "all" }; export default class Selection extends ui.Selection { // Override to translate the connected links correctly, see comments /FN/ translateSelectedElements( dx, dy, opt = {} ) { // FN: adding opt to be able to send key when keyboard is used for translating // 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. const processedCells = {}; const translatedLinks = []; // FN: Used only for updating the clipping if necessary const { collection } = this; const { graph, translateConnectedLinks } = this.options; collection.toArray().forEach( cell => { // TODO: snap to grid. if ( processedCells[cell.id] ) { return; } // Make sure that selection won't update itself when not necessary Object.assign( opt, { selection: this.cid } ); // Translate the cell itself. cell.translate( dx, dy, opt ); processedCells[cell.id] = true; cell.getEmbeddedCells( { deep: true } ).forEach( function ( embed ) { processedCells[embed.id] = true; } ); if ( translateConnectedLinks !== ConnectedLinksTranslation.NONE ) { // Translate link vertices as well. const connectedLinks = graph.getConnectedLinks( cell ); connectedLinks.forEach( link => { if ( processedCells[link.id] ) { return; } // FN: store the reference also in the translated links obj translatedLinks.push( link ); if ( translateConnectedLinks === ConnectedLinksTranslation.SUBGRAPH ) { const sourceCell = link.getSourceCell(); if ( sourceCell && !collection.get( sourceCell ) ) { return; } const targetCell = link.getTargetCell(); if ( targetCell && !collection.get( targetCell ) ) { return; } if ( !sourceCell || !targetCell ) { return; } // FN: We are handling this case in the LinkView already when the el is not // yet selected but when it is selected it needs to be handled here as well. // If target === source, we translate the link in the drag method if ( sourceCell && sourceCell === targetCell ) { return; } } link.translate( dx, dy, opt ); processedCells[link.id] = true; } ); } } ); // FN: dispatch an event to inform about translated links if ( translatedLinks.length ) { this.trigger( "translatelinks", this, translatedLinks ); } } // An override to allow allowTranslate be a function startSelectionInteraction( evt, cellView ) { const { paper, allowTranslate } = this.options; const activeElementView = cellView; // FN: allowTranslate now supports functions const aT = typeof allowTranslate === "function" ? allowTranslate( this, cellView ) : allowTranslate; // Start translating selected elements. if ( aT && ( !activeElementView || activeElementView.can( "elementMove" ) ) ) { this.startTranslatingSelection( evt ); } else { this.eventData( evt, { action: "translating", interactionPrevented: true } ); } this.eventData( evt, { activeElementView } ); const localPoint = paper.snapToGrid( evt.clientX, evt.clientY ); this.notify( "selection-box:pointerdown", evt, localPoint.x, localPoint.y ); this.delegateDocumentEvents( null, evt.data ); } // An override to pass the view over which the selection started. // This is used in the containers to limit the selection to subcontainers. // Additionally, setting the correct cursor. startSelecting( evt ) { evt = util.normalizeEvent( evt ); this.cancelSelection(); const { paperScroller, paper } = this.options; const origin = paper.localToPaperPoint( paper.clientToLocalPoint( { x: evt.clientX, y: evt.clientY } ) ); this.$el.css( { width: 0, height: 0, left: origin.x, top: origin.y } ); this.showLasso(); this.showCrosshairCursor(); // FN const scrollWhileDragging = paperScroller && paperScroller.options.scrollWhileDragging; this.eventData( evt, { action: "selecting", clientX: evt.clientX, clientY: evt.clientY, origin: origin, scrollWhileDragging, selectionInitiatedOverView: paper.findView( evt.target ) // FN } ); this.delegateDocumentEvents( null, evt.data ); this._action = "selecting"; } // An override to fix a bug in jointjs 4.1.1, reported to clientIO: // https://github.com/clientIO/joint/issues/3009 adjustSelection( evt ) { evt = util.normalizeEvent( evt ); let dx; let dy; const { collection } = this; const { paperScroller, paper, graph } = this.options; const localPoint = paper.clientToLocalPoint( { x: evt.clientX, y: evt.clientY } ); const data = this.eventData( evt ); const { action, scrollWhileDragging, interactionPrevented = false, defaultCellViewInteraction = false } = data; switch ( action ) { case "selecting": { const current = paper.localToPaperPoint( localPoint ); dx = current.x - data.origin.x; dy = current.y - data.origin.y; const width = Math.abs( dx ); const height = Math.abs( dy ); const borderWidth = parseFloat( window.getComputedStyle( this.el ).getPropertyValue( "border-width" ) ); this.$el.css( { left: dx < 0 ? current.x : data.origin.x, top: dy < 0 ? current.y : data.origin.y, width: width - borderWidth * 2, height: height - borderWidth * 2 } ); break; } case "translating": { var snappedClientCoords = paper.snapToGrid( evt.clientX, evt.clientY ); var snappedClientX = snappedClientCoords.x; var snappedClientY = snappedClientCoords.y; if ( interactionPrevented || defaultCellViewInteraction ) { this.notify( "selection-box:pointermove", evt, snappedClientX, snappedClientY ); break; } dx = snappedClientX - data.snappedClientX; dy = snappedClientY - data.snappedClientY; // restrict to area const selectedElements = this.collection.filter( cell => cell.isElement() ); const elementsBBox = graph.getCellsBBox( selectedElements ); // FN: fix for the bug [dx, dy] = this.respectRestrictTranslate( dx, dy, elementsBBox ); if ( dx || dy ) { this.translateSelectedElements( dx, dy ); if ( !this.framesUpdated ) { if ( this.updateStrategy?.translationAvailable ) { // Translate each of the `selection-box` this.frames.translate( dx, dy ); // Translate the `selection-wrapper`. this.wrapper.translate( dx, dy ); } else { this.updateSelectionBoxes(); } } else if ( collection.length > 1 || paper.isAsync() || this.options.allowCellInteraction ) { // 1. If there is more than one cell in the selection, we need to update // the selection frames again. e.g when the first element went over the // edge of the paper, a translate event was triggered, which updated the selection // frames. After that all remaining elements were translated but the selection // frames stayed unchanged. // 2. If the paper is in async mode, we always update the selection frames. // 3. Correctly update selection frame when there is one element with allowCellInteraction this.updateSelectionBoxes(); } data.snappedClientX = snappedClientX; data.snappedClientY = snappedClientY; } this.notify( "selection-box:pointermove", evt, snappedClientX, snappedClientY ); break; } default: { // for example, resizing if ( action ) { this.pointermove( evt ); } break; } } this.framesUpdated = false; if ( scrollWhileDragging ) { paperScroller.scrollWhileDragging( evt, localPoint.x, localPoint.y, scrollWhileDragging ); } } // An override to be able to filter based on the view (or not) over which the selection began. // Additionally, removing the crosshair cursor. stopSelecting( evt ) { let localPoint; const paper = this.options.paper; const data = this.eventData( evt ); const action = data.action;// this._action; // FN switch ( action ) { case "selecting": { const offset = this.$el.offset(); let width = this.$el.width(); let height = this.$el.height(); // Convert offset coordinates to the local point of the root element viewport. localPoint = paper.pageToLocalPoint( offset.left, offset.top ); // Convert width and height to take current viewport scale into account const paperScale = paper.scale(); width /= paperScale.sx; height /= paperScale.sy; const selectedArea = g.Rect( localPoint.x, localPoint.y, width, height ); let cellViews = this.getCellsInSelectedArea( selectedArea ); const filter = this.options.filter; if ( Array.isArray( filter ) ) { cellViews = cellViews.filter( function ( view ) { return !filter.includes( view.model ) && !filter.includes( view.model.get( "type" ) ); } ); } else if ( util.isFunction( filter ) ) { cellViews = cellViews.filter( function ( view ) { // FN: We will pass the element view over which (if any) the selection began return !filter( view.model, data.selectionInitiatedOverView ); } ); } const models = cellViews.map( function ( view ) { return view.model; } ); this.hide(); this.hideCrosshairCursor(); // FN this.collection.reset( models, { ui: true } ); break; } case "translating": { if ( !data.interactionPrevented ) { this.options.graph.stopBatch( "selection-translate" ); } localPoint = paper.snapToGrid( evt.clientX, evt.clientY ); this.notify( "selection-box:pointerup", evt, localPoint.x, localPoint.y ); // Everything else is done during the translation. break; } default: { if ( !action ) { // Hide selection if the user clicked somewhere else in the document. this.cancelSelection(); } break; } } this._action = null; } // Override. Read /FN/. pointerup( evt ) { const data = this.eventData( evt ); const { action, scrollWhileDragging } = data; // FN: There is an issue with the subcontainers, resp. with lasso-selecting embedded elements // in a parent which is selected. // If a subcontainer was initially selected and we tried to make a lasso selection above it // to select the children, the pointer up was fired twice and the collection got reset to [] // because the lasso's area was 0 on the second event. // Adding `|| action === 'selecting' && action !== this._action` to narrow it down to this case // and not acting upon the 2nd click. if ( !action || action === "selecting" && action !== this._action ) { return; } evt = util.normalizeEvent( evt ); const { paper, paperScroller } = this.options; const { x, y } = paper.snapToGrid( { x: evt.clientX, y: evt.clientY } ); if ( action !== "selecting" && action !== "translating" ) { this.triggerAction( action, "pointerup", evt, x, y ); } if ( scrollWhileDragging ) { paperScroller.stopScrollWhileDragging( evt ); } this.stopSelecting( evt ); this.undelegateDocumentEvents(); this._action = null; } showCrosshairCursor() { const { paperScroller } = this.options; paperScroller?.el.classList.add( CLS_LASSO_SELECTING ); } hideCrosshairCursor() { const { paperScroller } = this.options; paperScroller?.el.classList.remove( CLS_LASSO_SELECTING ); } }