import {Component_A} from './basic/as-component.js';
import {FP_DropDown_A} from './fp-ext/fp-dropdown.js';
import {formatOptionsString, initDynamicOptions, generateOptionsString} from './utils/utils.js';
import {ErrorCode} from './../exception/exceptionDesc.js';
/**
* @ignore
*/
const logModule = 'as-dropdown';
/**
* @typedef TComponents.DropdownProps
* @prop {object} [options] Additional options for the dropdown component
* Items in this object include:
* - **responsive** (boolean, default: false): Whether the dropdown 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] Function to be called when dropdown value is changed
* @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 {string|null} [icon] Prefix icon for the dropdown
* @prop {string} [text] Dropdown text
* @prop {string} [color] Text color of the dropdown
* @prop {string} [backgroundColor] Background color of the dropdown
* @prop {object} [font] Font settings for the dropdown text
* This object controls text appearance:
* - **fontSize** (number, default: 16): Font size of the displayed text
* - **fontFamily** (string, default: 'Arial'): Font family of the displayed text
* - **style** (object): Font style configuration, containing `fontStyle`, `fontWeight`, `textDecoration`
* - **textAlign** (string, default: 'center'): Text alignment inside the dropdown
* @prop {string} [optionItems] Options for the dropdown 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' or 'variable'
* - **type** (string): Data source type
* - **isHidden** (boolean, default: false): Whether the option configuration is hidden
* - **variablePath** (string): Path of the variable used for dynamic options
* @prop {Array<Object>} [optionIcons] Icon configuration for each option
* @prop {string[]} [optionActions] Action configuration for each option
* @prop {string} [defaultState] Default state of the component
* @prop {boolean} [showArrow] Whether to show the dropdown arrow
* @prop {string} [dataStruct] Data structure associated with the component
* @memberof TComponents
*/
/**
* The Dropdown component allows users to choose from a list of options. Additional callbacks can be added with the {@link TComponents.DropDown#onChange|onChange} method.
* This class focuses on the specific properties of the DropDown 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.DropDown
* @extends TComponents.Component_A
* @memberof TComponents
* @param {HTMLElement} parent - HTML element that is going to be the parent of the component
* @param {TComponents.DropdownProps} props - The properties of the dropdown component
* @example
* const dropDown = new TComponents.DropDown(document.body, {
* position: 'absolute',
* zIndex: 1000,
* optionItems: `text1|value1;\ntext2|value2;\ntext3|value3`,
* });
*
* await dropDown.render();
*/
export class DropDown extends Component_A {
constructor(parent, props = {}) {
super(parent, props);
/**
* @instance
* @private
* @type {TComponents.SelectProps}
*/
this._props;
this._dropdown = new FP_DropDown_A();
}
/**
* @description Returns the default values of class properties (excluding parent properties).
* @member TComponents.DropDown#defaultProps
* @method
* @protected
* @returns {TComponents.DropDownProps} The default properties object.
*/
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: 140,
height: 32,
top: 0,
left: 0,
borderRadius: 4,
rotation: 0,
zIndex: 0,
icon: null,
text: 'Dropdown',
// font style
color: '#000000',
backgroundColor: 'rgba(255, 255, 255, 0)',
font: {
fontSize: 16,
fontFamily: 'Arial',
style: {
fontStyle: 'normal',
fontWeight: 'normal',
textDecoration: 'none',
},
textAlign: 'center',
},
optionItems: `text1|value1;\ntext2|value2;\ntext3|value3`,
optionConfig: {
mode: 'fixed', //fixed, variable
type: '',
isHidden: false,
variablePath: '',
},
optionIcons: [
{icon: '', toggledIcon: ''},
{icon: '', toggledIcon: ''},
{icon: '', toggledIcon: ''},
],
optionActions: ['', '', ''],
defaultState: 'show_enable',
showArrow: true,
dataStruct: '',
};
}
/**
* @description Initializes the component.
* @member TComponents.DropDown#onInit
* @method
* @async
* @returns {Promise<void>}
*/
async onInit() {}
/**
* @description there are something need to do after render once
* @member TComponents.DropDown#afterRenderOnce
* @method
* @protected
* @returns {void}
*/
afterRenderOnce() {
initDynamicOptions(this._props.optionConfig, (data) => {
this.setProps({
optionItems: generateOptionsString(data),
});
});
}
/**
* @description This attribute represents the text content of the input field.
* It can be read to get the current content. Setting this attribute will programmatically
* update the content and will not trigger the onchange callback function
* to be called.
* @member {string} TComponents.DropDown#text
* @instance
* @param {string} text
* @example
*
* const dropDown = new TComponents.DropDown(document.body, {
* position: 'absolute',
* zIndex: 1000,
* text: 'Dropdown title'
* });
*
* // Render the component.
* dropDown.render();
*
* dropDown.text = 'title updated';
*/
set text(text) {
this.commitProps({text: text});
this._dropdown.text = text;
}
/**
* @description return the display label of the dropdown
* @member {string} TComponents.DropDown#text
* @instance
* @param {string} text
* @example
*
* const dropDown = new TComponents.DropDown(document.body, {
* position: 'absolute',
* zIndex: 1000,
* text: 'Dropdown title'
* });
*
* // Render the component.
* dropDown.render();
*
* console.log(dropDown.text); // Output: 'Dropdown title'
*/
get text() {
return this._props.text;
}
/**
* @description Renders the select component.
* @member TComponents.DropDown#onRender
* @method
* @async
* @returns {Promise<void>}
*/
async onRender() {
try {
this._dropdown.text = this._props.text;
this._dropdown.icon = this._props.icon;
this._dropdown.borderRadius = this._props.borderRadius;
this._dropdown.color = this._props.color;
this._dropdown.backgroundColor = this._props.backgroundColor;
this._dropdown.font = this._props.font;
this._dropdown.showArrow = this._props.showArrow;
this._dropdown._onselection = this._cbOnChange.bind(this);
this._dropdown.props = this._props;
this._dropdown._model = this.generateModel();
const dropdownContainer = this.find('.tc-dropdown');
if (dropdownContainer) this._dropdown.attachToElement(dropdownContainer);
this._addTips();
Component_A.resolveBindingExpression(this._props.text, this);
} catch (e) {
// Runtime errors: write specific content to the log, throw error code
Logger.e(
logModule,
ErrorCode.FailedToRunOnRender,
`Error happens on onRender of Dropdown component ${this.compId}.`,
e,
);
throw new Error(ErrorCode.FailedToRunOnRender, {cause: e});
}
}
/**
* @description Returns the markup for the Dropdown component.
* @member TComponents.DropDown#markup
* @method
* @returns {string} The HTML markup string.
*/
markup() {
return /*html*/ `<div class="tc-dropdown"></div>`;
}
/**
* @description Generate dropdown model with value parameter
* @member TComponents.DropDown~generateModel
* @method
* @private
* @returns {object[]} The generated dropdown model.
*/
generateModel() {
let model = [];
for (let i = 0; i < this.optionItems.length; i++) {
model.push({
text: this.optionItems[i].text,
value: this.optionItems[i].value,
icon: this._props.optionIcons[i] && this._props.optionIcons[i].icon ? this._props.optionIcons[i].icon : '',
toggledIcon:
this._props.optionIcons[i] && this._props.optionIcons[i].toggledIcon
? this._props.optionIcons[i].toggledIcon
: '',
action: this._props.optionActions[i] ? this._props.optionActions[i] : '',
});
}
return model;
}
/**
* @description Handles the change event for the dropdown element.
* @member TComponents.DropDown~_cbOnChange
* @method
* @private
* @async
* @param {Event} e - The change event.
* @returns {Promise<void>}
*/
async _cbOnChange(item, index) {
if (this.enabled) {
if (item.action) {
try {
var action = Component_A.genFuncTemplate(item.action, this);
action && (await action());
} catch (e) {
Component_A.popupEventError(e, `Item ${index} Action`, logModule);
return;
}
}
this.selectedIndex = index;
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.genFuncTemplate(this._props.onChange, this);
} catch (e) {
return undefined;
}
if (typeof fn == 'function') return fn;
else return undefined;
}
/**
* @description Set the `onChange` handler for the dropdown.
* 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 dropdown 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.onClick = "console.log('Action done.');"`
* - Incorrect (Function Declaration): `xx.onClick = "function() { console.log('Action done.'); }"`
* @member {Function} TComponents.DropDown#onChange
* @instance
* @param {Function} t - The new change handler function.
* @example
* const dropDown = new TComponents.DropDown(document.body, {
* position: 'absolute',
* zIndex: 1000,
* optionItems: `text1|value1;\ntext2|value2;\ntext3|value3`,
* });
*
* await dropDown.render();
*
* // Example 1: Using a string as the handler:
* dropDown.onChange = "console.log('hello', this.selectedValue);"
* @example
* // Example 2: Using a arrow function as the handler:
* // Note that the `this` context will not refer to the dropDown object
* dropDown.onChange = () => { console.log(dropDown.selectedValue); }
* @example
* // Example 3: Using function with async operation:
* dropDown.onChange = async function () {
* console.log('hello', this.selectedValue);
* }
*/
set onChange(t) {
this.setProps({onChange: t});
}
/**
* @description The selected index of the dropdown.
* The obtained value should be treated as a read-only property and should not be used for manipulation.
* @member {number} TComponents.DropDown#selectedIndex
* @instance
*/
get selectedValue() {
if (typeof this.selectedIndex == 'undefined') return '';
return this.optionItems[this.selectedIndex].value;
}
/**
* @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.DropDown#optionItems
* @instance
* @param {string} itemsString - The formatted string of option items.
* @example
* const dropDown = new TComponents.DropDown(document.body, {
* position: 'absolute',
* zIndex: 1000,
* optionItems: `text1|value1;\ntext2|value2;\ntext3|value3`,
* });
*
* await dropDown.render();
*
* dropDown.optionItems = `newText1|newValue1;\nnewText2|newValue2;`;
*/
set optionItems(itemsString) {
const optionConfig = this._props.optionConfig;
if (optionConfig && optionConfig.mode == 'fixed') {
this.setProps({
optionItems: itemsString,
});
} else {
throw new Error(ErrorCode.FailedToUpdateComponentWithSetter, {
cause: 'Cannot set prop optionItems when optionConfig is not in fixed mode.',
});
}
}
}
/**
* @description Add css properties to the component
* @alias loadCssClassFromString
* @member TComponents.DropDown.loadCssClassFromString
* @method
* @static
* @param {string} css - The css string to be loaded into style tag
* @returns {void}
* @example
* TComponents.DropDown.loadCssClassFromString(`.tc-dropdown { background-color: red; }`);
*/
DropDown.loadCssClassFromString(/*css*/ `
.tc-dropdown {
height: inherit;
width: 100%;
min-width: 0px;
min-height: 0px;
padding: 0px;
margin: 0px;
}
.tc-dropdown .fp-components-dropdown-container{
height:100%;
}
.tc-dropdown .fp-components-dropdown {
border:none;
padding:0 8px;
display:flex;
flex-direction: row;
align-items: center;
align-content: center;
background:transparent;
}
.fp-components-dropdown-menu-hide{
display:none !important;
}
.tc-dropdown .fp-components-dropdown >i,
.tc-dropdown .fp-components-dropdown >span,
.tc-dropdown .fp-components-dropdown >canvas{
display:flex;
}
.tc-dropdown .fp-components-dropdown .fp-components-dropdown-arrow-icon {
margin-left: 8px;
}
.tc-dropdown .fp-components-dropdown-icon-font {
margin-right: 8px;
}
.tc-dropdown .fp-components-dropdown-menu .fp-components-dropdown-overlay{
background:red;
}
.tc-dropdown .fp-components-dropdown-disabled,
.tc-dropdown .fp-components-dropdown {
width: 100%;
height: 100%;
}
.tc-dropdown .fp-components-dropdown-disabled {
cursor:not-allowed !important;
user-select: none;
}
.tc-dropdown .fp-components-dropdown:hover {
opacity:0.7;
}
.custom-dropdown-option.selected,.custom-dropdown-option:hover{
background-color: #f0f0f0;
}
`);