2023-11-29 15:45:15 +00:00
import { useEffect , useRef , useState } from "react" ;
import Cytoscape from "cytoscape" ;
import CytoscapeComponent from "react-cytoscapejs" ;
import popper from "cytoscape-popper" ;
import { useCurrentQuiz } from "@root/quizes/hooks" ;
import { useQuestionsStore } from "@root/questions/store" ;
import { clearDragQuestionId } from "@root/questions/actions" ;
import { storeToNodes } from "./helper" ;
import "./styles.css" ;
import type {
Stylesheet ,
Core ,
NodeSingular ,
AbstractEventObject ,
ElementDefinition ,
} from "cytoscape" ;
import { QuestionsList } from "../BranchingPanel/QuestionsList" ;
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 ;
} ;
const stylesheet : Stylesheet [ ] = [
{
selector : "node" ,
style : {
shape : "round-rectangle" ,
width : 130 ,
height : 130 ,
backgroundColor : "#FFFFFF" ,
label : "data(label)" ,
"font-size" : "16" ,
color : "#4D4D4D" ,
"text-halign" : "center" ,
"text-valign" : "center" ,
"text-wrap" : "wrap" ,
"text-max-width" : "80" ,
} ,
} ,
{
selector : ".multiline-auto" ,
style : {
"text-wrap" : "wrap" ,
"text-max-width" : "80" ,
} ,
} ,
{
selector : "edge" ,
style : {
width : 30 ,
"line-color" : "#DEDFE7" ,
"curve-style" : "taxi" ,
"taxi-direction" : "horizontal" ,
"taxi-turn" : 60 ,
} ,
} ,
{
selector : ":selected" ,
style : {
"border-style" : "solid" ,
"border-width" : 1.5 ,
"border-color" : "#9A9AAF" ,
} ,
} ,
] ;
Cytoscape . use ( popper ) ;
export const CsComponent = ( ) = > {
const quiz = useCurrentQuiz ( ) ;
const { dragQuestionId , questions } = useQuestionsStore ( )
const [ startCreate , setStartCreate ] = useState ( "" ) ;
const [ startRemove , setStartRemove ] = useState ( "" ) ;
const cyRef = useRef < Core | null > ( null ) ;
const plusesContainer = useRef < HTMLDivElement | null > ( null ) ;
const crossesContainer = useRef < HTMLDivElement | null > ( null ) ;
const gearsContainer = useRef < HTMLDivElement | null > ( null ) ;
const addNode = ( { parentNodeId } : { parentNodeId : string } ) = > {
const cy = cyRef ? . current
//Если детей больше 1 - предупреждаем стор вопросов о б открытии модалки ветвления
// if (Object.keys(currentNode.children).length > 1) {
// setOpenedModalSettings(question.index)
// } else {
// //Если ребёнок первый - добавляем е г о родителю как дефолтный
// parentQuestion.question.content.rule.default = Object.keys(newNode)[0].split("_").pop()
// updateQuestionsList(quiz, parentQuestion.index, parentQuestion.question);
// }
console . log ( dragQuestionId )
// cy?.add({
// })
}
useEffect ( ( ) = > {
if ( startCreate ) {
addNode ( { parentNodeId : startCreate } ) ;
clearDragQuestionId ( )
setStartCreate ( "" ) ;
}
} , [ startCreate ] ) ;
useEffect ( ( ) = > {
if ( startRemove ) {
// removeNode(quiz, startRemove);
setStartRemove ( "" ) ;
}
} , [ startRemove ] ) ;
useEffect ( ( ) = > {
document . querySelector ( "#root" ) ? . addEventListener ( "mouseup" , clearDragQuestionId ) ;
const cy = cyRef . current ;
2023-12-01 00:31:09 +00:00
//cy?.add(storeToNodes(questions))
cy ? . add (
[
{
"data" : {
"id" : "1" ,
"label" : "2"
}
} ,
{
"data" : {
"id" : "1 2" ,
"label" : "Вы идёте в школу"
}
} ,
{
"data" : {
"id" : "1 3" ,
"label" : "1"
}
} ,
{
"data" : {
"id" : "1 2 4" ,
"label" : "3"
}
} ,
{
"data" : {
"id" : "1 2 6" ,
"label" : "5"
}
} ,
{
"data" : {
"id" : "1 3 5" ,
"label" : "4"
}
} ,
{
"data" : {
"id" : "1 3 7" ,
"label" : "6"
}
} ,
{
"data" : {
"id" : "1 2 6 9867874" ,
"label" : "7"
}
} ,
{
"data" : {
"id" : "1 2 6 7398789" ,
"label" : "8"
}
} ,
{
"data" : {
"id" : "1 2 6 9484789" ,
"label" : "11"
}
} ,
{
"data" : {
"source" : "1" ,
"target" : "1 2" ,
"id" : "c4881f18-03cf-4ed1-bbc4-1741007f11c5"
}
} ,
{
"data" : {
"source" : "1" ,
"target" : "1 3" ,
"id" : "3cc5a94a-0192-4ea2-bdc6-ce1a157b76d4"
}
} ,
{
"data" : {
"source" : "1 2" ,
"target" : "1 2 4" ,
"id" : "1baf1bc6-eb40-4c81-b137-27cdd3a15e60"
}
} ,
{
"data" : {
"source" : "1 2" ,
"target" : "1 2 6" ,
"id" : "78af38cc-7609-401c-bbff-ebdb3f67ec14"
}
} ,
{
"data" : {
"source" : "1 3" ,
"target" : "1 3 5" ,
"id" : "a1c80f9f-7c4b-455c-8ba9-ef5dce5522b5"
}
} ,
{
"data" : {
"source" : "1 3" ,
"target" : "1 3 7" ,
"id" : "85ed3ee9-fdd1-4874-8e36-484db46bf1c5"
}
} ,
{
"data" : {
"source" : "1 2 6" ,
"target" : "1 2 6 9867874" ,
"id" : "f139548a-abca-412b-9935-740f219a938d"
}
} ,
{
"data" : {
"source" : "1 2 6" ,
"target" : "1 2 6 7398789" ,
"id" : "ec8dd60c-df49-447f-b85a-4ae00cde1ae9"
}
} ,
{
"data" : {
"source" : "1 2 6" ,
"target" : "1 2 6 9484789" ,
"id" : "9b5ecc61-d0ca-4872-a2a4-4fd72835345e"
}
}
]
)
2023-11-29 15:45:15 +00:00
return ( ) = > {
document . querySelector ( "#root" ) ? . removeEventListener ( "mouseup" , clearDragQuestionId ) ;
plusesContainer . current ? . remove ( ) ;
crossesContainer . current ? . remove ( ) ;
gearsContainer . current ? . remove ( ) ;
} ;
} , [ ] ) ;
const removeButtons = ( id : string ) = > {
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 initialCS = ( ) = > {
const cy = cyRef . current ;
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 ) ;
}
cy ? . nodes ( )
. toArray ( )
? . forEach ( ( item ) = > {
const node = item as NodeSingularWithPopper ;
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 ( ) ) ;
plusesContainer . current ? . appendChild ( plusElement ) ;
plusElement . addEventListener ( "mouseup" , ( ) = >
setStartCreate ( node . id ( ) )
) ;
return plusElement ;
} ,
} ) ;
const crossesPopper = node . popper ( {
popper : {
placement : "top-end" ,
modifiers : [
{ name : "flip" , options : { boundary : node } } ,
{
name : "hide" ,
options : { enabled : true } ,
} ,
] ,
} ,
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 ( ) ) ;
crossesContainer . current ? . appendChild ( crossElement ) ;
crossElement . addEventListener ( "click" , ( ) = > {
console . log ( '#' + node . id ( ) + ' node' , cy . edges ( '[source = "' + node . id ( ) + '"]' ) )
console . log ( '[source = "' + node . id ( ) + '"]' )
console . log ( cy ? . collection )
// cy?.remove('[source = "'+node.id()+'"]')
console . log ( "папа" )
console . log ( cy ? . $ ( '[target = "' + node . id ( ) + '"]' ) . data ( ) . source )
cy ? . remove ( '#' + node . id ( ) )
// setStartRemove(node.id())
}
) ;
node . on ( 'remove' , evt = > {
console . log ( cy . edges ( ) )
// cy?.remove('#'+evt.target.target())
} )
return crossElement ;
} ,
} ) ;
const gearsPopper = node . popper ( {
popper : {
placement : "left" ,
modifiers : [ { name : "flip" , options : { boundary : node } } ] ,
} ,
content : ( [ item ] ) = > {
const itemId = item . id ( ) ;
// if (item.id() === elements[0].data.id) {
// return;
// }
const itemElement = gearsContainer . current ? . querySelector (
` .popper-gear[data-id=' ${ itemId } '] `
) ;
if ( itemElement ) {
return itemElement ;
}
const gearsElement = document . createElement ( "div" ) ;
gearsElement . classList . add ( "popper-gear" ) ;
gearsElement . setAttribute ( "data-id" , item . id ( ) ) ;
gearsContainer . current ? . appendChild ( gearsElement ) ;
// gearsElement.addEventListener("click", () =>
// setOpenedModalSettings(
// findQuestionById(quiz, node.id().split("_").pop() || "").index
// )
// );
return gearsElement ;
} ,
} ) ;
const update = async ( ) = > {
await plusesPopper . update ( ) ;
await crossesPopper . update ( ) ;
await gearsPopper . 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 ] } } ,
{
name : "hide" ,
options : { enabled : true } ,
} ,
] ,
} ) ;
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 = ` ${ 33 * zoom } px ` ;
element . style . height = ` ${ 14 * zoom } px ` ;
element . style . fontSize = ` ${ 24 * zoom } px ` ;
} ) ;
} ;
node ? . on ( "position" , update ) ;
cy ? . on ( "pan zoom resize render" , onZoom ) ;
} ) ;
} ;
return (
< CytoscapeComponent
wheelSensitivity = { 0.1 }
elements = { [ ] }
// elements={createGraphElements(tree, quiz)}
style = { { height : "480px" , background : "#F2F3F7" } }
layout = { {
name : 'preset' ,
positions : ( e ) = > {
2023-12-01 00:31:09 +00:00
const id = e . id ( )
const incomming = e . cy ( ) . edges ( ` [target=" ${ id } "] ` )
const layer = 0
e . removeData ( 'lastChild' )
if ( incomming . length === 0 ) {
const children = e . cy ( ) . edges ( ` [source=" ${ id } "] ` )
e . data ( 'layer' , layer )
e . data ( 'children' , children . targets ( ) . length )
const queue = [ ]
children . forEach ( n = > {
queue . push ( { task : n.target ( ) , layer : layer + 1 } )
} )
while ( queue . length ) {
const task = queue . pop ( )
task . task . data ( 'layer' , task . layer )
const children = e . cy ( ) . edges ( ` [source=" ${ task . task . id ( ) } "] ` )
task . task . data ( 'children' , children . targets ( ) . length )
if ( children . length !== 0 ) {
children . forEach ( n = > queue . push ( { task : n.target ( ) , layer : task.layer + 1 } ) )
}
}
queue . push ( { parent : e , children :children.targets ( ) } )
while ( queue . length ) {
const task = queue . pop ( )
if ( task . children . length === 0 ) {
task . parent . data ( 'subtreeWidth' , task . parent . height ( ) )
continue
}
const unprocessed = task ? . children . filter ( e = > e . data ( 'subtreeWidth' ) === undefined )
if ( unprocessed . length !== 0 ) {
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 ) )
}
return { x :200 * e . data ( 'layer' ) , y :0 }
} else {
const parent = e . cy ( ) . edges ( ` [target=" ${ e . id ( ) } "] ` ) [ 0 ] . source ( )
// console.log(e.data('subtreeWidth'), e.id(),(parent.data('children')-1) )
const wing = parent . data ( 'subtreeWidth' ) / 2
const lastOffset = parent . data ( 'lastChild' )
const step = wing * 2 / ( parent . data ( 'children' ) - 1 )
if ( e . data ( 'layer' ) === 1 )
console . log ( e . data ( 'subtreeWidth' ) , e . id ( ) , ( parent . data ( 'children' ) - 1 ) , step , wing , e . data ( 'layer' ) , parent . id ( ) , lastOffset )
//e.removeData('subtreeWidth')
//console.log('poss', e.id(), 'children', parent.data('children'),'lo', lastOffset, 'v', wing)
if ( lastOffset !== undefined ) {
parent . data ( 'lastChild' , lastOffset + step )
return { x :200 * e . data ( 'layer' ) , y : lastOffset + step }
} else {
parent . data ( 'lastChild' , parent . position ( ) . y - wing )
return { x :200 * e . data ( 'layer' ) , y : parent.position ( ) . y - wing }
}
}
2023-11-29 15:45:15 +00:00
} , // 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 : undefined , // the pan level to set (prob want fit = false if set)
2023-12-01 00:31:09 +00:00
fit : false , // whether to fit to viewport
2023-11-29 15:45:15 +00:00
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 true ; } , // 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
2023-12-01 00:31:09 +00:00
ready : ( e ) = > console . log ( 'ready' , e ) , // callback on layoutready
stop : ( e ) = > console . log ( 'stop' , e ) , // callback on layoutstop
2023-11-29 15:45:15 +00:00
transform : function ( node , position ) { return position ; } // transform a given node position. Useful for changing flow direction in discrete layouts
} }
stylesheet = { stylesheet }
cy = { ( cy ) = > {
cyRef . current = cy ;
} }
/ >
) ;
} ;