UNPKG

@squirrel-forge/ui-core

Version:

A collection of interface, classes, functions and abstracts made for the browser and babel compatible.

538 lines (490 loc) 16.8 kB
/** * Requires */ import { ComponentStates } from '../States/ComponentStates.js'; import { Exception, EventDispatcher, Config, Plugins, attributeJSON, requireUniqid, strCreate, isPojo } from '@squirrel-forge/ui-util'; /** * @typedef {Object} UiComponentDefaultConfig * @property {null|string} eventPrefix - Prefix for event type names */ /** * Ui component exception * @class * @extends Exception */ class UiComponentException extends Exception {} /** * Ui component * @abstract * @class * @extends EventDispatcher */ export class UiComponent extends EventDispatcher { /** * Convert attribute value to config value * @public * @param {null|string} value - Attribute value * @return {*} - Converted value */ static configValueFromAttr( value ) { if ( typeof value === 'string' ) { if ( value.length ) { if ( value.toLowerCase() === 'true' ) { value = true; } else if ( value.toLowerCase() === 'false' ) { value = false; } else if ( /^[0-9]+$/.test( value ) ) { value = parseInt( value ); } else if ( /^[0-9]*\.[0-9]+$/.test( value ) ) { value = parseFloat( value ); } else if ( value[ 0 ] === '[' || value[ 0 ] === '{' ) { try { value = JSON.parse( value ); } catch ( error ) { return value; } } } else { value = true; } } return value; } /** * Convert attribute name to config dot path * @public * @param {string} name - Attribute name * @return {string} - Config name */ static configDotNameFromAttr( name ) { name = name.replace( /-/g, '.' ); if ( name.substr( 0, 5 ) === 'data.' ) { name = name.substr( 5 ); } return name; } /** * Convert config dot path to camel case * @public * @param {string} name - Dot path * @return {string} - Camel case */ static configCamelNameFromDot( name ) { return name.toLowerCase().replace( /\.(.)/g, ( m, g ) => { return g.toUpperCase(); } ); } /** * Make ui component * @param {HTMLElement} element - Element * @param {null|Object} settings - Config object * @param {null|Array} plugins - Plugins array * @param {null|EventDispatcher|HTMLElement} parent - Parent object * @param {null|false|console|Object} debug - Debug object * @param {Function} Construct - Component constructor * @return {UiComponent} - Component object */ static make( element, settings = null, plugins = null, parent = null, debug = null, Construct = null ) { if ( !( element instanceof HTMLElement ) ) { throw new UiComponentException( 'Argument element must be a HTMLElement' ); } Construct = Construct || this; if ( debug === null ) { const value = element.getAttribute( 'debug' ) || element.getAttribute( 'data-debug' ); if ( Construct.configValueFromAttr( value ) === true ) { debug = console; } } else if ( debug === true ) { debug = console; } if ( debug ) window.console.warn( 'UiComponent.make', Construct.name, { element, settings, plugins, parent, debug, Construct } ); return new Construct( element, settings, null, null, null, plugins, parent, debug, true ); } /** * Initialize all ui elements in context * @param {null|Object} settings - Config object * @param {null|Array} plugins - Plugins array * @param {null|EventDispatcher|HTMLElement} parent - Parent object * @param {document|HTMLElement} context - Context to initialize * @param {null|console|Object} debug - Debug object * @param {Function} Construct - Component constructor * @return {Array<UiComponent>} - Initialized components */ static makeAll( settings = null, plugins = null, parent = null, context = document, debug = null, Construct = null ) { Construct = Construct || this; if ( debug ) window.console.warn( 'UiComponent.makeAll', Construct.name, { settings, plugins, parent, context, debug, Construct } ); const result = []; const elements = context.querySelectorAll( Construct.selector ); for ( let i = 0; i < elements.length; i++ ) { result.push( Construct.make( elements[ i ], settings, plugins, parent, debug, Construct ) ); } return result; } /** * Element selector getter * @public * @return {string} - Element selector */ static get selector() { return '[is="ui-component"]:not([data-state])'; } /** * Dom reference * @private * @property * @type {null|HTMLElement} */ #dom = null; /** * Component config * @private * @property * @type {null|Config} */ #config = null; /** * Component states * @private * @property * @type {null|ComponentStates} */ #states = null; /** * Component plugins * @private * @property * @type {null|Plugins} */ #plugins = null; /** * Initialized * @private * @property * @type {boolean} */ #initialized = false; /** * Component children * @private * @property * @type {Array} */ #children = []; /** * Children initialized * @private * @property * @type {boolean} */ #children_initialized = false; /** * Constructor * @constructor * @param {HTMLElement} element - Dom element * @param {null|Object} settings - Config object * @param {Object} defaults - Default config * @param {Array<Object>} extend - Config defaults extension for inheritance * @param {Object} states - States definition * @param {Array<Function|Array<Function,*>>} plugins - Plugins to load * @param {null|EventDispatcher|HTMLElement} parent - Parent object * @param {null|console|Object} debug - Debug object * @param {boolean} init - Run init method */ constructor( element, settings = null, defaults = null, extend = null, states = null, plugins = null, parent = null, debug = null, init = true ) { super( element, parent, debug ); if ( debug ) window.console.warn( 'UiComponent.constructor >', this.constructor.name, { element, settings, defaults, extend, states, plugins, parent, debug, init } ); if ( !( element instanceof HTMLElement ) ) throw new UiComponentException( 'Argument element must be a HTMLElement' ); this.#dom = element; // Require element id and mark as ui-component requireUniqid( element, this.constructor.name.toLowerCase() + '-', true ); this.#markAsUi(); // Initialize plugins and extend defaults extend = extend || []; this.#plugins = plugins ? new Plugins( plugins, this, true, debug ) : null; this.#plugins?.run( 'extendDefaultConfig', [ extend ] ); // Create component config this.#config = new Config( defaults || { eventPrefix : null }, extend ); // Set config options from attributes this.#setConfigFromAttributes(); // Ensure the config property overrides any attributes this.#loadElementConfig(); // Apply any plugin scoped configs this.#plugins?.run( 'applyConfig', [ this.config ] ); // Allow for a forced extend of the config let override_extend = false; if ( settings && settings.__forceExtend === true ) { override_extend = true; delete settings.__forceExtend; } // Apply settings explicitly provided by constructor arguments if ( isPojo( settings ) ) this.config.merge( settings, override_extend ); // Create states handler and extend with any plugin states this.#states = new ComponentStates( this, states || {} ); this.#plugins?.run( 'extendAvailableStates', [ this.#states ] ); // Initialize component if ( init ) this.init(); } /** * Type getter * @public * @return {string} - Component type */ get type() { return this.constructor.name; } /** * Dom getter * @public * @return {HTMLElement} - Component dom element */ get dom() { return this.#dom; } /** * Config getter * @public * @return {Config} - Component config */ get config() { return this.#config; } /** * States getter * @public * @return {ComponentStates} - Component states */ get states() { return this.#states; } /** * Plugins getter * @public * @return {Plugins} - Component plugins */ get plugins() { return this.#plugins; } /** * Children getter * @public * @return {Array} - Component children */ get children() { return [ ...this.#children ]; } /** * Load and merge element config * @private * @return {void} */ #loadElementConfig() { const config = attributeJSON( 'data-config', this.#dom ); if ( config ) { this.config.merge( config ); if ( this.debug ) this.debug.log( this.constructor.name + '::loadElementConfig', config ); } } /** * Mark as ui component * @private * @return {void} */ #markAsUi() { this.#dom.setAttribute( 'data-ui', this.constructor.name ); } /** * Initialize component * @public * @param {null|Function} afterInitialized - Run function after initialized event * @return {void} */ init( afterInitialized = null ) { if ( this.#initialized ) { throw new UiComponentException( 'Component already initialized' ); } this.#initialized = true; this.#plugins?.run( 'initComponent' ); this.#states.set( 'initialized' ); // Delay the init dispatch and children for object availability reasons window.setTimeout( () => { this.dispatchEvent( ( this.config.get( 'eventPrefix' ) || '' ) + 'initialized' ); if ( afterInitialized ) afterInitialized( this ); }, 1 ); } /** * Initialize child components * @protected * @return {void} */ _initChildren() { if ( this.#children_initialized ) { throw new UiComponentException( 'Component children already initialized' ); } this.#children_initialized = true; const options = this.#config.get( 'children' ); if ( options && isPojo( options ) ) { const types = Object.entries( options ); if ( types.length && this.debug ) this.debug.group( this.constructor.name + '::_initChildren', types ); for ( let i = 0; i < types.length; i++ ) { // Build arguments const [ name, Construct ] = types[ i ]; const params = Construct instanceof Array ? Construct : [ Construct ]; // Attempt to initialize each type try { this.#children = this.#children.concat( this.#initChildType( ...params ) ); } catch ( e ) { throw new UiComponentException( 'Failed to initialize child type: ' + name, e ); } } if ( types.length && this.debug ) this.debug.groupEnd(); this.dispatchEvent( ( this.config.get( 'eventPrefix' ) || '' ) + 'children.initialized' ); } } /** * Initialize children by type * @private * @param {Function|UiComponent} Construct - Component constructor * @param {null|Object} settings - Config object * @param {Array} plugins - Plugins * @return {Array<UiComponent>} - Initialized components */ #initChildType( Construct, settings = null, plugins = null ) { if ( typeof Construct !== 'function' ) { throw new UiComponentException( 'Argument Construct must be a Function' ); } return Construct.makeAll( settings, plugins, this, this.#dom, this.debug, Construct ); } /** * Cycle children * @public * @param {string|Array|Function} filter - Filter or callback function * @param {null|Function} callback - Callback when using a filter * @return {void} */ eachChild( filter, callback = null ) { if ( typeof filter === 'function' ) { callback = filter; filter = null; } else if ( typeof callback !== 'function' ) { throw new UiComponentException( 'Argument callback must be a Function' ); } let x = 0; for ( let i = 0; i < this.#children.length; i++ ) { const child = this.#children[ i ]; if ( !filter || filter === child.type || filter instanceof Array && filter.includes( child.type ) ) { const br = callback( child, x, i ); if ( br === true ) break; x++; } } } /** * Get config from attributes * @public * @param {Array<string>} disregard - Disregard options names * @return {null|Object} - Config object */ getConfigFromAttributes( disregard = [ 'id', 'class', 'type', 'state', 'config' ] ) { if ( this.#dom.hasAttributes() ) { const result = {}; const attrs = this.#dom.attributes; for ( let i = 0; i < attrs.length; i++ ) { const name = this.constructor.configDotNameFromAttr( attrs[ i ].name ); const value = this.constructor.configValueFromAttr( attrs[ i ].value ); if ( !disregard.includes( name ) ) { strCreate( name, value, result, true, true, this.debug ); } const camel = this.constructor.configCamelNameFromDot( name ); if ( !disregard.includes( camel ) ) { strCreate( camel, value, result, true, true, this.debug ); } } return result; } return null; } /** * Set config from attributes * @private * @return {void} */ #setConfigFromAttributes() { const result = this.getConfigFromAttributes(); if ( result ) { this.config.merge( result ); if ( this.debug ) this.debug.log( this.constructor.name + '::getConfigFromAttributes', result ); } } /** * Get dom references from config * @public * @param {string} name - Reference name * @param {boolean} multiple - Set false to return one element * @return {null|HTMLElement|NodeList} - Dom reference/s */ getDomRefs( name, multiple = true ) { const ref = this.config.get( 'dom.' + name ); if ( !name || !name.length || !ref ) { return multiple ? [] : null; } const method = 'querySelector' + ( multiple ? 'All' : '' ); return this.#dom[ method ]( ref ); } /** * Require dom references * @public * @param {Array<Array<string,boolean>>} refs - Reference requirements * @return {void} */ requireDomRefs( refs ) { for ( let i = 0; i < refs.length; i++ ) { const [ name, multiple ] = refs[ i ]; const ref = this.getDomRefs( name, multiple ); if ( !ref || multiple && !ref.length ) { throw new UiComponentException( 'Component requires a dom reference for: ' + name ); } } } /** * Set state from event * @public * @param {Event} event - Event object * @param {string|null} prefix - Event type prefix * @return {void} */ event_state( event, prefix ) { if ( !event || typeof event.type !== 'string' || !event.type.length ) { throw new UiComponentException( 'No valid event type available' ); } let type = event.type; if ( typeof prefix !== 'string' ) prefix = this.config.get( 'eventPrefix' ); if ( prefix && prefix.length && type.substring( 0, prefix.length ) === prefix ) { type = type.substring( prefix.length ); } this.states.set( type ); } }