as-slider.js

import {Component_A} from './basic/as-component.js';
import {FP_Slider_A} from './fp-ext/fp-slider-ext.js';
import {getDecimalPlaces} from './utils/utils.js';
import {ErrorCode} from '../exception/exceptionDesc.js';

/**
 * @typedef TComponents.SliderProps
 * @prop {object} [options] General options for the slider behavior.
 * Items in this object include:
 * - **responsive** (boolean, default: false): Whether the slider width should adapt to the container width.
 * @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} [onPointerRelease] Callback function that is called when the pointer is released after dragging the slider.
 * @prop {Function} [onPointerDown] Callback function that is called while the pointer is dragging the slider.
 * @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} [color] Font color of the description label.
 * @prop {object} [font] Font configuration for the description label.
 * This object controls text appearance:
 * - **fontSize** (number, default: 14): Font size in pixels.
 * - **fontFamily** (string, default: 'Segoe UI'): Font family name.
 * - **style** (object): Font style configuration, containing `fontStyle`, `fontWeight`, `textDecoration`.
 * @prop {string} [rangeValueColor] Font color of the min/max range values.
 * @prop {object} [rangeValueFont] Font configuration for the min/max range values.
 * This object controls range 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} [backgroundColor] Background color of the slider container.
 * @prop {string} [border] CSS border style of the slider container.
 * @prop {boolean} [displayLabel] Whether to display the description label.
 * @prop {string} [descrLabel] Text content of the description label.
 * @prop {boolean} [displayValue] Whether to display the current value label.
 * @prop {number} [value] Current value of the slider.
 * @prop {number} [min] Minimum value of the slider range.
 * @prop {number} [max] Maximum value of the slider range.
 * @prop {boolean} [enabled] Whether the slider is enabled and interactive.
 * @prop {string} [activeColor] Color of the active (filled) part of the slider track.
 * @prop {string} [inactiveColor] Color of the inactive part of the slider track.
 * @prop {boolean} [displayTicks] Whether to display tick marks along the track.
 * @prop {number} [tickStep] Step between tick marks and value increments.
 * @prop {string} [defaultState] Initial state of the component.
 * @memberof TComponents
 */

/**
 * @ignore
 */
const logModule = 'as-slider';

/**
 * @description Slider element. Additional callbacks can be added with the {@link TComponents.Slider#onChange|onChange} method.
 * This class focuses on the specific properties of the Slider 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.Slider
 * @extends TComponents.Component_A
 * @memberof TComponents
 * @param {HTMLElement} parent - HTML element that is going to be the parent of the component
 * @param {TComponents.SliderProps} props
 * @example
 * const slider = new TComponents.Slider(document.body, {
 *   position: 'absolute',
 *   zIndex: 1000,
 *   max:200,
 *   min:50,
 *   value:100,
 * });
 *
 * // Render the component.
 * slider.render();
 */
export class Slider extends Component_A {
  constructor(parent, props = {}) {
    super(parent, props);

    /**
     * @instance
     * @private
     * @type {TComponents.SliderProps}
     */
    this._props;

    this._slider = new FP_Slider_A();
  }

  /**
   * @description Returns the default values of class properties (excluding parent properties).
   * @member TComponents.Slider#defaultProps
   * @method
   * @protected
   * @returns {TComponents.SliderProps}
   */
  defaultProps() {
    return {
      options: {
        responsive: false,
      },
      tips: '',
      // life cycle
      onCreated: '',
      onMounted: '',
      onDispose: '',

      onPointerRelease: '',
      onPointerDown: '',
      // X/Y/W/H/B/R
      position: 'static',
      width: 200,
      height: 80,
      top: 0,
      left: 0,
      borderRadius: 4,
      rotation: 0,
      zIndex: 0,
      //label font color
      color: '#000000',
      //label font style
      font: {
        fontSize: 14,
        fontFamily: 'Segoe UI',
        style: {
          fontStyle: 'normal',
          fontWeight: 'bold',
          textDecoration: 'none',
        },
      },
      // range value font style
      rangeValueColor: '#0000008e',
      rangeValueFont: {
        fontSize: 12,
        fontFamily: 'Segoe UI',
        style: {
          fontStyle: 'normal',
          fontWeight: 'normal',
          textDecoration: 'none',
        },
      },
      // Background
      backgroundColor: '#ffffff',
      //  Border
      border: '0px solid #dbdbdb',
      // label
      displayLabel: true,
      descrLabel: 'Label',
      // value
      displayValue: true,
      value: 30,
      min: 0,
      max: 100,
      enabled: true,
      // range track
      activeColor: '#3366ff',
      inactiveColor: '#d3d3d3',
      displayTicks: true,
      tickStep: 10,
      defaultState: 'show_enable',
    };
  }

  /**
   * @description Initializes the Slider component.
   * @member TComponents.Slider#onInit
   * @method
   * @returns {void}
   */
  onInit() {}

  /**
   * @description Renders the Slider component.
   * @member TComponents.Slider#onRender
   * @method
   * @throws {Error} Throws an error if rendering fails.
   * @returns {void}
   */
  onRender() {
    try {
      this.removeAllEventListeners();

      //background color and radius
      this._slider._backgroundColor = this._props.backgroundColor;
      this._slider._border = this._props.border;
      this._slider._borderRadius = this._props.borderRadius;

      //label
      this._slider._displayLabel = this._props.displayLabel;
      this._slider._label = Component_A.tParse(this._props.descrLabel);
      this._slider._labelFont = this._props.font;
      this._slider._labelColor = this._props.color;

      //value
      this._slider._displayValue = this._props.displayValue;
      this._slider._value = this._props.value;

      // range
      this._slider._activeColor = this._props.activeColor;
      this._slider._inactiveColor = this._props.inactiveColor;
      this._slider._displayTicks = this._props.displayTicks;
      this._slider._tickStep = this._props.tickStep;
      this._slider._numberOfDecimals = getDecimalPlaces(this._props.tickStep);
      this._slider._max = this._props.max;
      this._slider._min = this._props.min;

      // range value color
      this._slider._rangeValueFont = this._props.rangeValueFont;
      this._slider._rangeValueColor = this._props.rangeValueColor;

      // enable status
      this._slider.enabled = this.enabled;

      //onPointerRelease
      this._slider.onrelease = this._onPointerRelease.bind(this);
      this._slider.ondrag = this._onPointerDown.bind(this);

      this._dynamicRender();
      this._addTips();
    } catch (e) {
      // Runtime errors: write specific content to the log, throw error code
      Logger.e(
        logModule,
        ErrorCode.FailedToRunOnRender,
        `Error happens on onRender of slider component ${this.compId}.`,
        e,
      );
      throw new Error(ErrorCode.FailedToRunOnRender, {cause: e});
    }
  }

  /**
   * @description Dynamically renders the Slider component.
   * @member TComponents.Slider~_dynamicRender
   * @method
   * @private
   * @returns {void}
   */
  _dynamicRender() {
    if (this._props.options.responsive) {
      setTimeout(() => {
        const {clientWidth} = this.container;
        this._slider._width = clientWidth - 28;
        const sliderContainer = this.find('.tc-slider');
        if (sliderContainer) this._slider.attachToElement(sliderContainer);
      });
    } else {
      this._slider._width = this._props.width - 28;
      const sliderContainer = this.find('.tc-slider');
      if (sliderContainer) this._slider.attachToElement(sliderContainer);
    }
  }

  /**
   * @description Generates the markup for the Slider component.
   * @member TComponents.Slider#markup
   * @method
   * @returns {string} HTML markup string
   */
  markup() {
    return /*html*/ `
    <div class="tc-slider"></div>
    `;
  }

  /**
   * @description Pointer up/leave event handler.
   * @member TComponents.Slider~_onPointerRelease
   * @method
   * @private
   * @async
   * @throws {Error} If the synchronization with popup fails or if the user-defined function throws an error.
   * @returns {Promise<void>}
   */
  async _onPointerRelease(value) {
    if (this.enabled && this._props.onPointerRelease) {
      this._slider.value = value;
      this.commitProps({value: value});
      try {
        var fn = Component_A.genFuncTemplate(this._props.onPointerRelease, this);
        if (typeof fn == 'function') {
          await fn(value);
        }
      } catch (e) {
        Component_A.popupEventError(e, 'onPointerRelease', logModule);
      }
    }
  }

  /**
   * @description Pointer down event handler.
   * @member TComponents.Slider~_onPointerDown
   * @method
   * @private
   * @async
   * @throws {Error} If the synchronization with popup fails or if the user-defined function throws an error.
   * @returns {Promise<void>}
   */
  async _onPointerDown(value) {
    if (this.enabled && this._props.onPointerDown) {
      try {
        var fn = Component_A.genFuncTemplate(this._props.onPointerDown, this);
        fn && (await fn(value));
      } catch (e) {
        Component_A.popupEventError(e, 'onPointerDown', logModule);
        return;
      }
    }
  }

  /**
   * @returns {Function|undefined}
   */
  get onPointerRelease() {
    try {
      var fn = Component_A.genFuncTemplateWithPopup(this._props.onPointerRelease, this);
    } catch (e) {
      return undefined;
    }
    if (typeof fn == 'function') return fn;
    else return undefined;
  }

  /**
   * @description Sets the pointer release event handler for the Slider component.
   * 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 slider 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.onPointerRelease = "console.log('Action done.');"`
   * - Incorrect (Function Declaration): `xx.onPointerRelease = "function() { console.log('Action done.'); }"`
   * @member {Function} TComponents.Slider#onPointerRelease
   * @instance
   * @param {Function} t - The new pointer release handler function.
   * @example
   * const slider = new TComponents.Slider(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   max:200,
   *   min:50,
   *   value:100,
   * });
   *
   * // Render the component.
   * slider.render();
   *
   * // Example 1: Using a string as the handler:
   * slider.onPointerRelease = "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 slider object
   * slider.onPointerRelease = () => { console.log('state changed', slider.text); };
   * @example
   * // Example 3: Using a common function as the handler:
   * slider.onPointerRelease = async function() {
   *   console.log('state changed', this.text);
   * };
   */
  set onPointerRelease(t) {
    this.setProps({onPointerRelease: t});
  }

  /**
   * @description Sets the pointer down event handler for the Slider component.
   *    * 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 slider 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.onPointerDown = "console.log('Action done.');"`
   * - Incorrect (Function Declaration): `xx.onPointerDown = "function() { console.log('Action done.'); }"`
   * @member {Function} TComponents.Slider#onPointerDown
   * @instance
   * @param {Function} t - The new pointer down handler function.
   * @example
   * const slider = new TComponents.Slider(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   max:200,
   *   min:50,
   *   value:100,
   * });
   *
   * // Render the component.
   * slider.render();
   *
   * // Example 1: Using a string as the handler:
   * slider.onPointerDown = "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 slider object
   * slider.onPointerDown = () => { console.log('state changed', slider.text); };
   * @example
   * // Example 3: Using a common function as the handler:
   * slider.onPointerDown = async function() {
   *   console.log('state changed', this.text);
   * };
   */
  set onPointerDown(t) {
    this.setProps({onPointerDown: t});
  }

  /**
   * @description Sets the value of the Slider component.
   * @member {number} TComponents.Slider#value
   * @instance
   * @param {number} v - The value
   * @example
   * const slider = new TComponents.Slider(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   max:200,
   *   min:50,
   *   value:100,
   * });
   *
   * // Render the component.
   * slider.render();
   *
   * // Set value.
   * slider.value = 20;
   */
  set value(v) {
    // check validity
    if (v > this.max || v < this.min) {
      Logger.e(logModule, ErrorCode.FailedToSetNum, 'The value must be between the min value and the max value.', v);
      throw new Error(ErrorCode.FailedToSetNum, {
        cause: 'The value must be between the min value and the max value.',
      });
    }
    this._slider.value = v;
    this.commitProps({text: v});
  }

  /**
   * @returns {string}
   */
  get value() {
    return this._slider.value;
  }

  /**
   * @returns {number}
   */
  get max() {
    return this._props.max;
  }

  /**
   * @description Sets the maximum value of the slidr
   * @member {number} TComponents.Slider#max
   * @instance
   * @param {number} v the max
   * @example
   * const slider = new TComponents.Slider(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   max:200,
   *   min:50,
   *   value:100,
   * });
   *
   * // Render the component.
   * slider.render();
   *
   * // Set max value.
   * slider.max = 100;
   */
  set max(v) {
    if (typeof v !== 'number') {
      throw new Error(ErrorCode.FailedToSetNum, {
        cause: `The min value must be a number.`,
      });
    }
    if (v <= this.min || v < this.value) {
      throw new Error(ErrorCode.FailedToSetMax, {
        cause: `The max value must be larger than min value`,
      });
    }
    this.setProps({
      max: v,
    });
  }

  /**
   * @returns {number}
   */
  get min() {
    return this._props.min;
  }
  /**
   * @description Sets the minimum value of the Slider component range.
   * @member {number} TComponents.Slider#min
   * @instance
   * @param {number} e - The min state to set.
   * @example
   * const slider = new TComponents.Slider(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   max:200,
   *   min:50,
   *   value:100,
   * });
   *
   * // Render the component.
   * slider.render();
   *
   * // Set max value.
   * slider.min = 10;
   */
  set min(v) {
    // check validity
    if (typeof v !== 'number') {
      throw new Error(ErrorCode.FailedToSetNum, {
        cause: `The min value must be a number.`,
      });
    }
    if (v >= this.max || v > this.value) {
      Logger.e(logModule, ErrorCode.FailedToSetMin, 'The min value must be less than the max value', v);
      throw new Error(ErrorCode.FailedToSetMin, {cause: 'The min value must be less than the max value'});
    }
    this.setProps({
      min: v,
    });
  }

  /**
   * @returns {boolean}
   */
  get displayLabel() {
    return this._props.displayLabel;
  }

  /**
   * @description Sets the property to true to display the description lanel in the Slider component.
   * @member {boolean} TComponents.Slider#displayLabel
   * @instance
   * @param {boolean} b - True if description label shall display.
   * @example
   * const slider = new TComponents.Slider(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   max:200,
   *   min:50,
   *   value:100,
   * });
   *
   * // Render the component.
   * slider.render();
   *
   * // Set max value.
   * slider.displayLabel = 'hello world';
   */
  set displayLabel(b) {
    this.setProps({
      displayLabel: b,
    });
  }

  /**
   * @returns {boolean}
   */
  get displayValue() {
    return this._props.displayValue;
  }

  /**
   * @description Sets the property to true to display the value lanel in the Slider component.
   * @member {boolean} TComponents.Slider#displayValue
   * @instance
   * @param {boolean} b - True if value label shall display.
   * @example
   * const slider = new TComponents.Slider(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   max:200,
   *   min:50,
   *   value:100,
   * });
   *
   * // Render the component.
   * slider.render();
   *
   * // Set max value.
   * slider.displayValue = false;
   */
  set displayValue(b) {
    this.setProps({
      displayValue: b,
    });
  }

  /**
   * @returns {string}
   */
  get descrLabel() {
    return this._props.descrLabel;
  }

  /**
   * @description Sets the content of description label.
   * @member {string} TComponents.Slider#descrLabel
   * @instance
   * @param {string} c - The text content of description label
   * @example
   * const slider = new TComponents.Slider(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   max:200,
   *   min:50,
   *   value:100,
   * });
   *
   * // Render the component.
   * slider.render();
   *
   * // Set max value.
   * slider.descrLabel = 'hello world';
   */
  set descrLabel(c) {
    this.setProps({
      descrLabel: c,
    });
  }

  /**
   * @returns {string}
   */
  get activeColor() {
    return this._props.activeColor;
  }

  /**
   * @description Sets the color of active track of the Slider component.
   * @member {string} TComponents.Slider#activeColor
   * @instance
   * @param {string} c - The color of active track of the Slider component.
   * @example
   * const slider = new TComponents.Slider(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   max:200,
   *   min:50,
   *   value:100,
   * });
   *
   * // Render the component.
   * slider.render();
   *
   * // Set max value.
   * slider.activeColor = 'red';
   */
  set activeColor(c) {
    this.setProps({
      activeColor: c,
    });
  }

  /**
   * @returns {string}
   */
  get inactiveColor() {
    return this._props.inactiveColor;
  }

  /**
   * @description Sets the color of inactive track of the Slider component.
   * @member {string} TComponents.Slider#inactiveColor
   * @instance
   * @param {string} c - The color of inactive track of the Slider component.
   * @example
   * const slider = new TComponents.Slider(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   max:200,
   *   min:50,
   *   value:100,
   * });
   *
   * // Render the component.
   * slider.render();
   *
   * // Set max value.
   * slider.inactiveColor = 'red';
   */
  set inactiveColor(c) {
    this.setProps({
      inactiveColor: c,
    });
  }

  /**
   * @returns {boolean}
   */
  get displayTicks() {
    return this._props.displayTicks;
  }

  /**
   * @description Sets to true to display step ticks in the Slider component.
   * @member {boolean} TComponents.Slider#displayTicks
   * @instance
   * @param {boolean} d - True if expecting step ticks to be displayed.
   * @example
   * const slider = new TComponents.Slider(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   max:200,
   *   min:50,
   *   value:100,
   * });
   *
   * // Render the component.
   * slider.render();
   *
   * // Set max value.
   * slider.displayTicks = false;
   */
  set displayTicks(d) {
    this.setProps({
      displayTicks: d,
    });
  }
}

/**
 * @description Add css properties to the component
 * @alias loadCssClassFromString
 * @member TComponents.Slider.loadCssClassFromString
 * @method
 * @static
 * @param {string} css - The css string to be loaded into style tag
 * @returns {void}
 * @example
 * TComponents.Slider.loadCssClassFromString(`
 * .tc-slider {
 *   height: inherit;
 *  }`
 * );
 */
Slider.loadCssClassFromString(/*css*/ `
.tc-slider {
  height: inherit;
  width: 100%;
  min-width: 0px;
  min-height: 0px;
  padding: 0px;
  margin: 0px;
}

.tc-slider .fp-components-slider {
  max-width: 100%;
  box-sizing: border-box;
  max-height:100%;
  gap: calc(10px + 1%);
  justify-content: space-between;
  height: inherit;
  width: inherit;
  overflow: hidden;
}
  
.tc-slider .fp-components-slider > *:last-child {
  margin-bottom: 0 !important;
}

.tc-slider .fp-components-slider-disabled{
  cursor:not-allowed !important;
}

.tc-slider .fp-components-slider:hover{
  opacity:0.7;
}

.tc-slider .fp-components-slider__labels-wrapper{
  height: auto;
  margin-bottom: auto;
}

.tc-slider .fp-components-slider__range-wrapper {
  position: relative;
  height: 4px;
  width: 100%;
  background-position-x: -1px;
  border-radius:4px;
  background-image: linear-gradient(to right, white 1px, lightgrey 1px);   
  min-height: 4px;
}

.tc-slider .fp-components-slider__track {
  height: 4px;
  top: 50%;
  position: absolute;
  transform: translate(0%, -50%);
  pointer-events: none;
  background-color:transparent;
 }

.tc-slider .fp-components-slider__tick{
  width: 2px;
  height: 4px;
  position: absolute;
  background-color: transparent;
  transform: translate(-50%, -50%);
  top:50%;
  pointer-events: none;
}

.tc-slider .fp-components-slider__track--active {
  background-color: var(--fp-color-BLUE-60);
  pointer-events: inherit;
  border-radius:4px;
}

.tc-slider .fp-components-slider__track-touchbox {
  position: absolute;
  top:50%;
  transform: translate(-50%, -50%);
}
`);