import { AnyModel } from './any-model' ; import { computeDifference } from '@meetalva/util' ; import { Element , ElementContent , ElementProperty } from './element' ; import { ElementAction } from './element-action' ; import * as Message from '@meetalva/message' ; import * as Mobx from 'mobx' ; import * as _ from 'lodash' ; import { Page } from './page' ; import { PatternSearch } from './pattern-search' ; import { Pattern , PatternSlot } from './pattern' ; import { PatternLibrary } from './pattern-library' ; import { AnyPatternProperty } from './pattern-property' ; import * as Types from '@meetalva/types' ; import { UserStore } from './user-store' ; import { UserStoreEnhancer , defaultCode , defaultJavaScript } from './user-store-enhancer' ; import { UserStoreReference } from './user-store-reference' ; import * as uuid from 'uuid' ; import { PlaceholderPosition } from '@meetalva/components' ; import { builtinPatternLibrary } from './pattern-library/builtin-pattern-library' ; export interface ProjectProperties { draft : boolean ; id ? : string ; version ? : 0 | 1 | 2 | 3 ; name : string ; pages : Page [ ] ; path : string ; patternLibraries : PatternLibrary [ ] ; userStore : UserStore ; } export interface ProjectCreateInit { name : string ; draft : boolean ; path : string ; } export interface ModelResolver < T > { getModelByName ( name ? ) : T ; } export class Project { public readonly model = Types . ModelName . Project ; private readonly version : 0 | 1 | 2 | 3 = 2 ; private syncing : boolean = false ; @ Mobx . observable private draft : boolean ; @ Mobx . observable private elements : Map < string , Element > = new Map ( ) ; @ Mobx . observable private elementActions : Map < string , ElementAction > = new Map ( ) ; @ Mobx . observable private elementContents : Map < string , ElementContent > = new Map ( ) ; @ Mobx . observable private id : string ; @ Mobx . observable private name : string ; @ Mobx . observable private internalPages : Map < string , Page > = new Map ( ) ; @ Mobx . observable private pageList : string [ ] = [ ] ; @ Mobx . observable private path : string ; @ Mobx . observable private patternLibraries : Map < string , PatternLibrary > = new Map ( ) ; @ Mobx . observable private userStore : UserStore ; @ Mobx . computed private get flattenedLibraries ( ) : PatternLibrary [ ] { return [ ... this . patternLibraries . values ( ) ] ; } @ Mobx . computed private get activePage ( ) : Page { return this . pages . find ( p => p . getActive ( ) ) || this . pages [ 0 ] ; } @ Mobx . computed private get patterns ( ) : Pattern [ ] { return this . getPatternLibraries ( ) . reduce < Pattern [ ] > ( ( ps , lib ) => [ ... ps , ... lib . getPatterns ( ) ] , [ ] ) ; } @ Mobx . computed private get patternProperties ( ) : AnyPatternProperty [ ] { return this . getPatternLibraries ( ) . reduce < AnyPatternProperty [ ] > ( ( ps , lib ) => [ ... ps , ... lib . getPatternProperties ( ) ] , [ ] ) ; } @ Mobx . computed private get patternSlots ( ) : PatternSlot [ ] { return this . getPatternLibraries ( ) . reduce < PatternSlot [ ] > ( ( ps , lib ) => [ ... ps , ... lib . getSlots ( ) ] , [ ] ) ; } @ Mobx . computed private get patternSearch ( ) : PatternSearch { return new PatternSearch ( { patterns : this . patterns } ) ; } @ Mobx . computed private get selectedElements ( ) : Element [ ] { return this . getElements ( ) . filter ( e => e . getSelected ( ) ) ; } @ Mobx . computed private get highlightedElements ( ) : Element [ ] { return this . getElements ( ) . filter ( e => e . getHighlighted ( ) ) ; } @ Mobx . computed private get placeholderHighlightedElements ( ) : Element [ ] { return this . getElements ( ) . filter ( e => e . getPlaceholderHighlighted ( ) ) ; } @ Mobx . computed private get highlightedElementContents ( ) : ElementContent [ ] { return this . getElementContents ( ) . filter ( e => e . getHighlighted ( ) ) ; } @ Mobx . computed private get draggedElements ( ) : Element [ ] { return this . getElements ( ) . filter ( e => e . getDragged ( ) ) ; } @ Mobx . computed private get elementProperties ( ) : Map < string , ElementProperty > { const elementProperties = new Map ( ) ; this . elements . forEach ( element => { element . getProperties ( ) . forEach ( p => elementProperties . set ( p . getId ( ) , p ) ) ; } ) ; return elementProperties ; } @ Mobx . computed private get focusedItem ( ) : Element | Page | undefined { const element = this . getElements ( ) . find ( e => e . getFocused ( ) ) ; if ( element ) { return element ; } const page = this . getPages ( ) . find ( p => p . getFocused ( ) ) ; if ( page ) { return page ; } return ; } @ Mobx . computed private get focusedItemType ( ) : Types . ItemType { if ( this . focusedItem instanceof Page ) { return Types . ItemType . Page ; } if ( this . focusedItem instanceof Element ) { return Types . ItemType . Element ; } return Types . ItemType . None ; } @ Mobx . computed private get pages ( ) : Page [ ] { return [ ... this . internalPages . values ( ) ] . filter ( p => this . pageList . includes ( p . getId ( ) ) ) . sort ( ( a , b ) => this . pageList . indexOf ( a . getId ( ) ) - this . pageList . indexOf ( b . getId ( ) ) ) ; } public constructor ( init ) { this . name = init . name ; this . id = init . id ? init . id : uuid . v4 ( ) ; this . path = init . path ; this . userStore = init . userStore ; this . draft = init . draft ; if ( typeof init . version !== 'undefined' ) { this . version = init . version ; } init . pages . forEach ( page => { this . addPage ( page ) ; } ) ; init . patternLibraries . forEach ( patternLibrary => { this . addPatternLibrary ( patternLibrary ) ; } ) ; } @ Mobx . action public static create ( init ) { const userStore = new UserStore ( { id : uuid . v4 ( ) , enhancer : new UserStoreEnhancer ( { id : uuid . v4 ( ) , typeScript : defaultCode , javaScript : defaultJavaScript } ) } ) ; const project = new Project ( { name : init . name , pages : [ ] , path : init . path , patternLibraries : [ builtinPatternLibrary ] , userStore , draft : init . draft } ) ; const page = Page . create ( { active : true , id : uuid . v4 ( ) , name : 'Page 1' } , { project } ) ; project . addPage ( page ) ; userStore . getPageProperty ( ) . setProject ( project ) ; return project ; } public static from ( serialized ) { const userStore = UserStore . from ( serialized . userStore ) ; const project = new Project ( { draft : Boolean ( serialized . draft ) , id : serialized . id , name : serialized . name , path : serialized . path , pages : [ ] , patternLibraries : [ ] , userStore } ) ; serialized . patternLibraries . forEach ( p => project . addPatternLibrary ( PatternLibrary . from ( p ) ) ) ; const pageList = serialized . pageList || serialized . pages . map ( p => p . id ) ; serialized . pages . sort ( ( a , b ) => pageList . indexOf ( a . id ) - pageList . indexOf ( b . id ) ) . forEach ( page => project . addPage ( Page . from ( page , { project } ) ) ) ; serialized . elements . forEach ( element => project . addElement ( Element . from ( element , { project } ) ) ) ; serialized . elementContents . forEach ( elementContent => project . addElementContent ( ElementContent . from ( elementContent , { project } ) ) ) ; serialized . elementActions . forEach ( elementAction => { project . addElementAction ( ElementAction . from ( elementAction , { userStore } ) ) ; } ) ; userStore . getPageProperty ( ) . setProject ( project ) ; return project ; } public static toResult ( serialized ) { try { return { status : Types . ProjectStatus . Ok , result : Project . from ( serialized ) } ; } catch ( error ) { return { status : Types . ProjectStatus . Error , error } ; } } public static fromResult ( result ) { if ( result . state === Types . PersistenceState . Error ) { return { status : Types . ProjectStatus . Error , error : result . error } ; } return Project . toResult ( result . contents ) ; } public static equals ( a , b ) ; public static equals ( a , b ) { const toData = ( input ) => input instanceof Project ? input . toDisk ( ) : input ; return _ . isEqual ( toData ( a ) , toData ( b ) ) ; } @ Mobx . action public addElement ( element ) { element . setProject ( this ) ; this . elements . set ( element . getId ( ) , element ) ; } @ Mobx . action public addElementAction ( action ) { this . elementActions . set ( action . getId ( ) , action ) ; } @ Mobx . action public addElementContent ( elementContent ) { elementContent . setProject ( this ) ; this . elementContents . set ( elementContent . getId ( ) , elementContent ) ; } @ Mobx . action public addPatternLibrary ( lib ) { this . patternLibraries . set ( lib . getId ( ) , lib ) ; } @ Mobx . action public addPage ( page ) { this . internalPages . set ( page . getId ( ) , page ) ; this . pageList . push ( page . getId ( ) ) ; page . setProject ( this ) ; } public getSelectedPage ( ) { return this . pages . find ( p => p . getId ( ) === this . getUserStore ( ) . getPageProperty ( ) . getValue ( ) ) ; } public getBuiltinPatternLibrary ( ) { return [ ... this . patternLibraries . values ( ) ] . find ( lib => lib . builtin ) ! ; } public getElementActionById ( id ) { return this . elementActions . get ( id ) ; } public getElementActions ( ) { return [ ... this . elementActions . values ( ) ] ; } public getElementById ( id ) { return this . elements . get ( id ) ; } public getElementContentById ( id ) { return this . elementContents . get ( id ) ; } public getElementContentByElement ( element ) { return this . getElementContents ( ) . find ( content => content . getElements ( ) . some ( e => e . getId ( ) === element . getId ( ) ) ) ; } public getElementContents ( ) { return [ ... this . elementContents . values ( ) ] ; } public getElements ( ) { return [ ... this . elements . values ( ) ] ; } public getElementsByPattern ( pattern ) { return this . getElements ( ) . filter ( e => e . hasPattern ( pattern ) ) ; } public getHighlightedElements ( ) { return this . highlightedElements ; } public getHighlightedElementContents ( ) { return this . highlightedElementContents ; } public getItem ( id , type ) { switch ( type ) { case Types . ItemType . Page : case 'page' : return this . getPageById ( id ) ; case Types . ItemType . Element : case 'element' : return this . getElementById ( id ) ; default : return ; } } public getFocusedItem ( ) { return this . focusedItem ; } public getFocusedItemType ( ) { return this . focusedItemType ; } public getId ( ) { return this . path ? Buffer . from ( [ this . id , this . path ] . join ( ':' ) , 'utf-8' ) . toString ( 'base64' ) : this . id ; } public getName ( ) { return this . name ; } public hasFriendlyName ( ) { return this . name !== this . id ; } public getNextPage ( ) { const page = this . pages . find ( p => p . getActive ( ) ) ; if ( ! page ) { return ; } const nextIndex = page . getIndex ( ) + 1 ; if ( typeof nextIndex !== 'number' || Number . isNaN ( nextIndex ) ) { return ; } if ( nextIndex < 0 || nextIndex > this . pages . length - 1 ) { return ; } return this . pages [ nextIndex ] ; } public getObject ( constructorName , id ) { if ( constructorName === 'Element' ) { return this . getElementById ( id ) ; } if ( constructorName === 'ElementAction' ) { return this . getElementActionById ( id ) ; } if ( constructorName === 'ElementContent' ) { return this . getElementContentById ( id ) ; } if ( constructorName === 'Page' ) { return this . getPageById ( id ) ; } if ( constructorName === 'Pattern' ) { return this . getPatternById ( id ) ; } if ( constructorName === 'PatternLibrary' ) { return this . getPatternLibraryById ( id ) ; } if ( constructorName === 'Project' ) { return this ; } if ( constructorName === 'UserStore' ) { return this . getUserStore ( ) ; } if ( constructorName === 'UserStoreAction' ) { return this . getUserStore ( ) . getActionById ( id ) ; } if ( constructorName === 'UserStoreProperty' ) { return this . getUserStore ( ) . getPropertyById ( id ) ; } if ( constructorName === 'UserStoreEnhancer' ) { return this . getUserStore ( ) . getEnhancer ( ) ; } if ( constructorName === 'UserStoreReference' ) { return this . getUserStore ( ) . getReferenceById ( id ) ; } return ; } public getPageById ( id ) { return this . pages . find ( page => page . getId ( ) === id ) ; } public getPages ( ) { return this . pages ; } public getPath ( ) { return this . path ; } public getPatternById ( id ) { return this . patterns . find ( p => p . getId ( ) === id ) ; } public getPatterns ( ) { return this . patterns ; } public getPatternProperties ( ) { return this . patternProperties ; } public getPatternLibraries ( ) { return this . flattenedLibraries ; } public getPatternLibraryById ( id ) { return this . patternLibraries . get ( id ) ; } public getPatternLibraryByName ( name ) { return this . flattenedLibraries . find ( p => p . getName ( ) === name ) ; } public getPatternLibraryByContextId ( contextid ) { return this . flattenedLibraries . find ( p => p . contextId === contextid ) ; } public getPatternLibraryByPackageName ( name ) { return this . flattenedLibraries . find ( p => p . getPackageName ( ) === name ) ; } public getPatternPropertyById ( id ) { return Array . from ( this . patternLibraries . values ( ) ) . reduce < AnyPatternProperty | undefined > ( ( result , lib ) => { if ( result ) { return result ; } return lib . getPatternPropertyById ( id ) ; } , undefined ) ; } public getPatternSearch ( ) { return this . patternSearch ; } public getPatternSlotById ( id ) { return this . patternSlots . find ( p => p . getId ( ) === id ) ; } public getPreviousPage ( ) { const page = this . pages . find ( p => p . getActive ( ) ) ; if ( ! page ) { return ; } const previousIndex = page . getIndex ( ) - 1 ; if ( typeof previousIndex !== 'number' || Number . isNaN ( previousIndex ) ) { return ; } if ( previousIndex < 0 || previousIndex > this . pages . length - 1 ) { return ; } return this . pages [ previousIndex ] ; } public getSelectedElement ( ) { const selected = this . selectedElements [ 0 ] ; return selected ? selected : this . activePage ? this . activePage . getRoot ( ) : undefined ; } public getUserStore ( ) { return this . userStore ; } public getDraft ( ) { return this . draft ; } public setDraft ( draft ) { this . draft = draft ; } public setId ( raw ) { const [ id ] = Buffer . from ( raw , 'base64' ) . toString ( 'utf-8' ) . split ( ':' ) ; this . id = id ; } public getElementPropertyById ( id ) { return this . elementProperties . get ( id ) ; } public getElementPropertyByReference ( reference ) { return this . getElementPropertyById ( reference . getElementPropertyId ( ) ) ; } @ Mobx . action public importElement ( element ) { element . getDescendants ( ) . forEach ( descendant => this . importElement ( descendant ) ) ; element . getContents ( ) . forEach ( content => this . addElementContent ( content ) ) ; this . addElement ( element ) ; } @ Mobx . action public importPage ( page ) { const rootElement = page . getRoot ( ) ; if ( rootElement ) { this . importElement ( rootElement ) ; } this . addPage ( page ) ; } @ Mobx . action public movePageAfter ( opts ) { this . movePage ( { ... opts , offset : 1 } ) ; } @ Mobx . action public movePageBefore ( opts ) { this . movePage ( { ... opts , offset : 0 } ) ; } @ Mobx . action public movePage ( opts ) { const targetAvailable = this . pages . some ( p => p . getId ( ) === opts . targetPage . getId ( ) ) ; const pageAvailable = this . pages . some ( p => p . getId ( ) === opts . page . getId ( ) ) ; if ( ! targetAvailable || ! pageAvailable ) { return ; } const index = this . pages . findIndex ( p => opts . page . getId ( ) === p . getId ( ) ) ; this . pageList . splice ( index , 1 ) ; const targetIndex = this . pages . findIndex ( p => opts . targetPage . getId ( ) === p . getId ( ) ) ; this . pageList . splice ( targetIndex + opts . offset , 0 , opts . page . getId ( ) ) ; } @ Mobx . action public removeElement ( element ) { this . elements . delete ( element . getId ( ) ) ; element . getContents ( ) . forEach ( content => { this . removeElementContent ( content ) ; } ) ; } @ Mobx . action public removeElementAction ( elementAction ) { this . elementActions . delete ( elementAction . getId ( ) ) ; } @ Mobx . action public removeElementContent ( elementContent ) { elementContent . getElements ( ) . forEach ( element => { this . removeElement ( element ) ; } ) ; this . elementContents . delete ( elementContent . getId ( ) ) ; } @ Mobx . action public removePage ( page ) { this . internalPages . delete ( page . getId ( ) ) ; const index = this . pageList . indexOf ( page . getId ( ) ) ; if ( index === - 1 ) { return ; } this . pageList . splice ( index , 1 ) ; } @ Mobx . action public removePatternLibrary ( patternLibrary ) { this . patternLibraries . delete ( patternLibrary . getId ( ) ) ; } @ Mobx . action public setActivePage ( page ) { this . unsetActivePage ( ) ; this . unsetSelectedElement ( ) ; page . setActive ( true ) ; this . setFocusedItem ( page ) ; } @ Mobx . action public setActivePageById ( id ) { const page = this . getPageById ( id ) ; if ( ! page ) { return ; } return this . setActivePage ( page ) ; } @ Mobx . action public setActivePageByIndex ( index ) { if ( index < 0 || index > this . pages . length - 1 ) { return ; } const page = this . pages [ index ] ; if ( ! page ) { return ; } this . setActivePage ( page ) ; } @ Mobx . action public setFocusedItem ( payload ) { const previousFocusItem = this . getFocusedItem ( ) ; if ( previousFocusItem ) { previousFocusItem . setFocused ( false ) ; } if ( payload ) { payload . setFocused ( true ) ; } } @ Mobx . action public reArrangePagesIndex ( position , page ) { if ( position > this . pages . length ) { return false ; } this . pageList . splice ( page . getIndex ( ) , 1 ) ; this . pageList . splice ( position , 0 , page . getId ( ) ) ; return true ; } @ Mobx . action public setName ( name ) { this . name = name ; } @ Mobx . action public setPath ( path ) { this . path = path ; } @ Mobx . action public setHighlightedElement ( element ) { this . unsetHighlightedElement ( ) ; element . setHighlighted ( true ) ; } @ Mobx . action public setSelectedElement ( element ) { this . unsetSelectedElement ( ) ; element . setSelected ( true ) ; } public toDisk ( ) { const data = this . toJSON ( ) ; data . elements = this . getElements ( ) . map ( e => e . toDisk ( ) ) ; return data ; } public toJSON ( ) { return { draft : this . draft , model : this . model , elements : this . getElements ( ) . map ( e => e . toJSON ( ) ) , elementActions : this . getElementActions ( ) . map ( e => e . toJSON ( ) ) , elementContents : this . getElementContents ( ) . map ( e => e . toJSON ( ) ) , id : this . id , name : this . name , pages : this . pages . filter ( Boolean ) . map ( p => p . toJSON ( ) ) , pageList : Array . from ( this . pageList ) , path : this . path , patternLibraries : this . getPatternLibraries ( ) . map ( p => p . toJSON ( ) ) , userStore : this . userStore . toJSON ( ) , version : this . version } ; } public getByPath ( path ) { function getByPath ( fragments , origin ) { if ( ! origin ) { return ; } const fragment = fragments . shift ( ) ; if ( ! fragment ) { return origin ; } if ( Mobx . isObservableMap ( origin ) ) { return getByPath ( fragments , origin . get ( fragment ) ) ; } return getByPath ( fragments , origin [ fragment ] ) ; } return getByPath ( path . split ( '/' ) , this ) ; } public sync ( sender , tree ) { if ( this . syncing ) { return ; } this . syncing = true ; sender . match < Message . ProjectUpdate > ( Message . MessageType . ProjectUpdate , m => { this . onProjectUpdate ( m , tree ) ; } ) ; } public apply ( change , tree ) { const target = this . getByPath ( change . path ) ; if ( ! target ) { return ; } if ( Mobx . isObservableMap ( target ) ) { const c = change . change as Mobx . IMapDidChange ; switch ( c . type ) { case 'add' : case 'update' : { const ValueModel = typeof c . newValue === 'object' && c . newValue !== null ? tree . getModelByName ( c . newValue . model as Types . ModelName ) : undefined ; const value = ValueModel ? ( ValueModel as any ) . from ( c . newValue , { project : this } ) : c . newValue ; target . set ( c . name , value ) ; break ; } case 'delete' : target . delete ( c . name ) ; } return ; } if ( Mobx . isObservableObject ( target ) ) { const c = change . change as Mobx . IObjectDidChange ; switch ( c . type ) { case 'add' : case 'update' : { const ValueModel = typeof c . newValue === 'object' ? tree . getModelByName ( c . newValue . model as Types . ModelName ) : undefined ; const value = ValueModel ? ( ValueModel as any ) . from ( c . newValue , { project : this } ) : c . newValue ; ( target as any ) [ c . name ] = value ; break ; } case 'remove' : { delete ( target as any ) [ c . name ] ; } } return ; } if ( Mobx . isObservableArray ( target ) && change . change . type === 'splice' ) { const c = change . change as Mobx . IArraySplice ; target . splice ( c . index , c . removedCount , ... c . added ) ; return ; } } private onProjectUpdate ( m , tree ) { const { projectId } = m . payload ; if ( projectId !== this . getId ( ) ) { return ; } this . apply ( m . payload , tree ) ; } @ Mobx . action public unsetActivePage ( ) { this . pages . forEach ( page => page . setActive ( false ) ) ; } @ Mobx . action public unsetSelectedElement ( options ? ) { this . selectedElements . filter ( element => ! options || options . ignore . getId ( ) !== element . getId ( ) ) . forEach ( selectedElement => selectedElement . setSelected ( false ) ) ; } @ Mobx . action public unsetDraggedElements ( options ? ) { this . draggedElements . filter ( element => ! options || options . ignore . getId ( ) !== element . getId ( ) ) . forEach ( element => element . setDragged ( false ) ) ; } @ Mobx . action public unsetHighlightedElement ( options ? ) { this . highlightedElements . filter ( element => ! options || options . ignore . getId ( ) !== element . getId ( ) ) . forEach ( selectedElement => selectedElement . setHighlighted ( false ) ) ; } @ Mobx . action public unsetHighlightedElementContent ( options ? ) { this . highlightedElementContents . filter ( elementContent => ! options || options . ignore . getId ( ) !== elementContent . getId ( ) ) . forEach ( elementContent => elementContent . setHighlighted ( false ) ) ; } @ Mobx . action public unsetPlaceholderHighlightedElement ( options ? ) { this . placeholderHighlightedElements . filter ( element => ! options || options . ignore . getId ( ) !== element . getId ( ) ) . forEach ( element => element . setPlaceholderHighlighted ( PlaceholderPosition . None ) ) ; } public update ( b ) { this . name = b . name ; this . path = b . path ; this . userStore . update ( b . userStore ) ; const elementChanges = computeDifference ( { before : this . getElements ( ) , after : b . getElements ( ) } ) ; elementChanges . removed . forEach ( change => this . removeElement ( change . before ) ) ; elementChanges . added . forEach ( change => this . addElement ( change . after ) ) ; elementChanges . changed . forEach ( change => change . before . update ( change . after ) ) ; const elementActionChanges = computeDifference ( { before : this . getElementActions ( ) , after : b . getElementActions ( ) } ) ; elementActionChanges . removed . forEach ( change => this . removeElementAction ( change . before ) ) ; elementActionChanges . added . forEach ( change => this . addElementAction ( change . after ) ) ; elementActionChanges . changed . forEach ( change => change . before . update ( change . after ) ) ; const elementContentChanges = computeDifference ( { before : this . getElementContents ( ) , after : b . getElementContents ( ) } ) ; elementContentChanges . removed . forEach ( change => this . removeElementContent ( change . before ) ) ; elementContentChanges . added . forEach ( change => this . addElementContent ( change . after ) ) ; elementContentChanges . changed . forEach ( change => change . before . update ( change . after ) ) ; const patternLibraryChanges = computeDifference ( { before : this . getPatternLibraries ( ) , after : b . getPatternLibraries ( ) } ) ; patternLibraryChanges . removed . forEach ( change => this . removePatternLibrary ( change . before ) ) ; patternLibraryChanges . added . forEach ( change => this . addPatternLibrary ( change . after ) ) ; patternLibraryChanges . changed . forEach ( change => change . before . update ( change . after ) ) ; const pageChanges = computeDifference ( { before : this . getPages ( ) , after : b . getPages ( ) } ) ; pageChanges . removed . forEach ( change => this . removePage ( change . before ) ) ; pageChanges . added . forEach ( change => this . addPage ( change . after ) ) ; pageChanges . changed . forEach ( change => change . before . update ( change . after ) ) ; this . pageList = b . pageList ; } public clone ( ) { return Project . from ( this . toJSON ( ) ) ; } }