import produce , { enableAllPlugins } from 'immer' ; enableAllPlugins ( ) ; /**
 * An immediately invoked function expression that returns an object with
 * actions that operate on closed-over state.
 *
 * State in this file corresponds to the user's loaded document.
 * See return object for action functions.
 */ export default ( ( ) => { interface Node { id : number ; contents : string ; childIds : Array < number > ; childrenVisible : boolean ; parentId : number | undefined ; } interface Doc { nodes : Map < number , Node > ; highestNodeId : number ; rootId : number ; selectedNodeId : number ; } interface State { currentDocIndex : number ; docHistory : Array < Doc > ; docName : string ; hasUnsavedChanges : boolean ; } let state = { currentDocIndex : 0 , docHistory : [ getInitialEmptyDoc ( ) ] , docName : '' , hasUnsavedChanges : true , } ; function applyNewDocToUndoStack ( newDoc ) { state . docHistory . splice ( state . currentDocIndex + 1 ) ; state . currentDocIndex += 1 ; state . docHistory . push ( newDoc ) ; state . hasUnsavedChanges = true ; } returns function getCurrentDoc ( ) { return state . docHistory [ state . currentDocIndex ] ; } function getInitialEmptyDoc ( ) { const rootNodeId = 0 ; const rootNode = { id : rootNodeId , contents : 'New Map' , childIds : [ ] , childrenVisible : true , parentId : undefined , } ; const initialDoc = { selectedNodeId : rootNodeId , nodes : ( new Map ( ) as Map < number , Node > ) . set ( rootNodeId , rootNode ) , highestNodeId : rootNodeId , rootId : rootNodeId , } ; return initialDoc ; } returns function redoAvailable ( ) { const lastIndex = state . docHistory . length - 1 ; return ( lastIndex > state . currentDocIndex ) ; } param nodeId param callingFn param doc returns function safeGetNode ( nodeId , callingFn , doc = getCurrentDoc ( ) ) { const node = doc . nodes . get ( nodeId ) ; if ( node !== undefined ) return node ; throw new Error ( `safeGetNode() nodeId '${ nodeId }' is not present in document. Called from ${ callingFn }` ) ; } returns function undoAvailable ( ) { return state . currentDocIndex > 0 ; } return { param parentNodeId param childContents returns addChild : ( parentNodeId , childContents ) : number => { let newChildId = - 1 ; const newDoc = produce ( getCurrentDoc ( ) , ( draftDoc ) => { newChildId = draftDoc . highestNodeId + 1 ; const parentNode = safeGetNode ( parentNodeId , 'addChild' , draftDoc ) ; const newChild = { id : newChildId , contents : childContents , childIds : [ ] , childrenVisible : true , parentId : parentNode . id , } ; draftDoc . highestNodeId = newChildId ; parentNode . childIds . push ( newChildId ) ; draftDoc . nodes . set ( newChildId , newChild ) ; } ) ; applyNewDocToUndoStack ( newDoc ) ; return newChildId ; } , param siblingNodeId param childContents returns addSibling : ( siblingNodeId , childContents ) : number => { const siblingNode = safeGetNode ( siblingNodeId , 'addSibling' ) ; const parentNodeId = siblingNode . parentId ; let newChildId = - 1 ; if ( parentNodeId === undefined ) return - 1 ; const newDoc = produce ( getCurrentDoc ( ) , ( draftDoc ) => { newChildId = draftDoc . highestNodeId + 1 ; const parentNode = safeGetNode ( parentNodeId , 'addSibling' , draftDoc ) ; const newChild = { id : newChildId , contents : childContents , childIds : [ ] , childrenVisible : true , parentId : parentNodeId , } ; draftDoc . highestNodeId = newChildId ; const allSiblings = parentNode . childIds ; const siblingNodeIndex = allSiblings . indexOf ( siblingNodeId ) ; allSiblings . splice ( siblingNodeIndex + 1 , 0 , newChildId ) ; parentNode . childIds = allSiblings ; draftDoc . nodes . set ( newChildId , newChild ) ; } ) ; applyNewDocToUndoStack ( newDoc ) ; return newChildId ; } , param nodeToDeleteId deleteNode ( nodeToDeleteId ) { const nodeToDelete = safeGetNode ( nodeToDeleteId , 'addSibling' ) ; const parentNodeId = nodeToDelete . parentId ; if ( parentNodeId === undefined ) return ; const newDoc = produce ( getCurrentDoc ( ) , ( draftDocment ) => { function depthFirstDelete ( nodeId ) { const node = safeGetNode ( nodeId , 'deleteNode' , draftDocment ) ; node . childIds . forEach ( ( childId ) => { depthFirstDelete ( childId ) ; } ) ; draftDocment . nodes . delete ( nodeId ) ; } depthFirstDelete ( nodeToDeleteId ) ; const parentNode = safeGetNode ( parentNodeId , 'deleteNode' , draftDocment ) ; const indexOfNodeToDelete = parentNode . childIds . indexOf ( nodeToDeleteId ) ; parentNode . childIds . splice ( indexOfNodeToDelete , 1 ) ; draftDocment . selectedNodeId = parentNodeId ; } ) ; applyNewDocToUndoStack ( newDoc ) ; } , param nodeId returns getChildrenVisible : ( nodeId ) : boolean => safeGetNode ( nodeId , 'getChildrenVisible' ) . childrenVisible , returns getCurrentDocAsJson : ( ) : string => { const currentDoc = getCurrentDoc ( ) ; const nodeMapAsArray = [ ] ; currentDoc . nodes . forEach ( ( nodeValue ) => { nodeMapAsArray . push ( nodeValue ) ; } ) ; const jsonCapableMap = { ... currentDoc , nodes : nodeMapAsArray , } ; return JSON . stringify ( jsonCapableMap ) ; } , returns getDocName : ( ) : string => state . docName , param nodeId returns getNodeChildIds : ( nodeId ) : Array < number > => safeGetNode ( nodeId , 'getNodeChildIds' ) . childIds . slice ( ) , param nodeId returns getNodeContents : ( nodeId ) : string => safeGetNode ( nodeId , 'getNodeContents' ) . contents , returns getRedoIsAvailable : ( ) : boolean => redoAvailable ( ) , returns getRootNodeId : ( ) : number => getCurrentDoc ( ) . rootId , returns getSelectedNodeId : ( ) : number => getCurrentDoc ( ) . selectedNodeId , returns getUndoIsAvailable : ( ) : boolean => undoAvailable ( ) , returns hasUnsavedChanges : ( ) : boolean => state . hasUnsavedChanges , param nodeId moveNodeDownInSiblingList : ( nodeId ) => { const thisNode = safeGetNode ( nodeId , 'moveNodeUpInSiblingList' ) ; const { parentId } = thisNode ; if ( parentId === undefined ) { return ; } const newDoc = produce ( getCurrentDoc ( ) , ( draftDoc ) => { const parentNode = safeGetNode ( parentId , 'moveNodeUpInSiblingList' , draftDoc ) ; const allSiblings = parentNode . childIds ; const thisNodeIndex = allSiblings . indexOf ( nodeId ) ; if ( allSiblings . length !== 1 ) { if ( thisNodeIndex === allSiblings . length - 1 ) { allSiblings . pop ( ) ; allSiblings . unshift ( nodeId ) ; } else { const lowerSiblingId = allSiblings [ thisNodeIndex + 1 ] ; allSiblings [ thisNodeIndex + 1 ] = nodeId ; allSiblings [ thisNodeIndex ] = lowerSiblingId ; } } parentNode . childIds = allSiblings ; } ) ; applyNewDocToUndoStack ( newDoc ) ; } , param nodeId moveNodeUpInSiblingList : ( nodeId ) => { const thisNode = safeGetNode ( nodeId , 'moveNodeUpInSiblingList' ) ; const { parentId } = thisNode ; if ( parentId === undefined ) { return ; } const newDoc = produce ( getCurrentDoc ( ) , ( draftDoc ) => { const parentNode = safeGetNode ( parentId , 'moveNodeUpInSiblingList' , draftDoc ) ; const allSiblings = parentNode . childIds ; const thisNodeIndex = allSiblings . indexOf ( nodeId ) ; if ( allSiblings . length !== 1 ) { if ( thisNodeIndex === 0 ) { allSiblings . shift ( ) ; allSiblings . push ( nodeId ) ; } else { const higherSiblingId = allSiblings [ thisNodeIndex - 1 ] ; allSiblings [ thisNodeIndex - 1 ] = nodeId ; allSiblings [ thisNodeIndex ] = higherSiblingId ; } } parentNode . childIds = allSiblings ; } ) ; applyNewDocToUndoStack ( newDoc ) ; } , /**
         * Redo the last editor change (do nothing if no redo available)
         */ redo : ( ) => { if ( redoAvailable ( ) ) { state . currentDocIndex += 1 ; state . hasUnsavedChanges = true ; } } , param docName param jsonEncodedDoc replaceCurrentDocFromJson : ( docName , jsonEncodedDoc ) => { const docUsingArrayForNodes = JSON . parse ( jsonEncodedDoc ) as Doc ; const nodesAsMap = new Map < number , Node > ( ) ; docUsingArrayForNodes . nodes . forEach ( ( node ) => { nodesAsMap . set ( node . id , node ) ; } ) ; const docUsingNodesAsMap = { ... docUsingArrayForNodes , nodes : nodesAsMap , } ; state = { currentDocIndex : 0 , docHistory : [ docUsingNodesAsMap ] , docName , hasUnsavedChanges : false , } ; } , /**
         * Replace the currently loaded document with an empty document
         */ replaceCurrentDocWithNewEmptyDoc : ( ) => { state = { currentDocIndex : 0 , docHistory : [ getInitialEmptyDoc ( ) ] , docName : '' , hasUnsavedChanges : true , } ; } , param nodeId param newContents replaceNodeContents : ( nodeId , newContents ) => { const newDoc = produce ( getCurrentDoc ( ) , ( draftDoc ) => { const nodeToReplace = safeGetNode ( nodeId , 'replaceNodeContents' , draftDoc ) ; nodeToReplace . contents = newContents ; } ) ; applyNewDocToUndoStack ( newDoc ) ; } , param docName setDocName : ( docName ) => { state . docName = docName ; } , param hasUnsavedChanges setHasUnsavedChanges : ( hasUnsavedChanges ) => { state . hasUnsavedChanges = hasUnsavedChanges ; } , param nodeId setSelectedNodeId : ( nodeId ) => { const newDoc = produce ( getCurrentDoc ( ) , ( draftDoc ) => { draftDoc . selectedNodeId = nodeId ; } ) ; state . docHistory [ state . currentDocIndex ] = newDoc ; } , param nodeId toggleChildrenVisibility : ( nodeId ) => { const newDoc = produce ( getCurrentDoc ( ) , ( draftDoc ) => { const node = safeGetNode ( nodeId , 'toggleChildrenVisibility' , draftDoc ) ; node . childrenVisible = ! node . childrenVisible ; } ) ; state . docHistory [ state . currentDocIndex ] = newDoc ; } , /**
         * Undo the last editor change (do nothing if no undo available)
         */ undo : ( ) => { if ( undoAvailable ( ) ) { state . currentDocIndex -= 1 ; state . hasUnsavedChanges = true ; } } , } ; } ) ( ) ;