2023-12-20 10:46:38 +00:00
import { updateOpenedModalSettingsId } from "@root/uiTools/actions" ;
import type { MutableRefObject } from "react" ;
import type {
PresetLayoutOptions ,
LayoutEventObject ,
NodeSingular ,
AbstractEventObject ,
} from "cytoscape" ;
type usePopperArgs = {
layoutsContainer : MutableRefObject < HTMLDivElement | null > ;
plusesContainer : MutableRefObject < HTMLDivElement | null > ;
crossesContainer : MutableRefObject < HTMLDivElement | null > ;
gearsContainer : MutableRefObject < HTMLDivElement | null > ;
setModalQuestionParentContentId : ( id : string ) = > void ;
setOpenedModalQuestions : ( open : boolean ) = > void ;
setStartCreate : ( id : string ) = > void ;
setStartRemove : ( id : string ) = > void ;
} ;
type PopperItem = {
id : ( ) = > string ;
} ;
type Modifier = {
name : string ;
options : unknown ;
} ;
type PopperConfig = {
popper : {
placement : string ;
modifiers? : Modifier [ ] ;
} ;
content : ( items : PopperItem [ ] ) = > void ;
} ;
type Popper = {
update : ( ) = > Promise < void > ;
setOptions : ( modifiers : { modifiers? : Modifier [ ] } ) = > void ;
} ;
type NodeSingularWithPopper = NodeSingular & {
popper : ( config : PopperConfig ) = > Popper ;
} ;
export const usePopper = ( {
layoutsContainer ,
plusesContainer ,
crossesContainer ,
gearsContainer ,
setModalQuestionParentContentId ,
setOpenedModalQuestions ,
setStartCreate ,
setStartRemove ,
} : usePopperArgs ) = > {
const removeButtons = ( id : string ) = > {
layoutsContainer . current
? . querySelector ( ` .popper-layout[data-id=' ${ id } '] ` )
? . remove ( ) ;
plusesContainer . current
? . querySelector ( ` .popper-plus[data-id=' ${ id } '] ` )
? . remove ( ) ;
crossesContainer . current
? . querySelector ( ` .popper-cross[data-id=' ${ id } '] ` )
? . remove ( ) ;
gearsContainer . current
? . querySelector ( ` .popper-gear[data-id=' ${ id } '] ` )
? . remove ( ) ;
} ;
const initialPopperIcons = ( { cy } : LayoutEventObject ) = > {
const container =
( document . body . querySelector (
".__________cytoscape_container"
) as HTMLDivElement ) || null ;
if ( ! container ) {
return ;
}
container . style . overflow = "hidden" ;
if ( ! plusesContainer . current ) {
plusesContainer . current = document . createElement ( "div" ) ;
plusesContainer . current . setAttribute ( "id" , "popper-pluses" ) ;
container . append ( plusesContainer . current ) ;
}
if ( ! crossesContainer . current ) {
crossesContainer . current = document . createElement ( "div" ) ;
crossesContainer . current . setAttribute ( "id" , "popper-crosses" ) ;
container . append ( crossesContainer . current ) ;
}
if ( ! gearsContainer . current ) {
gearsContainer . current = document . createElement ( "div" ) ;
gearsContainer . current . setAttribute ( "id" , "popper-gears" ) ;
container . append ( gearsContainer . current ) ;
}
if ( ! layoutsContainer . current ) {
layoutsContainer . current = document . createElement ( "div" ) ;
layoutsContainer . current . setAttribute ( "id" , "popper-layouts" ) ;
container . append ( layoutsContainer . current ) ;
}
const ext = cy . extent ( ) ;
const nodesInView = cy . nodes ( ) . filter ( ( n ) = > {
const bb = n . boundingBox ( ) ;
return (
bb . x2 > ext . x1 && bb . x1 < ext . x2 && bb . y2 > ext . y1 && bb . y1 < ext . y2
) ;
} ) ;
nodesInView . toArray ( ) ? . forEach ( ( item ) = > {
const node = item as NodeSingularWithPopper ;
const layoutsPopper = node . popper ( {
popper : {
placement : "left" ,
modifiers : [ { name : "flip" , options : { boundary : node } } ] ,
} ,
content : ( [ item ] ) = > {
const itemId = item . id ( ) ;
const itemElement = layoutsContainer . current ? . querySelector (
` .popper-layout[data-id=' ${ itemId } '] `
) ;
if ( itemElement ) {
return itemElement ;
}
const layoutElement = document . createElement ( "div" ) ;
layoutElement . style . zIndex = "0" ;
layoutElement . classList . add ( "popper-layout" ) ;
layoutElement . setAttribute ( "data-id" , item . id ( ) ) ;
layoutElement . addEventListener ( "mouseup" , ( ) = > {
//Узнаём грани, идущие от этой ноды
setModalQuestionParentContentId ( item . id ( ) ) ;
setOpenedModalQuestions ( true ) ;
} ) ;
layoutsContainer . current ? . appendChild ( layoutElement ) ;
return layoutElement ;
} ,
} ) ;
const plusesPopper = node . popper ( {
popper : {
placement : "right" ,
modifiers : [ { name : "flip" , options : { boundary : node } } ] ,
} ,
content : ( [ item ] ) = > {
const itemId = item . id ( ) ;
const itemElement = plusesContainer . current ? . querySelector (
` .popper-plus[data-id=' ${ itemId } '] `
) ;
if ( itemElement ) {
return itemElement ;
}
const plusElement = document . createElement ( "div" ) ;
plusElement . classList . add ( "popper-plus" ) ;
plusElement . setAttribute ( "data-id" , item . id ( ) ) ;
plusElement . style . zIndex = "1" ;
plusElement . addEventListener ( "mouseup" , ( ) = > {
setStartCreate ( node . id ( ) ) ;
} ) ;
plusesContainer . current ? . appendChild ( plusElement ) ;
return plusElement ;
} ,
} ) ;
const crossesPopper = node . popper ( {
popper : {
placement : "top-end" ,
modifiers : [ { name : "flip" , options : { boundary : node } } ] ,
} ,
content : ( [ item ] ) = > {
const itemId = item . id ( ) ;
const itemElement = crossesContainer . current ? . querySelector (
` .popper-cross[data-id=' ${ itemId } '] `
) ;
if ( itemElement ) {
return itemElement ;
}
const crossElement = document . createElement ( "div" ) ;
crossElement . classList . add ( "popper-cross" ) ;
crossElement . setAttribute ( "data-id" , item . id ( ) ) ;
crossElement . style . zIndex = "2" ;
crossesContainer . current ? . appendChild ( crossElement ) ;
crossElement . addEventListener ( "mouseup" , ( ) = > {
setStartRemove ( node . id ( ) ) ;
} ) ;
return crossElement ;
} ,
} ) ;
let gearsPopper : Popper | null = null ;
if ( node . data ( ) . root !== true ) {
gearsPopper = node . popper ( {
popper : {
placement : "left" ,
modifiers : [ { name : "flip" , options : { boundary : node } } ] ,
} ,
content : ( [ item ] ) = > {
const itemId = item . id ( ) ;
const itemElement = gearsContainer . current ? . querySelector (
` .popper-gear[data-id=' ${ itemId } '] `
) ;
if ( itemElement ) {
return itemElement ;
}
const gearElement = document . createElement ( "div" ) ;
gearElement . classList . add ( "popper-gear" ) ;
gearElement . setAttribute ( "data-id" , item . id ( ) ) ;
gearElement . style . zIndex = "1" ;
gearsContainer . current ? . appendChild ( gearElement ) ;
2023-12-20 10:55:38 +00:00
gearElement . addEventListener ( "mouseup" , ( ) = > {
2023-12-20 10:46:38 +00:00
updateOpenedModalSettingsId ( item . id ( ) ) ;
} ) ;
return gearElement ;
} ,
} ) ;
}
const update = async ( ) = > {
await plusesPopper . update ( ) ;
await crossesPopper . update ( ) ;
await gearsPopper ? . update ( ) ;
await layoutsPopper . update ( ) ;
} ;
const onZoom = ( event : AbstractEventObject ) = > {
const zoom = event . cy . zoom ( ) ;
//update();
crossesPopper . setOptions ( {
modifiers : [
{ name : "flip" , options : { boundary : node } } ,
{ name : "offset" , options : { offset : [ - 5 * zoom , - 30 * zoom ] } } ,
] ,
} ) ;
layoutsPopper . setOptions ( {
modifiers : [
{ name : "flip" , options : { boundary : node } } ,
{ name : "offset" , options : { offset : [ 0 , - 130 * zoom ] } } ,
] ,
} ) ;
plusesPopper . setOptions ( {
modifiers : [
{ name : "flip" , options : { boundary : node } } ,
{ name : "offset" , options : { offset : [ 0 , 0 * zoom ] } } ,
] ,
} ) ;
gearsPopper ? . setOptions ( {
modifiers : [
{ name : "flip" , options : { boundary : node } } ,
{ name : "offset" , options : { offset : [ 0 , 0 ] } } ,
] ,
} ) ;
layoutsContainer . current
? . querySelectorAll ( "#popper-layouts > .popper-layout" )
. forEach ( ( item ) = > {
const element = item as HTMLDivElement ;
element . style . width = ` ${ 130 * zoom } px ` ;
element . style . height = ` ${ 130 * zoom } px ` ;
} ) ;
plusesContainer . current
? . querySelectorAll ( "#popper-pluses > .popper-plus" )
. forEach ( ( item ) = > {
const element = item as HTMLDivElement ;
element . style . width = ` ${ 40 * zoom } px ` ;
element . style . height = ` ${ 40 * zoom } px ` ;
element . style . fontSize = ` ${ 40 * zoom } px ` ;
element . style . borderRadius = ` ${ 6 * zoom } px ` ;
} ) ;
crossesContainer . current
? . querySelectorAll ( "#popper-crosses > .popper-cross" )
. forEach ( ( item ) = > {
const element = item as HTMLDivElement ;
element . style . width = ` ${ 24 * zoom } px ` ;
element . style . height = ` ${ 24 * zoom } px ` ;
element . style . fontSize = ` ${ 24 * zoom } px ` ;
element . style . borderRadius = ` ${ 6 * zoom } px ` ;
} ) ;
gearsContainer ? . current
? . querySelectorAll ( "#popper-gears > .popper-gear" )
. forEach ( ( item ) = > {
const element = item as HTMLDivElement ;
element . style . width = ` ${ 60 * zoom } px ` ;
element . style . height = ` ${ 40 * zoom } px ` ;
} ) ;
} ;
//node?.on("position", update);
let pressed = false ;
let hide = false ;
cy ? . on ( "mousedown" , ( ) = > {
pressed = true ;
} ) ;
cy ? . on ( "mouseup" , ( ) = > {
pressed = false ;
hide = false ;
const gc = gearsContainer . current ;
if ( gc ) gc . style . display = "block" ;
const pc = plusesContainer . current ;
const xc = crossesContainer . current ;
const lc = layoutsContainer . current ;
if ( pc ) pc . style . display = "block" ;
if ( xc ) xc . style . display = "block" ;
if ( lc ) lc . style . display = "block" ;
update ( ) ;
} ) ;
cy ? . on ( "mousemove" , ( ) = > {
if ( pressed && ! hide ) {
hide = true ;
const gc = gearsContainer . current ;
if ( gc ) gc . style . display = "none" ;
const pc = plusesContainer . current ;
const xc = crossesContainer . current ;
const lc = layoutsContainer . current ;
if ( pc ) pc . style . display = "none" ;
if ( xc ) xc . style . display = "none" ;
if ( lc ) lc . style . display = "block" ;
}
} ) ;
cy ? . on ( "zoom render" , onZoom ) ;
} ) ;
} ;
const readyLO = ( event : LayoutEventObject ) = > {
if ( event . cy . data ( "firstNode" ) === "nonroot" ) {
event . cy . data ( "firstNode" , "root" ) ;
event . cy
. nodes ( )
. sort ( ( a , b ) = > ( a . data ( "root" ) ? 1 : - 1 ) )
. layout ( layoutOptions )
. run ( ) ;
} else {
event . cy . data ( "changed" , false ) ;
event . cy . removeData ( "firstNode" ) ;
}
//удаляем иконки
event . cy . nodes ( ) . forEach ( ( ele : any ) = > {
const data = ele . data ( ) ;
data . id && removeButtons ( data . id ) ;
} ) ;
initialPopperIcons ( event ) ;
} ;
const layoutOptions : PresetLayoutOptions = {
name : "preset" ,
positions : ( node ) = > {
if ( ! node . cy ( ) . data ( "changed" ) ) {
return node . data ( "oldPos" ) ;
}
const id = node . id ( ) ;
const incomming = node . cy ( ) . edges ( ` [target=" ${ id } "] ` ) ;
const layer = 0 ;
node . removeData ( "lastChild" ) ;
if ( incomming . length === 0 ) {
if ( node . cy ( ) . data ( "firstNode" ) === undefined )
node . cy ( ) . data ( "firstNode" , "root" ) ;
node . data ( "root" , true ) ;
const children = node . cy ( ) . edges ( ` [source=" ${ id } "] ` ) . targets ( ) ;
node . data ( "layer" , layer ) ;
node . data ( "children" , children . length ) ;
const queue = [ ] ;
children . forEach ( ( n ) = > {
queue . push ( { task : n , layer : layer + 1 } ) ;
} ) ;
while ( queue . length ) {
const task = queue . pop ( ) ;
task . task . data ( "layer" , task . layer ) ;
task . task . removeData ( "subtreeWidth" ) ;
2023-12-20 10:55:38 +00:00
const children = node
2023-12-20 10:46:38 +00:00
. cy ( )
. edges ( ` [source=" ${ task . task . id ( ) } "] ` )
. targets ( ) ;
task . task . data ( "children" , children . length ) ;
if ( children . length !== 0 ) {
children . forEach ( ( n ) = >
queue . push ( { task : n , layer : task.layer + 1 } )
) ;
}
}
2023-12-20 10:55:38 +00:00
queue . push ( { parent : node , children : children } ) ;
2023-12-20 10:46:38 +00:00
while ( queue . length ) {
const task = queue . pop ( ) ;
if ( task . children . length === 0 ) {
task . parent . data ( "subtreeWidth" , task . parent . height ( ) + 50 ) ;
continue ;
}
2023-12-20 10:55:38 +00:00
const unprocessed = task ? . children . filter ( ( node ) = > {
2023-12-20 10:46:38 +00:00
return node . data ( "subtreeWidth" ) === undefined ;
} ) ;
if ( unprocessed . length !== 0 ) {
queue . push ( task ) ;
unprocessed . forEach ( ( t ) = > {
queue . push ( {
parent : t ,
children : t.cy ( ) . edges ( ` [source=" ${ t . id ( ) } "] ` ) . targets ( ) ,
} ) ;
} ) ;
continue ;
}
task ? . parent . data (
"subtreeWidth" ,
task . children . reduce ( ( p , n ) = > p + n . data ( "subtreeWidth" ) , 0 )
) ;
}
const pos = { x : 0 , y : 0 } ;
node . data ( "oldPos" , pos ) ;
2023-12-20 10:55:38 +00:00
queue . push ( { task : children , parent : node } ) ;
2023-12-20 10:46:38 +00:00
while ( queue . length ) {
const task = queue . pop ( ) ;
const oldPos = task . parent . data ( "oldPos" ) ;
let yoffset = oldPos . y - task . parent . data ( "subtreeWidth" ) / 2 ;
task . task . forEach ( ( n ) = > {
const width = n . data ( "subtreeWidth" ) ;
n . data ( "oldPos" , {
x : 250 * n . data ( "layer" ) ,
y : yoffset + width / 2 ,
} ) ;
yoffset += width ;
queue . push ( {
task : n.cy ( ) . edges ( ` [source=" ${ n . id ( ) } "] ` ) . targets ( ) ,
parent : n ,
} ) ;
} ) ;
}
node . cy ( ) . data ( "changed" , false ) ;
return pos ;
} else {
const opos = node . data ( "oldPos" ) ;
if ( opos ) {
return opos ;
}
}
} , // map of (node id) => (position obj); or function(node){ return somPos; }
zoom : undefined , // the zoom level to set (prob want fit = false if set)
pan : 1 , // the pan level to set (prob want fit = false if set)
fit : false , // whether to fit to viewport
padding : 30 , // padding on fit
animate : false , // whether to transition the node positions
animationDuration : 500 , // duration of animation in ms if enabled
animationEasing : undefined , // easing of animation if enabled
animateFilter : function ( node , i ) {
return false ;
} , // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts
ready : readyLO , // callback on layoutready
transform : function ( node , position ) {
return position ;
} , // transform a given node position. Useful for changing flow direction in discrete layouts
} ;
return { layoutOptions } ;
} ;