/*! * Copyright (c) 2023, 2025, Oracle and/or its affiliates. */ /** * @file * * Extended joint.ui.Stencil with additional functionality and/or overrides. * * Stencil is the "palette" where the user can drag new elements from. */ import { getToolByEl } from "./actions/tool.mjs"; const { ui, util } = joint; export default class Stencil extends ui.Stencil { #dropEventOpts = {}; onDrag( evt ) { const { _dragging, options } = this; if ( !_dragging ) { return; } evt.preventDefault(); const normalizedEvt = util.normalizeEvent( evt ); if ( this.isDragCanceled() ) { // Drag was canceled by calling cancelDrag() not from the `drag` event callback. const { cloneView, validDropTarget, cloneArea } = _dragging; this.notifyDragEnd( cloneView, normalizedEvt, cloneArea, validDropTarget ); this._dragging = null; return; } const { clone, cloneView } = _dragging; const { paper, snaplines, usePaperGrid, validateDrop = () => true // FN: allow additional validation of the drop } = options; const validDropTarget = validateDrop( cloneView, { x: normalizedEvt.clientX, y: normalizedEvt.clientY } ) && this.insideValidArea( { x: normalizedEvt.clientX, y: normalizedEvt.clientY } ); const cloneArea = this.getCloneArea( cloneView, normalizedEvt, validDropTarget && usePaperGrid ); const { x, y } = cloneArea.center(); const { x: clientX, y: clientY } = paper.localToClientPoint( x, y ); util.assign( _dragging, { clientX, clientY, validDropTarget, cloneArea } ); this.setPaperDragOffset( clientX, clientY, _dragging ); this.positionCell( clone, 0, 0, { silent: true } ); cloneView.translate(); this.positionCell( clone, cloneArea.x, cloneArea.y, { stencil: this.cid } ); if ( this.hasSnaplinesEnabled() ) { if ( validDropTarget ) { snaplines.snapWhileMoving( cloneView, normalizedEvt, cloneArea.x, cloneArea.y ); } else { snaplines.hide(); } } const { embeddingMode } = paper.options; if ( embeddingMode && clone.isElement() ) { cloneView.eventData( normalizedEvt, { paper } ); const data = _dragging.cloneViewEventData = cloneView.eventData( normalizedEvt ); if ( validDropTarget ) { cloneView.processEmbedding( data, evt, x, y ); } else { cloneView.clearEmbedding( data ); } } // Allow anyone from outside to change the cloneView model. cloneView.startListening(); this.trigger( "element:drag", cloneView, normalizedEvt, cloneArea.clone(), validDropTarget ); cloneView.stopListening(); if ( this.isDragCanceled() ) { this.notifyDragEnd( cloneView, normalizedEvt, cloneArea, validDropTarget ); this._dragging = null; } } // Override to add before_drop event. onDrop( dragCloneView, normalizedEvt ) { const { options } = this; const { paper, graph } = options; const { embeddingMode } = paper.options; const { model: dragClone } = dragCloneView; // Start the dragging batch // Batch might contain `add`, `change:parent`, `change:embeds` events. graph.startBatch( "stencil-drag" ); const endClone = this.getDragEndClone( dragClone ); // FN: before event const dropPoint = this.getCurrentDropTarget().center(); const droppedOverView = paper.findView( normalizedEvt.target ) || null; const droppedOverTool = droppedOverView ? getToolByEl( droppedOverView, normalizedEvt.target ) : null; this.trigger( "element:beforedrop", dragCloneView, endClone, normalizedEvt, dropPoint.x, dropPoint.y, droppedOverView, droppedOverTool ); this.drop( endClone, dropPoint, this.#dropEventOpts ); // FN: pass our param // FN: reset our dropEventOpts this.setDropEventOpts( {} ); // embedding if ( embeddingMode && dragClone.isElement() ) { dragCloneView.eventData( normalizedEvt, { model: endClone, paper, initialParentId: util.uuid(), // dummy parent id whenNotAllowed: "remove" } ); dragCloneView.finalizeEmbedding( dragCloneView.eventData( normalizedEvt ) ); } // snaplines // it's hide on document mouseup by the plugin itself // If the element is not in the graph, it must have been invalid unembedding if ( graph.getCell( endClone ) ) { const { x, y } = paper.clientToLocalPoint( normalizedEvt.clientX, normalizedEvt.clientY ); const endCloneView = endClone.findView( paper ); this.trigger( "element:drop", endCloneView, normalizedEvt, x, y, droppedOverView, droppedOverTool ); // FN: added dropped over this.removePaperAfterDragging( dragClone ); } else { this.onDropInvalid( dragCloneView, normalizedEvt ); } // End the dragging batch. graph.stopBatch( "stencil-drag" ); } // Override. drop( endClone, dropPoint, eOpts = {} ) { // FN: added eOpts const { graph, autoZIndex } = this.options; const { width, height } = endClone.getBBox(); // Note: the `endClone` is not in the graph yet, so no events are triggered. // Usually we don't want the cell to have the same `z` level as it had in the stencil // or the drag clone. We want the cell to be on top of all other cells. if ( autoZIndex ) { // `z` level will be set automatically in the `this.graph.addCell()` method. endClone.unset( "z" ); } this.positionCell( endClone, dropPoint.x - width / 2, dropPoint.y - height / 2 ); graph.addCell( endClone, Object.assign( eOpts, { // FN: merge with our eOpts stencil: this.cid } ) ); } setDropEventOpts( opts ) { this.#dropEventOpts = opts; } }