import {Component_A} from './basic/as-component.js';
import {FP_Radio_A} from './fp-ext/fp-radio-ext.js';
import {formatOptionsString, generateOptionsString, initDynamicOptions} from './utils/utils.js';
import {ErrorCode} from '../exception/exceptionDesc.js';
/**
* @typedef TComponents.RadioProps
* @prop {object} [options] Additional options for the radio component.
* Items in this object include:
* - **responsive** (boolean, default: false): Whether the radio group should be responsive.
* @prop {string} [tips] Tooltip text for the component.
* @prop {Function} [onCreated] Function to be called when the component is created.
* @prop {Function} [onMounted] Function to be called when the component is mounted.
* @prop {Function} [onChange] Callback function that is called when the selected radio option changes.
* @prop {string} [position] CSS position property.
* @prop {number} [width] Width of the component.
* @prop {number} [height] Height of the component.
* @prop {number} [top] Top position of the component.
* @prop {number} [left] Left position of the component.
* @prop {number} [rotation] Rotation angle of the component.
* @prop {number} [zIndex] Z-index of the component.
* @prop {object} [functionality] Configuration object for component functionality.
* This object contains settings to control the component's behavior:
* - **types** (Array<string>): A list of functionality types enabled for the component.
* - **params** (Array<object>): Additional parameters, each containing `type`, `variablePath`, `isHidden`.
* @prop {string} [color] Text color of the component.
* @prop {object} [font] Font settings for the radio labels.
* This object controls text appearance:
* - **fontSize** (number, default: 12): Font size in pixels.
* - **fontFamily** (string, default: 'Segoe UI'): Font family name.
* - **style** (object): Font style configuration, containing `fontStyle`, `fontWeight`, `textDecoration`.
* @prop {string} [optionItems] Radio options in the format `text1|value1;text2|value2;text3|value3`.
* @prop {object} [optionConfig] Configuration for dynamic radio options.
* This object controls how options are provided:
* - **mode** (string, default: 'fixed'): Options mode, e.g. 'fixed', 'sync', 'initialize'.
* - **type** (string): Type of the options data source.
* - **isHidden** (boolean, default: false): Whether this option configuration is hidden.
* - **variablePath** (string): Variable path used when options are dynamic.
* @prop {string} [defaultState] Initial state of the component.
* @prop {string} [text] Current selected option value, corresponds to the `value` field in `optionItems`.
* @prop {number} [selectedIndex] Index of the initially selected option, `-1` means no option is selected.
* @prop {object} [inputVar] Input variable binding configuration.
* This object configures the bound input variable:
* - **type** (string): Binding variable type. Default is `Component_A.INPUTVAR_TYPE.ANY`.
* - **func** (string): Binding mode. Default is `Component_A.INPUTVAR_FUNC.CUSTOM`.
* - **value** (string): Initial value or variable path used for binding.
* - **isHidden** (boolean, default: false): Whether this binding is hidden in variable selectors.
* @prop {string} [dataStruct] Data structure associated with the component.
* @memberof TComponents
*/
/**
* @ignore
*/
const logModule = 'as-Radio';
/**
* @description Additional callbacks can be added with the {@link TComponents.Radio#onChange|onChange} method.
* This class focuses on the specific properties of the Radio component.
* Since it inherits from Accessor_A, all basic properties (e.g., height, width) are available but documented in the Accessor_A part.
* @class TComponents.Radio
* @extends TComponents.Component_A
* @memberof TComponents
* @param {HTMLElement} parent - HTML element that is going to be the parent of the component
* @param {TComponents.RadioProps} props - Properties to initialize the Radio component
* @example
* const radio = new TComponents.Radio(document.body, {
* position: 'absolute',
* zIndex: 1000,
* optionItems: 'Option 1|value1;Option 2|value2;Option 3|value3',
* selectedIndex: 0,
* });
*
* // Render the component
* await radio.render();
*/
export class Radio extends Component_A {
constructor(parent, props = {}) {
super(parent, props);
/**
* @instance
* @private
* @type {TComponents.RadioProps}
*/
this._props;
this.beforOnChangeFuncList = [];
// this.initPropsDep(['optionItems']);
}
/**
* @description Returns the default values of class properties (excluding parent properties).
* @member TComponents.Radio#defaultProps
* @method
* @protected
* @returns {TComponents.RadioProps}
*/
defaultProps() {
return {
options: {
responsive: false,
},
tips: '',
// life cycle
onCreated: '',
onMounted: '',
onDispose: '',
onChange: '',
// X/Y/W/H/B/R
position: 'static',
width: 320,
height: 32,
top: 0,
left: 0,
rotation: 0,
zIndex: 0,
// Function
functionality: {
types: [],
params: [
{type: '', variablePath: '', isHidden: false},
{type: 'num', variablePath: '', isHidden: false},
],
},
// layout
// layout: {
// isHorizontal: true,
// itemWidth: 0,
// },
// font color
color: '#000000',
// font style
font: {
fontSize: 12,
fontFamily: 'Segoe UI',
style: {
fontStyle: 'normal',
fontWeight: 'normal',
textDecoration: 'none',
},
},
// Data
optionItems: `text1|value1;text2|value2;text3|value3`,
optionConfig: {
mode: 'fixed', //fixed, sync,initialize
type: '',
isHidden: false,
variablePath: '',
},
defaultState: 'show_enable',
// sync property
text: '',
selectedIndex: -1,
inputVar: {
type: Component_A.INPUTVAR_TYPE.ANY,
func: Component_A.INPUTVAR_FUNC.CUSTOM,
value: '0',
isHidden: false,
},
dataStruct: '',
};
}
/**
* @description Initializes the radio component.
* @member TComponents.Radio#defaultProps
* @method
* @async
* @returns {Promise<void>}
*/
async onInit() {
//if user set selectedIndex but not set text, set text to the value of selectedIndex
// so the searchedIndex can take effect at the first time, and override the text property
if (this._props.selectedIndex > -1 && this._props.text == '') {
const targetItem = this.optionItems[this._props.selectedIndex];
if (targetItem) {
this.commitProps({text: targetItem.value});
}
}
}
/**
* @description there are something need to do after render once
* @member TComponents.Radio#afterRenderOnce
* @method
* @protected
* @returns {void}
*/
afterRenderOnce() {
if (this._props.inputVar.func == 'sync') {
this._bindData = Component_A.getBindData(this._props.inputVar.value, this);
}
initDynamicOptions(this._props.optionConfig, (data) => {
this.setProps({
optionItems: generateOptionsString(data),
});
});
}
/**
* @description Returns an object containing the components mapped to their identifiers.
* As one of the methods of component lifecycle,
* **we do not recommend that users call this function manually.**
* @member TComponents.Radio#mapComponents
* @method
* @returns {object} An object mapping identifiers to components.
*/
mapComponents() {
return {};
}
/**
* Maps the internal components.
* @returns {Object} The mapped components
*/
async _mapComponents() {
// Special case, the `this.child` within the lifecycle is only initialized once during construction and will not be dynamically added or removed afterward.
// Here, we will use external properties to control the dynamic addition and removal of `child`. Therefore, to ensure that it is assigned each time, `this.child` must be set to null once.
// This code is reflected in the `initChildrenComponents` function of `Component_A`.
const child = [];
for (let i = 0; i < this.optionItems.length; i++) {
let radio = new FP_Radio_A();
radio._desc = this.optionItems[i].text;
radio.props = this._props;
radio.font = this._props.font;
radio.color = this._props.color;
radio.checked = this.optionItems[i].value == this._props.text ? true : false;
radio.onclick = this._onChange.bind(this, i);
radio.enabled = this.enabled;
child.push(radio);
}
return child;
}
/**
* @description Maps the internal components.
* @member TComponents.Radio#groupComponents
* @method
* @protected
* @returns {array} The mapped components
*/
groupComponents() {
// Special case, the `this.child` within the lifecycle is only initialized once during construction and will not be dynamically added or removed afterward.
// Here, we will use external properties to control the dynamic addition and removal of `child`. Therefore, to ensure that it is assigned each time, `this.child` must be set to null once.
// This code is reflected in the `initChildrenComponents` function of `Component_A`.
const child = [];
for (let i = 0; i < this.optionItems.length; i++) {
let radio = new FP_Radio_A();
radio._desc = this.optionItems[i].text;
radio.props = this._props;
radio.font = this._props.font;
radio.color = this._props.color;
radio.checked = this.optionItems[i].value == this._props.text ? true : false;
radio.onclick = this._onChange.bind(this, i);
radio.enabled = this.enabled;
child.push(radio);
}
return child;
}
/**
* @description Renders the radio component.
* @member TComponents.Radio#onRender
* @method
* @async
* @throws {Error} Throws an error if rendering fails.
* @returns {Promise<void>}
*/
async onRender() {
try {
this.removeAllEventListeners();
const radioContainer = this.find('.fp-components-radio-group');
if (radioContainer) {
// radioContainer.style.gap = `10px`;
radioContainer.classList.add('horizontal-layout');
}
this.child = this.groupComponents();
for (let i = 0; i < this.child.length; i++) {
this.child[i].attachToElement(radioContainer);
}
this._updateChildStatus();
this._addTips();
Component_A.resolveBindingExpression(this._props.text, this, 'value');
} catch (e) {
// Runtime errors: write specific content to the log, throw error code
Logger.e(
logModule,
ErrorCode.FailedToRenderComponent,
`Failed to render Radio component with id ${this.compId}`,
e,
);
throw new Error(ErrorCode.FailedToRunOnRender, {cause: e});
}
}
/**
* @description Generates the markup for the radio component.
* @member TComponents.Radio#markup
* @method
* @returns {string} HTML markup string
*/
markup() {
return /*html*/ `
<div class="tc-radio">
<div class="fp-components-radio-group"></div>
</div>
`;
}
/**
* @description Callback function which is called when the button is pressed, it triggers any function registered with {@link TComponents.Radio#onChange|onChange}.
* @member TComponents.Radio~_onChange
* @method
* @private
* @async
* @param {Number} index - The index of the clicked radio component.
* @returns {Promise<void>}
*/
async _onChange(index) {
if (this.enabled) {
const beforeValue = this.value;
try {
const value = this.optionItems[index].value;
await this.syncInputData(value);
this.value = value;
} catch (e) {
this.value = beforeValue;
Component_A.popupEventError(e, 'syncInputData', logModule);
return;
}
try {
var fn = Component_A.genFuncTemplate(this._props.onChange, this);
fn && (await fn(index));
} catch (e) {
Component_A.popupEventError(e, 'onChange', logModule);
}
}
}
/**
* @description Updates the checked status of child radio components based on the current text property.
* @member TComponents.Radio~_updateChildStatus
* @method
* @private
*/
_updateChildStatus() {
const optionItems = this.optionItems;
for (let i = 0; i < optionItems.length; i++) {
this.child[i].checked = optionItems[i].value.toLowerCase() === this._props.text.toLowerCase();
}
}
/**
* @description Sets the text of the selected radio option.
* @member {string} TComponents.Radio#text
* @instance
* @param {string} t - The text of the radio option to be selected.
* @example
* const radio = new TComponents.Radio(document.body, {
* position: 'absolute',
* zIndex: 1000,
* optionItems: 'Option 1|value1;Option 2|value2;Option 3|value3',
* selectedIndex: 0,
* });
*
* // Render the component
* await radio.render();
*
* radio.text = 'value2';
*/
set text(t) {
// this._updateChildStatus();
const optionItems = this.optionItems;
const current = optionItems.find((item) => item.value === t);
if (current) {
this.value = current.value;
}
}
/**
* @returns {string}
*/
get text() {
const selectedItem = this.optionItems.find((item) => item.value.toLowerCase() === this._props.text.toLowerCase());
if (selectedItem) {
return selectedItem.text;
}
return '';
}
/**
* This attribute is used to set the text of the Radio component.
* When you set this attribute, the component will attempt to synchronize the new text value with any bound variables or data sources.
* @member {string} TComponents.Radio#setText
* @method
* @param {string} text
* @example
* const radio = new TComponents.Radio(document.body, {
* position: 'absolute',
* zIndex: 1000,
* });
*
* // Render the component.
* radio.render();
*
* radio.setText(0);
*/
async setText(text) {
const optionItems = this.optionItems;
const current = optionItems.find((item) => item.value === text);
if (current) {
await this.syncInputData(current.value);
this.value = current.value;
}
}
/**
* @description Sets the value of the selected radio option.
* @member {string} TComponents.Radio#value
* @instance
* @param {string} v - The value of the radio option to be selected.
* @example
* const radio = new TComponents.Radio(document.body, {
* position: 'absolute',
* zIndex: 1000,
* optionItems: 'Option 1|value1;Option 2|value2;Option 3|value3',
* selectedIndex: 0,
* });
*
* // Render the component
* await radio.render();
*
* radio.value = 'value2';
*/
set value(v) {
this.commitProps({text: v});
this._updateChildStatus();
}
/**
* @description sets the index of the currently selected option.
* @member {number}TComponents.Radio#selectedIndex
* @instance
* @param {number} index - The index of the option to be selected.
* @example
* const radio = new TComponents.Radio(document.body, {
* position: 'absolute',
* zIndex: 1000,
* optionItems: 'Option 1|value1;Option 2|value2;Option 3|value3',
* selectedIndex: 0,
* });
*
* // Render the component
* await radio.render();
*
* radio.selectedIndex = 0;
*/
set selectedIndex(index) {
const targetItem = this.optionItems[index];
if (targetItem) {
this.value = targetItem.value;
} else {
throw new Error(ErrorCode.FailedToUpdateComponentWithSetter, {cause: 'Error during set the index out of range'});
}
}
/**
* @returns {number}
*/
get selectedIndex() {
return this.optionItems.findIndex((item) => item.value.toLowerCase() === this._props.text.toLowerCase());
}
/**
* @returns {string}
*/
get value() {
// return this.text;
const selectedItem = this.optionItems.find((item) => item.value.toLowerCase() === this._props.text.toLowerCase());
if (selectedItem) {
return selectedItem.value;
}
return '';
}
/**
* @returns {Function|undefined}
*/
get onChange() {
try {
var fn = Component_A.genFuncTemplate(this._props.onChange, this);
} catch (e) {
return undefined;
}
if (typeof fn == 'function') return fn;
else return undefined;
}
/**
* @description Sets the `onChange` event handler.
* The handler can either be a string representing a function to be executed or a function itself.
* 1. If you are using an arrow function, like `()=>{}`,
* the `this` property of the scope may not refer to the radio object.
* 2. If you are using string assignment to define code execution,
* the string should contain `only the body of the code (executable statements)`,
* not a complete function declaration. Therefore, including function keywords like function or async function is incorrect.
* - Correct (Statements Only): `xx.onChange = "console.log('Action done.');"`
* - Incorrect (Function Declaration): `xx.onChange = "function() { console.log('Action done.'); }"`
* @member {Function} TComponents.Radio#onChange
* @param {Function} t
* @instance
* @example
* const radio = new TComponents.Radio(document.body, {
* position: 'absolute',
* zIndex: 1000,
* optionItems: 'Option 1|value1;Option 2|value2;Option 3|value3',
* selectedIndex: 0,
* });
*
* await radio.render();
*
* // Example 1: Using a string as the handler:
* radio.onChange = "console.log('state changed', this.selectedIndex);";
* @example
* // Example 2: Using a arrow function as the handler:
* // Note that the `this` context will not refer to the radio object
* radio.onChange = () => { console.log('state changed', radio.selectedIndex); };
* @example
* // Example 3: Using a common function as the handler:
* radio.onChange = async function() {
* console.log('state changed', this.selectedIndex);
* };
*/
set onChange(t) {
this.setProps({onChange: t});
}
/**
* @returns {object[]}
*/
get optionItems() {
const data = formatOptionsString(this._props.optionItems);
for (let i = 0; i < data.length; i++) {
data[i].text = Component_A.tParse(data[i].text.replace(/^\n+/, ''));
}
return data;
}
/**
* @description Sets the option items from a formatted string.
* @member {string} TComponents.Radio#optionItems
* @instance
* @param {string} itemsString - The formatted string of option items.
* @example
* const radio = new TComponents.Radio(document.body, {
* position: 'absolute',
* zIndex: 1000,
* optionItems: 'Option 1|value1;Option 2|value2;Option 3|value3',
* selectedIndex: 0,
* });
*
* // Render the component
* await radio.render();
*
* radio.optionItems = "Option 4|value4;Option 5|value5;Option 6|value6";
*/
set optionItems(itemsString) {
const optionsConfig = this._props.optionConfig;
if (optionsConfig && optionsConfig.mode == 'fixed') {
this.setProps({
optionItems: itemsString,
});
} else {
throw new Error(ErrorCode.FailedToUpdateComponentWithSetter, {
cause: 'The optionConfig is not in fixed mode, can not set optionItems',
});
}
}
}
/**
* @description Add css properties to the component
* @member TComponents.Radio.loadCssClassFromString
* @method
* @static
* @param {string} css - The css string to be loaded into style tag
* @returns {void}
* @example
* TComponents.Radio.loadCssClassFromString(`
* .tc-radio {
* height: inherit;
* }`
* );
*/
Radio.loadCssClassFromString(/*css*/ `
.tc-radio {
height: inherit;
width: 100%;
min-width: 0px;
min-height: 0px;
padding: 0px;
margin: 0px;
}
.tc-radio .fp-components-radio-group {
width: 100%;
height: 100%;
min-width: 0px;
min-height: 0px;
padding: 0px;
margin: 0px;
overflow: hidden;
}
.tc-radio .fp-components-radio-container{
display: flex;
flex-direction: row;
align-items: center;
margin-right: 10px;
}
.tc-radio .fp-components-radio-group > *:last-child {
margin-right: 0px;
}
.tc-radio .fp-components-radio-group .fp-components-radio-disabled{
cursor: not-allowed !important;
border-color:var(--fp-color-BLACK-OPACITY-30);
}
.tc-radio .fp-components-radio-group .fp-components-radio:hover{
opacity:0.7;
}
.horizontal-layout {
display: flex;
flex-direction: row;
overflow: hidden;
flex-wrap: wrap;
align-content: space-around;
justify-content: space-between;
align-items: center;
}
.vertical-layout {
display: flex;
flex-direction: column;
overflow: hidden;
}
`);