import API from '../../api/index.js';
import {Accessors_A} from './as-accessors.js';
import {Eventing_A} from './as-event.js';
import {Popup_A} from '../as-popup.js';
import {
ErrorCode,
ExceptionIdMap,
getExceptionIdByErrorCode,
checkIfKnownRWSError,
} from './../../exception/exceptionDesc.js';
import {Signal, Rapid, Variable, UAS, Success} from '../../store/const.js';
import Store from '../../store/store.js';
import {appendDataToErrInstance} from '../../utils/utils.js';
/**
* @description Loads a CSS file
* @alias tComponentsLoadCSS
* @memberof TComponents
* @param {string} href - Path of css file
*/
function tComponentsLoadCSS(href) {
let head = document.getElementsByTagName('head')[0];
let link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = href;
head.appendChild(link);
}
tComponentsLoadCSS('framework/components/style/t-components.css');
/**
* @typedef TComponents.ComponentProps
* @prop {string} [label] Label text
* @prop {string} [labelPos] Label position: "top|bottom|left|right|top-center|bottom-center"
* @prop {object} [options] A set of options to modify the behaviour of the component
* - async : if true, the subcomponents are instantiated asynchronously and onRender is executed inmediatelly without
* waiting for the subcomponents to finish.
*/
/**
* @ignore
*/
const logModule = 'as-component';
/**
* @description Creates an instance of TComponents.Component class.
* This is the base parent class of all TComponent.
* @class TComponents.Component_A
* @extends TComponents.Accessors_A
* @memberof TComponents
* @param {HTMLElement} parent - HTMLElement that is going to be the parent of the component.
* @param {TComponents.ComponentProps} props Parameters to create the component.
* @example
* const component = new TComponents.Component_A(document.body, {
* label: 'My Component',
* labelPos: 'top',
* options: {
* async: true
* }
* });
*/
export class Component_A extends Accessors_A {
constructor(parent, props = {}) {
super(props);
/**
* @instance
* @private
* @type {TComponents.ComponentProps}
*/
this._props;
if (!Component_A._isHTMLElement(parent) && parent !== null) {
Logger.e(
logModule,
ErrorCode.FailedToFindParentElement,
'The parent element is not a valid HtMLElement:',
parent,
);
throw ErrorCode.FailedToFindParentElement;
}
this._conditionDefaultProps = Object.assign(this.defaultProps(), props);
this.compId = `${this.constructor.name}_${API.generateUUID()}`;
this.child = null;
/**
* Parent HTML element where the component is attached to.
* @type {HTMLElement}
*/
this.parent = parent;
this.container = document.createElement('div');
this.container.id = this.compId;
this.container.classList.add('t-component');
this.parentComponentId = '';
this.template = null;
this.initialized = false;
this._initCalled = false;
this._enabled = true;
this._deinstallFunction = null;
this._eventListeners = new Map();
this._fUpdate = false;
Object.defineProperty(this, '_isTComponent', {
value: true,
writable: false,
});
this.once('before:init', () => {
this.onCreated();
});
this.once('after:render', () => {
// this.initState();
this.onMounted();
this.afterRenderOnce();
});
this.once('before:destroy', () => {
if (this._props.onDestroy) {
this.onDispose();
}
});
}
/**
* @description Lifecycle hook called once after the component is rendered for the first time.
* @member TComponents.Component_A#afterRenderOnce
* @method
* @protected
* @returns {void}
*/
afterRenderOnce() {}
/**
* @description Lifecycle hook called when the component is created.
* @member TComponents.Component_A#onCreated
* @method
* @protected
* @async
* @returns {Promise<void>}
*/
async onCreated() {
const fn = Component_A.genFuncTemplate(this._props.onCreated, this);
fn && (await fn());
}
/**
* @description Lifecycle hook called when the component is mounted.
* @member TComponents.Component_A#onMounted
* @method
* @protected
* @async
* @returns {Promise<void>}
*/
async onMounted() {
const fn = Component_A.genFuncTemplate(this._props.onMounted, this);
fn && (await fn());
}
/**
* @description Lifecycle hook called when the component is about to be disposed.
* This hook is triggered when the component is being removed from the renderer
* and all associated resources should be released.
* @member TComponents.Component_A#onDispose
* @method
* @protected
* @async
* @returns {Promise<void>}
*/
async onDispose() {
const fn = Component_A.genFuncTemplate(this._props.onDispose, this);
fn && (await fn());
}
/**
* @description Replace the content of the component's container.
* @member TComponents.Component_A#replaceContent
* @method
* @param {string|HTMLElement} content - The new content to replace the current content.
* @returns {void}
* @example
* const component = new TComponents.Component_A(document.body, {});
* component.replaceContent('New Content');
*/
replaceContent(content) {
this.container.innerHTML = '';
if (typeof content == 'string') {
this.container.innerHTML = content;
} else if (content instanceof HTMLElement) {
this.container.appendChild(content);
} else {
this.container.innerHTML = content;
}
}
/**
* @description Returns the default values of class properties (excluding parent properties).
* @member TComponents.Component_A#defaultProps
* @method
* @protected
* @returns {TComponents.ComponentProps}
*/
defaultProps() {
return {label: '', labelPos: 'top'};
}
/**
* @description Initialization of a component. Any asynchronous operation (like access to controller) is done here.
* The {@link TComponents.Component_A#onInit()} method of the component is triggered by this method.
* @member TComponents.Component_A#init
* @method
* @async
* @returns {Promise<object>} The TComponents instance on which this method was called.
*/
async init() {
try {
this.trigger('before:init', this);
/**
* Clean up before initializing. Relevant from the second time the component is initialized.
* - Remove all event listeners
*/
this.removeAllEventListeners();
/**
* Parent HTML element to which the component is attached.
*/
this.parent;
/**
* Initialization of internal states
* @private
*/
this.initialized = false;
this._initCalled = true;
/**
* Reset values before onInit
* Resetting enabled only if previously an error occurred, for a next try, otherwise it was explicitly disabled by the user
*/
if (this.error) this.enabled = true;
this.error = false;
try {
this._deinstallFunction = await this.onInit();
} catch (e) {
this.error = true;
// throw e;
Logger.e(logModule, ErrorCode.FailedToInitComponent, `Failed to run onInit() for component ${this.compId}.`, e);
}
this.trigger('init', this);
return await this.render();
} catch (e) {
Logger.e(logModule, ErrorCode.FailedToInitComponent, `Failed to initialize the component ${this.compId}.`, e);
}
}
/**
* @description Update the content of the instance into the Document Object Model (DOM).
* The {@link TComponents.Component_A#onRender()} method of this component and eventual initialization
* of sub-components are managed by this method.
* @member TComponents.Component_A#render
* @method
* @async
* @param {object} [data] - Data that can be passed to the component, which may be required for the rendering process.
* @returns {Promise<object>} The TComponents instance on which this method was called.
*/
async render(data = null) {
this.container.innerHTML = '';
try {
this.trigger('before:render', this);
if (this._initCalled === false) {
await this.init();
return;
}
this._handleData(data);
this._createTemplate();
await this.initChildrenComponents();
} catch (e) {
this.error = true;
Logger.e(logModule, ErrorCode.FailedToRenderComponent, `Failed to render the component ${this.compId}.`, e);
}
if (this.container.hasChildNodes() && this._labelStart()) {
// Insert before the first child node
this.container.insertBefore(this.template.content, this.container.firstChild);
} else {
this.container.appendChild(this.template.content);
}
this.parent && this.attachToElement(this.parent);
this.error && (this.enabled = false);
this.setContainerBasicCss();
this.onRender();
this.initialized = true;
this.trigger('render', this);
this.trigger('after:render', this);
return this;
}
/**
* @description Add the tooltip HTML Element for the component.
* @member TComponents.Component_A#addTips
* @method
* @protected
* @returns {void}
*/
addTips() {
this._addTips();
}
/**
* @member TComponents.Component_A~_addTips
* @method
* @private
* @returns {void}
*/
_addTips() {
if (this._props.tips) {
this.addEventListener(this.container, 'mouseenter', this._enterTip.bind(this));
this.addEventListener(this.container, 'mouseleave', this._leaveTip.bind(this));
}
}
/**
* @description Shows the tooltip when mouse enters.
* @member TComponents.Component_A#enterTip
* @method
* @protected
* @returns {void}
*/
enterTip() {
this._enterTip();
}
/**
* @member TComponents.Component_A~_enterTip
* @method
* @private
* @returns {void}
*/
_enterTip() {
if (!this.enabled) {
const tips = document.createElement('div');
tips.classList.add('t-component-tips');
tips.textContent = Component_A.tParse(this._props.tips);
this.container.appendChild(tips);
const maxTipWidth = 240;
const minTipWidth = 80;
const naturalWidth = tips.scrollWidth;
const safeWidth = Math.max(minTipWidth, Math.min(maxTipWidth, naturalWidth));
tips.style.width = `${safeWidth}px`;
const containerTop = this.container.getBoundingClientRect().top;
const tipsHeight = tips.getBoundingClientRect().height;
const topGap = 8;
if (containerTop < tipsHeight + topGap) {
// tips.style.bottom = 'auto';
// tips.style.top = '1px';
}
}
}
/**
* @description Hides the tooltip when mouse leaves.
* @member TComponents.Component_A#leaveTip
* @method
* @protected
* @returns {void}
*/
leaveTip() {
this._leaveTip();
}
/**
* @member TComponents.Component_A~_leaveTip
* @method
* @private
* @returns {void}
*/
_leaveTip() {
if (!this.enabled) {
const tips = this.container.querySelectorAll('.t-component-tips');
if (tips) tips.forEach((tip) => tip.remove());
}
}
/**
* @description Contains component specific asynchronous implementation (like access to controller).
* This method is called internally during initialization process orchestrated by {@link TComponents.Component_A#init init}.
* @member TComponents.Component_A#onInit
* @method
* @abstract
* @async
*/
async onInit() {}
/**
* @description Contains all synchronous operations/setups that may be required for any sub-component after its initialization and/or manipulation of the DOM.
* This method is called internally during rendering process orchestrated by {@link TComponents.Component_A#render render}.
* @member TComponents.Component_A#onRender
* @method
* @abstract
*/
onRender() {}
/**
* @description Synchronously initializes all child TComponents returned by {@link #mapComponents() mapComponents} method.
* This method is internally called by {@link TComponents.Component_A#render() render} method.
* @member TComponents.Component_A#initChildrenComponents
* @method
* @private
* @async
* @returns {void}
*/
async initChildrenComponents() {
const newChildren = this.mapComponents();
const toDispose = [];
if (Object.keys(newChildren).length === 0) return;
// Initialize this.child if it's not already initialized
if (!this.child) {
this.child = newChildren;
} else {
for (const key in newChildren) {
const newChild = newChildren[key];
const oldChild = this.child[key];
if (Component_A.isTComponent(oldChild) && Component_A.isTComponent(newChild)) {
// const shouldUpdate = !Base_A._equalProps(oldChild._props, newChild._props) || oldChild._fUpdate;
const shouldUpdate = oldChild._fUpdate || !Component_A._equalProps(oldChild._props, newChild._props);
if (shouldUpdate) {
// If the properties are not equal or if _fUpdate is true,
// Ensure old child is properly destroyed
toDispose.push(oldChild);
// Replace the existing child
this.child[key] = newChild;
} else {
// If the properties are equal and _fUpdate is false, just attach the old child to the new DOM element
if (newChild.compId !== oldChild.compId) {
// cleaning up newChild since not needed, but just if it is a different instace as the old one
oldChild.attachToElement(newChild.parent);
newChild.destroy();
}
}
} else {
// If not a TComponent, replace the existing child anyway
this.child[key] = newChild;
}
}
}
const arrAll = Object.entries(this.child).reduce((acc, [key, value]) => {
if (value instanceof Promise)
throw new Error(`Promise detected but not expected at ${this.compId}--mapComponent element ${key}...`);
const sortComponent = (value) => {
if (Component_A.isTComponent(value)) {
value.parentComponentId = this.compId;
acc.push(value);
}
};
if (Array.isArray(value)) {
value.forEach((v) => {
sortComponent(v);
});
} else {
sortComponent(value);
}
return acc;
}, []);
const initChildren = function () {
return arrAll.map((child) => {
return child._initCalled ? child : child.init();
});
};
const status = this._props.options.async ? initChildren() : await Promise.all(initChildren());
// Clean up the replaced old children objects
toDispose.forEach((child) => child.destroy());
}
/**
* @description Instantiation of TComponents sub-components that shall be initialized in a synchronous way.
* All these components are then accessible within {@link TComponents.Component_A#onRender onRender} method by using this.child.<component-instance>
* @member TComponents.Component_A#mapComponents
* @method
* @abstract
* @returns {object} Contains all child TComponents instances used within the component.
*/
mapComponents() {
return {};
}
/**
* @description Generates the HTML definition corresponding to the component.
* @member TComponents.Component_A#markup
* @method
* @abstract
* @param {object} self - The TComponents instance on which this method was called.
* @returns {string}
*/
markup(self) {
return /*html*/ '';
}
/**
* @description Adds an event listener to the specified element and keeps tracking of it.
* @member TComponents.Component_A#addEventListener
* @method
* @protected
* @param {HTMLElement} element - The target element to which the event listener will be added.
* @param {string} eventType - The type of the event to listen for (e.g., 'click', 'mouseover').
* @param {Function} listener - The function that will be called when the event is triggered.
* @param {boolean|AddEventListenerOptions} [options] - Optional options object or useCapture flag.
* @throws {Error} Throws an error if the specified element is not found.
* @returns {void}
*/
addEventListener(element, eventType, listener, options) {
if (!element) throw new Error('Element not found');
element.addEventListener(eventType, listener, options);
if (!this._eventListeners.has(element)) {
this._eventListeners.set(element, []);
}
this._eventListeners.get(element).push({eventType, listener});
}
/**
* @description Removes all the event listeners that were added using addEventListener.
* @member TComponents.Component_A#removeAllEventListeners
* @method
* @protected
* @returns {void}
*/
removeAllEventListeners() {
if (!this._eventListeners) return;
this._eventListeners.forEach((listeners, element) => {
listeners.forEach(({eventType, listener}) => {
element.removeEventListener(eventType, listener);
});
});
this._eventListeners.clear();
}
/**
* @description Clean up before initializing. Relevant from the second time the component is initialized.
* - Call onDestroy method
* - Call return function from onInit method if it exists
* - Detach the component from the parent element
* - Remove all local events, like this.on('event', callback)
* - Remove all event listeners attached with this.addEventListener
* @member TComponents.Component_A#destroy
* @method
* @returns {void}
*/
destroy() {
// calling instance specific onDestroy method
try {
this.trigger('before:destroy', this);
this.onDestroy();
} catch (error) {
Logger.w(logModule, ErrorCode.FailedToDestroyComponent, 'Failed to destroy component', error);
}
// clean reference to attached callbacks
// Before the component is destroyed, it should clean up all events.
this.cleanUpEvents([]);
// deinstall function (returned by onInit method)
if (this._deinstallFunction && typeof this._deinstallFunction === 'function') this._deinstallFunction();
if (this.container.parentElement) this.container.parentElement.removeChild(this.container);
this.removeAllEventListeners();
if (this.child) {
Object.keys(this.child).forEach((key) => {
if (Component_A.isTComponent(this.child[key])) this.child[key].destroy();
});
}
}
/**
* @description This method is called internally during clean up process orchestrated by {@link destroy() destroy}.
* @member TComponents.Component_A#onDestroy
* @method
* @abstract
* @async
*/
onDestroy() {}
/**
* @description Static method to check if an object is a TComponent.
* @member TComponents.Component_A.isTComponent
* @static
* @param {object} obj - The object to check.
* @returns {boolean} True if the object is a TComponent, false otherwise.
*/
static isTComponent(obj) {
return obj && obj._isTComponent ? true : false;
}
/**
* @description Compares the properties of two objects to determine if they are equal.
* @member TComponents.Component_A._equalProps
* @static
* @private
* @param {object} newProps
* @param {object} prevProps
* @returns {boolean}
*/
static _equalProps(newProps, prevProps) {
// use JSON.stringify with helper function to convert function to string to compare objects
const stringify = (obj) => {
return JSON.stringify(obj, (key, value) => {
if (Component_A.isTComponent(value)) {
return value.compId;
}
if (typeof value === 'function') {
return value.toString();
}
return value;
});
};
return stringify(newProps) === stringify(prevProps);
}
/**
* @description Changes the DOM element in which this component is to be inserted.
* @member TComponents.Component_A#attachToElement
* @method
* @param {HTMLElement | Element} element - Container DOM element
* @returns {void}
*/
attachToElement(element) {
if (!Component_A._isHTMLElement(element)) {
Logger.w(logModule, ErrorCode.FailedToAttachToElement, 'Element to be attached is not HTML element');
return;
}
// throw new Error(`HTML element container required but not detected`);
if (!this.parent) {
this.parent = element;
this.parent.appendChild(this.container);
} else if (this.parent === element) {
// only add if it not already exists
this.parent.contains(this.container) === false && this.parent.appendChild(this.container);
} else {
// remove from old parent to attach to new one
this.parent.contains(this.container) && this.parent.removeChild(this.container);
this.parent = element;
this.parent.appendChild(this.container);
}
}
/**
* @description Returns the first Element within the component that matches the specified selector. If no matches are found, null is returned.
* @member TComponents.Component_A#find
* @method
* @param {string} selector - A string containing one selector to match. This string must be a valid CSS selector string
* @returns {HTMLElement | Element} An Element object representing the first element within the component that matches the specified set of CSS selectors, or null if there are no matches.
*/
find(selector) {
const el = this.template && this.template.content.querySelector(selector);
return el ? el : this.container.querySelector(selector);
}
/**
* @description Returns an Array representing the component's elements that matches the specified selector. If no matches are found, an empty Array is returned.
* @member TComponents.Component_A#all
* @method
* @param {string} selector - A string containing one selector to match. This string must be a valid CSS selector string
* @returns {Element[]} An Array of Elements that matches the specified CSS selector, or an empty array if there are no matches.
*/
all(selector) {
var aContainer = Array.from(this.container.querySelectorAll(selector));
var nlTemplate = this.template && this.template.content.querySelectorAll(selector);
if (nlTemplate) {
Array.from(nlTemplate).forEach(function (tElem) {
var isDuplicate = aContainer.some((cElem) => tElem === cElem);
if (!isDuplicate) {
aContainer[aContainer.length] = tElem;
}
});
}
return aContainer;
}
/**
* @description Initialize the state for component according to the "defaultState" property
* @member TComponents.Component_A~initState
* @method
* @private
* @param {string} defaultState - A string describing the initialization state, the value can be show_disable|show_enable|hide.
* @returns {void}
*/
initState(defaultState) {
const state = defaultState || this.props.defaultState;
if (!state) {
return;
}
switch (state) {
case 'show':
this.show();
break;
case 'hide':
this.hide();
break;
case 'enable':
this.enabled = true;
break;
case 'disabled':
this.enabled = false;
break;
case 'show_disable':
this.show();
this.enabled = false;
break;
case 'show_enable':
default:
this.show();
this.enabled = true;
break;
}
}
/**
* @description Changes visibility of the component to not show it in the view.
* @member TComponents.Component_A#hide
* @method
* @returns {void}
*/
hide() {
this.container.classList.add('tc-hidden');
}
/**
* @description Changes visibility of the component to show it in the view.
* @member TComponents.Component_A#show
* @method
* @returns {void}
*/
show() {
this.container.classList.remove('tc-hidden');
}
/**
* @returns {boolean}
*/
get hidden() {
return this.container.classList.contains('tc-hidden');
}
/**
* @description Changes apperance of the component (border and background color) to frame it or not.
* @member TComponents.Component_A#cssBox
* @method
* @param {boolean} enable - if true, the component is framed, if false, not frame is shown
* @returns {void}
*/
cssBox(enable = true) {
enable ? this.container.classList.add('tc-container-box') : this.container.classList.remove('tc-container-box');
}
/**
* @description Sets or returns the contents of a style declaration as a string.
* @member TComponents.Component_A#css
* @method
* @param {string|Array} properties - Specifies the content of a style declaration.
* E.g.: "background-color:pink;font-size:55px;border:2px dashed green;color:white;"
* @returns {string}
*/
css(properties) {
if (!properties) {
this.container.style.cssText = '';
return;
}
let s = '';
if (typeof properties === 'string') s = properties;
else if (Array.isArray(properties)) {
s = properties.join(';');
s += ';';
} else if (typeof properties === 'object') {
for (const [key, val] of Object.entries(properties)) {
s += `${key} : ${val};`;
}
}
this.container.style.cssText = s;
}
/**
* @description Adds a class to underlying element(s) containing the input selector
* @member TComponents.Component_A#cssAddClass
* @method
* @param {string} selector - CSS selector, if class: ".selector", if identifier: "#selector"
* @param {string | string[]} classNames - name of the class to appy (without dot)
* @param {boolean} [all] - if true it will apply the class to all selector found, otherwise it applies to the first one found
* @returns {void}
*/
cssAddClass(selector, classNames, all = false) {
if (!selector || !classNames) return;
let arrClassNames = Array.isArray(classNames) ? classNames : [...classNames.replace(/^\s/g, '').split(' ')];
// check if array is empty
if (arrClassNames.length === 0) return;
// filter out emmpty strings
arrClassNames = arrClassNames.filter((c) => c !== '');
if (selector === 'this') this.container.classList.add(...arrClassNames);
else {
const el = all ? this.all(selector) : this.find(selector);
if (el)
Array.isArray(el) ? el.forEach((el) => el.classList.add(...arrClassNames)) : el.classList.add(...arrClassNames);
}
}
/**
* @description Removes a class to underlying element(s) containing the input selector
* @member TComponents.Component_A#cssRemoveClass
* @method
* @param {string} selector - CSS selector, if class: ".selector", if identifier: "#selector"
* @param {string} classNames - name of the class to appy (without dot)
* @param {boolean} [all] - if true it will apply the class to all selector found, otherwise it applies to the first one found
* @returns {void}
*/
cssRemoveClass(selector, classNames, all = false) {
if (!selector || !classNames) return;
let arrClassNames = Array.isArray(classNames) ? classNames : [...classNames.replace(/^\s/g, '').split(' ')];
// check if array is empty
if (arrClassNames.length === 0) return;
// filter out emmpty strings
arrClassNames = arrClassNames.filter((c) => c !== '');
if (selector === 'this') this.container.classList.remove(...arrClassNames);
else {
const el = all ? this.all(selector) : this.find(selector);
if (el)
Array.isArray(el)
? el.forEach((el) => el.classList.remove(...arrClassNames))
: el.classList.remove(...arrClassNames);
}
}
/**
* @description Force a rerender when a component is handled inside the mapComponents method of a higher order component.
* Normally this happens only when the props has changed. If this function is called inside a component.
* @member TComponents.Component_A#forceUpdate
* @method
* @private
* @returns {void}
*/
forceUpdate() {
this._fUpdate = true;
}
/**
* @description Synchronizes the input data.
* This method checks whether the input data is valid and triggers synchronization
* using an external component. It handles errors gracefully and provides
* feedback through a popup if synchronization fails.
* @member TComponents.Component_A#syncInputData
* @method
* @async
* @param {any} value - The value to be synchronized. This can be of any type depending on the implementation.
* @throws {Error} If binding data is null or synchronization fails unexpectedly.
* @returns {Promise<boolean>} A promise that resolves to `true` if the synchronization is successful,
* or `false` if an error occurs or binding data is null.
*/
async syncInputData(value) {
if (this._props.inputVar && this._props.inputVar.func === Component_A.INPUTVAR_FUNC.SYNC) {
if (this._bindData !== null) {
await Component_A.syncData(value, this);
} else {
throw new Error(ErrorCode.FailedToSyncBindingData, {
cause: 'Binded input variable is empty',
});
}
} else if (this._props.inputVar && this._props.inputVar.func === Component_A.INPUTVAR_FUNC.CUSTOM) {
return true;
} else {
throw new Error(ErrorCode.FailedToSyncBindingData, {
cause: 'Binded input variable is not valid.',
});
}
}
/**
* @description Synchronizes the input data with a popup notification.
* @member TComponents.Component_A#syncInputDataWithPopup
* @method
* @param {any} value - The value to be synchronized.
* @throws {Error} If synchronization fails.
* @returns {Promise<void>}
*/
async syncInputDataWithPopup(value) {
try {
await this.syncInputData(value);
} catch (e) {
Logger.e(logModule, 'Failed to synchronize binding data for component', e);
Popup_A.danger(
`${ExceptionIdMap.FailedToSyncBindingData}-${Component_A.t(`framework:${ExceptionIdMap.FailedToSyncBindingData}.title`)}`,
Component_A.t(`framework:${ExceptionIdMap.FailedToSyncBindingData}.causes`),
e.cause,
);
throw new Error(ErrorCode.FailedToSyncBindingData, {
cause: 'Failed to synchronize binding data for component.',
});
}
}
/**
* @description Determines if the label should be positioned at the start (top or left).
* @member TComponents.Component_A#_labelStart
* @method
* @private
* @returns {boolean} True if the label should be at the start, false otherwise.
*/
_labelStart() {
return this._props.label && (this._props.labelPos.includes('top') || this._props.labelPos.includes('left'));
}
/**
* @description Determines if the label should be positioned at the end (bottom or right).
* @member TComponents.Component_A#_labelEnd
* @method
* @private
* @returns {boolean} True if the label should be at the end, false otherwise.
*/
_labelEnd() {
return this._props.label && (this._props.labelPos.includes('bottom') || this._props.labelPos.includes('right'));
}
/**
* @description Generates the markup for the component including the label if necessary.
* @member TComponents.Component_A#_markupWithLabel
* @method
* @private
* @returns {string} The HTML markup of the component.
*/
_markupWithLabel() {
const markup = this.markup(this);
return /*html*/ `
${markup}
`;
}
/**
* @description Handles and updates the internal data object with the provided data.
* @member TComponents.Component_A#_handleData
* @method
* @private
* @param {object} data - The data to handle and update.
* @returns {void}
*/
_handleData(data) {
if (data) {
if (!this._data) this._data = {};
Object.keys(data).forEach((key) => (this._data[key] = data[key]));
}
}
/**
* @description Creates a new template element and sets its innerHTML with the component's markup including the label.
* @member TComponents.Component_A#_createTemplate
* @method
* @private
* @returns {void}
*/
_createTemplate() {
this.template = document.createElement('template');
this.template.innerHTML = this._markupWithLabel();
}
/**
* @description Recursively search for property of an object and underlying objects of type TComponents and FPComponents.
* @member TComponents.Component_A._hasChildOwnProperty
* @method
* @static
* @private
* @param {object} obj
* @param {string} property
* @param {object[]} [result=[]] result - Array of objects already found during the recursively execution
* @returns {object[]} Array of objects found or empty array if nothing found
*/
static _hasChildOwnProperty(obj, property, result = []) {
if (typeof obj === 'object' && obj !== null && (Component_A.isTComponent(obj) || Component_A._isFPComponent(obj))) {
for (const val of Object.values(obj)) {
if (typeof val === 'object' && val !== null && val !== obj && !Component_A._isHTMLElement(val)) {
if (Object.prototype.hasOwnProperty.call(val, property)) {
result.push(val);
}
Component_A._hasChildOwnProperty(val, property, result);
}
}
}
return result;
}
/**
* @description Recursively check for instances of type TComponent and FPComponent
* @member TComponents.Component_A._hasChildComponent
* @method
* @static
* @returns {boolean} true if the object has child components, false otherwise
*/
static _hasChildComponent(obj, result = []) {}
/**
* @description Checks if the object is an instance of any FPComponent.
* @member TComponents.Component_A._isFPComponent
* @method
* @static
* @param {any} o
* @returns {boolean} true if the object is an FPComponent, false otherwise
*/
static _isFPComponent(o) {
return Object.values(FPComponents).some((FPComponent) => o instanceof FPComponent);
}
/**
* @description Check if an entry is HTML Element
* @member TComponents.Component_A._isHTMLElement
* @method
* @static
* @param {any} o
* @returns {boolean} true if entry is an HTMLElement, false otherwise
*/
static _isHTMLElement(o) {
return typeof HTMLElement === 'object'
? o instanceof HTMLElement //DOM2
: o && typeof o === 'object' && o !== null && o.nodeType === 1 && typeof o.nodeName === 'string';
}
/**
* @description Loads a CSS stylesheet from a string and inserts it into the document.
* This method uses a hash to avoid injecting the same CSS twice.
* @member TComponents.Component_A.loadCssClassFromString
* @method
* @static
* @param {string} css - The CSS string to load.
* @throws {Error} If the provided css argument is not a string.
* @returns {void}
*/
static loadCssClassFromString(css) {
if (typeof css !== 'string') throw new Error('css must be a string');
const existingStyles = document.querySelectorAll('style');
// Check if any existing <style> tag has the same CSS content
for (let style of existingStyles) {
if (style.innerHTML === css) {
// A matching <style> tag is found, so we don't need to insert a new one
return;
}
}
// No matching <style> tag found, proceed to insert a new one
const tComponentStyle = document.createElement('style');
tComponentStyle.innerHTML = css;
const ref = document.querySelector('script');
if (ref) {
ref.parentNode.insertBefore(tComponentStyle, ref);
}
}
/**
* @description Loads a unique CSS stylesheet from a string and inserts it into the document.
* This method uses a hash to avoid injecting the same CSS twice.
* @member TComponents.Component_A.loadCssClassFromStringUnique
* @method
* @static
* @param {string} css - The CSS string to load.
* @param {string} selector - The CSS selector to apply the styles to.
* @throws {Error} If the provided arguments are not a string.
* @returns {void}
*/
static loadCssClassFromStringUnique(css, selector) {
if (typeof css !== 'string') throw new Error('css must be a string');
if (!selector || typeof selector !== 'string') throw new Error('selector must be a string');
const existingStyle = document.querySelector(selector);
// Check if any existing <style> tag has the same CSS content
if (existingStyle) {
existingStyle.innerHTML = css;
} else {
// No matching <style> tag found, proceed to insert a new one
const tComponentStyle = document.createElement('style');
if (selector.startsWith('.')) {
tComponentStyle.setAttribute('class', selector.substring(1));
} else if (selector.startsWith('#')) {
tComponentStyle.setAttribute('id', selector.substring(1));
}
tComponentStyle.innerHTML = css;
const ref = document.querySelector('script');
if (ref) {
ref.parentNode.insertBefore(tComponentStyle, ref);
}
}
}
/**
* @description Returns markup based on a condition.
* @member TComponents.Component_A.mIf
* @method
* @static
* @param {boolean} condition - The condition to evaluate.
* @param {string} markup - The markup to return if the condition is true.
* @param {string} [elseMarkup=''] - The markup to return if the condition is false.
* @returns {string} The appropriate markup based on the condition.
*/
static mIf(condition, markup, elseMarkup = '') {
return condition ? markup : elseMarkup;
}
/**
* @description Maps an array to a string of markup.
* @member TComponents.Component_A.mFor
* @method
* @static
* @param {any[]} array - The array to map.
* @param {Function} markup - The function to generate markup for each item.
* @returns {string} The concatenated string of markup.
*/
static mFor(array, markup) {
return array.map((item, index) => markup(item, index)).join('');
}
/**
* @description Validates if the provided text can be interpreted as a boolean or number. (specific for switch,digitalled)
* @member TComponents.Component_A#validateText
* @method
* @protected
* @param {string|boolean|number} t - The text value to be validated.
* @returns {boolean} True if the text is valid, false otherwise.
*/
validateText(t) {
if (typeof t === 'boolean' || typeof t === 'number') {
return true;
} else if (typeof t === 'string') {
const lowerCaseText = t.trim().toLowerCase();
if (
lowerCaseText === '1' ||
lowerCaseText === 'true' ||
lowerCaseText === '0' ||
lowerCaseText === 'false' ||
lowerCaseText === ''
) {
return true;
} else {
return false;
}
}
return false;
}
/**
* @description Converts the provided text to a boolean value. (specific for switch,digitalled)
* @member TComponents.Component_A#convertDataToBool
* @method
* @protected
* @param {string|boolean|number} t - The text value to be converted.
* @returns {boolean} True if the text is true|TRUE|1, false otherwise.
*/
convertDataToBool(t) {
if (typeof t === 'boolean' || typeof t === 'number') {
return t === true || t === 1;
} else if (typeof t === 'string') {
const lowerCaseText = t.trim().toLowerCase();
return lowerCaseText === '1' || lowerCaseText === 'true';
}
return false;
}
/**
* @description Updates the active state based on the provided text value. (specific for switch,digitalled)
* @member TComponents.Component_A~_updateActiveFromText
* @method
* @private
* @param {string|boolean|number } t - The text value to be converted.
* @throws {Error} If the provided text is invalid.
* @returns {void}
*/
_updateActiveFromText(t) {
try {
if (typeof t === 'boolean') {
this.active = t;
} else if (typeof t === 'number') {
this.active = t === 1;
} else if (typeof t === 'string') {
const lowerCaseText = t.trim().toLowerCase();
if (lowerCaseText === '1' || lowerCaseText === 'true') {
this.active = true;
} else if (lowerCaseText === '0' || lowerCaseText === 'false') {
this.active = false;
} else if (lowerCaseText === '') {
// do nothing
} else {
throw new Error(ErrorCode.FailedToSetText, {
cause: 'Invalid text value',
});
}
} else {
this.active = false;
}
} catch (error) {
Logger.e(logModule, ErrorCode.FailedToSetText, `Invalid text data for component ${this.compId}.`, t);
this.active = false;
throw new Error(ErrorCode.FailedToSetText, {cause: error});
}
}
/**
* @description Updates the properties of the component.
* @member TComponents.Component_A#updateProps
* @method
* @param {object} [_newProps={}] - The new properties to merge with the current properties.
* @returns {void}
*/
updateProps(_newProps = {}) {
this.props = Object.assign(this.props, _newProps || this.defaultProps());
}
/**
* @description Handles component state changes based on resource and action.
* @member TComponents.Component_A.handleComponentOn
* @method
* @static
* @param {object} self - The component instance.
* @param {object} options - The options for the handler.
* @param {string} options.resource - The resource to monitor.
* @param {object} options.instance - The instance information.
* @param {string} options.state - The state to monitor.
* @param {string} action - The action to perform.
* @returns {void}
*/
static async handleComponentOn(self, {resource, instance, state}, action) {
const actions = {
disable: (condition) => (condition ? (self.enabled = false) : (self.enabled = true)),
hide: (condition) => (condition ? self.hide() : self.show()),
update: (condition) => {
// eslint-disable-next-line no-undef
condition && self.updateProps(updateProps);
},
};
const eventHandlers = {
OpMode: {
event: 'op-mode',
monitorFn: API.CONTROLLER.monitorOperationMode,
callback: monitorOpMode,
states: [API.CONTROLLER.OPMODE.Auto, API.CONTROLLER.OPMODE.ManualR],
},
Execution: {
event: 'execution-state',
monitorFn: API.RAPID.monitorExecutionState,
callback: monitorExecutionState,
states: [API.RAPID.EXECUTIONSTATE.Running, API.RAPID.EXECUTIONSTATE.Stopped],
},
Motor: {
event: 'controller-state',
monitorFn: API.CONTROLLER.monitorControllerState,
callback: monitorControllerState,
states: [API.CONTROLLER.STATE.MotorsOn, API.CONTROLLER.STATE.MotorsOff],
},
Error: {
event: 'controller-state',
monitorFn: API.CONTROLLER.monitorControllerState,
callback: monitorControllerState,
states: [API.CONTROLLER.STATE.SysFailure, API.CONTROLLER.STATE.EStop, API.CONTROLLER.STATE.GuardStop],
},
};
const handler = eventHandlers[resource];
if (handler) {
handler.states.forEach(async (s) => {
const cb = (state) => {
actions[action](state === s);
};
if (state === s) {
Component_A.globalEvents.on(handler.event, cb);
// trigger once the callback depending on the event
let currentState;
if (handler.event === 'op-mode') {
currentState = await RWS.Controller.getOperationMode();
} else if (handler.event === 'execution-state') {
currentState = await RWS.Rapid.getExecutionState();
} else if (handler.event === 'controller-state') {
currentState = await RWS.Controller.getControllerState();
}
cb(currentState);
}
});
if (Component_A.globalEvents.count(handler.event) <= 1) {
try {
await handler.monitorFn(handler.callback);
} catch (e) {
Popup_A.error(e, 'TComponents.Component_A');
}
}
}
}
/**
* @description Disables the component based on a condition.
* @member TComponents.Component_A.disableComponentOn
* @method
* @static
* @param {object} self - The component instance.
* @param {object} condition - The condition to evaluate.
* @returns {void}
*/
static disableComponentOn(self, condition) {
this.handleComponentOn(self, condition, 'disable');
}
/**
* @description Hides the component based on a condition.
* @member TComponents.Component_A.hideComponentOn
* @method
* @static
* @param {object} self - The component instance.
* @param {object} condition - The condition to evaluate.
* @returns {void}
*/
static hideComponentOn(self, condition) {
this.handleComponentOn(self, condition, 'hide');
}
/**
* @description Processes conditions and updates the component properties accordingly.
* @member TComponents.Component_A.conditionProcessing
* @method
* @static
* @param {object} self - The component instance.
* @param {any[]} states - The states to process.
* @returns {void}
*/
static conditionProcessing(self, states) {
// Defined namespace mapping to store
const namespaceMapping = {
signal: Signal,
variable: Rapid,
rapid: Rapid,
uas: UAS,
};
// Loop through each state and set up monitors for the conditions
const monitors = new Map(); // To avoid setting up multiple monitors for the same key
for (const state of states) {
state.condition.forEach((cond) => {
if (!monitors.has(`${cond.mode}-${cond.key}`)) {
monitors.set(`${cond.mode}-${cond.key}`, {
mode: cond.mode,
type: cond.type,
key: cond.key,
states: [],
});
}
if (!monitors.get(`${cond.mode}-${cond.key}`).states.find((s) => s.uuid === state.uuid)) {
monitors.get(`${cond.mode}-${cond.key}`).states.push(state);
}
});
}
// Build an array of monitor values in a way compatible
// Since Map.prototype.values() is not supported with older browsers
const monitorValuesArray = [];
monitors.forEach(function (v) {
monitorValuesArray.push(v);
});
monitorValuesArray.forEach((cond) => {
switch (cond.mode) {
case Signal: {
if (cond.key) {
const vs = cond.key.split('/');
const mappingKey = Store.generateMappingKey(Signal, vs);
API.SIGNALMONITOR.monitorDigitalSignal(
{
type: cond.type,
network: vs[1],
device: vs[2],
name: vs[3],
},
(vvv) => {
Store.getNamespace(Signal).setMapping(mappingKey, {
state: Success,
value: vvv,
});
triggerState(cond.states);
},
);
}
break;
}
case Variable:
case Rapid: {
var vs = cond.key.split('/');
const mappingKey = Store.generateMappingKey(Rapid, vs); // type,task,module,name
API.VARIABLEMONITOR.monitorVariable(
{
type: cond.type,
task: vs[1],
module: vs[2],
name: vs[3],
},
(vvv) => {
Store.getNamespace(Rapid).setMapping(mappingKey, {
state: Success,
value: vvv,
});
triggerState(cond.states);
},
);
break;
}
case UAS: {
if (cond.key) {
const grantKey = cond.key;
API.UAS.monitorUserGrant(grantKey, () => {
triggerState(cond.states);
});
}
break;
}
default:
break;
}
});
// Function to check all states and update component properties accordingly
function triggerState(states) {
for (const item of states) {
let pass = true;
for (var cond of item.condition) {
// mode:'signal' | 'variable',
// type:'bool' | 'num' | 'string',
// key:'ACOK' | 'Rapid/T_ROB1/BASE/n155',
const namespace = namespaceMapping[cond.mode];
const vs = cond.key.split('/');
const parameters = vs;
const mappingKey = Store.generateMappingKey(namespace, parameters);
const namespaceStore = Store.getNamespace(namespace);
const mapping = namespaceStore && namespaceStore.getMapping(mappingKey);
const {value: current_value = undefined} = mapping || {
state: undefined,
value: undefined,
};
let currentValue = current_value;
const triggerValue = cond.value;
if (vs.length > 4) {
// array with subindex or record with field, example: T_TOB1.BASE.bArray.1 or T_TOB1.BASE.rData.subfield
const indentifier = [cond.type, `${vs[1]}.${vs[2]}`].concat(vs.slice(3)); // formater: bool,T_TOB1.BASE, barray, 1
const {a: ap, r: rp, d: rd} = TComponents.Component_A.getDataStruct(indentifier, self);
let a = ap;
let r = rp;
const d = rd;
if (a.length > 0) {
currentValue = API.RAPID.parseRapidArrayValue(currentValue, a);
}
if (d) {
d.parseFromRapid(currentValue);
currentValue = d.getValueByNamePath(r);
}
}
if (!pass) break;
switch (cond.judgment) {
case '≠':
if (!(currentValue != triggerValue)) {
pass = false;
}
break;
case '>':
if (!(currentValue > triggerValue)) {
pass = false;
}
break;
case '<':
if (!(currentValue < triggerValue)) {
pass = false;
}
break;
case '≥':
if (!(currentValue >= triggerValue)) {
pass = false;
}
break;
case '≤':
if (!(currentValue <= triggerValue)) {
pass = false;
}
break;
default:
if (!(currentValue == triggerValue)) {
pass = false;
}
break;
}
}
if (pass) {
if (item.tips) {
Object.assign(item.props, {tips: item.tips});
}
self.updateProps(item.props);
switch (item.action) {
case 'show':
self.show();
break;
case 'hide':
self.hide();
break;
case 'enable':
self.enabled = true;
break;
case 'disabled':
self.enabled = false;
break;
case 'show_enable':
self.show();
self.enabled = true;
break;
case 'show_disable':
self.show();
self.enabled = false;
break;
}
}
}
}
}
/**
* @description Generates dynamic text based on the input type.
* @member TComponents.Component_A.dynamicText
* @method
* @static
* @param {string|number} text - The text to process.
* @param {object} self - The component instance.
* @returns {string|number} The processed text.
*/
static dynamicText(text, self) {
return Component_A.dynamicProperty(text, self, 'text');
}
/**
* @deprecated This interface is to be deprecated.
* If you need to perform multilingual parsing, please use {@link TComponent.tParse}.
* @description Generates dynamic value based on the input type.
* @member TComponents.Component_A.dynamicProperty
* @method
* @static
* @param {string|number} text - The text to process.
* @param {object} self - The component instance.
* @param {String} propName - The property to update.
* @returns {string|number} The processed text.
*/
static dynamicProperty(text, self, propName = 'text') {
if (!(typeof text == 'string' && text.trim() !== '')) {
return text;
}
//{{App.container1.a}}-----------webdata
if (text.startsWith('{{') && text.endsWith('}}')) {
const key = text.slice(2, -2);
self &&
API.WEBDATAMONITOR.monitorWebdata(key, (vvv) => {
self.updateProps({
[propName]: API.formatValue(vvv),
});
});
return text;
}
//$$digitalsignal.ManualMode$$------------signal
else if (text.startsWith('$$') && text.endsWith('$$')) {
const vs = text.slice(2, -2).split('.');
if (vs.length === 2) {
//digitalsignal.signal
self &&
API.SIGNALMONITOR.monitorSignal(
{
type: vs[0],
name: vs[1],
},
(vvv) => {
self.updateProps({
[propName]: API.formatValue(vvv),
});
},
);
} else if (vs.length === 4) {
//digitalsignal.network.device.signal
self &&
API.SIGNALMONITOR.monitorSignal(
{
type: vs[0],
network: vs[1],
device: vs[2],
name: vs[3],
},
(vvv) => {
self.updateProps({
[propName]: API.formatValue(vvv),
});
},
);
}
return text;
}
//@@tooldata|task1.module1|tooldata1@@----------rapid
else if (text.startsWith('@@') && text.endsWith('@@')) {
const vs = text.slice(2, -2).split('|');
var task_module = vs[1].split('.');
if (vs.length === 3) {
self &&
API.VARIABLEMONITOR.monitorVariable(
{
type: vs[0],
task: task_module[0],
module: task_module[1],
name: vs[2],
},
(vvv) => {
self.updateProps({
[propName]: API.formatValue(vvv),
});
},
);
} else if (vs.length > 3) {
// Trying to subscribe specific part of a RAPID variable
// @@robtarget|task1.module1|r1|trans|x@@----------rapid => subscribe the x part of the robtarget r1
const {a: ap, r: rp, d: rd} = TComponents.Component_A.getDataStruct(vs, self);
self &&
API.VARIABLEMONITOR.monitorVariable(
{
type: vs[0],
task: task_module[0],
module: task_module[1],
name: vs[2],
},
(vvv) => {
try {
let a = ap;
let r = rp;
const d = rd;
let v = API.formatValue(vvv);
if (a.length > 0) {
v = API.RAPID.parseRapidArrayValue(v, a);
}
if (d) {
d.parseFromRapid(v);
v = d.getValueByNamePath(r);
}
self.updateProps({
[propName]: API.formatValue(v),
});
} catch (e) {
Logger.e(
logModule,
ErrorCode.FailedToParseMonitoredData,
'Failed to parse monitored data with stored data structure.',
e,
);
Popup_A.danger(
`${ExceptionIdMap.FailedToParseMonitoredData}-${Component_A.t(`framework:${ExceptionIdMap.FailedToParseMonitoredData}.title`)}`,
Component_A.t(`framework:${ExceptionIdMap.FailedToParseMonitoredData}.causes`),
);
}
},
);
}
return text;
} else if (text.startsWith('!!') && text.endsWith('!!')) {
return Component_A.t(text.slice(2, -2));
} else {
return text || '';
}
}
/**
* @description Parse the specific value according to the bound text.
* @member TComponents.Component_A.resolveBindingExpression
* @method
* @static
* @param {string|number} text - The text to process.
* @param {object} self - The component instance.
* @param {String} propName - The property to update.
* @returns {string|number} The processed text.
*/
static resolveBindingExpression(text, self, propName = 'text') {
if (!(typeof text == 'string' && text.trim() !== '')) {
return text;
}
//{{App.container1.a}}-----------webdata
if (text.startsWith('{{') && text.endsWith('}}')) {
const key = text.slice(2, -2);
self &&
API.WEBDATAMONITOR.monitorWebdata(key, (vvv) => {
if (self[propName] !== undefined) {
self[propName] = API.formatValue(vvv);
return;
}
self.updateProps({
[propName]: API.formatValue(vvv),
});
});
return text;
}
//$$digitalsignal.ManualMode$$------------signal
else if (text.startsWith('$$') && text.endsWith('$$')) {
const vs = text.slice(2, -2).split('.');
if (vs.length === 2) {
//digitalsignal.signal
self &&
API.SIGNALMONITOR.monitorSignal(
{
type: vs[0],
name: vs[1],
},
(vvv) => {
if (self[propName] !== undefined) {
self[propName] = API.formatValue(vvv);
return;
}
self.updateProps({
[propName]: API.formatValue(vvv),
});
},
);
} else if (vs.length === 4) {
//digitalsignal.network.device.signal
self &&
API.SIGNALMONITOR.monitorSignal(
{
type: vs[0],
network: vs[1],
device: vs[2],
name: vs[3],
},
(vvv) => {
if (self[propName] !== undefined) {
self[propName] = API.formatValue(vvv);
return;
}
self.updateProps({
[propName]: API.formatValue(vvv),
});
},
);
}
return text;
}
//@@tooldata|task1.module1|tooldata1@@----------rapid
else if (text.startsWith('@@') && text.endsWith('@@')) {
const vs = text.slice(2, -2).split('|');
if (vs.length < 3) return text;
var task_module = vs[1].split('.');
if (vs.length === 3) {
self &&
API.VARIABLEMONITOR.monitorVariable(
{
type: vs[0],
task: task_module[0],
module: task_module[1],
name: vs[2],
},
(vvv) => {
if (self[propName] !== undefined) {
self[propName] = API.formatValue(vvv);
return;
}
self.updateProps({
[propName]: API.formatValue(vvv),
});
},
);
} else if (vs.length > 3) {
// Trying to subscribe specific part of a RAPID variable
// @@robtarget|task1.module1|r1|trans|x@@----------rapid => subscribe the x part of the robtarget r1
const {a: ap, r: rp, d: rd} = TComponents.Component_A.getDataStruct(vs, self);
self &&
API.VARIABLEMONITOR.monitorVariable(
{
type: vs[0],
task: task_module[0],
module: task_module[1],
name: vs[2],
},
(vvv) => {
try {
let a = ap;
let r = rp;
const d = rd;
let v = API.formatValue(vvv);
if (a.length > 0) {
v = API.RAPID.parseRapidArrayValue(v, a);
}
if (d) {
d.parseFromRapid(v);
v = d.getValueByNamePath(r);
}
if (self[propName] !== undefined) {
self[propName] = API.formatValue(v);
return;
}
self.updateProps({
[propName]: API.formatValue(v),
});
} catch (e) {
Logger.e(
logModule,
ErrorCode.FailedToParseMonitoredData,
'Failed to parse monitored data with stored data structure.',
e,
);
Popup_A.danger(
`${ExceptionIdMap.FailedToParseMonitoredData}-${Component_A.t(`framework:${ExceptionIdMap.FailedToParseMonitoredData}.title`)}`,
Component_A.t(`framework:${ExceptionIdMap.FailedToParseMonitoredData}.causes`),
);
}
},
);
}
return text;
} else if (text.startsWith('!!') && text.endsWith('!!')) {
if (self[propName] !== undefined) {
self[propName] = Component_A.t(text.slice(2, -2));
return;
}
self.updateProps({
[propName]: Component_A.t(text.slice(2, -2)),
});
return text;
} else {
return text || '';
}
}
/**
* Extracts data structure information from the variable string.
* @member TComponents.Component_A.getDataStruct
* @method
* @static
* @param {any} vs - The variable string parts.
* @param {any} self - The component instance.
* @returns {object} The extracted data structure information.
*/
static getDataStruct(vs, self) {
try {
const a = vs
.slice(2)
.filter((vvs) => {
return vvs.match(/^\d+/);
})
.map(Number);
const r = vs.slice(a.length + 3);
let dataStruct = self._props.dataStruct ? JSON.parse(self._props.dataStruct) : undefined;
let d;
if (dataStruct) {
d = API.RAPID.getRecordData(vs[0], '');
} else {
try {
//get system dataStructure
d = API.RAPID.getRecordData(vs[0], '');
dataStruct = API.RAPID.getDataStructure(d.type);
} catch (error) {
console.log(error);
Logger.w(logModule, 'No data structure stored or stored data structure is not valid.', error);
d = undefined;
}
}
if (d && dataStruct) {
d = d.fromJSON(dataStruct);
}
return {a, r, d};
} catch (e) {
Logger.e(logModule, ErrorCode.FailedToGetDataStructure, e);
throw ErrorCode.FailedToGetDataStructure;
}
}
/**
* @description Extracts binding data from a string.
* @member TComponents.Component_A.getBindData
* @method
* @static
* @param {string} strData - The string to extract data from.
* @param {object} self - The component instance.
* @returns {Object|null} The binding data or null if not applicable.
*/
static getBindData(strData, self) {
let bindData = null;
if (typeof strData !== 'string') {
return bindData;
}
// {{App.container1.a}} 标识webdata
if (strData.indexOf('{{') === 0 && strData.lastIndexOf('}}') === strData.length - 2) {
bindData = {
type: 'webdata',
key: strData.replace(/{{/g, '').replace(/}}/g, ''),
};
}
// $$digitalsignal.ManualMode$$ 标识signal
if (strData.indexOf('$$') === 0 && strData.lastIndexOf('$$') === strData.length - 2) {
const vs = strData.replace(/\$\$/g, '').split('.');
bindData = {
type: vs[0],
key: vs[vs.length - 1],
};
}
// @@tooldata|task1.module1|tooldata1@@
if (strData.indexOf('@@') === 0 && strData.lastIndexOf('@@') === strData.length - 2) {
const vs = strData.replace(/@@/g, '').split('|');
const variablePath = vs[1].split('.').concat([vs[2]]);
bindData = {
type: 'rapiddata',
dataType: vs[0],
task: variablePath[0],
module: variablePath[1],
name: variablePath[2],
};
// Trying to bind specific part of a RAPID variable
// @@robtarget|task1.module1|r1|trans|x@@----------rapid => bind the x part of the robtarget r1
if (vs.length > 3) {
try {
const {a: ap, r: rp, d: rd} = Component_A.getDataStruct(vs, self);
bindData.arrPath = ap;
bindData.recPath = rp;
bindData.dataStruct = rd;
} catch (e) {
Logger.e(logModule, ErrorCode.FailedToGetBindedVariable, e);
throw ErrorCode.FailedToGetBindedVariable;
}
}
}
return bindData;
}
/**
* @description Synchronizes data with a specified value.
* @member TComponents.Component_A.syncData
* @method
* @static
* @async
* @param {any} value - The value to synchronize.
* @param {object} self - The component instance.
* @returns {Promise<boolean>} A promise that resolves to true if synchronization is successful, otherwise false.
*/
static async syncData(value, self) {
// try {
if (self._bindData.type === 'webdata') {
await API.WEBDATAMONITOR.setWebdata(self._bindData.key, value);
}
if (self._bindData.type === 'digitalsignal') {
let numVar = value;
if (typeof value == 'boolean') numVar = value == true ? 1 : 0;
if (typeof value == 'string') numVar = Number(value);
if (isNaN(numVar)) {
throw new Error(ErrorCode.InvalidDataForSync);
}
await API.RWS.SIGNAL.setSignalValue(self._bindData.key, numVar);
}
if (self._bindData.type === 'groupsignal') {
let numVar = value;
if (typeof value == 'string') numVar = Number(value);
if (isNaN(numVar)) {
throw new Error(ErrorCode.InvalidDataForSync);
}
await API.RWS.SIGNAL.setSignalValue(self._bindData.key, numVar);
}
if (self._bindData.type === 'rapiddata') {
// Do not sync PERS if controller in running execution status
// let executionStatus = await RWS.Rapid.getExecutionState();
// const isRunning = executionStatus === RWS.Rapid.ExecutionStates.running;
if (self._bindData.arrPath || self._bindData.recPath) {
// try {
let rawValue = await API.RWS.RAPID.getVariableRawValue(
self._bindData.module,
self._bindData.name,
self._bindData.task,
);
const a = self._bindData.arrPath;
const r = self._bindData.recPath;
let d = self._bindData.dataStruct;
let v = rawValue;
// If subscribing to a specific part of a RECORD variable belongs to an ARRAY
// Need to parse the value of the ARRAY first
if (a && a.length > 0 && r && r.length > 0) {
v = API.RAPID.parseRapidArrayValue(v, a);
}
// If subscribing to a specific part of a RECORD variable belongs to an ARRAY
// Need to parse the value of the ARRAY first
if (r && r.length > 0) {
d.parseFromRapid(v);
d.setValueByNamePath(r, value);
value = d.getRapidValue();
}
if (a && a.length > 0) {
v = API.RAPID.generateRapidArrayValue(value, rawValue, a);
value = v;
}
// } catch (error) {
// throw new Error(ErrorCode.InvalidDataForSync);
// }
}
await API.RAPID.setVariableValue(self._bindData.module, self._bindData.name, value, self._bindData.task, {
syncPers: false,
});
}
// } catch (e) {
// throw new Error(ErrorCode.FailedToSyncBindingData);
// }
}
/**
* @description Generates a function template from a string.
* @member TComponents.Component_A.genFuncTemplate
* @method
* @static
* @param {string | Function | any} data - The string containing the function body.
* @param {object} self - The component instance.
* @returns {Function|null} The generated function or null if the input is not a valid function.
*/
static genFuncTemplate(data, self) {
try {
let tempFn = null;
if (typeof data === 'string') {
if (!data) return null;
eval(`tempFn = async function(...args){
${data}
}`);
} else if (typeof data === 'function') {
tempFn = data;
}
return typeof tempFn === 'function' ? tempFn.bind(self) : null;
} catch (e) {
const errorInst = new Error(ErrorCode.FailedToGenerateFunction, {
cause: `${JSON.stringify(e)}`,
});
appendDataToErrInstance(errorInst, {
msgParams: {name: 'onChange'},
severity: 'danger',
});
throw errorInst;
}
}
/**
* @description Generates a function template from a string and pop up dialog if error happens.
* @member TComponents.Component_A.genFuncTemplateWithPopup
* @method
* @deprecated Will remove this
* @static
* @param {any} data
* @param {any} self
*/
static genFuncTemplateWithPopup(data, self) {
try {
return this.genFuncTemplate(data, self);
} catch (e) {
if (ErrorCode.FailedToGenerateFunction == e.message) {
Popup_A.danger(
`${ExceptionIdMap.FailedToGenerateFunction}-${Component_A.t(`framework:${ExceptionIdMap.FailedToGenerateFunction}.title`, {name: 'onChange'})}`,
Component_A.t(`framework:${ExceptionIdMap.FailedToGenerateFunction}.causes`),
e.cause,
);
}
throw new Error(ExceptionIdMap.FailedToGenerateFunction);
}
}
/**
* @description Sets the language adapter for the component.
* @member TComponents.Component_A.setLanguageAdapter
* @method
* @static
* @param {object} adapter - The language adapter.
* @param {Function} adapter.t - The translation function.
* @throws {Error} If adapter.t is not a function.
* @returns {void}
*/
static setLanguageAdapter(adapter) {
if (typeof adapter.t !== 'function') throw new Error('func must be a function');
Component_A.languageAdapter = adapter;
}
/**
* @description Translates a key using the language adapter.
* @member TComponents.Component_A.t
* @method
* @static
* @param {string} key - The key to translate.
* @returns {string} The translated key.
*/
static t(key, params = {}) {
if (Component_A.languageAdapter && Component_A.languageAdapter.t) return Component_A.languageAdapter.t(key, params);
return key;
}
/**
* @description Parses translation keys and replaces them with their corresponding values.
* @member TComponents.Component_A.tParse
* @method
* @static
* @param {string} key - The key to translate.
* @returns {string} The translated value.
*/
static tParse(key) {
if (typeof key === 'string' && key.startsWith('!!') && key.endsWith('!!') && key.trim().length > 4) {
return Component_A.t(key.slice(2, -2));
}
return key;
}
/**
* @description Retrieve instance attributes from the global instance table based on the key value
* @member TComponents.Component_A.getInstanceProperty
* @method
* @static
* @param {string} key - The key name of the instance.
* @returns {Component_A} The target instance.
*/
static getInstanceProperty(key) {
if (window.Instance && window.Instance[key]) {
return window.Instance[key];
}
return null;
}
/**
* Enables or disables any FPComponent component (see Omnicore App SDK) declared within the component as an own property (e.g. this.btn = new FPComponent()).
* @returns {boolean}
* @see {@link https://developercenter.robotstudio.com/omnicore-sdk|Omnicore-SDK}
*/
get enabled() {
return this._enabled;
}
/**
* @description Enables or disables the component and all its child components.
* @member TComponents.Component_A#enabled
* @instance
* @param {boolean} en - The enabled state to set.
* @example
* this.enabled = true;
*/
set enabled(en) {
this._enabled = en;
//Support user enable all the children by api
const objects = Component_A._hasChildOwnProperty(this, '_enabled');
objects.forEach((o) => {
o.enabled = en;
});
if (this.child) {
if (this.child instanceof Array) {
this.child.forEach((c) => {
c.enabled = en;
});
} else if (this.child instanceof Object) {
for (const key in this.child) {
if (this.child[key] && Object.prototype.hasOwnProperty.call(this.child[key], '_enabled')) {
this.child[key].enabled = en;
}
}
}
}
this.container.setAttribute('enable', en);
}
/**
* @description Processes event execution error.
* @member TComponents.Component_A.processEventError
* @method
* @static
* @private
* @param {any} e Error to be processed.
* @param {string} eventName Event name.
*/
static processEventError(e, eventName) {
let causeArray = [Component_A.t(`framework:${ExceptionIdMap.FailedToExecuteEvent}.causes`), ''];
if (e instanceof Error) {
if (e.cause && e.cause.message) {
causeArray.push(String(e.cause.message));
} else {
causeArray.push(e);
}
} else if (typeof e === 'string') {
causeArray.push(e);
} else {
causeArray.push(String(e));
}
Popup_A.danger(
`${ExceptionIdMap.FailedToExecuteEvent}-${Component_A.t(`framework:${ExceptionIdMap.FailedToExecuteEvent}.title`, {name: eventName})}`,
causeArray,
);
}
/**
* To merge to processEventError if possible
* @param {*} e
* @param {*} eventName
*/
static popupEventError(e, eventName, _logModule = logModule) {
if (!e) {
Logger.e(_logModule, `Error happened in event ${eventName}, and the error object is empty or undefined.`);
return;
}
const errorCode = e.message || (e instanceof Error ? e.message : String(e));
const exceptionId = getExceptionIdByErrorCode(errorCode);
const popFunc = e.severity === 'warning' ? Popup_A.warning : Popup_A.danger;
const loggerFunc = e.severity === 'warning' ? Logger.w : Logger.e;
const extraMsg = e.description || '';
// handle the case when the error is known with translation
if (exceptionId) {
if (extraMsg) {
loggerFunc(_logModule, `The error happened in event ${eventName}: `, e, ` Extra message: ${extraMsg}`);
} else {
loggerFunc(_logModule, `The error happened in event ${eventName}: `, e);
}
const causes = [];
if (
e &&
e.cause &&
e.cause.controllerStatus &&
e.cause.controllerStatus.code &&
checkIfKnownRWSError(e.cause.controllerStatus.code)
) {
// handle the known error from omnicore-app sdk
// if there is specific cause for the error, use it, otherwise just show the RWS error code
if (Component_A.languageAdapter.exists(`framework:${exceptionId}.causes`)) {
causes.push(Component_A.t(`framework:${exceptionId}.causes`));
causes.push(JSON.stringify(e.cause));
} else {
causes.push(Component_A.t(`framework:${e.cause.controllerStatus.code}`));
}
} else {
// TODO: in case there is no "causes", in case it is from RWS directly (should be same handling as omnicore-app sdk)
// TODO: remove below hard code caused by the Masterhip handling in Omnicore-app SDK
if (e && e.cause && e.cause.message && e.cause.message.includes('Failed to get Mastership.')) {
if (typeof window.appSpocWriteAccessRequired === 'function') {
// RW8 + Masterhip related error, ignore because appSpocWriteAccessRequired will handle it
return;
} else {
// RW7 + Masterhip related error, show passed specific message directly
causes.push(JSON.stringify(e.cause));
}
} else {
if (Component_A.languageAdapter.exists(`framework:${exceptionId}.causes`)) {
causes.push(Component_A.t(`framework:${exceptionId}.causes`));
} else if (e && e.cause) {
// no translated cause available, just show the cause directly if it exists
causes.push(JSON.stringify(e.cause));
} else {
causes.push('No specific cause available for this error.');
}
}
}
const msgParams = e.msgParams || {};
popFunc(
`${exceptionId}-${Component_A.t(`framework:${exceptionId}.title`, Object.assign({}, msgParams))}`,
causes,
);
return;
}
// handle the case where error code is not handled by users and captured by event
if (extraMsg) {
loggerFunc(
_logModule,
`The error happened in event ${eventName}, and the error code is ${errorCode}. Extra message: ${extraMsg}. `,
e,
);
} else {
loggerFunc(_logModule, `The error happened in event ${eventName}, and the error code is ${errorCode}`, e);
}
popFunc(
`${ExceptionIdMap.FailedToExecuteEvent}-${Component_A.t(`framework:${ExceptionIdMap.FailedToExecuteEvent}.title`, {name: eventName})}`,
[
Component_A.t(`framework:${ExceptionIdMap.FailedToExecuteEvent}.causes`),
e.cause
? JSON.stringify(e.cause)
: typeof e === 'string'
? e
: e instanceof Error
? e.message
: JSON.stringify(e),
],
);
}
}
/**
* @description language adapter for the Component_A class.
* @member {object} TComponents.Component_A#languageAdapter
* @instance
*/
Component_A.languageAdapter = null;
/**
* @description Global event handler for the Component_A class.
* @member {Eventing_A} TComponents.Component_A#globalEvents
* @instance
*/
Component_A.globalEvents = new Eventing_A();
/**
* @description Enum-like object that defines various input variable types.
* These types are used to categorize the types of input data that can be handled by the component.
* @member {object} TComponents.Component_A#INPUTVAR_TYPE
* @instance
* @property {string} NUM - Represents a numeric input type.
* @property {string} ANY - Represents a generic input type, not limited to a specific data type.
* @property {string} BOOL - Represents a boolean input type (true/false).
* @property {string} STRING - Represents a string input type.
*/
Component_A.INPUTVAR_TYPE = {
NUM: 'num',
ANY: 'any',
BOOL: 'bool',
STRING: 'string',
};
/**
* @description Enum-like object that defines various input variable handling strategies.
* These strategies specify how input variables are processed within the component.
* @member {object} TComponents.Component_A#INPUTVAR_FUNC
* @instance
* @property {string} CUSTOM - A custom processing strategy for input variables.
* @property {string} SYNC - A synchronous processing strategy for input variables.
*/
Component_A.INPUTVAR_FUNC = {
CUSTOM: 'custom',
SYNC: 'sync',
};
/**
* @description Monitors and triggers the operation mode event.
* @async
* @param {string} value - The operation mode to set.
*/
const monitorOpMode = async (value) => {
Component_A.globalEvents.trigger('op-mode', value);
};
/**
* @description Monitors and triggers the execution state event.
* @async
* @param {string} value - The execution state to set.
*/
const monitorExecutionState = async (value) => {
Component_A.globalEvents.trigger('execution-state', value);
};
/**
* @description Monitors and triggers the controller state event.
* @async
* @param {string} value - The controller state to set.
*/
const monitorControllerState = async (value) => {
Component_A.globalEvents.trigger('controller-state', value);
};
/**
* @description Maximum allowed gap in rem units.
* @constant {number}
*/
const maxGap = 16;
/**
* @description Maximum allowed padding in rem units.
* @constant {number}
*/
const maxPadding = 16;
/**
* @description Maximum allowed margin in rem units.
* @constant {number}
*/
const maxMargin = 16;
/**
* @description Generates CSS styles for padding classes.
* @returns {string} - The generated CSS styles for padding.
*/
const generatePaddingStyles = () => {
let styles = '';
for (let i = 1; i <= maxPadding; i++) {
const paddingValue = (i * 0.25).toFixed(2); // Calculate padding value based on class number.
styles += `
.pl-${i} { padding-left: ${paddingValue}rem; /* ${i * 4}px */ }
.pr-${i} { padding-right: ${paddingValue}rem; /* ${i * 4}px */ }
.pt-${i} { padding-top: ${paddingValue}rem; /* ${i * 4}px */ }
.pb-${i} { padding-bottom: ${paddingValue}rem; /* ${i * 4}px */ }
.px-${i} { padding-left: ${paddingValue}rem; padding-right: ${paddingValue}rem; /* ${i * 4}px */ }
.py-${i} { padding-top: ${paddingValue}rem; padding-bottom: ${paddingValue}rem; /* ${i * 4}px */ }
`;
}
return styles;
};
/**
* @description Generates CSS styles for margin classes.
* @returns {string} - The generated CSS styles for margin.
*/
function generateMarginStyles() {
let styles = '';
for (let i = 1; i <= maxMargin; i++) {
const value = i * 0.25;
styles += `
.ml-${i} { margin-left: ${value}rem; /* ${i * 4}px */ }
.mr-${i} { margin-right: ${value}rem; /* ${i * 4}px */ }
.mt-${i} { margin-top: ${value}rem; /* ${i * 4}px */ }
.mb-${i} { margin-bottom: ${value}rem; /* ${i * 4}px */ }
.mx-${i} { margin-left: ${value}rem; margin-right: ${value}rem; /* ${i * 4}px */ }
.my-${i} { margin-top: ${value}rem; margin-bottom: ${value}rem; /* ${i * 4}px */ }
`;
}
return styles;
}
/**
* @description Generates CSS styles for gap classes.
* @returns {string} - The generated CSS styles for gap.
*/
function generateGapStyles() {
let styles = '';
for (let i = 0; i <= maxGap; i++) {
const value = i * 0.25;
styles += `
.flex-row.gap-${i} > * + * { margin-left: ${value}rem; /* ${i * 4}px */ }
.flex-col.gap-${i} > * + * { margin-top: ${value}rem; /* ${i * 4}px */ }
`;
}
return styles;
}
/**
* Loads CSS classes from a string and applies them to the Component_A.
* @param {string} cssString - The CSS string to be loaded.
*/
Component_A.loadCssClassFromString(/*css*/ `
${generatePaddingStyles()}
${generateMarginStyles()}
${generateGapStyles()}
`);