import { omit } from "lodash" ; import EventTarget from "@ungap/event-target" ; import { BuilderRouteOrBrickNode , BuilderRouteNode , Story , BuilderCustomTemplateNode , } from "@next-core/brick-types" ; import { JsonStorage } from "@next-core/brick-utils" ; import { AbstractBuilderDataManager , BuilderCanvasData , BuilderContextMenuStatus , BuilderRuntimeEdge , BuilderRuntimeNode , EventDetailOfNodeAdd , EventDetailOfNodeAddStored , EventDetailOfNodeMove , EventDetailOfNodeReorder , EventDetailOfContextUpdated , SnippetNodeDetail , EventDetailOfSnippetApply , EventDetailOfSnippetApplyStored , SharedEditorConf , BuilderDroppingStatus , } from "../interfaces" ; import { getBuilderNode } from "./getBuilderNode" ; import { getUniqueNodeId } from "./getUniqueNodeId" ; import { reorderBuilderEdges } from "./reorderBuilderEdges" ; import { deleteNodeFromTree } from "./deleteNodeFromTree" ; import { getRelatedNodesBasedOnEvents , RelatedNodesBasedOnEventsMap , } from "../processors/getRelatedNodesBasedOnEvents" ; import { expandTemplateEdges } from "./expandTemplateEdges" ; import { getAppendingNodesAndEdges } from "./getAppendingNodesAndEdges" ; import { isParentExpandableTemplate } from "./isParentExpandableTemplate" ; enum BuilderInternalEventType { NODE_ADD = "builder.node.add" , NODE_MOVE = "builder.node.move" , NODE_REORDER = "builder.node.reorder" , NODE_CLICK = "builder.node.click" , SNIPPET_APPLY = "builder.snippet.apply" , CONTEXT_MENU_CHANGE = "builder.contextMenu.change" , DATA_CHANGE = "builder.data.change" , SHARED_EDITOR_LIST_CHANGE = "builder.sharedEditorList.change" , ROUTE_LIST_CHANGE = "builder.routeList.change" , HOVER_NODE_CHANGE = "builder.hoverNode.change" , SHOW_RELATED_NODES_BASED_ON_EVENTS = "builder.showRelatedNodesBasedOnEvents.change" , HIGHLIGHT_NODES_CHANGE = "builder.highlightNodes.change" , OUTLINE_DISABLED_NODES_CHANGE = "builder.outlineDisabledNodes.change" , DROPPING_STATUS_CHANGE = "builder.droppingStatus.change" } const storageKeyOfOutlineDisabledNodes = "builder-outline-disabled-nodes" ; export class BuilderDataManager implements AbstractBuilderDataManager { private data : BuilderCanvasData = { rootId : null , nodes : [ ] , edges : [ ] , } ; private hoverNodeUid : number ; private sharedEditorList : SharedEditorConf [ ] ; private routeList : BuilderRouteNode [ ] ; private storyList : Story [ ] ; private readonly eventTarget = new EventTarget ( ) ; private contextMenuStatus : BuilderContextMenuStatus = { active : false , } ; private showRelatedNodesBasedOnEvents : boolean ; private relatedNodesBasedOnEventsMap : RelatedNodesBasedOnEventsMap ; private highlightNodes : Set < number > = new Set ( ) ; private templateSourceMap : Map < string , BuilderCustomTemplateNode > ; private droppingStatus : BuilderDroppingStatus = new Map ( ) ; private readonly localJsonStorage = new JsonStorage < { [ storageKeyOfOutlineDisabledNodes ] : string [ ] ; } > ( localStorage ) ; private readonly outlineDisabledNodes : Set < string > = new Set ( this . localJsonStorage . getItem ( storageKeyOfOutlineDisabledNodes ) ?? [ ] ) ; getData ( ) { return this . data ; } getContextMenuStatus ( ) { return this . contextMenuStatus ; } getRelatedNodesBasedOnEventsMap ( ) { return this . relatedNodesBasedOnEventsMap ; } sharedEditorListInit ( data ) { this . sharedEditorList = data ; this . eventTarget . dispatchEvent ( new CustomEvent ( BuilderInternalEventType . SHARED_EDITOR_LIST_CHANGE ) ) ; } getSharedEditorList ( ) { return this . sharedEditorList ?? [ ] ; } onSharedEditorListChange ( fn ) { this . eventTarget . addEventListener ( BuilderInternalEventType . SHARED_EDITOR_LIST_CHANGE , fn ) ; return ( ) : void => { this . eventTarget . removeEventListener ( BuilderInternalEventType . SHARED_EDITOR_LIST_CHANGE , fn ) ; } ; } routeListInit ( data ) { this . routeList = data ; this . eventTarget . dispatchEvent ( new CustomEvent ( BuilderInternalEventType . ROUTE_LIST_CHANGE ) ) ; } getRouteList ( ) { return this . routeList ?? [ ] ; } storyListInit ( data ) { this . storyList = data ; } getStoryList ( ) { return this . storyList ; } onRouteListChange ( fn ) { this . eventTarget . addEventListener ( BuilderInternalEventType . ROUTE_LIST_CHANGE , fn ) ; return ( ) : void => { this . eventTarget . removeEventListener ( BuilderInternalEventType . ROUTE_LIST_CHANGE , fn ) ; } ; } dataInit ( root , templateSourceMap ? ) { this . templateSourceMap = templateSourceMap ; const rootId = getUniqueNodeId ( ) ; const newData = { rootId , ... getAppendingNodesAndEdges ( root , rootId , templateSourceMap , this . storyList ) , } ; this . data = { ... newData , edges : expandTemplateEdges ( newData ) , } ; this . triggerDataChange ( ) ; } private triggerDataChange ( ) { const { rootId , nodes } = this . data ; const rootNode = nodes . find ( ( node ) => node . $$uid === rootId ) ; const rootNodeIsCustomTemplate = rootNode . type === "custom-template" ; this . relatedNodesBasedOnEventsMap = getRelatedNodesBasedOnEvents ( nodes , rootNodeIsCustomTemplate ) ; this . eventTarget . dispatchEvent ( new CustomEvent ( BuilderInternalEventType . DATA_CHANGE ) ) ; } runAddNodeAction = ( detail ) => { const { rootId , nodes , edges } = this . data ; const { nodeUid , parentUid , nodeUids , nodeData } = detail ; const { nodes : addNodes , edges : addEdges } = getAppendingNodesAndEdges ( omit ( nodeData , [ "parent" , ] ) as Partial < BuilderRouteOrBrickNode > as BuilderRouteOrBrickNode , nodeUid , this . templateSourceMap , this . getStoryList ( ) ) ; const newNodes = nodes . concat ( addNodes ) ; const newEdges = edges . concat ( { parent : parentUid , child : nodeUid , mountPoint : nodeData . mountPoint , sort : undefined , $$isTemplateDelegated : isParentExpandableTemplate ( nodes , parentUid ) , } ) . concat ( addEdges ) ; const newData = { rootId , nodes : newNodes , edges : newEdges , } ; this . data = { ... newData , edges : reorderBuilderEdges ( newData , { parentUid , nodeUids , } ) , } ; this . triggerDataChange ( ) ; } ; updateBrick ( detail ) { this . data = deleteNodeFromTree ( detail . nodeUid , this . data ) ; this . runAddNodeAction ( detail ) ; } nodeAdd ( detail ) { this . runAddNodeAction ( detail ) ; this . eventTarget . dispatchEvent ( new CustomEvent ( BuilderInternalEventType . NODE_ADD , { detail } ) ) ; } nodeAddStored ( detail ) { const { rootId , nodes , edges } = this . data ; const { nodeUid , nodeData } = detail ; this . data = { rootId , nodes : nodes . map ( ( node ) => node . $$uid === nodeUid ? { ... node , id : nodeData . id , instanceId : nodeData . instanceId } : node ) , edges , } ; this . triggerDataChange ( ) ; } snippetApply ( detail ) { const { rootId , nodes , edges } = this . data ; const { nodeDetails , parentUid , nodeUids } = detail ; const newNodes = nodes . slice ( ) ; const newEdges = edges . slice ( ) ; const walk = ( { parentUid , nodeUid , nodeData , children , } ) : void => { const { nodes : appendingNodes , edges : appendingEdges } = getAppendingNodesAndEdges ( omit ( nodeData , [ "parent" , ] ) as Partial < BuilderRouteOrBrickNode > as BuilderRouteOrBrickNode , nodeUid , this . templateSourceMap , this . storyList ) ; newNodes . push ( ... appendingNodes ) ; newEdges . push ( { parent : parentUid , child : nodeUid , mountPoint : nodeData . mountPoint , sort : nodeData . sort , $$isTemplateDelegated : isParentExpandableTemplate ( newNodes , parentUid ) , } , ... appendingEdges ) ; for ( const item of children ) { walk ( item ) ; } } ; for ( const item of nodeDetails ) { walk ( item ) ; } const newData = { rootId , nodes : newNodes , edges : newEdges , } ; this . data = { ... newData , edges : reorderBuilderEdges ( newData , { parentUid , nodeUids , } ) , } ; this . triggerDataChange ( ) ; this . eventTarget . dispatchEvent ( new CustomEvent ( BuilderInternalEventType . SNIPPET_APPLY , { detail } ) ) ; } snippetApplyStored ( detail ) { const { rootId , nodes , edges } = this . data ; const { flattenNodeDetails } = detail ; this . data = { rootId , nodes : nodes . map ( ( node ) => { const found = flattenNodeDetails . find ( ( n ) => n . nodeUid === node . $$uid ) ; return found ? { ... node , id : found . nodeData . id , instanceId : found . nodeData . instanceId , } : node ; } ) , edges , } ; this . triggerDataChange ( ) ; } nodeMove ( detail ) { const { rootId , nodes , edges } = this . data ; const { nodeUid , parentUid , nodeUids , nodeData } = detail ; const newData = { rootId , nodes , edges : edges . filter ( ( edge ) => edge . child !== nodeUid ) . concat ( { parent : parentUid , child : nodeUid , mountPoint : nodeData . mountPoint , sort : undefined , $$isTemplateDelegated : isParentExpandableTemplate ( nodes , parentUid ) , } ) , } ; this . data = { ... newData , edges : reorderBuilderEdges ( newData , { parentUid , nodeUids , } ) , } ; this . triggerDataChange ( ) ; this . eventTarget . dispatchEvent ( new CustomEvent ( BuilderInternalEventType . NODE_MOVE , { detail } ) ) ; } contextUpdated ( detail ) { const { rootId , nodes , edges } = this . data ; this . data = { rootId , edges , nodes : nodes . map ( ( node ) => node . $$uid === rootId ? { ... node , context : detail . context } : node ) , } ; this . triggerDataChange ( ) ; } nodeReorder ( detail ) { const { nodeUids , parentUid } = detail ; this . data = { ... this . data , edges : reorderBuilderEdges ( this . data , { parentUid , nodeUids } ) , } ; this . triggerDataChange ( ) ; this . eventTarget . dispatchEvent ( new CustomEvent ( BuilderInternalEventType . NODE_REORDER , { detail } ) ) ; } nodeDelete ( detail ) { this . data = deleteNodeFromTree ( detail . $$uid , this . data ) ; this . triggerDataChange ( ) ; } nodeClick ( detail ) { this . eventTarget . dispatchEvent ( new CustomEvent ( BuilderInternalEventType . NODE_CLICK , { detail } ) ) ; } contextMenuChange ( detail ) { this . contextMenuStatus = detail ; this . eventTarget . dispatchEvent ( new CustomEvent ( BuilderInternalEventType . CONTEXT_MENU_CHANGE ) ) ; } onDataChange ( fn ) { this . eventTarget . addEventListener ( BuilderInternalEventType . DATA_CHANGE , fn ) ; return ( ) : void => { this . eventTarget . removeEventListener ( BuilderInternalEventType . DATA_CHANGE , fn ) ; } ; } onNodeAdd ( fn ) { this . eventTarget . addEventListener ( BuilderInternalEventType . NODE_ADD , fn as EventListener ) ; return ( ) : void => { this . eventTarget . removeEventListener ( BuilderInternalEventType . NODE_ADD , fn as EventListener ) ; } ; } onSnippetApply ( fn ) { this . eventTarget . addEventListener ( BuilderInternalEventType . SNIPPET_APPLY , fn as EventListener ) ; return ( ) : void => { this . eventTarget . removeEventListener ( BuilderInternalEventType . SNIPPET_APPLY , fn as EventListener ) ; } ; } onNodeReorder ( fn ) { this . eventTarget . addEventListener ( BuilderInternalEventType . NODE_REORDER , fn as EventListener ) ; return ( ) => { this . eventTarget . removeEventListener ( BuilderInternalEventType . NODE_REORDER , fn as EventListener ) ; } ; } onNodeMove ( fn ) { this . eventTarget . addEventListener ( BuilderInternalEventType . NODE_MOVE , fn as EventListener ) ; return ( ) => { this . eventTarget . removeEventListener ( BuilderInternalEventType . NODE_MOVE , fn as EventListener ) ; } ; } onNodeClick ( fn ) { this . eventTarget . addEventListener ( BuilderInternalEventType . NODE_CLICK , fn as EventListener ) ; return ( ) => { this . eventTarget . removeEventListener ( BuilderInternalEventType . NODE_CLICK , fn as EventListener ) ; } ; } onContextMenuChange ( fn ) { this . eventTarget . addEventListener ( BuilderInternalEventType . CONTEXT_MENU_CHANGE , fn as EventListener ) ; return ( ) => { this . eventTarget . removeEventListener ( BuilderInternalEventType . CONTEXT_MENU_CHANGE , fn as EventListener ) ; } ; } setShowRelatedNodesBasedOnEvents ( show ) { this . showRelatedNodesBasedOnEvents = show ; this . eventTarget . dispatchEvent ( new CustomEvent ( BuilderInternalEventType . SHOW_RELATED_NODES_BASED_ON_EVENTS ) ) ; } getShowRelatedNodesBasedOnEvents ( ) { return this . showRelatedNodesBasedOnEvents ; } onShowRelatedNodesBasedOnEventsChange ( fn ) { this . eventTarget . addEventListener ( BuilderInternalEventType . SHOW_RELATED_NODES_BASED_ON_EVENTS , fn ) ; return ( ) : void => { this . eventTarget . removeEventListener ( BuilderInternalEventType . SHOW_RELATED_NODES_BASED_ON_EVENTS , fn ) ; } ; } setHoverNodeUid ( uid ) { if ( this . hoverNodeUid !== uid ) { this . hoverNodeUid = uid ; this . eventTarget . dispatchEvent ( new CustomEvent ( BuilderInternalEventType . HOVER_NODE_CHANGE ) ) ; } } getHoverNodeUid ( ) { return this . hoverNodeUid ; } onHoverNodeChange ( fn ) { this . eventTarget . addEventListener ( BuilderInternalEventType . HOVER_NODE_CHANGE , fn ) ; return ( ) : void => { this . eventTarget . removeEventListener ( BuilderInternalEventType . HOVER_NODE_CHANGE , fn ) ; } ; } toggleOutline ( nodeInstanceId ) { if ( this . outlineDisabledNodes . has ( nodeInstanceId ) ) { this . outlineDisabledNodes . delete ( nodeInstanceId ) ; } else { this . outlineDisabledNodes . add ( nodeInstanceId ) ; } this . localJsonStorage . setItem ( storageKeyOfOutlineDisabledNodes , Array . from ( this . outlineDisabledNodes ) ) ; this . eventTarget . dispatchEvent ( new CustomEvent ( BuilderInternalEventType . OUTLINE_DISABLED_NODES_CHANGE ) ) ; } isOutlineEnabled ( nodeInstanceId ) { return ! this . outlineDisabledNodes . has ( nodeInstanceId ) ; } onOutlineEnabledNodesChange ( fn ) { this . eventTarget . addEventListener ( BuilderInternalEventType . OUTLINE_DISABLED_NODES_CHANGE , fn ) ; return ( ) : void => { this . eventTarget . removeEventListener ( BuilderInternalEventType . OUTLINE_DISABLED_NODES_CHANGE , fn ) ; } ; } setHighlightNodes ( nodeUids ) { this . highlightNodes = nodeUids ; this . eventTarget . dispatchEvent ( new CustomEvent ( BuilderInternalEventType . HIGHLIGHT_NODES_CHANGE ) ) ; } getHighlightNodes ( ) { return this . highlightNodes ; } onHighlightNodesChange ( fn ) { this . eventTarget . addEventListener ( BuilderInternalEventType . HIGHLIGHT_NODES_CHANGE , fn ) ; return ( ) : void => { this . eventTarget . removeEventListener ( BuilderInternalEventType . HIGHLIGHT_NODES_CHANGE , fn ) ; } ; } updateDroppingStatus ( nodeUid , mountPoint , isDraggingOver ) { const nodeStatus = this . droppingStatus . get ( nodeUid ) ; this . droppingStatus = new Map ( Array . from ( this . droppingStatus . entries ( ) ) . concat ( [ [ nodeUid , new Map ( ( nodeStatus ? Array . from ( nodeStatus ) : [ ] ) . concat ( [ [ mountPoint , isDraggingOver ] , ] ) ) , ] , ] ) ) ; this . eventTarget . dispatchEvent ( new CustomEvent ( BuilderInternalEventType . DROPPING_STATUS_CHANGE ) ) ; if ( this . hoverNodeUid && Array . from ( this . droppingStatus . values ( ) ) . flatMap ( ( nodeStatus ) => Array . from ( nodeStatus . values ( ) ) ) . some ( Boolean ) ) { this . setHoverNodeUid ( undefined ) ; } } getDroppingStatus ( ) { return this . droppingStatus ; } onDroppingStatusChange ( fn ) { this . eventTarget . addEventListener ( BuilderInternalEventType . DROPPING_STATUS_CHANGE , fn ) ; return ( ) : void => { this . eventTarget . removeEventListener ( BuilderInternalEventType . DROPPING_STATUS_CHANGE , fn ) ; } ; } }