import {Component_A} from './basic/as-component.js';
import {FP_Switch_A} from './fp-ext/fp-switch-ext.js';
import {checkCssSupport} from './utils/utils.js';
import {ErrorCode} from '../exception/exceptionDesc.js';
/**
* @typedef TComponents.SwitchProps
* @prop {object} [options] Additional options for the switch component.
* Items in this object include:
* - **responsive** (boolean, default: false): Whether the switch 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 {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 {number} [borderRadius] <span style="color:red; font-weight: bold;"> Invalid attribute. Border radius of the component. </span>
* @prop {(string|number|boolean)} [text] Text value bound to the switch state.
* @prop {object} [inputVar] Input variable binding configuration.
* This object configures the bound input variable:
* - **type** (string): Binding variable type. Default is `'bool'`.
* - **func** (string): Binding mode. Default is `'custom'`.
* - **value** (string): Initial value or variable path used for binding.
* - **isHidden** (boolean, default: false): Whether this binding is hidden in variable selectors.
* @prop {Function} [onChange] Function to be called when the switch state changes.
* @prop {boolean} [readOnly] Indicates whether the switch is read-only.
* @prop {string} [defaultState] Initial state of the component.
* @prop {string} [align] Alignment of the switch inside its container.
* @prop {string} [dataStruct] Custom data structure metadata for integration with other systems.
* @memberof TComponents
*/
/**
* @ignore
*/
const logModule = 'as-switch-a';
/**
* @description Switch element. Additional callbacks can be added with the {@link TComponents.Switch#onChange|onChange} method.
* This class focuses on the specific properties of the Switch 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.Switch
* @extends TComponents.Component_A
* @memberof TComponents
* @param {HTMLElement} parent - HTML element that is going to be the parent of the component
* @param {TComponents.SwitchProps} props
* @example
* const switchInstance = new TComponents.Switch(document.body, {
* position: 'absolute',
* zIndex: 1000
* });
*
* // Render the component
* switchInstance.render();
*/
export class Switch extends Component_A {
constructor(parent, props = {}) {
super(parent, props);
/**
* @type {TComponents.SwitchProps}
* @instance
* @private
*/
this._props;
this._switch = new FP_Switch_A();
/*
* If bound to web data, `this._bindData` will have the format: { type: 'webdata', key: 'xxx' }.
* If bound to digital signal data, `this._bindData` will have the format: { type: 'digitalsignal', key: 'xxxx' }.
* If bound to rapid data, `this._bindData` will have the format: { type: 'rapiddata', dataType: 'xxx', module: 'xxx', name: 'xxx', task: 'xxx' }.
*/
this._bindData = null;
}
/**
* @description Returns the default values of class properties (excluding parent properties).
* @member TComponents.Switch#defaultProps
* @method
* @protected
* @returns {TComponents.SwitchProps}
*/
defaultProps() {
return {
options: {
responsive: false,
},
tips: '',
// life cycle
onCreated: '',
onMounted: '',
onDispose: '',
// X/Y/W/H/B/R
position: 'static',
width: 64,
height: 32,
top: 0,
left: 0,
rotation: 0,
zIndex: 0,
borderRadius: 0, // Invalid attribute
// Input variable binding properties.
text: '0',
inputVar: {
type: 'bool', // 'any' | 'string' | 'num' | 'bool'
func: 'custom', // 'custom' | 'sync'
value: '0', // string
isHidden: false,
},
onChange: '',
// Special properties.
readOnly: false,
defaultState: 'show_enable',
align: 'center',
dataStruct: '',
};
}
/**
* @description Initializes the switch component.
* @member TComponents.Switch#onInit
* @method
* @return {void}
*/
onInit() {
this._switch.onchange = this._cbOnChange.bind(this);
}
/**
* @description there are something need to do after render once
* @member TComponents.Switch#afterRenderOnce
* @method
* @protected
* @returns {void}
*/
afterRenderOnce() {
if (this._props.inputVar.func == 'sync') {
this._bindData = Component_A.getBindData(this._props.inputVar.value, this);
}
}
/**
* @description Renders the switch component.
* @member TComponents.Switch#onRender
* @method
* @throws {Error} Throws an error if rendering fails.
* @return {void}
*/
onRender() {
try {
this.removeAllEventListeners();
const wrap = this.find('.tc-switch');
if (Component_A._isHTMLElement(wrap)) {
const align = this._props.align;
wrap.style.justifyContent = `${align == 'left' ? 'flex-start' : align == 'right' ? 'flex-end' : 'center'}`;
}
this._switch.attachToElement(wrap);
if (this.validateText(this._props.text)) {
this._updateActiveFromText(this._props.text);
}
this._addTips();
this._compatibilityRender();
Component_A.resolveBindingExpression(this._props.text, this);
} catch (e) {
// Runtime errors: write specific content to the log, throw error code
Logger.e(
logModule,
ErrorCode.FailedToRenderComponent,
`Error happens on onRender of Switch component ${this.compId}.`,
e,
);
throw new Error(ErrorCode.FailedToRunOnRender, {cause: e});
}
}
/**
* @description Adjusts the layout for compatibility with older browsers.
* @member TComponents.Switch~_compatibilityRender
* @method
* @private
* @return {void}
*/
_compatibilityRender() {
if (!checkCssSupport('aspect-ratio', '2/1')) {
setTimeout(() => {
let {clientWidth, clientHeight} = this.container;
if (clientWidth < clientHeight * 2) {
clientHeight = clientWidth / 2;
} else {
clientWidth = clientHeight * 2;
}
if (Math.min(clientWidth, clientHeight) < 0) {
clientHeight = this.props.height;
clientWidth = this.props.height * 2;
}
const switchContainer = this.find('.fp-components-switch-a-container');
switchContainer.style.cssText = `
width: ${clientWidth}px;
height: ${clientHeight}px;
padding:0;
`;
});
}
}
/**
* @description Generates the markup for the switch component.
* @member TComponents.Switch#markup
* @method
* @returns {string} HTML markup string
*/
markup() {
return /*html*/ `<div class="tc-switch flex"></div>`;
}
/**
* @description Callback function which is called when the switch is pressed, it triggers any function registered with {@link TComponents.Switch#onChange|onChange}.
* @member TComponents.Switch~_cbOnChange
* @private
* @async
* @param {any} value
* @throws {Error} If the synchronization with popup fails or if the user-defined function throws an error.
* @return {Promise<void>}
*/
async _cbOnChange(value) {
let tempVal = value;
try {
await this.syncInputData(tempVal);
this.active = tempVal;
} catch (e) {
// Restore to the previous state.
this.active = !tempVal;
Component_A.popupEventError(e, 'syncInputData', logModule);
return;
}
try {
var fn = Component_A.genFuncTemplate(this._props.onChange, this);
fn && (await fn(this.active));
} catch (e) {
// User operation failed, log detailed error stack information to the console
Component_A.popupEventError(e, 'onChange', logModule);
}
// Updates to bound variables are done through subscriptions
// if (this._props.inputVar.func == Component_A.INPUTVAR_FUNC.CUSTOM) this.active = value;
}
/**
* @returns {string}
*/
get text() {
return this._props.text;
}
/**
* @description Sets the text value of the switch component. If the text is '1' or 'true', the switch will be active; otherwise, it will be inactive.
* @member {string} TComponents.Switch#text
* @instance
* @param {string} t - The new text value.
* @example
* const switchInstance = new TComponents.Switch(document.body, {
* position: 'absolute',
* zIndex: 1000
* });
*
* // Render the component
* switchInstance.render();
*
* // Set the text.
* switchInstance.text = 0;
*/
set text(t) {
this._updateActiveFromText(t);
}
/**
* This attribute is used to set the text of the Switch 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.Switch#setText
* @method
* @param {string} text
* @example
* const switchInstance = new TComponents.Switch(document.body, {
* position: 'absolute',
* zIndex: 1000,
* text: 1
* });
*
* // Render the component.
* switchInstance.render();
*
* switchInstance.setText(0);
*/
async setText(text) {
const next = this.convertDataToBool(text);
await this.syncInputData(next);
this._switch.active = next;
this._props.text = next ? '1' : '0';
}
/**
* @returns {boolean}
*/
get active() {
const status = this._switch.active;
return status;
}
/**
* @description Sets the active status of the switch component.
* @member {boolean} TComponents.Switch#active
* @instance
* @param {boolean} value - The new active status.
* @example
* const switchInstance = new TComponents.Switch(document.body, {
* position: 'absolute',
* zIndex: 1000
* });
*
* // Render the component
* switchInstance.render();
*
* // Set the text.
* switchInstance.active = false;
*/
set active(value) {
this._switch.active = value;
this.commitProps({text: value ? '1' : '0'});
}
/**
* @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 switch 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.Switch#onChange
* @param {Function} t - The new pointer release handler function.
* @instance
* @example
* const switchInstance = new TComponents.Switch(document.body, {
* position: 'absolute',
* zIndex: 1000
* });
*
* // Render the component
* switchInstance.render();
*
* // Example 1: Using a string as the handler:
* switchInstance.onChange = "console.log('state changed', this.text);";
* @example
* // Example 2: Using a arrow function as the handler:
* // Note that the `this` context will not refer to the switch object
* switchInstance.onChange = () => { console.log('state changed', switch.text); };
* @example
* // Example 3: Using a common function as the handler:
* switchInstance.onChange = async function() {
* console.log('state changed', this.text);
* };
*/
set onChange(t) {
this.setProps({onChange: t});
}
}
if (checkCssSupport('aspect-ratio', '2/1')) {
//special style for aspect-ratio supported browsers
Switch.loadCssClassFromString(/*css*/ `
.tc-switch .fp-components-switch-a-container,
.tc-switch .fp-components-switch-a-disabled {
height: 100%;
aspect-ratio: 2 / 1;
max-width: 100%;
max-height: 100%;
display: block;
position: relative;
overflow: hidden;
padding:0;
}
.tc-switch .fp-components-switch-a {
width: 100%;
max-height: 100%;
aspect-ratio: 2 / 1;
align-items: center;
justify-content: center;
border-radius: 1000px;
display:flex;
border-width:0px;
height:auto;
position:relative;
}
.tc-switch .fp-components-switch-a:before{
content: "";
display: block;
position:absolute;
height:100%;
aspect-ratio: 1 / 1;
background:#fff;
left:0;
top:0;
z-index:1;
border-radius:50%;
transform: translateX(0);
transition: all 0.3s ease;
}
`);
} else {
//special style for aspect-ratio not supported browsers
Switch.loadCssClassFromString(/*css*/ `
.tc-switch .fp-components-switch-a {
width:100%;
height:100%;
border-radius:1000px;
display:flex;
align-items:center;
justify-content:center;
margin:0;
padding:0;
}
.tc-switch .fp-components-switch-a:before{
content: "";
position:absolute;
height:100%;
width:50%;
background:#fff;
left:0;
top:0;
z-index:1;
border-radius:50%;
transition: all 0.3s ease;
}
`);
}
/**
* @description Add css properties to the component
* @member TComponents.Switch.loadCssClassFromString
* @method
* @static
* @param {string} css - The css string to be loaded into style tag
* @returns {void}
* @example
* TComponents.Switch.loadCssClassFromString(`
* .tc-switch {
* height: inherit;
* }`
* );
*/
Switch.loadCssClassFromString(/*css*/ `
.tc-switch {
height: 100%;
min-width: 0px;
min-height: 0px;
padding: 0px;
margin: 0px;
align-items: flex-start;
justify-content: flex-start;
}
.tc-switch .fp-components-switch-a{
background-color: var(--fp-color-BLACK-OPACITY-6);
position:relative;
}
.tc-switch .fp-components-switch-a-active{
background-color: var(--fp-color-BLUE-60);
}
.tc-switch .fp-components-switch-a-disabled{
cursor: not-allowed !important;
opacity:0.7;
}
.tc-switch .fp-components-switch-a:hover {
opacity:0.7;
}
.tc-switch .fp-components-switch-a:before{
box-shadow:0 0 0px 3px var(--fp-color-BLACK-OPACITY-30) inset;
transform: translateX(0);
}
.tc-switch .fp-components-switch-a-active:before{
box-shadow:0 0 0px 3px var(--fp-color-BLUE-60) inset;
transform: translateX(100%);
}
`);