Reference Source

js/base/layout/layout_simple.js

// NPM IMPORTS
// import assert from 'assert'
import _ from 'lodash'
import vdom_as_json from 'vdom-as-json'
const vdom_from_json = vdom_as_json.fromJson
import VNode from 'virtual-dom/vnode/vnode'

// COMMON IMPORTS
import T from 'devapt-core-common/dist/js/utils/types'

// BROWSER IMPORTS
import Layout from './layout'


const context = 'browser/base/layout/layout_simple'



/**
 * @file LayoutSimple class.
 * 
 * @author Luc BORIES
 * 
 * @license Apache-2.0
 */
export default class LayoutSimple extends Layout
{
	/**
	 * Creates an instance of LayoutSimple:a simple layout with a content element which contains a menubar, a breadcrumbs, a separator and a main view.
	 * @extends Layout
	 * 
	 * A Layout configuration is a simple object with this common attributes:
	 * 		- name:string - command unique name.
	 * 		- type:string - type of commnand from command factory known types list (example: display).
	 * 
	 * 	API
	 * 		->get_name():string - get command name (INHERITED).
	 * 		->get_type():string - get command type (INHERITED).
	 * 		->get_settings():object - get instance type (INHERITED).
	 * 		->is_valid():boolean - check if instance is valid (settings...) (INHERITED).
	 * 
	 * 		->switch(arg_previous_component, arg_target_component_name, arg_is_menubar, arg_is_breadcrumbs):Promise({...}) - Switch previously displayed component with target component.
	 * 		->render_page_content():Promise - render page content components.
	 * 		->process_rendering_result(arg_rendering_result):array - Process a RenderingResult instance.
	 * 
	 * 		->get_content_element():Element - Get page content element.
	 * 		->init_content_element():Element - Init page content element.
	 * 		->clear_page_content(do_not_hide_components):Promise - clear page content components.
	 * 
	 * 		->render_page_content_vnode(arg_vnode, arg_rendering_result, arg_credentials):array - Render page content with a global VNode.
	 * 
	 * @param {object}           arg_runtime     - runtime.
	 * @param {object}           arg_settings    - instance settings.
	 * @param {string|undefined} arg_log_context - context of traces of this instance (optional).
	 * 
	 * @returns {nothing}
	 */
	constructor(arg_runtime, arg_settings, arg_log_context)
	{
		const log_context = arg_log_context ? arg_log_context : context
		super(arg_runtime, arg_settings, log_context)
		
		this.is_simple_layout = true

		this._content_element = undefined
		this._content_id = 'content'

		// this.enable_trace()
		this.disable_trace()
		this.update_trace_enabled()
	}



	/**
	 * Switch previously displayed component with target component.
	 * 
	 * @param {Component} arg_previous_component    - previous component instance.
	 * @param {string}    arg_target_component_name - target component name.
	 * @param {boolean}   arg_is_menubar            - target component is a menubar?
	 * @param {boolean}   arg_is_breadcrumbs        - target component is a breadcrumbs?
	 * 
	 * @returns {Promise} - Promise of { switch:false, component:arg_previous_component }
	 */
	switch(arg_previous_component, arg_target_component_name, arg_is_menubar, arg_is_breadcrumbs)
	{
		this.enter_group('switch: for ' + (arg_target_component_name ? arg_target_component_name : 'bad target name') )

		if ( ! T.isString(arg_target_component_name) )
		{
			this.leave_group('switch:error bad target name for ' + (arg_target_component_name ? arg_target_component_name.toString().splice(0, 20) : '') )
			return Promise.reject(context + ':switch:bad target name string')
		}

		if (arg_previous_component && arg_previous_component.get_name() == arg_target_component_name )
		{
			this.debug('switch:previous component exists and has same name as target component name=' + arg_target_component_name)

			arg_previous_component.show()
			arg_previous_component.update()
			arg_previous_component.is_menubar     = arg_is_menubar
			arg_previous_component.is_breadcrumbs = arg_is_breadcrumbs

			
			this.leave_group('switch:async for ' + (arg_target_component_name ? arg_target_component_name : 'bad target name') )
			return Promise.resolve( { switch:false, component:arg_previous_component } )
		}

		let previous_parent = undefined
		if (arg_previous_component && arg_previous_component.get_name() != arg_target_component_name )
		{
			this.debug('switch:previous component [' + arg_previous_component.get_name() + '] exists and has a different name as target component name=' + arg_target_component_name)
			arg_previous_component.hide()
			previous_parent = arg_previous_component.get_dom_parent()
		}

		if ( ! this._ui.has(arg_target_component_name) )
		{
			this.debug('switch:target component doesn t exists for name=' +arg_target_component_name)

			// CREATE ELEMENT
			// const dom_id = component.get_dom_id()
			// let element = document.getElementById(dom_id)
			// if (! element)
			// {
			// 	this.debug('create:component dom element is created as a div:' + dom_id)
			// 	element = document.createElement('div')
			// 	element.setAttribute('id', dom_id)
			// 	const content_element = this._runtime.ui()._ui_layout.get_content_element()
			// 	if (! content_element)
			// 	{
			// 		const msg = 'create:no content element'
			// 		this.error(msg)
			// 		this.leave_group(msg)
			// 		return { component:undefined, promise:Promise.reject(context + msg) }
			// 	}
			// 	content_element.appendChild(element)
			// }


			const target_component_promise = this._ui.create(arg_target_component_name)
			.then(
				(component)=>{
					this.debug('switch:target component is created for ' + arg_target_component_name)

					// console.log(context + ':render_page_content:%s:with view=', component.get_name(), component)

					this._ui.page.content = component
					this.is_menubar     = false
					this.is_breadcrumbs = false

					return component.render().then(()=>{return component})
				}
			)
			.then(
				(component)=>{
					this.debug('switch:target component is rendered for ' + arg_target_component_name)

					component.set_dom_parent_of(arg_previous_component)
					component.show()
					component.update()
					component.is_menubar     = arg_is_menubar
					component.is_breadcrumbs = arg_is_breadcrumbs
					
					return component
				}
			)
			.then(
				(component)=>{
					this.debug('switch:target component is shown and updated for ' + arg_target_component_name)
					
					return { switch:true, component:component }
				}
			)
			.catch(
				(reason)=>{
					this.error('switch:an error occures:' + reason)
				}
			)

			this.leave_group('switch:async for ' + (arg_target_component_name ? arg_target_component_name : 'bad target name') )
			return target_component_promise
		}

		this.debug('switch:target component exists for ' + arg_target_component_name)
		const component = this._ui.get(arg_target_component_name)
		if (! component.has_dom_parent(previous_parent) )
		{
			this.debug('switch:target component [' + arg_target_component_name + ']:set dom parent')
			component.set_dom_parent(previous_parent)
		}
		component.show()
		component.update()
		component.is_menubar     = arg_is_menubar
		component.is_breadcrumbs = arg_is_breadcrumbs

		return Promise.resolve( { switch:true, component:component } )
	}



	/**
	 * Render page content with components names.
	 * 
	 * @param {object} arg_components - components to display: { view:'', menubar:'', breadcrumbs:'' }
	 * 
	 * @returns {Promise}
	 */
	render_page_content(arg_components)
	{
		this.enter_group('render_page_content')

		if (! T.isObject(arg_components) )
		{
			return Promise.reject(context + ':render_page_content:bad components object')
		}

		const separator_name   = 'separator'
		const middleware       = T.isString(arg_components.middleware)       ? arg_components.middleware : undefined
		const route            = T.isString(arg_components.route)            ? arg_components.route : undefined
		let view_name          = T.isString(arg_components.view)             ? arg_components.view : undefined
		let menubar_name       = T.isString(arg_components.menubar)          ? arg_components.menubar : undefined
		let breadcrumbs_name   = T.isString(arg_components.breadcrumbs)      ? arg_components.breadcrumbs : undefined
		const rendering_result = T.isObject(arg_components.rendering_result) ? arg_components.rendering_result : undefined

		this.debug('render_page_content:middleware:',       middleware)
		this.debug('render_page_content:route:',            route)
		this.debug('render_page_content:view_name:',        view_name)
		this.debug('render_page_content:menubar_name:',     menubar_name)
		this.debug('render_page_content:breadcrumbs_name:', breadcrumbs_name)
		this.debug('render_page_content:rendering_result:', rendering_result && rendering_result.settings ? rendering_result.settings.name : '')

		let promise = Promise.resolve()
		

		// GET CONTENT STATE ITEMS
		const body_contents_path = ['views', 'content', 'state', 'body_contents']
		let content_state_items = this.get_state_store().get_state().getIn(body_contents_path)

		content_state_items = content_state_items ? content_state_items.toJS() : []
		this.debug('render_page_content:content_state_items', content_state_items)

		if (content_state_items.length > 0)
		{
			if ( T.isString(menubar_name) && content_state_items[0] != menubar_name)
			{
				content_state_items[0] = menubar_name
			}
		}
		if (content_state_items.length > 1)
		{
			if ( T.isString(separator_name) && content_state_items[1] != separator_name)
			{
				content_state_items[1] = separator_name
			}
		}
		if (content_state_items.length > 2)
		{
			if ( T.isString(view_name) && content_state_items[2] != view_name)
			{
				content_state_items[2] = view_name
			}
		}

		// GET CONTENT ELEMENT
		const content_element = this.get_content_element()
		if (! content_element)
		{
			this.error('render_page_content:no content element')
			this.leave_group('render_page_content:error:no content element')
			return Promise.reject('render_page_content:no content element')
		}
		
		
		// INIT CONTENT ELEMENTS
		if (! content_state_items)
		{
			this.error('render_page_content:error:no content state item')
			this.leave_group('render_page_content:error:no content state item')
			return Promise.reject('render_page_content:no content state items')
		}
		_.forEach(content_state_items,
			(item)=>{
				let element = document.getElementById(item)
				if (element)
				{
					this.debug('render_page_content:content item exists:' + item)
					return
				}
				this.debug('render_page_content:content item is created as a div:' + item)
				element = document.createElement('div')
				element.setAttribute('id', item)
				content_element.appendChild(element)
			}
		)

		// REQUEST MIDDLEWARE
		if(middleware)
		{
			this.debug('render_page_content:request middleware:async')

			promise = promise
			.then(
				()=>this._ui.request_middleware(middleware, route)
			)
			.then(
				(rendering_result)=>{
					this.debug('render_page_content:request middleware:rendering result')

					const components = this.process_rendering_result(rendering_result)
					return components
				}
			)
		}

		// PROCESS RENDERING RESULT
		if (rendering_result)
		{
			this.debug('render_page_content:process rendering result:async')

			promise = promise
			.then(
				()=>{
					const components = this.process_rendering_result(rendering_result)
					return components
				}
			)
		}
		
		// PROCESS COMPONENTS
		promise = promise.then(
			(components)=>{
				this.debug('render_page_content:process components:async')

				// SEARCH VIEW NAME, MENUBAR NAME AND BREADCRUMBS NAME INSIDE COMPONENTS LIST
				// console.log(context + 'render_page_content:process components:components=', components)
				components = T.isArray(components) ? components : []
				_.forEach(components,
					(component)=>{
						const classes = component.get_dom_element().className

						this.debug('render_page_content:search components:component=' + component.get_name())
						this.debug('render_page_content:search components:component.is_menubar=' + component.is_menubar)
						this.debug('render_page_content:search components:component.is_breadcrumbs=' + component.is_breadcrumbs)
						this.debug('render_page_content:search components:component.is_view=' + component.is_view)
						this.debug('render_page_content:search components:component.classes=' + classes)
						this.debug('render_page_content:search components:component.has menubar class=' + (classes.search('devapt-kindof-menubar') > -1))
						this.debug('render_page_content:search components:component.has breadcrumbs class=' + (classes.search('devapt-kindof-breadcrumbs') > -1))

						if (! T.isString(menubar_name) && (component.is_menubar || classes.search('devapt-kindof-menubar') > -1) )
						{
							menubar_name = component.get_name()
							return
						}
						
						if (! T.isString(breadcrumbs_name) && (component.is_breadcrumbs || classes.search('devapt-kindof-breadcrumbs') > -1) )
						{
							breadcrumbs_name = component.get_name()
							return
						}

						if (! T.isString(view_name) && component.get_name() != separator_name)
						{
							view_name = component.get_name()
							return
						}
					}
				)

				this.debug('render_page_content:view_name:',        view_name)
				this.debug('render_page_content:menubar_name:',     menubar_name)
				this.debug('render_page_content:breadcrumbs_name:', breadcrumbs_name)

				let promises = []

				if( T.isString(view_name) )
				{
					const content_promise = this.switch(this._ui.page.content, view_name, false, false)
					.then(
						(component_switch)=>{
							this._ui.page.content = component_switch.component
							return component_switch
						}
					)
					promises.push(content_promise)
				}

				if( T.isString(menubar_name) )
				{
					const menubar_promise = this.switch(this._ui.page.menubar, menubar_name, true, false)
					.then(
						(component_switch)=>{
							this._ui.page.menubar = component_switch.component
							return component_switch
						}
					)
					promises.push(menubar_promise)
				}

				if( T.isString(breadcrumbs_name) )
				{
					const  breadcrumbs_promise = this.switch(this._ui.page. breadcrumbs, breadcrumbs_name, false, true)
					.then(
						(component_switch)=>{
							this._ui.page. breadcrumbs = component_switch.component
							return component_switch
						}
					)
					promises.push( breadcrumbs_promise)
				}

				return Promise.all(promises)
			}
		)
		.then(
			(components_switchs)=>{
				
				// UPDATE PAGE CONTENT BODY STATE
				const components_names = []
				_.forEach(components_switchs,
					(component_switch)=>{
						const component = component_switch.component
						// const should_switch = component_switch.switch
						const name = component.get_name()

						components_names.push(name)
					}
				)

				const action = { type:'SET_PAGE_CONTENT', resource:'content', content_body:components_names }
				this.get_state_store().dispatch(action)

				// EXECUTE BOOTSTRAP HANDLERS
				window.devapt().content_rendered()
			}
		)
		.catch(
			(reason)=>{
				return Promise.reject(context + ':render_page_content:' + reason)
			}
		)

		this.leave_group('render_page_content:async')
		return promise
	}



	/**
	 * Process a RenderingResult instance.
	 * 
	 * @param {RenderingResult} arg_rendering_result - rendering result.
	 * 
	 * @param {array} - rendered components.
	 */
	process_rendering_result(arg_rendering_result)
	{
		this.enter_group('process_rendering_result')

		// DEBUG
		// console.log(context + ':process_rendering_result:arg_rendering_result', arg_rendering_result)
		// console.log(context + ':process_rendering_result:arg_rendering_result.vtrees', arg_rendering_result.vtrees)

		const vnodes = arg_rendering_result.vtrees
		this.debug('process_rendering_result:rendering_result has vnodes count=' + _.size(vnodes))

		let components = []

		_.forEach(vnodes,
			(vnode_json, id)=>{
				this.debug('process_rendering_result:id:', id)

				const credentials = undefined

				// DESERIALIZE VNODE JSON
				let vnode = vdom_from_json(vnode_json)
				vnode.prototype = VNode.prototype
				this.debug('process_rendering_result:vnode:', vnode)
				
				// PROCESS CONTENT VNODE
				if (id == this._content_id)
				{
					const content_components = this.render_page_content_vnode(vnode, arg_rendering_result, credentials)
					components = components.concat(content_components)
					return
				}

				// PROCESS OTHER COMPONENT VNODE
				const component = this._ui.get(id)
				if (component && component.is_component)
				{
					this.debug('process_rendering_result:component found for id ' + id)

					component.process_rendering_vnode(vnode)
				} else {
					this.warn('process_rendering_result:component not found for id ' + id)
					components.push(component)
				}
			}
		)

		this.leave_group('process_rendering_result:components.length=' + components.length)
		return components
	}



	/**
	 * Get page content element.
	 * 
	 * @returns {Element}
	 */
	get_content_element()
	{
		if (! this._content_element)
		{
			this.init_content_element()
			if (! this._content_element)
			{
				this.error('get_content_element:content:no content element')
				return undefined
			}
		}

		return this._content_element
	}



	/**
	 * Init page content element.
	 * 
	 * @returns {Element}
	 */
	init_content_element()
	{
		this._content_element = document.getElementById(this._content_id)
		if (! this._content_element)
		{
			this._content_element = document.createElement('div')
			this._content_element.setAttribute('id', this._content_id)
			document.body.appendChild(this._content_element)
		}
	}



	/**
	 * Clear page content components.
	 * 
	 * @param {array} arg_do_not_hide_components - components names array to leave unchanged.
	 * 
	 * @returns {Promise}
	 */
/*	clear_page_content(arg_do_not_hide_components=[])
	{
		this.enter_group('clear_content')

		// GET CONTENT ELEMENT
		const content_element = this.get_content_element()
		if (! content_element)
		{
			this.error('clear_content:content:no content element')
			this.leave_group('clear_content:error')
			return
		}

		const hidden_children = {}
		let child_index = 0
		let children_count = content_element.children.length
		while(child_index < children_count)
		{
			const child_element = content_element.children[child_index]
			this.debug('clear_content:remove: child_element', child_element)


			const child_id = child_element.getAttribute('id')
			if (! child_id)
			{
				this.debug('clear_content:child without id at position ' + child_index - 1)
				content_element.removeChild(child_element)
				children_count = content_element.children.length
				continue
			}

			if ( arg_do_not_hide_components.indexOf(child_id) > -1 )
			{
				this.debug('clear_content:do not hide ' + child_id)
				++child_index
				continue
			}
			
			const child_component = this._ui.get(child_id)
			if (! child_component)
			{
				this.debug('clear_content:child component not found for id  ' + child_id)
				content_element.removeChild(child_element)
				children_count = content_element.children.length
				continue
			}
			
			const classes = child_component.get_dom_element().className
			if (classes.search('devapt-layout-persistent') > -1)
			{
				this.debug('clear_content:peristent child component found for id  ' + child_id)
				++child_index
				continue
			}

			this.debug('clear_content:child component is hidden for id  ' + child_id)
			hidden_children[child_id] = child_component
			child_component.hide()
			++child_index
		}

		this.leave_group('clear_content:children count=' + children_count + ' hidden count=' + Object.keys(hidden_children).length)
	}*/


	
	/**
	 * Render page content with a global VNode.
	 * 
	 * @param {VNode}           arg_vnode            - rendered virtual dom node.
	 * @param {RenderingResult} arg_rendering_result - rendering result associated with given vnode.
	 * @param {Credentials}     arg_credentials      - user session credentials.
	 * 
	 * @returns {array} - array of rendered Component instances.
	 */
	render_page_content_vnode(arg_vnode, arg_rendering_result, arg_credentials)
	{
		this.enter_group('render_page_content_vnode')

		// GET CONTENT ELEMENT
		const content_element = this.get_content_element()
		if (! content_element)
		{
			this.error('render_page_content_vnode:content:no content element')
			this.leave_group('render_page_content_vnode:error')
			return
		}

		// DEBUG
		// console.log(context + ':render_page_content_vnode:arg_vnode', arg_vnode)
		// console.log(context + ':render_page_content_vnode:arg_rendering_result', arg_rendering_result)

		// GET CONTENT STATE ITEMS
		const body_contents_path = ['views', 'content', 'state', 'body_contents']
		let content_state_items = this.get_state_store().get_state().getIn(body_contents_path)

		content_state_items = content_state_items ? content_state_items.toJS() : undefined
		this.debug('render_page_content_vnode:content_state_items', content_state_items)

		// GET VNODE CHILDREN NAMES
		const get_vnode_id_fn = (vnode)=>{
			return vnode && vnode.properties && vnode.properties.id ? vnode.properties.id : undefined
		}
		const vnode_children = arg_vnode.children
		const vnode_children_names = vnode_children.map(get_vnode_id_fn).filter( (v)=>! T.isUndefined(v) )
		this.debug('process_content_vnode:vnode_children_names', vnode_children_names)

		// GET ORDERED CHILDREN LIST
		const ordered_children_names = vnode_children_names && vnode_children_names.length > 0 ? vnode_children_names : content_state_items
		this.debug('render_page_content_vnode:ordered_children_names', ordered_children_names)
		
		// LOOP ON VNODE CHILDREN
		const children_components = []
		const components_names = []
		_.forEach(ordered_children_names,
			(child_name)=>{
				this.debug('render_page_content_vnode:loop on child node:' + child_name)

				const child_vnode = vnode_children.find( (vnode)=>vnode.properties.id == child_name )
				// console.log(context + ':process_content_vnode:child name=%s child_vnode=', child_name, child_vnode)

				if (! child_vnode)
				{
					console.warn(context + ':render_page_content_vnode:child name=%s bad child_vnode', child_name)
					return
				}

				const child_id = child_name
				this.debug('render_page_content_vnode:content child found for id ' + child_id)

				// GET CONTENT CHILD COMPONENT
				const child_component = this._ui.get(child_name)
				if (! child_component || ! child_component.is_component)
				{
					this.warn('render_page_content_vnode:bad child component for name [' + child_name + ']', child_component)
					return
				}

				// RENDER CONTENT CHILD COMPONENT
				this.debug('render_page_content_vnode:process rendering vnode for ' + child_id)
				child_component.process_rendering_vnode(child_vnode, arg_rendering_result, arg_credentials)
				
				// APPEND CHILD DOM ELEMENT TO CONTENT DOM ELEMENT
				if ( ! child_component.has_dom_parent(content_element) )
				{
					let child_element = child_component.get_dom_element()
					if (! child_element)
					{
						this.error('render_page_content_vnode:no dom element for child ' + child_id)
						return
					}
					content_element.appendChild(child_element)
				}

				children_components.push(child_component)
				components_names.push(child_component.get_name())
			}
		)


		// UPDATE ASSETS URL
		this._ui._ui_rendering.process_assets_urls_templates(arg_rendering_result.assets_urls_templates)

		// UPDATE HEADERS AND ASSETS
		this._ui._ui_rendering.process_rendering_result_headers(arg_rendering_result.headers, arg_credentials)
		
		// PROCESS HEAD STYLES AND SCRIPTS
		this._ui._ui_rendering.process_rendering_result_styles_urls (document.head, arg_rendering_result.head_styles_urls, arg_credentials)
		this._ui._ui_rendering.process_rendering_result_styles_tags (document.head, arg_rendering_result.head_styles_tags, arg_credentials)
		this._ui._ui_rendering.process_rendering_result_scripts_urls(document.head, arg_rendering_result.head_scripts_urls, arg_credentials)
		this._ui._ui_rendering.process_rendering_result_scripts_tags(document.head, arg_rendering_result.head_scripts_tags, arg_credentials)

		// PROCESS BODY STYLES AND SCRIPTS
		this._ui._ui_rendering.process_rendering_result_styles_urls (document.body, arg_rendering_result.body_styles_urls, arg_credentials)
		this._ui._ui_rendering.process_rendering_result_styles_tags (document.body, arg_rendering_result.body_styles_tags, arg_credentials)
		this._ui._ui_rendering.process_rendering_result_scripts_urls(document.body, arg_rendering_result.body_scripts_urls, arg_credentials)
		this._ui._ui_rendering.process_rendering_result_scripts_tags(document.body, arg_rendering_result.body_scripts_tags, arg_credentials)

		this.leave_group('render_page_content_vnode:content')
		return children_components
	}
}