import {Component_A} from './basic/as-component.js';
import {FP_Select_A} from './fp-ext/fp-select-ext.js';
import {formatOptionsString, generateOptionsString, initDynamicOptions} from './utils/utils.js';
import {ErrorCode} from '../exception/exceptionDesc.js';
/**
* @typedef TComponents.SelectProps
* @prop {object} [options] Additional options for the select component.
* Items in this object include:
* - **responsive** (boolean, default: false): Whether the select 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 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} [borderRadius] Border radius 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} [size] Size style template for the component.
* @prop {string} [labelPos] Position of the label relative to the select component.
* @prop {string} [placeHolder] Placeholder text shown when no option is selected.
* @prop {string} [color] Font color of the selected text.
* @prop {object} [font] Font configuration for the selected text.
* 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`.
* - **textAlign** (string, default: 'left'): Text alignment of the text.
* @prop {string} [border] CSS border style of the select element.
* @prop {any} [value] <span style="color:red; font-weight: bold;"> Invalid attribute.</span>
* @prop {string} [optionItems] Options list in the format `text1|value1;\\ntext2|value2;\\ntext3|value3`.
* @prop {object} [optionConfig] Configuration for dynamic 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.
* - **sync** (boolean, default: true): Whether to synchronize the options with the data source.
* @prop {string} [text] Current selected option value, corresponds to the `value` field in `optionItems`.
* @prop {number} [selectedIndex] Index of the initially selected option.
* @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} [defaultState] Initial state of the component.
* @prop {string} [dataStruct] Data structure associated with the component.
* @memberof TComponents
*/
/**
* @ignore
*/
const logModule = 'as-select';
/**
* @description This component allows users to choose from a list of options. Additional callbacks can be added with the {@link TComponents.Select#onChange|onChange} method.
* This class focuses on the specific properties of the Select 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.Select
* @extends TComponents.Component_A
* @memberof TComponents
* @param {HTMLElement} parent - HTML element that is going to be the parent of the component
* @param {TComponents.SelectProps} [props]
* @property {TComponents.SelectProps} props
* @example
* const selectBox = new TComponents.Select(document.body, {
* position: 'absolute',
* zIndex: 1000,
* optionItems: 'Option 1|value1;Option 2|value2;Option 3|value3',
* selectedIndex: 0,
* });
*
* // Render the component.
* selectBox.render();
*/
export class Select extends Component_A {
constructor(parent, props = {}) {
super(parent, props);
/**
* @instance
* @private
* @type {TComponents.SelectProps}
*/
this._props;
this._select = new FP_Select_A();
// this.initPropsDep(['optionItems']);
}
/**
* @description Returns the default values of class properties (excluding parent properties).
* @member TComponents.Select#defaultProps
* @method
* @protected
* @returns {TComponents.SelectProps}
*/
defaultProps() {
return {
options: {
responsive: false,
},
tips: '',
// life cycle
onCreated: '',
onMounted: '',
onDispose: '',
onChange: '',
// ⭐ W/H/X/Y/B/R/Z: Component required attributes.
position: 'static',
width: 100,
height: 32,
top: 0,
left: 0,
borderRadius: 4,
rotation: 0,
zIndex: 0,
// Function
functionality: {
types: [],
params: [
{type: '', variablePath: '', isHidden: false},
{type: 'num', variablePath: '', isHidden: false},
],
},
// Style template
size: 'small',
// Font
// label: 'label',
labelPos: 'top',
placeHolder: 'select a value',
color: '#000000',
// font style
font: {
fontSize: 12,
fontFamily: 'Segoe UI',
style: {
fontStyle: 'normal',
fontWeight: 'normal',
textDecoration: 'none',
},
textAlign: 'left',
},
// Border
border: '1px solid #dbdbdb',
// Data
value: '', // Invalid attribute
optionItems: `text1|value1;\ntext2|value2;\ntext3|value3`,
optionConfig: {
mode: 'fixed', //fixed, sync,initialize
type: '',
isHidden: false,
variablePath: '',
sync: true,
},
text: '',
selectedIndex: 0,
inputVar: {
type: Component_A.INPUTVAR_TYPE.ANY,
func: Component_A.INPUTVAR_FUNC.CUSTOM,
value: '0',
isHidden: false,
},
defaultState: 'show_enable',
dataStruct: '',
};
}
/**
* @description Initializes the component.
* @member TComponents.Select#onInit
* @method
* @async
* @returns {Promise<void>}
*/
async onInit() {
this._select._onselection = this._cbOnChange.bind(this);
// Option values are always strings (from formatOptionsString). Normalize text
// to string so strict === comparisons don't fail when the prop was saved as a number.
if (this._props.text !== '' && typeof this._props.text !== 'string') {
this.commitProps({text: String(this._props.text)});
}
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.Select#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 Renders the select component.
* @member TComponents.Select#onRender
* @async
* @method
* @throws {Error} Throws an error if rendering fails.
* @returns {Promise<void>}
*/
async onRender() {
try {
this.removeAllEventListeners();
this._select.props = this._props;
this._select.enabled = this.enabled;
this._select.items = this.optionItems;
const selectContainer = this.find('.tc-select');
if (selectContainer) this._select.attachToElement(selectContainer);
// this.addEventListener(this._select._root, 'change', this._onChange.bind(this));
this._addTips();
this._updateChildStatus();
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.FailedToRunOnRender,
`Error happens on onRender of select component ${this.compId}.`,
e,
);
throw new Error(ErrorCode.FailedToRunOnRender, {cause: e});
}
}
/**
* @description Returns the markup for the select component.
* @member TComponents.Select#markup
* @method
* @returns {string} The HTML markup string.
*/
markup() {
return /*html*/ `<div class="tc-select"></div>`;
}
/**
* @returns {boolean}
*/
get highlight() {
return this._select._highlight;
}
/**
* @description Sets whether the select component is highlighted.
* @member {boolean} TComponents.Select#highlight
* @instance
* @param {boolean} value - True to highlight, false otherwise.
* @example
* const selectBox = new TComponents.Select(document.body, {
* position: 'absolute',
* zIndex: 1000,
* optionItems: 'Option 1|value1;Option 2|value2;Option 3|value3',
* selectedIndex: 0,
* });
*
* // Render the component.
* selectBox.render();
*
* selectBox.highlight = true;
*/
set highlight(value) {
this._select._highlight = value ? true : false;
}
/**
* @description Handles the change event for the select element.
* @member TComponents.Select~_cbOnChange
* @method
* @private
* @async
* @param {object} option - The clicked item.
* @param {number} index - The index of the clicked item.
* @returns {Promise<void>}
*/
async _cbOnChange(option, index) {
if (this.enabled) {
try {
await this.syncInputData(option.value);
this.value = option.value;
} catch (e) {
Component_A.popupEventError(e, 'syncInputData', logModule);
return;
}
try {
var fn = Component_A.genFuncTemplate(this._props.onChange, this);
fn && (await fn());
} catch (e) {
Component_A.popupEventError(e, 'onChange', logModule);
}
}
}
/**
* @returns {Function|undefined}
*/
get onChange() {
try {
var fn = Component_A.genFuncTemplateWithPopup(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 select 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.Select#onChange
* @instance
* @param {Function} t
* @example
* const selectBox = new TComponents.Select(document.body, {
* position: 'absolute',
* zIndex: 1000,
* optionItems: 'Option 1|value1;Option 2|value2;Option 3|value3',
* selectedIndex: 0,
* });
*
* // Render the component.
* selectBox.render();
*
* // Example 1: Using a string as the handler:
* selectBox.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 selectBox object
* selectBox.onChange = () => { console.log('state changed', selectBox.selectedIndex); };
* @example
* // Example 3: Using a common function as the handler:
* selectBox.onChange = async function() {
* console.log('state changed', this.selectedIndex);
* };
*/
set onChange(t) {
this.setProps({onChange: t});
}
/**
* @description Updates the child component's status.
* This method is called whenever a property that affects the child component is changed.
* It ensures that the child component reflects the current state of the parent component.
* @member TComponents.Select~_updateChildStatus
* @method
* @private
* @returns {void}
*/
_updateChildStatus() {
this._select.props = this._props;
this._select.rebuild();
}
/**
* @description Sets the text to enable selected option.
* @member {string} TComponents.Select#text
* @instance
* @param {string} t - The text of the option to be selected.
* @example
* const selectBox = new TComponents.Select(document.body, {
* position: 'absolute',
* zIndex: 1000,
* optionItems: 'Option 1|value1;Option 2|value2;Option 3|value3',
* selectedIndex: 0,
* });
*
* // Render the component.
* selectBox.render();
*
* selectBox.text = 'value2';
*/
set text(t) {
const optionItems = this.optionItems;
const current = optionItems.find((item) => item.text === t);
if (current) {
this.commitProps({text: current.value});
this._select.text = current.value;
}
}
/**
* @returns {string}
*/
get text() {
const optionItems = this.optionItems;
const current = optionItems.find((item) => item.value === this._props.text);
if (current) return current.text;
return '';
}
/**
* This attribute is used to set the text of the Select 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.Select#setText
* @method
* @param {string} text
* @example
* const selectBox = new TComponents.Select(document.body, {
* position: 'absolute',
* zIndex: 1000,
* });
*
* // Render the component.
* selectBox.render();
*
* selectBox.setText(0);
*/
async setText(text) {
await this.syncInputData(text);
this.value = text;
}
/**
* @returns {string}
*
*/
get value() {
const optionItems = this.optionItems;
const current = optionItems.find((item) => item.value === this._props.text);
if (current) return current.value;
return '';
}
/**
* @description Sets the value to enable selected option.
* @member {string} TComponents.Select#value
* @instance
* @param {string} v - The value of the option to be selected.
* @example
* const selectBox = new TComponents.Select(document.body, {
* position: 'absolute',
* zIndex: 1000,
* optionItems: 'Option 1|value1;Option 2|value2;Option 3|value3',
* selectedIndex: 0,
* });
*
* // Render the component.
* selectBox.render();
*
* selectBox.value = 'value2';
*/
set value(v) {
const optionItems = this.optionItems;
const current = optionItems.find((item) => item.value === v);
if (current) {
this.commitProps({text: current.value});
this._select.text = v;
}
}
/**
* @returns {number}
*/
get selectedIndex() {
const optionItems = this.optionItems;
return optionItems.findIndex((item) => item.value === this._props.text);
}
/**
* @description Sets the index of the currently selected option.
* @member TComponents.Select#selectedIndex
* @instance
* @param {number} index - The index of the selected option.
* @example
* const selectBox = new TComponents.Select(document.body, {
* position: 'absolute',
* zIndex: 1000,
* optionItems: 'Option 1|value1;Option 2|value2;Option 3|value3',
* selectedIndex: 0,
* });
*
* // Render the component.
* selectBox.render();
*
* selectBox.selectedIndex = 1;
*/
set selectedIndex(index) {
const targetItem = this.optionItems[index];
if (targetItem) {
this.value = targetItem.value;
} else {
throw new Error(ErrorCode.FailedToUpdateComponentWithSetter, {
cause: `Index out of range: ${index}`,
});
}
}
/**
* @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.Select#optionItems
* @instance
* @param {string} itemsString - The formatted string of option items.
* @example
* const selectBox = new TComponents.Select(document.body, {
* position: 'absolute',
* zIndex: 1000,
* optionItems: 'Option 1|value1;Option 2|value2;Option 3|value3',
* selectedIndex: 0,
* });
*
* // Render the component.
* selectBox.render();
*
* selectBox.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 optionsConfig is not in fixed mode, can not set optionItems',
});
}
}
}
/**
* @description Add css properties to the component
* @member TComponents.Select.loadCssClassFromString
* @method
* @static
* @param {string} css - The css string to be loaded into style tag
* @returns {void}
* @example
* TComponents.Select.loadCssClassFromString(`
* .tc-select {
* height: inherit;
* }`
* );
*/
Select.loadCssClassFromString(/*css*/ `
.tc-select {
height: inherit;
width: 100%;
min-width: 0px;
min-height: 0px;
padding: 0px;
margin: 0px;
}
.tc-select .fp-components-select-container-disabled,
.tc-select .fp-components-select-container {
width: 100%;
height: 100%;
display: flex;
align-content: center;
justify-content: center;
align-items: center;
flex-direction: row;
position: relative;
}
.tc-select .fp-components-select-container-disabled .custom-select-display {
cursor:not-allowed !important;
opacity:0.7;
}
.tc-select .fp-components-select-container:hover {
opacity:0.7;
}
.tc-select .fp-components-select-container .custom-select-display {
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 5px;
cursor: pointer;
background-color: white;
display: flex;
align-items: center;
text-overflow: ellipsis;
}
.custom-select-option.selected,.custom-select-option:hover{
background-color: #f0f0f0;
}
.custom-select-options:hover{
display: block !important;
}
`);