(function() {
/**
* all param types currently available
* @typedef {Object} Help4.jscore.ControlBase.Types
* @property {'atomic'} atomic - atomic data type
* @property {'array'} array - array data type
* @property {'array_null'} array_null - array that can be null
* @property {'boolean'} boolean - boolean data type
* @property {'boolean_null'} boolean_null - boolean that can be null
* @property {'dataContainer'} dataContainer - data container data type
* @property {'string'} string - string
* @property {'string_null'} string_null - string that can be null
* @property {'number'} number - number data type
* @property {'number_null'} number_null - number that can be null
* @property {'element'} element - element data type
* @property {'instance'} instance - any instance
* @property {'instanceArray'} instanceArray - an array of instances
* @property {'json'} json - json data type
* @property {'json_null'} json_null - json that can be null
* @property {'leftTop'} leftTop - {left: number, top: number}
* @property {'leftTop_null'} leftTop_null - {left: number, top: number} | null
* @property {'object'} object - any object
* @property {'object_null'} object_null - objects that can be null
* @property {'widthHeight'} widthHeight - {width: number, height: number}
* @property {'widthHeight_null'} widthHeight_null - {width: number, height: number} | null
* @property {'wh'} wh - {w: number, h: number}
* @property {'wh_null'} wh_null - {w: number, h: number} | null
* @property {'xy'} xy - {x: number, y: number}
* @property {'xy_null'} xy_null - {x: number, y: number} | null
* @property {'xywh'} xywh - {x: number, y: number, w: number, h: number}
* @property {'xywh_null'} xywh_null - {x: number, y: number, w: number, h: number} | null
*/
/**
* all text types currently available
* @typedef {Object} Help4.jscore.ControlBase.TextTypes
* @property {'innerText'} innerText - node.innerText
* @property {'innerHTML'} innerHTML - node.innerHTML
* @property {'title'} title - node.title
* @property {'placeholder'} placeholder - node.placeholder
* @property {'alt'} alt - image.getAttribute('alt')
*/
/**
* the function to set defined properties
* @typedef {Function} Help4.jscore.ControlBase.FunSet
* @param {Object} data - the data source
* @param {string} name - the attribute name
* @param {*} value - the value to be stored in data[name]
* @param {*} [def] - default value; internal use only
* @returns {boolean|undefined} whether data has been modified
*/
/**
* the function to get defined properties
* @typedef {Function} Help4.jscore.ControlBase.FunGet
* @param {Object} data - the data source
* @param {string} name - the attribute name
* @returns {*} the value for data[name]
*/
/**
* the function to compare defined properties
* @typedef {Function} Help4.jscore.ControlBase.FunEquals
* @param {*} value1
* @param {*} value2
* @returns {boolean} whether both values are equal
*/
/**
* the function to set texts to the DOM
* @typedef {Function} Help4.jscore.ControlBase.FunTextSet
* @param {HTMLElement} element - the HTML element
* @param {string} text - the to-be-applied text
* @returns {boolean}
*/
/**
* the function to get texts from the DOM
* @typedef {Function} Help4.jscore.ControlBase.FunTextGet
* @param {HTMLElement} element - the HTML element
* @returns {string} the text of that element
*/
/**
* the property change event
* @typedef {Object} Help4.jscore.ControlBase.PropertyChangeEvent
* @property {Help4.jscore.ControlBase|Help4.jscore.ControlBase[]} [target] - the control (chain) responsible for the value change
* @property {string} name - the property name
* @property {*} value - new value of the property
* @property {*} [oldValue] - old value of the property
*/
/**
* the function that is called on property change
* @typedef {Function} Help4.jscore.ControlBase.OnPropertyChange
* @param {Help4.jscore.ControlBase.PropertyChangeEvent} event - the change event
*/
/**
* the configuration of one parameter:
* - accessible like attributes w/o using getter and setter functions;
* - internally and externally accessible via <instance>.<key>;
* - will provide high performance auto-clone to prevent copy-by-reference issues;
* - will provide on update notification in case a value changes
* @typedef {Object} Help4.jscore.ControlBase.Param
* @property {Help4.jscore.ControlBase.Types} type - the property type
* @property {*} [init] - the init value
* @property {boolean} [readonly = false] - whether the property is readonly
* @property {boolean} [mandatory = false] - whether the property must be defined while class instantiation
* @property {boolean} [private = false] - whether the property is meant to be publicly accessible
*/
/**
* additional class configuration
* @typedef {Object} Help4.jscore.ControlBase.Config
* @property {string} [css] - css classes of this control
* @property {Help4.jscore.ControlBase.OnPropertyChange} [onPropertyChange] - sync listener for property change events
*/
/**
* the class configuration
* @typedef {Help4.jscore.Base.Params} Help4.jscore.ControlBase.Params
* @property {?Object} [params] - class parameters; each is of type {@link Help4.jscore.ControlBase.Param}
* @property {Help4.jscore.ControlBase.Config} [config] - class configuration
* @property {Object} [texts] - class text configuration: <id>: {@link Help4.jscore.ControlBase.TextTypes}
* @property {Object} [dataFunctions] - can be used to override dataFunctions with custom functionality
*/
/**
* @typedef {Help4.jscore.Base.ClassConfig} Help4.jscore.ControlBase.ClassConfig
* @property {?Object} [params] - dynamic class params; each is of type {@link Help4.jscore.ControlBase.Param}
* @property {string[]} _param_keys - keys of all dynamic class params
* @property {Function} _dataChangeListener - for simplified dynamic param change monitoring
* @property {Object} _data
*/
/**
* UI controls base class
* @augments Help4.jscore.Base
* @abstract
* @property {Help4.jscore.ControlBase.ClassConfig} ____classConfig - class configuration
* @property {Help4.jscore.DataFunctions.Embedded} dataFunctions
*/
Help4.jscore.ControlBase = class extends Help4.jscore.Base {
/**
* @override
* @param {Object} [params]
* @param {Help4.jscore.ControlBase.Params} [classConfig]
* @param {boolean} [isDerived = false]
*/
constructor(params, classConfig, isDerived = false) {
super(classConfig, true);
const {DATA_KEY, DATA_CHANGE_LISTENER_KEY} = this.constructor;
classConfig = this.____getClassConfig();
classConfig[DATA_KEY] = {};
classConfig[DATA_CHANGE_LISTENER_KEY] = event => this._listeners?.dataChange?.async?.onEvent(event);
if (!isDerived) { // derived classes execute other code
this.____resolveDerivedParams(classConfig);
params = _initParams(params || {}, classConfig.config);
this.____defineProperties(params);
this.____defineStaticProperties();
this.____defineDataFunctions();
}
}
static CUSTOM_TYPE = 'custom';
static PARAM_KEYS = '_param_keys';
static DATA_KEY = '_data';
static DATA_CHANGE_LISTENER_KEY = '_dataChangeListener';
/**
* @memberof Help4.jscore.ControlBase
* @type {Help4.jscore.ControlBase.Types}
*/
static TYPES = {};
static SET = {};
static GET = {};
static EQUALS = {};
/**
* @memberof Help4.jscore.ControlBase
* @type {Help4.jscore.ControlBase.TextTypes}
*/
static TEXT_TYPES = {};
static TEXT_CONTENT_TYPE = {};
static TEXT_SET = {};
static TEXT_GET = {};
/**
* register a new data type with setter, getter, equals
* @param {string} name - the data type name; see {@link Help4.jscore.ControlBase.Types}
* @param {Help4.jscore.ControlBase.FunSet} setter - setter function
* @param {Help4.jscore.ControlBase.FunGet} getter - getter function
* @param {Help4.jscore.ControlBase.FunEquals} equals - equals function
*/
static addDataType(name, setter, getter, equals) {
const {TYPES, SET, GET, EQUALS} = Help4.jscore.ControlBase;
TYPES[name] = name;
SET[name] = setter;
GET[name] = getter;
EQUALS[name] = equals;
}
/**
* register a new text type with setter, getter
* @param {string} name - text type
* @param {string} contentType - content type for this text; see {@link Help4.jscore.ControlBase.TextTypes}
* @param {Help4.jscore.ControlBase.FunTextSet} setter - setter function
* @param {Help4.jscore.ControlBase.FunTextGet} getter - getter function
*/
static addTextType(name, contentType, setter, getter) {
const {TEXT_TYPES, TEXT_CONTENT_TYPE, TEXT_SET, TEXT_GET} = Help4.jscore.ControlBase;
TEXT_TYPES[name] = name;
TEXT_CONTENT_TYPE[name] = contentType;
TEXT_SET[name] = setter;
TEXT_GET[name] = getter;
}
/** destroys this instance */
destroy() {
this.dataFunctions?.onChange?.destroy();
delete this.dataFunctions;
const classConfig = this.____getClassConfig();
for (const [key, info] of Object.entries(classConfig?.params || {})) {
const accessKey = info.private ? '__' + key : key;
delete this[accessKey];
}
super.destroy();
}
/**
* @override
* @param {string|string[]} eventType - event type to be observed
* @param {Help4.EmbeddedEvent.Listener} listener - the callback function
* @returns {Help4.jscore.ControlBase}
*/
addListener(eventType, listener) {
if (Help4.isArray(eventType)) {
eventType.forEach(type => this.addListener(type, listener));
} else {
if (typeof listener === 'function' && eventType === 'dataChange') {
// "dataChange" listeners are shortcuts
// original: this.dataFunctions.onChange.addListener(listener)
// shortcut: this.addListener('dataChange', listener)
const listeners = this._listeners[eventType] ||= {};
const embeddedEvent = listeners.async ||= new Help4.EmbeddedEvent();
const nbr = embeddedEvent
.addListener(listener)
.countListeners();
if (nbr === 1) {
const {DATA_CHANGE_LISTENER_KEY} = this.constructor;
const classConfig = this.____getClassConfig();
this.dataFunctions.onChange.addListener(classConfig[DATA_CHANGE_LISTENER_KEY]);
}
} else {
super.addListener(eventType, listener);
}
}
return this;
}
/**
* @override
* @param {string|string[]} eventType
* @param {Help4.EmbeddedEvent.Listener} listener
* @returns {Help4.jscore.ControlBase}
*/
removeListener(eventType, listener) {
if (Help4.isArray(eventType)) {
eventType.forEach(type => this.removeListener(type, listener))
} else {
if (typeof listener === 'function' && eventType === 'dataChange') {
// "dataChange" listeners are shortcuts
// original: this.dataFunctions.onChange.addListener(listener)
// shortcut: this.addListener('dataChange', listener)
const {_listeners} = this;
const embeddedEvent = _listeners[eventType]?.async;
const nbr = embeddedEvent
?.removeListener(listener)
?.countListeners();
if (nbr === 0) {
const {DATA_CHANGE_LISTENER_KEY} = this.constructor;
const classConfig = this.____getClassConfig();
this.dataFunctions.onChange.removeListener(classConfig[DATA_CHANGE_LISTENER_KEY]);
embeddedEvent.destroy();
delete _listeners[eventType];
}
} else {
super.removeListener(eventType, listener);
}
}
return this;
}
/**
* @returns {Object}
* @protected
*/
____getData() {
return this.____getClassConfig()[this.constructor.DATA_KEY];
}
/**
* @param {Object} dest
* @param {Object} src
* @returns {Object}
* @protected
*/
____extendParams(dest, src) {
for (const [key, value] of Object.entries(src)) {
if (value.type && !value.allowTypeOverride && dest[key]?.type) {
throw new Error(`Type override not allowed for "${key}" without "allowTypeOverride"!`);
}
dest[key] = Help4.extendObject(dest[key] || {}, value);
}
return dest;
}
/**
* @override
* @param {Help4.jscore.ControlBase.Params} level0
* @param {string} type
* @param {*} value
*/
____mergeDerivedParam(level0, type, value) {
const extendConfig = (dest, src) => {
for (const [key, value] of Object.entries(src)) {
if (key === 'css' && dest[key]) {
dest[key] += ' ' + value;
} else {
dest[key] = value;
}
}
return dest;
}
switch (type) {
case 'params':
level0[type] = this.____extendParams(level0[type] || {}, value);
break;
case 'config':
level0[type] = extendConfig(level0[type] || {}, value);
break;
case 'texts':
case 'dataFunctions':
level0[type] = Help4.extendObject(level0[type] || {}, value);
break;
default:
super.____mergeDerivedParam(level0, type, value);
break;
}
}
/**
* @param {Object} params
* @param {?Object} [derived = null]
* @param {Object} derived.GET
* @param {Object} derived.SET
* @protected
*/
____defineProperties(params, derived = null) {
const {PARAM_KEYS, CUSTOM_TYPE} = this.constructor;
const classConfig = /** @type {Help4.jscore.ControlBase.ClassConfig} */ this.____getClassConfig();
const paramKeys = classConfig[PARAM_KEYS] = Object.keys(classConfig.params || {});
const {GET, SET} = derived || this.constructor;
for (const key of paramKeys) {
const info = classConfig.params[key];
const {type, get, set} = info;
if (!type) throw new Error(`Property "${key}" need to define a type!`);
const funGet = type === CUSTOM_TYPE ? get : GET[type];
const funSet = type === CUSTOM_TYPE ? set : SET[type];
if (funGet && funSet) {
this.____defineProperty({key, info, funGet, funSet});
} else {
throw new Error(`Invalid type "${type}" for property or missing getter or setter!`);
}
this.____setInitValue({key, info, params, funSet});
}
}
/**
* @param {Object} args
* @param {string} [args.type = 'param'] - for Help4.jscore.DataBase
* @param {string} args.key
* @param {Help4.jscore.ControlBase.Param} args.info
* @param {Help4.jscore.ControlBase.FunGet} args.funGet
* @param {Help4.jscore.ControlBase.FunSet} args.funSet
* @protected
*/
____defineProperty({type = 'param', key, info, funGet, funSet}) {
const accessKey = type === 'param' && info.private ? `__${key}` : key;
Object.defineProperty(this, accessKey, {
get: function() {
const data = this.____getData();
return funGet.call(this, data, key);
},
set: function(value) {
if (this.isDestroyed()) return;
if (info.readonly) throw new Error(`${key} is readonly`);
const data = this.____getData();
const oldValue = data[key];
const modified = !!funSet.call(this, data, key, value);
if (modified) {
/** @type {Help4.jscore.ControlBase.PropertyChangeEvent} */
const event = {
target: this,
name: key,
// do not use value here, as funSet could have changed it, e.g. due to type conversion
// do not use data[name] here, as this would allow access to the reference
value: funGet.call(this, data, key),
oldValue: oldValue
};
this.____getClassConfig().config?.onPropertyChange?.(event); // sync!
this.dataFunctions.onChange.onEvent(event); // async!
}
},
configurable: true,
enumerable: true
});
}
/**
* @param {Object} args
* @param {string} args.key
* @param {Help4.jscore.ControlBase.Param} args.info
* @param {Object} args.params
* @param {Help4.jscore.ControlBase.FunSet} args.funSet
* @protected
*/
____setInitValue({key, info, params, funSet}) {
const data = this.____getData();
if (!data.hasOwnProperty(key)) {
data[key] = info.init ?? null;
}
if (params[key] !== undefined) {
funSet.call(this, data, key, params[key]);
} else if (info.mandatory) {
throw new Error(`Parameter "${key}" is mandatory!`);
}
funSet.call(this, data, key, data[key]); // make sure that defaults are set
}
/** @protected */
____defineDataFunctions() {
const {DataFunctions} = Help4.jscore;
const {dataFunctions = {}} = this.____getClassConfig();
this.dataFunctions = {
toObject: () => {
return dataFunctions.toObject
? dataFunctions.toObject(DataFunctions.toObject.bind(this))
: DataFunctions.toObject.call(this);
},
equals: (instance) => {
return dataFunctions.equals
? dataFunctions.equals(instance, DataFunctions.equals.bind(this))
: DataFunctions.equals.call(this, instance);
},
get: (...keys) => {
return dataFunctions.get
? dataFunctions.get(keys, DataFunctions.get.bind(this))
: DataFunctions.get.call(this, keys);
},
set: (json, params = {}) => {
return dataFunctions.set
? dataFunctions.set(json, params, DataFunctions.set.bind(this))
: DataFunctions.set.call(this, json, params);
},
clone: () => {
return dataFunctions.clone
? dataFunctions.clone(DataFunctions.clone.bind(this))
: DataFunctions.clone.call(this);
},
getKeys: () => {
return dataFunctions.getKeys
? dataFunctions.getKeys(DataFunctions.getKeys.bind(this))
: DataFunctions.getKeys.call(this);
},
getConfig: (name) => {
return dataFunctions.getConfig
? dataFunctions.getConfig(name, DataFunctions.getConfig.bind(this))
: DataFunctions.getConfig.call(this, name);
},
forEach: (executor) => {
return dataFunctions.forEach
? dataFunctions.forEach(executor, DataFunctions.forEach.bind(this))
: DataFunctions.forEach.call(this, executor);
},
every: (executor) => {
return dataFunctions.every
? dataFunctions.every(executor, DataFunctions.every.bind(this))
: DataFunctions.every.call(this, executor);
},
merge: (instance, mergeFn) => {
return dataFunctions.merge
? dataFunctions.merge(instance, mergeFn, DataFunctions.merge.bind(this))
: DataFunctions.merge.call(this, instance, mergeFn);
},
getTexts: () => {
return DataFunctions.getTexts.call(this);
},
onChange: dataFunctions.onChange || new Help4.EmbeddedEvent()
};
}
}
/**
* @memberof Help4.jscore.ControlBase#
* @param {Object} params
* @param {Help4.jscore.ControlBase.Config} config
* @returns {Object}
* @private
*/
function _initParams(params, {css}) {
if (css) {
params.css = params.css
? params.css + ' ' + css
: css;
}
// remove duplicates
params.css &&= [...new Set(params.css.split(/\s+/))].join(' ');
return params;
}
})();