import {clamp} from "lodash";
import {classMap} from "lit/directives/class-map.js";
import {type CSSResultGroup, html, unsafeCSS} from 'lit';
import {FormControlController, validValidityState} from "../../internal/form";
import {property, query, state} from 'lit/decorators.js';
import {styleMap} from 'lit/directives/style-map.js';
import {unsafeHTML} from "lit/directives/unsafe-html.js";
import type {ZincFormControl} from '../../internal/zinc-element';
import ZincElement from '../../internal/zinc-element';
import styles from './rating.scss';
/**
* @summary Short summary of the component's intended use.
* @documentation https://zinc.style/components/rating
* @status experimental
* @since 1.0
*
* @dependency zn-example
*
* @event zn-event-name - Emitted as an example.
*
* @slot - The default slot.
* @slot example - An example slot.
*
* @csspart base - The component's base wrapper.
*
* @cssproperty --example - An example CSS custom property.
*/
export default class ZnRating extends ZincElement implements ZincFormControl {
static styles: CSSResultGroup = unsafeCSS(styles);
private readonly formControlController = new FormControlController(this, {
assumeInteractionOn: ['zn-blur', 'zn-input']
});
@query('.rating') rating: HTMLElement;
@state() private hoverValue: number = 0;
@state() private isHovering: boolean = false;
@property() label: string;
@property() name: string;
@property({type: Number}) value: number = 0;
@property({type: Number}) max: number = 5;
@property({type: Number}) precision: number = 1;
@property({type: Boolean}) readonly: boolean = false;
@property({type: Boolean}) disabled: boolean = false;
@property({}) size: 'small' | 'medium' | 'large' = 'medium';
@property() getSymbol: (value: number) => string = () => '';
/** Gets the validity state object */
get validity() {
return validValidityState;
}
/** Gets the validation message */
get validationMessage() {
return "";
}
/** Checks the validity but does not show a validation message. Returns `true` when valid and `false` when invalid. */
checkValidity(): boolean {
return true;
}
/** Gets the associated form, if one exists. */
getForm(): HTMLFormElement | null {
return this.formControlController.getForm();
}
/** Checks for validity and shows the browser's validation message if the control is invalid. */
reportValidity() {
return true;
}
/** Sets a custom validation message. Pass an empty string to restore validity. */
setCustomValidity() {
this.formControlController.updateValidity();
}
private _roundToPrecision(value: number, precision: number): number {
const factor = 1 / precision;
return Math.ceil(value * factor) / factor;
}
private _getValueFromXCoordinate(coordinate: number): number {
const {left, width} = this.rating.getBoundingClientRect();
const value = this._roundToPrecision(((coordinate - left) / width) * this.max, this.precision);
return clamp(value, 0, this.max);
}
private _getValueFromMousePosition(event: MouseEvent): number {
return this._getValueFromXCoordinate(event.clientX);
}
private _getValueFromTouchPosition(event: TouchEvent): number {
return this._getValueFromXCoordinate(event.touches[0].clientX);
}
private _setValue(value: number) {
if (this.disabled || this.readonly) {
return;
}
this.value = value === this.value ? 0 : value;
this.isHovering = false;
}
private _handleClick(event: MouseEvent) {
if (this.readonly || this.disabled) {
return;
}
this._setValue(this._getValueFromMousePosition(event));
}
private _handleMouseEnter(event: MouseEvent) {
this.isHovering = true;
this.hoverValue = this._getValueFromMousePosition(event);
}
private _handleMouseMove(event: MouseEvent) {
this.hoverValue = this._getValueFromMousePosition(event);
}
private _handleMouseLeave() {
this.isHovering = false;
}
private _handleTouchStart(event: TouchEvent) {
this.isHovering = true;
this.hoverValue = this._getValueFromTouchPosition(event);
}
private _handleTouchMove(event: TouchEvent) {
this.hoverValue = this._getValueFromTouchPosition(event);
}
private _handleTouchEnd(event: TouchEvent) {
this.isHovering = false;
this._setValue(this._getValueFromTouchPosition(event));
event.preventDefault();
}
render() {
const counter = Array.from(Array(this.max).keys());
let displayValue = 0;
if (this.disabled || this.readonly) {
displayValue = this.value;
} else {
displayValue = this.isHovering ? this.hoverValue : this.value;
}
return html`
${counter.map(index => {
if (displayValue > index && displayValue < index + 1) {
return html`
${unsafeHTML(this.getSymbol(index + 1))}
${unsafeHTML(this.getSymbol(index + 1))}
`;
}
return html`
= index + 1
})}
role="presentation">
${unsafeHTML(this.getSymbol(index + 1))}
`;
})}
`;
}
}