/*! * Copyright (c) 2023, 2024, Oracle and/or its affiliates. */ /** * @file * * Extended joint.ui.Selection with additional functionality and/or overrides. */ import { highlightView, unhighlightView } from './actions/highlight.mjs'; const { ui, util } = joint; const ConnectedLinksTranslation = { NONE: 'none', SUBGRAPH: 'subgraph', ALL: 'all' }; export default class CellSelection extends ui.Selection { #originalPaperInteractivity; constructor(options) { super(options); const { paper } = options; this.listenTo(paper, { 'element:pointerdown': this.onElementPointerDown.bind(this), 'element:touchstart': this.onElementPointerDown.bind(this) }); } onElementPointerDown(elementView, e) { const { collection } = this; const { model } = elementView; if (collection.has(model)) { e.stopPropagation(); e.stopImmediatePropagation(); this.onSelectionBoxPointerDown(e); } } setPaperInteractivity() { const { collection } = this; const { paper } = this.options; this.#originalPaperInteractivity = paper.options.interactive; paper.setInteractivity(view => { const r = (collection.length === 1 || !collection.has(view.model)) ? true : { elementMove: false }; return r; }); } onSelectionBoxPointerDown(e) { e.stopPropagation(); e = util.normalizeEvent(e); const { paper, allowTranslate } = this.options; const activeElementView = this.getCellView(e.target); // Start translating selected elements. if (allowTranslate && (!activeElementView || activeElementView.can('elementMove'))) { this.setPaperInteractivity(); this.startTranslatingSelection(e); } else if (activeElementView) { activeElementView.preventDefaultInteraction(e); } this.eventData(e, { activeElementView }); const localPoint = paper.snapToGrid(e.clientX, e.clientY); this.notify('selection-box:pointerdown', e, localPoint.x, localPoint.y); this.delegateDocumentEvents(null, e.data); } pointerup(e) { const { paper } = this.options; if (this._action === 'translating') { paper.setInteractivity(this.#originalPaperInteractivity); this.#originalPaperInteractivity = null; } // We can't call super.pointerup because there is a bug in joint where the // events are not undelegated. Thus, let's do it (following is modified super call): const data = this.eventData(e); const action = data.action; if (!action) { // undelegate events we talked about above this.undelegateDocumentEvents(); return; } e = util.normalizeEvent(e); const { x, y } = this.options.paper.snapToGrid({ x: e.clientX, y: e.clientY }); this.triggerAction(action, 'pointerup', e, x, y); this.stopSelecting(e); this.undelegateDocumentEvents(); this._action = null; } getCellView(element) { const viewEl = element.closest('[model-id]'); const cell = this.model.get(viewEl.getAttribute('model-id')); return cell && cell.findView(this.options.paper); } // Override to translate the connected links correctly, see comment /FN/ translateSelectedElements(dx, dy, opt = {}) { // 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 { collection } = this; const { graph, translateConnectedLinks } = this.options; collection.each((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(function(link) { if (processedCells[link.id]) { return; } 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; }); } }); } // BELOW SELECTION WITH MASKING OVERRIDES onResetElements(elements, { previousModels = [] }) { this.destroyAllSelectionBoxes(previousModels); elements.each(this.createSelectionBox.bind(this)); this.updateSelectionWrapper(); } createSelectionBox(element) { const elementView = element.findView(this.options.paper); if (elementView) { elementView.el.classList.add('selected'); elementView.el.dataset.selectionHighlightId = highlightView(elementView, { padding: 1, className: 'selection-highlight', layer: 'front', attrs: { 'stroke': 'var(--a-diagram-cell-selection, orange)', 'stroke-width': 2, } }); this.showSelected(); this._boxCount += 1; } } destroyAllSelectionBoxes(cells = []) { cells.forEach((cell) => { const elementView = cell.findView(this.options.paper); if (elementView) { if (cell.isElement()) { unhighlightView(elementView, elementView.el.dataset.selectionHighlightId); } elementView.el.classList.remove('selected'); } }); this._boxCount = 0; this.hide(); } destroySelectionBox(element) { const elementView = element.findView(this.options.paper); unhighlightView(elementView, elementView.el.dataset.selectionHighlightId); //FN: why are/were they using max here instead of just subtracting 1? this._boxCount = this._boxCount - 1; // Math.max(0, this._boxCount - 1); if (!this._boxCount) { this.hide(); } } _updateSelectionBoxes() { if (this._boxCount) { this.showSelected(); this.updateSelectionWrapper(); } else { this.hide(); } } }