js/base/component/unused_component.js
// NPM IMPORTS
import assert from 'assert'
import _ from 'lodash'
// COMMON IMPORTS
import T from '../../../node_modules/devapt-core-common/dist/js/utils/types'
import uid from '../../../node_modules/devapt-core-common/dist/js/utils/uid.js'
// BROWSER IMPORTS
import BindingsLoader from './bindings_loader'
import Dom from './dom'
const context = 'browser/base/component'
/**
* @file UI component class.
*
* @author Luc BORIES
*
* @license Apache-2.0
*/
export default class Component extends Dom
{
/**
* Creates an instance of Component.
* @extends Dom
*
* API
* ->render():Promise - Render component DOM element.
* ->process_rendering_vnode(arg_rendering_result, arg_credentials):nothing - Process rendering VNode: create or update DOM element.
* ->save_rendering():nothing - Save rendering virtul node. Update component VNode with current component HTML.
*
* ->update():Promise - Update view with current state.
* ->update_children():Promise - Update view with current state.
*
* ->clear():Promise - Clear component to initial values.
* ->destroy():Promise - Destroy component DOM element.
*
* ->load(arg_state):nothing - Load and apply a component configuration.
* ->init_bindings():nothing - Init bindings.
* ->unload():nothing - Unload a component configuration.
*
* ->dispatch_update_state_action(arg_new_state):nothing - Dispatch update state action.
* ->dispatch_update_state_value_action(arg_path, arg_value):nothing - Dispatch update state action.
*
* ->get_children_component():array - Get view children components.
*
* ->get_text_value():string - Get component content value string.
* ->set_text_value(arg_value):nothing - Set component content value string.
*
* ->get_object_value():object - Get component content value object.
* ->set_object_value(arg_value):nothing - Set component content value object.
*
* @param {RuntimeBase} arg_runtime - client runtime.
* @param {Immutable.Map} arg_state - component initial state.
* @param {string} arg_log_context - context of traces of this instance (optional).
*
* @returns {nothing}
*/
constructor(arg_runtime, arg_state, arg_log_context)
{
const log_context = arg_log_context ? arg_log_context : context
super(arg_runtime, arg_state, log_context)
this.is_component = true
this.is_menubar = false
this.is_breadcrumbs = false
// CHILDREN COMPONENTS
this._children_components = undefined
this._is_loaded = false
this._bindings = {}
this._ready_promise = Promise.resolve()
this._assets_dependancies = []
this._assets_promise = undefined
// console.info(context + ':constructor:creating component ' + this.get_name())
// this.enable_trace()
}
/**
* Render component DOM element.
*
* @param {boolean} arg_force - should force creation of a new VNode if a previous rendering exists.
*
* @returns {Promise} - Promise of this to chain promises.
*/
render(arg_force)
{
this.enter_group('render')
this._ready_promise = this._ready_promise.then(
()=>{
return this._render(arg_force)
}
)
this.leave_group('render:async')
return this._ready_promise
}
_render(arg_force)
{
// const is_rendered = this.get_state_value('is_rendered', false)
// if (! arg_force && this._is_rendered)
// {
// this.leave_group('render:already rendered')
// return Promise.resolve()
// }
let promise = Promise.resolve()
if (arg_force)
{
this.debug('render:force rendering')
promise = this._rendering.render()
}
promise = promise.then(
()=>{
// SHOULD RENDER VNODE
if ( ! this.has_dom_vnode())
{
this.debug('render:should create vnode')
const p = this._rendering.render()
this.leave_group('render:vnode is created')
return p
}
// DISPLAY VNODE
if ( this.has_dom_vnode())
{
const vnode = this.get_dom_vnode()
const p = this.process_rendering_vnode(vnode)
this.leave_group('render:vnode is rendered')
return p
}
// RENDERING FAILED
this.error('render:render:no vnode to render')
this.leave_group('render:no vnode to render')
return Promise.reject(context + ':render:no dom vnode to render for ' + this.get_name())
}
)
return promise
}
/**
* PROCESS RENDERING VNODE: CREATE OR UPDATE DOM ELEMENT.
*/
process_rendering_vnode(arg_rendering_result, arg_credentials)
{
this._rendering.process_rendering_vnode(arg_rendering_result, arg_credentials)
this._is_visible = true
// PROCESS CHILDREN
// ...
}
/**
* Save rendering virtul node. Update component VNode with current component HTML.
*
* @returns {nothing}
*/
save_rendering()
{
this._rendering.save_rendering()
}
/**
* Update view with current state.
*
* @returns {Promise}
*/
update()
{
this.enter_group('update')
console.info(context + ':update:%s', this.get_name())
this._ready_promise = this._ready_promise.then(
()=>{
return this._update()
}
)
this.leave_group('update:async')
return this._ready_promise
}
_update()
{
this.debug('update:name=' + this.get_name() + ',dom_id=' + this.get_dom_id() )
const new_elm = document.getElementById(this.get_dom_id())
const prev_elm = this.get_dom_element()
// console.log(prev_elm, context + ':update:prev_elm')
// console.log(new_elm, context + ':update:new_elm')
if (!new_elm)
{
// this.leave_group('update')
return Promise.resolve()
}
if (prev_elm != new_elm)
{
this.debug(':update:prev_elm <> new_elm')
if (prev_elm.parentNode)
{
prev_elm.parentNode.removeChild(prev_elm)
}
this._dom_element = new_elm
}
let promise = Promise.resolve()
if ( T.isFunction(this._update_self) )
{
this.debug(':update:call _update_self (async)')
promise = promise.then(
()=>{
this._update_self(prev_elm, new_elm)
}
)
}
promise = promise.then(
()=>
{
this.update_children()
}
)
return promise
}
/**
* Update view with current state.
*
* @returns {Promise}
*/
update_children()
{
this.enter_group('update_children')
this.get_children_component().forEach(
(component)=>{
this.debug(':update_children:component=' + component.get_name())
component.update()
}
)
this.leave_group('update_children')
}
/**
* Clear component to initial values.
*
* @returns {Promise}
*/
clear()
{
// TO OVERWRITE
}
/**
* Destroy component DOM element.
*
* @returns {Promise}
*/
destroy()
{
this.enter_group('destroy')
if (this._dom_element)
{
this._dom_element.parentNode.removeChild(this._dom_element)
}
this.leave_group('destroy')
}
/**
* Load and apply a component configuration.
*
* @param {Immutable.Map|undefined} arg_state - component state to load (optional).
*
* @returns {nothing|Promise}
*/
load(arg_state)
{
this.enter_group('load')
if (this._is_loaded)
{
// console.info(context + ':load:already loaded component ' + this.get_name())
this.leave_group('load:already loaded')
return
}
// const self = this
// console.info(context + ':load:loading component ' + this.get_name())
if (! this.store_unsubscribe)
{
this.store_unsubscribe = this.get_runtime().create_store_observer(this)
}
const state = arg_state ? arg_state : this.get_state()
// console.log(state, 'load bindinds')
if (! state)
{
this.leave_group('load:no state found')
return
}
this.init_assets()
// this.update()
this._is_loaded = true
this.leave_group('load')
}
/**
* Get assets promises.
*
* @returns {Promise}
*/
get_assets_promise()
{
return this._assets_promise
}
/**
* Get assets dependancies.
*
* @returns {array}
*/
get_assets_dependancies()
{
return this._assets_dependancies
}
/**
* Add assets dependancy.
*
* @param {string} arg_asset_id - asset element id.
*
* @returns {nothing}
*/
add_assets_dependancy(arg_asset_id)
{
return this._assets_dependancies.push(arg_asset_id)
}
/**
* Init assets promises.
*
* @returns {nothing}
*/
init_assets()
{
this.enter_group('init_assets')
const assets_promises = []
this._assets_dependancies.forEach(
(asset_id)=>{
assets_promises.push( window.devapt().asset_promise(asset_id) )
}
)
// console.log(context + ':init:%s:assets:', this.get_name(), this._assets_dependancies)
this._assets_promise = Promise.all(assets_promises)
this.leave_group('init_assets')
}
/**
* Init bindings.
*
* @returns {nothing}
*/
init_bindings()
{
this.enter_group('init_bindings')
const state = this.get_state()
const bindings = state.has('bindings') ? state.get('bindings').toJS() : undefined
if ( T.isObject(bindings) )
{
if ( T.isArray(bindings.services) )
{
bindings.services.forEach(
(bind_cfg) => {
bind_cfg.type = bind_cfg.timeline ? 'timeline' : (bind_cfg.dom_event ? 'emitter_jquery' : 'service')
const id = 'binding_' + uid()
this._bindings[id] = BindingsLoader.load(id, this._runtime, this, bind_cfg)
}
)
}
if ( T.isArray(bindings.streams) )
{
bindings.streams.forEach(
(bind_cfg) => {
// console.log(context + ':load:stream binding:', bind_cfg)
let stream = bind_cfg.source_stream ? bind_cfg.source_stream : undefined
if ( T.isString(stream) )
{
let source_component = this
if ( bind_cfg.source_type == 'views' && T.isNotEmptyString(bind_cfg.source_selector) )
{
source_component = window.devapt().ui(bind_cfg.source_selector)
}
stream = source_component.get_named_stream(stream)
}
if ( T.isObject(stream) && stream.is_stream )
{
bind_cfg.type = 'stream'
bind_cfg.source_stream = stream
const id = 'binding_' + uid()
this._bindings[id] = BindingsLoader.load(id, this._runtime, this, bind_cfg)
// console.log(context + ':load:stream bound:', id, bind_cfg)
}
}
)
}
if ( T.isArray(bindings.emitter_jquery) )
{
bindings.emitter_jquery.forEach(
(bind_cfg) => {
bind_cfg.type = 'emitter_jquery'
const id = 'binding_' + uid()
const binding_streams = BindingsLoader.load(id, this._runtime, this, bind_cfg)
if ( T.isArray(binding_streams) )
{
_.forEach(binding_streams,
(binding, index)=>{
this._bindings[id + '_' + index] = binding
}
)
} else {
this._bindings[id] = binding_streams
}
}
)
}
if ( T.isArray(bindings.emitter_dom) )
{
bindings.emitter_dom.forEach(
(bind_cfg) => {
bind_cfg.type = 'emitter_dom'
const id = 'binding_' + uid()
const binding_streams = BindingsLoader.load(id, this._runtime, this, bind_cfg)
if ( T.isArray(binding_streams) )
{
_.forEach(binding_streams,
(binding, index)=>{
this._bindings[id + '_' + index] = binding
}
)
} else {
this._bindings[id] = binding_streams
}
}
)
}
}
this.leave_group('init_bindings')
}
/**
* Unload a component configuration.
*
* @returns {nothing}
*/
unload()
{
this.enter_group('unload')
assert( T.isFunction(this.store_unsubscribe), context + ':unload:bad store_unsubscribe function')
// UNBIND ALL BINDINGS
_.forEach(this._bindings,
(binding/*, id*/)=>{
binding._unsubscribe()
if (binding._unsubscribe_state_update)
{
binding._unsubscribe_state_update()
}
}
)
// DETACH STORE CHANGE LISTENER
this.store_unsubscribe()
this.leave_group('unload')
}
/**
* Get a named stream.
*
* @param {string} arg_stream_name - stream name.
*
* @returns {Stream|undefined} - found stream.
*/
get_named_stream(arg_stream_name)
{
switch(arg_stream_name.toLocaleLowerCase()) {
case 'runtime_logs': return this._runtime.logs_stream
}
console.warn(context + ':get_named_stream:%s:unknow named stream', this.get_name(), arg_stream_name.toLocaleLowerCase())
return undefined
}
/**
* Dispatch update state action.
*
* @param {Immutable.Map} arg_new_state - new state Immutable Map.
*
* @returns {nothing}
*/
dispatch_update_state_action(arg_new_state)
{
if ( ! T.isObject(arg_new_state) )
{
return
}
const new_state = arg_new_state.toJS ? arg_new_state.toJS() : arg_new_state
// console.log(context + ':dispatch_update_state_action:new state:', new_state)
const action = { type:'ADD_JSON_RESOURCE', resource:this.get_name(), path:this.get_state_path(), json:new_state }
window.devapt().ui().store.dispatch(action)
}
/**
* Dispatch update state action.
*
* @param {array|string} arg_path - component state path.
* @param {any} arg_value - component state value.
*
* @returns {nothing}
*/
dispatch_update_state_value_action(arg_path, arg_value)
{
if (! T.isArray(arg_path) )
{
console.error(context + ':dispatch_update_state_value_action:bad path array:path,value:', arg_path, arg_value)
return
}
// console.log(context + ':dispatch_update_state_value_action:path,value:', arg_path, arg_value)
const new_state = this.get_state().setIn(arg_path, arg_value)
this.dispatch_update_state_action(new_state)
}
/**
* Get view children components.
*
* @returns {array} - list of Component.
*/
get_children_component()
{
// SKIP FOR MENUBAR
if (this.is_menubar)
{
return []
}
if ( ! this._children_components)
{
this._children_components = []
// GET APPLICATION STATE AND INIT APPLICATION STATE PATH
const ui = window.devapt().ui()
const current_app_state = ui.store.get_state()
const state_path = ['views']
// GET COMPONENT DESCRIPTION AND CHILDREN
const component_desc = ui._ui_factory.find_component_desc(current_app_state, this.get_name(), state_path)
const children = component_desc ? component_desc.get('children', undefined) : undefined
const children_names = children ? Object.keys( children.toJS() ) : []
this.debug(':get_children_component:init with children_names:', children_names)
// GET COMPONENT ITEMS
const items = this.get_state_value('items', [])
this.debug(':get_children_component:init with items:', JSON.stringify(items))
const all_children = _.concat(children_names, items)
// console.info(context + ':get_children_component:all_children:%s:', this.get_name(), all_children)
const unique_children = {}
all_children.forEach(
(item)=>{
if ( T.isObject(item) )
{
this.debug(':get_children_component:loop on item object')
if ( T.isString(item.view) )
{
if (item.viewitem in unique_children)
{
return
}
this.debug(':get_children_component:loop on item string:', item.view)
const component = window.devapt().ui(item.view)
if (component && component.is_component)
{
this._children_components.push(component)
unique_children[component.get_name()] = true
return
}
this.warn(':get_children_component:bad item component for:', item.view)
return
}
this.warn(':get_children_component:bad item object for:', JSON.stringify(item))
return
}
if ( T.isString(item) )
{
if (item in unique_children)
{
return
}
this.debug(':get_children_component:loop on item string:', item)
const component = window.devapt().ui(item)
if (component && component.is_component)
{
this._children_components.push(component)
unique_children[component.get_name()] = true
return
}
this.warn(':get_children_component:bad item component for:', item)
return
}
this.warn(':get_children_component:bad item type for:', item.toString())
}
)
}
this.debug(':get_children_component:', this._children_components)
return this._children_components
}
/**
* Get component content value string.
*
* @returns {string}
*/
get_text_value()
{
return this.get_dom_text()
}
/**
* Set component content value string.
*
* @param {string} arg_value - component values string.
*
* @returns {nothing}
*/
set_text_value(arg_value)
{
this.set_dom_text('' + arg_value)
}
/**
* Get component content value object.
*
* @returns {object}
*/
get_object_value()
{
let json = undefined
const str = this.get_dom_text()
try {
json = JSON.parse(str)
}
catch(e){
console.warn(context + ':get_object_value:error %s:bad json string=%s:', e, str)
}
return json
}
/**
* Set component content value object
*
* @param {object} arg_value - component values object.
*
* @returns {nothing}
*/
set_object_value(arg_value)
{
try {
const str = JSON.stringify(arg_value)
this.set_dom_text(str)
}
catch(e){
console.warn(context + ':set_object_value:error %s:bad object=:', e, arg_value)
}
}
/**
* Render a component inside this element from a json description.
*
* @param {object} arg_options - json source configuration.
*
* @returns {nothing}
*/
register_and_render_inside_from_json(arg_options)
{
this.enter_group('register_and_render_inside_from_json')
const json = this.register_from_json(arg_options)
if (! T.isObject(json) )
{
this.leave_group('register_and_render_inside_from_json:error:bad json object')
return
}
this.render_inside_from_json(json.name, json)
this.leave_group('register_and_render_inside_from_json')
}
/**
* Register a component description from a json content.
*
* @param {object} arg_options - json source configuration.
*
* @returns {nothing}
*/
register_from_json(arg_options)
{
this.enter_group('register_from_json')
console.log(context + ':register_from_json:options=', arg_options)
if (arg_options.is_event_handler)
{
arg_options = arg_options.data
}
// CHECK CONFIGURATION
if ( ! T.isObject(arg_options) )
{
console.warn(context + ':register_from_json:bad options object')
this.leave_group('register_from_json:error:bad options object')
return
}
if ( ! T.isString(arg_options.json_source_view) )
{
console.warn(context + ':register_from_json:bad options.json_source_view string')
this.leave_group('register_from_json:error:bad options.json_source_view string')
return
}
if ( ! T.isString(arg_options.json_source_getter) )
{
console.warn(context + ':register_from_json:bad options.json_source_getter string')
this.leave_group('register_from_json:error:bad options.json_source_getter string')
return
}
const source_object = this.get_runtime().ui().get(arg_options.json_source_view)
if ( ! T.isObject(source_object) || ! source_object.is_component )
{
console.warn(context + ':register_from_json:%s:view=%s:bad json source component', this.get_name(), arg_options.json_source_view, source_object)
this.leave_group('register_from_json:error:bad json source component')
return
}
if ( ! (arg_options.json_source_getter in source_object) )
{
console.warn(context + ':register_from_json:bad json source method for component')
this.leave_group('register_from_json:error:bad json source method for component')
return
}
if ( ! ( T.isFunction( source_object[arg_options.json_source_getter] )) )
{
console.warn(context + ':register_from_json:bad json source method for component')
this.leave_group('register_from_json:error:bad json source method for component')
return
}
// GET JSON FROM SOURCE
try{
const json = source_object[arg_options.json_source_getter]()
// DEBUG
console.log(context + ':register_from_json:json=', json)
// CHECK COMPONENT NAME
if ( ! T.isString(json.name) )
{
console.warn(context + ':register_from_json:bad json.name string')
this.leave_group('register_from_json:error:bad json.name string')
return
}
// STORE COMPONENT DESCRIPTION
const action = { type:'ADD_JSON_RESOURCE', resource:json.name, collection:'views', json:json }
this.get_runtime().get_state_store().dispatch(action)
this.leave_group('register_from_json')
return json
}
catch(e) {
console.warn(context + ':register_from_json:error %s', e)
this.leave_group('register_from_json:error:' + e)
return undefined
}
}
/**
* Render a component inside this element from a json description.
*
* @param {string} arg_name - component name.
* @param {object} arg_json_desc - component description.
*
* @returns {nothing}
*/
render_inside_from_json(arg_name, arg_json_desc)
{
this.enter_group('render_inside_from_json')
console.log(context + ':render_inside_from_json:name and description:', arg_name, arg_json_desc)
try{
// CREATE COMPONENT ELEMENT
const this_element = this.get_dom_element()
if ( ! this_element)
{
console.warn(context + ':render_inside_from_json:bad dom element')
this.leave_group('render_inside_from_json:error:bad dom element')
return
}
const this_document = this_element.ownerDocument
const existing_element = this_document.getElementById(arg_name)
let sub_element = undefined
if (existing_element)
{
if (existing_element.parentElement == this_element)
{
sub_element = existing_element
} else {
console.warn(context + ':render_inside_from_json:a previous element exist with given name=%s', arg_name)
this.leave_group('render_inside_from_json:error:bad dom element')
return
}
} else {
sub_element = this_element.ownerDocument.createElement('div')
sub_element.setAttribute('id', arg_name)
this_element.appendChild(sub_element)
}
const component = this.get_runtime().ui().create_local(arg_name, arg_json_desc)
component.render(true)
.then(
()=>{
window.devapt().content_rendered()
}
)
} catch(e){
console.warn(context + ':render_inside_from_json:error %s', e)
return
}
}
/**
* Expand component to fullscreen (inside browser window).
*
* @returns {nothing}
*/
expand_to_fullscreen()
{
console.log(context + ':expand_to_fullscreen')
let dom_elem = this.get_dom_element()
const bcolor = 'background-color'
const lmargin = 'margin-left'
if (dom_elem.parentNode && dom_elem.parentNode.className.indexOf('devapt-dock-item') > -1)
{
dom_elem = dom_elem.parentNode
}
dom_elem.className = dom_elem.className ? dom_elem.className : ''
if ( dom_elem.className.indexOf('devapt-fullscreen') < 0 )
{
this.saved_dom = {
body_overflow:document.body.style.overflow,
className:dom_elem.className,
style:{
position:dom_elem.style.position,
left: dom_elem.style.left,
top: dom_elem.style.top,
height: dom_elem.style.height,
width: dom_elem.style.width,
bcolor: dom_elem.style[bcolor],
lmargin: dom_elem.style[lmargin],
zindex: dom_elem.style['z-index']
}
}
document.body.style.overflow = 'hidden'
dom_elem.style.position = 'fixed'
dom_elem.style.left = '20px'
dom_elem.style.top = '20px'
dom_elem.style.height = '100%'
dom_elem.style.width = '100%'
dom_elem.style[bcolor] = 'white'
dom_elem.style[lmargin] = '0px'
dom_elem.style['z-index'] = 999
dom_elem.className += ' devapt-fullscreen'
this.resize(dom_elem.offsetWidth, dom_elem.offsetHeight)
}
}
/**
* Collapse component from fullscreen to original size.
*
* @returns {nothing}
*/
collapse_from_fullscreen()
{
// console.log(context + ':collapse_from_fullscreen')
let dom_elem = this.get_dom_element()
const bcolor = 'background-color'
const lmargin = 'margin-left'
if (dom_elem.parentNode && dom_elem.parentNode.className.indexOf('devapt-dock-item') > -1)
{
dom_elem = dom_elem.parentNode
}
dom_elem.className = dom_elem.className ? dom_elem.className : ''
if ( dom_elem.className.indexOf('devapt-fullscreen') > -1 && T.isObject(this.saved_dom) && T.isObject(this.saved_dom.style) )
{
document.body.style.overflow = this.saved_dom.body_overflow
dom_elem.style.position = this.saved_dom.style.position
dom_elem.style.left = this.saved_dom.style.left
dom_elem.style.top = this.saved_dom.style.top
dom_elem.style.height = this.saved_dom.style.height
dom_elem.style.width = this.saved_dom.style.width
dom_elem.style[bcolor] = this.saved_dom.style.bcolor
dom_elem.style[lmargin] = this.saved_dom.style.lmargin
dom_elem.style['z-index'] = this.saved_dom.style.zindex
dom_elem.className = this.saved_dom.className
this.saved_dom = {}
this.resize(dom_elem.offsetWidth, dom_elem.offsetHeight)
}
}
/**
* Toggle component fullscreen mode.
*
* @returns {nothing}
*/
toggle_from_fullscreen()
{
let dom_elem = this.get_dom_element()
if (dom_elem.parentNode && dom_elem.parentNode.className.indexOf('devapt-dock-item') > -1)
{
dom_elem = dom_elem.parentNode
}
dom_elem.className = dom_elem.className ? dom_elem.className : ''
if ( dom_elem.className.indexOf('devapt-fullscreen') > -1 )
{
this.collapse_from_fullscreen()
return
}
this.expand_to_fullscreen()
}
/**
* Toggle component fullscreen mode.
*
* @returns {nothing}
*/
save_()
{
}
/**
* Toggle component fullscreen mode.
*
* @returns {nothing}
*/
restore_()
{
}
/**
* Resize component.
*
* @param {any} arg_width - css width value.
* @param {any} arg_height - css height value.
*
* @returns {nothing}
*/
resize(arg_width, arg_height)
{
console.log(context + ':resize:width=%s height:%s', arg_width, arg_height)
// TODO
const dom_elem = this.get_dom_element()
if (! T.isNumber(arg_width))
{
arg_width = dom_elem.offsetWidth
}
if (! T.isNumber(arg_height))
{
arg_height = dom_elem.offsetHeight
}
console.log(context + ':resize:num width=%s num height:%s', arg_width, arg_height)
this.get_children_component().forEach(
(component)=>{
this.debug(':resize:component=' + component.get_name())
component.resize(arg_width, arg_height)
}
)
}
}