import { ElementAction } from '../element-action' ; import { ElementContent } from './element-content' ; import { ElementProperty } from './element-property' ; import * as _ from 'lodash' ; import * as Mobx from 'mobx' ; import { Page } from '../page' ; import { Pattern } from '../pattern' ; import { AnyPatternProperty } from '../pattern-property' ; import { Project } from '../project' ; import * as Types from '@meetalva/types' ; import { UserStoreReference } from '../user-store-reference' ; import * as uuid from 'uuid' ; import { PatternLibrary } from '../pattern-library' ; import { PlaceholderPosition } from '@meetalva/components' ; export interface ElementInit { contentIds : string [ ] ; dragged : boolean ; focused : boolean ; forcedOpen : boolean ; highlighted : boolean ; id ? : string ; name ? : string ; open : boolean ; patternId : string ; placeholderHighlighted : PlaceholderPosition ; propertyValues : [ string , Types . ElementPropertyValue ] [ ] ; role ? : Types . ElementRole ; selected : boolean ; setDefaults ? : boolean ; } export interface ElementContext { project : Project ; } export class Element { public readonly model = Types . ModelName . Element ; @ Mobx . observable private readonly contentIds : string [ ] = [ ] ; @ Mobx . observable private dragged : boolean ; @ Mobx . observable private editedName ? : string ; @ Mobx . observable private shouldFocus : boolean ; @ Mobx . observable private forcedOpen : boolean ; @ Mobx . observable private shouldHighlight : boolean ; @ Mobx . observable private id : string ; @ Mobx . observable private name : string ; @ Mobx . observable private nameEditable : boolean = false ; @ Mobx . observable private open : boolean = true ; @ Mobx . observable private parent ? : Element ; @ Mobx . observable private patternId : string ; @ Mobx . observable private shouldPlaceholderHighlight : PlaceholderPosition ; private project : Project ; @ Mobx . observable private propertyValues : Map < string , Types . ElementPropertyValue > = new Map ( ) ; @ Mobx . observable private role : Types . ElementRole ; @ Mobx . observable private selected : boolean ; @ Mobx . observable public nameState : Types . EditableTitleState = Types . EditableTitleState . Neutral ; /**
    /* TODO: Remove before beta
    /* Keep backward compat
    /*/ private readonly LEGACY_elementPropertyIds : Map < string , string > = new Map ( ) ; @ Mobx . computed private get mayHighiglight ( ) : boolean { if ( ! this . page || ! this . page . getActive ( ) ) { return false ; } return true ; } @ Mobx . computed private get highlighted ( ) : boolean { if ( ! this . shouldHighlight ) { return false ; } return this . mayHighiglight ; } @ Mobx . computed private get placeholderHighlighted ( ) : PlaceholderPosition { if ( ! this . shouldPlaceholderHighlight ) { return PlaceholderPosition . None ; } return this . mayHighiglight ? this . shouldPlaceholderHighlight : PlaceholderPosition . None ; } @ Mobx . computed private get focused ( ) : boolean { if ( ! this . shouldFocus ) { return false ; } return this . mayHighiglight ; } @ Mobx . computed private get page ( ) : Page | undefined { return this . project . getPages ( ) . find ( page => page . hasElement ( this ) ) ; } @ Mobx . computed private get pattern ( ) : Pattern | undefined { return this . project . getPatternById ( this . patternId ) ; } @ Mobx . computed private get patternProperties ( ) : AnyPatternProperty [ ] { if ( ! this . pattern ) { return [ ] ; } return this . pattern . getProperties ( ) ; } @ Mobx . computed private get properties ( ) : ElementProperty [ ] { return this . patternProperties . map ( patternProperty => ElementProperty . fromPatternProperty ( patternProperty , { LEGACY_ID : this . LEGACY_elementPropertyIds . get ( patternProperty . getId ( ) ) , element : this , project : this . project } ) ) ; } public constructor ( init , context ) { this . dragged = init . dragged ; this . shouldFocus = init . focused ; this . shouldHighlight = init . highlighted ; this . shouldPlaceholderHighlight = init . placeholderHighlighted ; this . id = init . id ? init . id : uuid . v4 ( ) ; this . patternId = init . patternId ; this . open = true ; this . forcedOpen = init . forcedOpen ; this . project = context . project ; this . role = init . role || Types . ElementRole . Node ; this . selected = init . selected ; const pattern = this . project . getPatternById ( this . patternId ) ; this . contentIds = init . contentIds ; this . name = typeof init . name !== 'undefined' ? init . name : pattern ? pattern . getName ( ) : 'New Element' ; this . editedName = this . name ; this . propertyValues = new Map ( init . propertyValues ) ; this . properties . filter ( p => ! this . propertyValues . has ( p . getPatternPropertyId ( ) ) ) . forEach ( p => this . propertyValues . set ( p . getPatternPropertyId ( ) , undefined ) ) ; } public static from ( serialized , context ) { const element = new Element ( { dragged : serialized . dragged , focused : serialized . focused , highlighted : serialized . highlighted , id : serialized . id , name : serialized . name , patternId : serialized . patternId , placeholderHighlighted : serialized . placeholderHighlighted as PlaceholderPosition , contentIds : serialized . contentIds , open : serialized . open , forcedOpen : serialized . forcedOpen , propertyValues : serialized . propertyValues , role : deserializeRole ( serialized . role ) , selected : serialized . selected } , context ) ; /**
         * TODO: Remove before beta
         * Keep backward compat
         */ if ( Array . isArray ( serialized . properties ) ) { serialized . properties . forEach ( ( property ) => { element . setPropertyValue ( property . patternPropertyId , property . value ) ; element . LEGACY_elementPropertyIds . set ( property . patternPropertyId , property . id ) ; } ) ; } return element ; } public static fromPattern ( pattern , init ) { return new Element ( { contentIds : init . contents . map ( e => e . getId ( ) ) , dragged : init . dragged || false , focused : false , highlighted : false , forcedOpen : false , open : false , patternId : pattern . getId ( ) , placeholderHighlighted : PlaceholderPosition . None , propertyValues : [ ] , setDefaults : true , selected : false } , { project : init . project } ) ; } public accepts ( child ) { const childrenContent = this . getContentBySlotType ( Types . SlotType . Children ) ; if ( ! childrenContent ) { return false ; } return childrenContent . accepts ( child ) ; } public acceptsChildren ( ) { return typeof this . getContentBySlotType ( Types . SlotType . Children ) !== 'undefined' ; } @ Mobx . action public addChild ( child , slotId , index ) { child . setParent ( { parent : this , slotId , index } ) ; } @ Mobx . action public addContent ( content ) { this . contentIds . push ( content . getId ( ) ) ; } @ Mobx . action public clone ( opts ? ) { const target = opts && opts . target ? opts . target : this . project ; const withState = Boolean ( opts && opts . withState ) ; const previousPattern = this . getPattern ( ) ! ; const previousLibrary = previousPattern . getPatternLibrary ( ) ; const nextLibrary = target . getPatternLibraryByContextId ( previousLibrary . contextId ) ! ; const nextPattern = nextLibrary . getPatternByContextId ( previousPattern . getContextId ( ) ) ; const clonedActions = this . properties . filter ( prop => { const patternProperty = prop . getPatternProperty ( ) ; return ( patternProperty && patternProperty . getType ( ) === Types . PatternPropertyType . EventHandler ) ; } ) . reduce ( ( clones , prop ) => { const id = prop . getValue ( ) as string ; if ( ! id ) { return clones ; } const elementAction = this . project . getElementActionById ( id ) ; if ( ! elementAction ) { return clones ; } clones . set ( prop . getPatternPropertyId ( ) , elementAction . clone ( ) ) ; return clones ; } , new Map ( ) ) ; const clonedContents = [ ... this . contentIds ] . map ( contentId => this . project . getElementContentById ( contentId ) ) . filter ( ( content ) : content is ElementContent => typeof content !== 'undefined' ) . map ( content => content . clone ( { withState , target } ) ) ; const propertyValues = [ ... this . propertyValues . entries ( ) ] . map ( ( [ id , value ] ) => { const clonedAction = clonedActions . get ( id ) ; const previousProperty = previousPattern . getPropertyById ( id ) ; const nextProperty = nextPattern ! . getPropertyByContextId ( previousProperty ! . getContextId ( ) ) ; const nextId = nextProperty ! . getId ( ) ; if ( clonedAction ) { return [ nextId , clonedAction . getId ( ) ] as [ string , string ] ; } return [ nextId , value ] as [ string , Types . ElementPropertyValue ] ; } ) ; const clone = new Element ( { dragged : false , focused : withState ? this . focused : false , highlighted : withState ? this . shouldHighlight : false , id : uuid . v4 ( ) , contentIds : clonedContents . map ( content => content . getId ( ) ) , name : this . name , open : withState ? this . open : false , forcedOpen : withState ? this . forcedOpen : false , patternId : nextPattern ! . getId ( ) , placeholderHighlighted : withState ? this . placeholderHighlighted : PlaceholderPosition . None , propertyValues , role : this . role , selected : withState ? this . selected : false } , { project : target } ) ; const clonedReferences = this . properties . map ( prop => prop . getUserStoreReference ( ) ) . filter ( ( prop ) : prop is UserStoreReference => typeof prop !== 'undefined' ) . map ( ref => { const sourceElementProperty = this . project . getElementPropertyById ( ref . getElementPropertyId ( ) ) ; if ( ! sourceElementProperty ) { return ; } const sourcePatternProperty = sourceElementProperty . getPatternProperty ( ) ; if ( ! sourcePatternProperty ) { return ; } const clonedProperty = clone . properties . find ( prop => prop . getPatternProperty ( ) === sourcePatternProperty ) ; if ( ! clonedProperty ) { return ; } return ref . clone ( { elementPropertyId : clonedProperty . getId ( ) , open : false } ) ; } ) . filter ( ( ref ) : ref is UserStoreReference => typeof ref !== 'undefined' ) ; clonedReferences . forEach ( clonedReference => { target . getUserStore ( ) . addReference ( clonedReference ) ; } ) ; clonedContents . forEach ( clonedContent => { clonedContent . setParentElement ( clone ) ; target . addElementContent ( clonedContent ) ; } ) ; [ ... clonedActions . values ( ) ] . forEach ( clonedAction => { target . addElementAction ( clonedAction ) ; } ) ; target . addElement ( clone ) ; return clone ; } public equals ( b ) { return _ . isEqual ( this . toJSON ( ) , b . toJSON ( ) ) ; } public getAncestors ( init = [ ] ) { const parent = this . getParent ( ) ; const container = this . getContainer ( ) ; if ( ! parent || ! container ) { return init ; } init . push ( parent ) ; init . push ( container ) ; parent . getAncestors ( init ) ; return init ; } public getContainer ( ) { return this . project . getElementContentByElement ( this ) ; } public getContainerType ( ) { const container = this . getContainer ( ) ; if ( ! container ) { return ; } return container . getSlotType ( ) ; } public getContentById ( contentId ) { let result ; for ( const content of this . getContents ( ) ) { if ( content . getId ( ) === contentId ) { result = content ; break ; } for ( const element of content . getElements ( ) ) { result = element . getContentById ( contentId ) ; if ( result ) { break ; } } if ( result ) { break ; } } return result ; } public getContentBySlotId ( slotId ) { return this . getContents ( ) . find ( content => content . getSlotId ( ) === slotId ) ; } public getContentBySlotContextId ( slotContextId ) { return this . getContents ( ) . find ( content => content . getSlotContextId ( ) === slotContextId ) ; } public getContentBySlotType ( slotType ) { return this . getContents ( ) . find ( content => content . getSlotType ( ) === slotType ) ; } public getContents ( ) { return [ ... this . contentIds ] . map ( contentId => this . project . getElementContentById ( contentId ) ) . filter ( ( elementContent ) : elementContent is ElementContent => typeof elementContent !== 'undefined' ) ; } public getDescendants ( ) { return this . getContents ( ) . reduce < Element [ ] > ( ( acc , content ) => [ ... acc , ... content . getDescendants ( ) ] , [ ] ) ; } public getDragged ( ) { return this . dragged ; } public getElementById ( id ) { let result ; if ( this . id === id ) { return this ; } for ( const content of this . getContents ( ) ) { for ( const element of content . getElements ( ) ) { result = element . getElementById ( id ) ; if ( result ) { break ; } } if ( result ) { break ; } } return result ; } public getForcedOpen ( ) { return this . forcedOpen ; } public getHighlighted ( ) { return this . highlighted ; } public getFocused ( ) { return this . focused ; } public getId ( ) { return this . id ; } public getIndex ( ) { const container = this . getContainer ( ) ; if ( ! container ) { return - 1 ; } return container . getElementIndexById ( this . getId ( ) ) ; } public getEditableState ( ) { if ( ! this . focused ) { return Types . EditableTitleState . Neutral ; } return this . focused && this . nameState === Types . EditableTitleState . Editing ? Types . EditableTitleState . Editing : Types . EditableTitleState . Editable ; } public setEditableState ( state ) { this . nameState = state ; } public getName ( options ? ) { if ( ( ! options || ! options . unedited ) && this . nameState === Types . EditableTitleState . Editing ) { return this . editedName || '' ; } return this . name ; } public getEditableName ( ) { return this . editedName || '' ; } public setEditableName ( editableName ) { this . editedName = editableName ; } public getNameEditable ( ) { return this . nameEditable ; } public getOpen ( ) { return this . open ; } public getPage ( ) { return this . page ; } public getParent ( ) { const container = this . getContainer ( ) ; if ( ! container || this . role === Types . ElementRole . Root ) { return ; } const containerParentId = container . getParentElementId ( ) ; if ( ! containerParentId ) { return ; } return this . project . getElementById ( containerParentId ) ; } public hasPattern ( pattern ) { return this . patternId === pattern . getId ( ) ; } public getPattern ( ) { return this . pattern ; } @ Mobx . action public setPattern ( pattern ) { const previousPattern = this . pattern ; this . patternId = pattern . getId ( ) ; if ( previousPattern ) { pattern . getProperties ( ) . forEach ( prop => { const previousProp = previousPattern . getPropertyById ( prop . getId ( ) ) ; if ( ! previousProp ) { return ; } const previousValue = this . getPropertyValue ( previousProp . getId ( ) ) ; this . propertyValues . delete ( previousProp . getId ( ) ) ; this . propertyValues . set ( prop . getId ( ) , previousValue ) ; } ) ; } } public getPlaceholderHighlighted ( ) { return this . placeholderHighlighted ; } public getPropertyByContextId ( contextId ) { return this . properties . find ( p => { const patternProp = p . getPatternProperty ( ) ; if ( ! patternProp ) { return false ; } return patternProp . getContextId ( ) === contextId ; } ) ; } public getProperties ( ) { return this . properties ; } public getPropertyValue ( id ) { return this . propertyValues . get ( id ) ; } @ Mobx . action public setPropertyValue ( id , value ) { this . propertyValues . set ( id , value ) ; } public getRole ( ) { return this . role ; } public getSelected ( ) { if ( ! this . page || ! this . page . getActive ( ) ) { return false ; } return this . selected ; } public isAncestorOfById ( id ) { const match = this . getElementById ( id ) ; return Boolean ( match ) ; } @ Mobx . action public remove ( ) { const container = this . getContainer ( ) ; if ( container ) { container . remove ( { element : this } ) ; } } @ Mobx . action public setDragged ( dragged ) { this . dragged = dragged ; } @ Mobx . action public setForcedOpen ( forcedOpen ) { this . forcedOpen = forcedOpen ; } @ Mobx . action public setHighlighted ( highlighted ) { this . shouldHighlight = highlighted ; } @ Mobx . action public setFocused ( focused ) { this . shouldFocus = focused ; if ( ! focused ) { this . nameState = Types . EditableTitleState . Neutral ; } } @ Mobx . action public setIndex ( index ) { const container = this . getContainer ( ) ; if ( ! container ) { return ; } if ( ! this . parent ) { return ; } this . setParent ( { parent : this . parent , slotId : container . getSlotId ( ) , index } ) ; } @ Mobx . action public setName ( name ) { this . name = name ; this . editedName = name ; } @ Mobx . action public setNameState ( state ) { this . editedName = this . name ; this . nameState = state ; } @ Mobx . action public setOpen ( open ) { this . open = open ; } @ Mobx . action public setParent ( init ) { const previousContainer = this . getContainer ( ) ; if ( previousContainer ) { previousContainer . remove ( { element : this } ) ; } const container = init . parent . getContentBySlotId ( init . slotId ) ; if ( container ) { container . insert ( { element : this , at : init . index } ) ; } } @ Mobx . action public setPlaceholderHighlighted ( placeholderHighlighted ) { this . shouldPlaceholderHighlight = placeholderHighlighted ; } public setProject ( project ) { this . project = project ; } @ Mobx . action public setSelected ( selected ) { this . selected = selected ; } public toDisk ( ) { const serialized = this . toJSON ( ) ; serialized . dragged = false ; serialized . highlighted = false ; serialized . placeholderHighlighted = PlaceholderPosition . None ; serialized . selected = false ; serialized . forcedOpen = false ; serialized . focused = false ; return serialized ; } @ Mobx . action public toggleHighlighted ( ) { this . setHighlighted ( ! this . getHighlighted ( ) ) ; } @ Mobx . action public toggleOpen ( ) { this . setOpen ( ! this . getOpen ( ) && ! this . getForcedOpen ( ) ) ; this . setForcedOpen ( false ) ; } @ Mobx . action public toggleSelected ( ) { this . setSelected ( ! this . getSelected ( ) ) ; } public toJSON ( ) { return { model : this . model , contentIds : Array . from ( this . contentIds ) , dragged : this . dragged , focused : this . focused , highlighted : this . shouldHighlight , id : this . id , name : this . name , open : this . open , forcedOpen : this . forcedOpen , patternId : this . patternId , placeholderHighlighted : this . placeholderHighlighted , propertyValues : Array . from ( this . propertyValues . entries ( ) ) , role : serializeRole ( this . role ) , selected : this . selected } ; } @ Mobx . action public update ( b ) { this . name = b . name ; Array . from ( b . propertyValues . entries ( ) ) . forEach ( ( [ key , value ] ) => { this . propertyValues . set ( key , value ) ; } ) ; } public getLibraryDependencies ( ) { const patterns = [ this . getPattern ( ) , ... this . getDescendants ( ) . map ( d => d . getPattern ( ) ) ] . filter ( ( p ) : p is Pattern => typeof p !== 'undefined' ) . map ( p => ( { id : p . getId ( ) , contextId : p . getContextId ( ) , libraryId : p . getPatternLibrary ( ) . getId ( ) , type : p . getType ( ) } ) ) ; return _ . uniqBy ( patterns . map ( p => this . project . getPatternLibraryById ( p . libraryId ) ) . filter ( ( p ) : p is PatternLibrary => typeof p !== 'undefined' ) , 'id' ) ; } } function deserializeRole ( input ) { switch ( input ) { case 'root' : return Types . ElementRole . Root ; case 'node' : return Types . ElementRole . Node ; } throw new Error ( `Unknown element role: ${ input }` ) ; } function serializeRole ( input ) { switch ( input ) { case Types . ElementRole . Root : return 'root' ; case Types . ElementRole . Node : return 'node' ; } throw new Error ( `Unknown element role: ${ input }` ) ; }