ngx-bar-rating
Version:
Angular Bar Rating
296 lines (288 loc) • 19.4 kB
JavaScript
import * as i0 from '@angular/core';
import { inject, TemplateRef, Directive, InjectionToken, forwardRef, model, input, numberAttribute, booleanAttribute, signal, computed, output, contentChild, Component, ChangeDetectionStrategy, NgModule } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
import { NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
class ActiveRating {
constructor() {
this.template = inject(TemplateRef);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.1", ngImport: i0, type: ActiveRating, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.1.1", type: ActiveRating, isStandalone: true, selector: "[ratingActive]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.1", ngImport: i0, type: ActiveRating, decorators: [{
type: Directive,
args: [{
selector: '[ratingActive]'
}]
}] });
class InactiveRating {
constructor() {
this.template = inject(TemplateRef);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.1", ngImport: i0, type: InactiveRating, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.1.1", type: InactiveRating, isStandalone: true, selector: "[ratingInactive]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.1", ngImport: i0, type: InactiveRating, decorators: [{
type: Directive,
args: [{
selector: '[ratingInactive]'
}]
}] });
class FractionRating {
constructor() {
this.template = inject(TemplateRef);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.1", ngImport: i0, type: FractionRating, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.1.1", type: FractionRating, isStandalone: true, selector: "[ratingFraction]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.1", ngImport: i0, type: FractionRating, decorators: [{
type: Directive,
args: [{
selector: '[ratingFraction]'
}]
}] });
const defaultOptions = {
theme: 'default',
maxValue: 5,
showText: false,
readonly: false
};
const BAR_RATING_OPTIONS = new InjectionToken('BAR_RATING_OPTIONS', {
providedIn: 'root',
factory: () => defaultOptions
});
function provideBarRatingOptions(options) {
return {
provide: BAR_RATING_OPTIONS,
useValue: { ...defaultOptions, ...options }
};
}
/** This allows support [(ngModel)] and ngControl. */
const RATING_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => BarRating),
multi: true
};
/** This allows control required validation. */
const RATING_VALUE_VALIDATOR = {
provide: NG_VALIDATORS,
useExisting: forwardRef(() => BarRating),
multi: true,
};
var BarRatingUnitStateEnum;
(function (BarRatingUnitStateEnum) {
BarRatingUnitStateEnum["active"] = "active";
BarRatingUnitStateEnum["inactive"] = "inactive";
BarRatingUnitStateEnum["selected"] = "selected";
BarRatingUnitStateEnum["fraction"] = "fraction";
})(BarRatingUnitStateEnum || (BarRatingUnitStateEnum = {}));
class BarRating {
constructor() {
this.defaultOptions = inject(BAR_RATING_OPTIONS);
this.onChange = () => {
};
this.onTouched = () => {
};
this.UNITS = BarRatingUnitStateEnum;
/** Current rating. Can be a decimal value like 3.14 */
this.rate = model();
/** Maximal rating that can be given using this widget. */
this.max = input(this.defaultOptions.maxValue, {
transform: numberAttribute
});
/** A flag indicating if rating can be updated. */
this.readonly = input(this.defaultOptions.readonly, {
transform: booleanAttribute,
alias: 'readonly'
});
/** Set the theme */
this.theme = input(this.defaultOptions.theme);
/** Show rating title */
this.showText = input(this.defaultOptions.showText, {
transform: booleanAttribute
});
/** Replace rate value with a title */
this.titles = input([]);
/** A flag indicating if rating is required for form validation. */
this.required = input(false, { transform: booleanAttribute });
this.tabIndex = input(0, { transform: numberAttribute });
this.hoveredIndex = signal(null);
this.contexts = computed(() => {
const length = this.max();
const currentRate = this.rate();
const hovered = this.hoveredIndex();
return Array.from({ length }, (_, i) => {
if (hovered) {
return i + 1 <= hovered ? BarRatingUnitStateEnum.active : BarRatingUnitStateEnum.inactive;
}
if (i + 1 <= currentRate) {
return BarRatingUnitStateEnum.selected;
}
if ((i + 1 === Math.round(currentRate) && currentRate % 1) >= 0.5) {
return BarRatingUnitStateEnum.fraction;
}
return BarRatingUnitStateEnum.inactive;
});
});
this.ratingText = computed(() => {
const value = this.hoveredIndex() || this.rate();
return this.titles()[value] || value;
});
/**
* A stream that forwards a bar rating click since clicks are not propagated
*/
this.barClick = output();
this.customActiveRating = contentChild(ActiveRating);
this.customInActiveRating = contentChild(InactiveRating);
this.customFractionRating = contentChild(FractionRating);
}
updateRating(value) {
this.rate.set(value);
this.onChange(value);
}
/**
* This is the initial value set to the component
*/
writeValue(value) {
if (value !== null) {
this.rate.set(value);
}
}
validate(c) {
return (this.required() && !c.value) ? { required: true } : null;
}
registerOnChange(fn) {
this.onChange = fn;
}
registerOnTouched(fn) {
this.onTouched = fn;
}
setDisabledState(isDisabled) {
this.disabled = isDisabled;
}
handleKeydown(event) {
if (this.readonly() || this.disabled)
return;
const currentRating = this.rate();
let newRating = currentRating;
switch (event.key) {
case 'ArrowRight':
newRating = Math.min(currentRating + 1, this.max()); // Increase rating
event.preventDefault();
break;
case 'ArrowLeft':
newRating = Math.max(currentRating - 1, 1); // Decrease rating
event.preventDefault();
break;
case 'Enter':
case ' ':
// Select the current rating (handled in click logic)
event.preventDefault();
break;
default:
return; // Ignore other keys
}
if (newRating !== currentRating) {
this.updateRating(newRating);
this.barClick.emit(newRating);
this.hoveredIndex.set(newRating);
}
}
handleClick(event) {
if (this.readonly() || this.disabled)
return;
const target = event.target;
const unitElement = target.closest('.br-unit');
const value = unitElement?.getAttribute('data-value');
if (value) {
const rating = parseInt(value, 10);
this.updateRating(rating);
this.barClick.emit(rating);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.1", ngImport: i0, type: BarRating, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.1.1", type: BarRating, isStandalone: true, selector: "bar-rating", inputs: { rate: { classPropertyName: "rate", publicName: "rate", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null }, showText: { classPropertyName: "showText", publicName: "showText", isSignal: true, isRequired: false, transformFunction: null }, titles: { classPropertyName: "titles", publicName: "titles", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { rate: "rateChange", barClick: "barClick" }, providers: [RATING_VALUE_ACCESSOR, RATING_VALUE_VALIDATOR], queries: [{ propertyName: "customActiveRating", first: true, predicate: ActiveRating, descendants: true, isSignal: true }, { propertyName: "customInActiveRating", first: true, predicate: InactiveRating, descendants: true, isSignal: true }, { propertyName: "customFractionRating", first: true, predicate: FractionRating, descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"br br-{{ customInActiveRating() ? '' : theme() }}\"\r\n [class.br-readonly]=\"readonly()\"\r\n [class.br-disabled]=\"disabled\"\r\n role=\"button\"\r\n [attr.tabindex]=\"disabled || readonly() ? -1 : tabIndex()\"\r\n (focusout)=\"onTouched()\"\r\n (click)=\"handleClick($event)\"\r\n (keydown)=\"handleKeydown($event)\">\r\n\r\n <div class=\"br-units\"\r\n (mouseleave)=\"hoveredIndex.set(0)\">\r\n @for (unit of contexts(); let i = $index; track i) {\r\n @let value = i + 1;\r\n <div class=\"br-unit\"\r\n [attr.data-value]=\"value\"\r\n (mouseenter)=\"hoveredIndex.set(value)\">\r\n @switch (unit) {\r\n @case (UNITS.fraction) {\r\n <ng-template [ngTemplateOutlet]=\"customFractionRating()?.template || fractionTemplate\"/>\r\n }\r\n @case (UNITS.inactive) {\r\n <ng-template [ngTemplateOutlet]=\"customInActiveRating()?.template || inactiveTemplate\"/>\r\n }\r\n @default {\r\n <ng-template [ngTemplateOutlet]=\"customActiveRating()?.template || activeTemplate\"/>\r\n }\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n @if (showText()) {\r\n <div class=\"br-text\">{{ ratingText() }}</div>\r\n }\r\n</div>\r\n\r\n<ng-template #fractionTemplate>\r\n <div class=\"br-unit-inner br-fraction\"></div>\r\n</ng-template>\r\n\r\n<ng-template #activeTemplate>\r\n <div class=\"br-unit-inner br-active\"></div>\r\n</ng-template>\r\n\r\n<ng-template #inactiveTemplate>\r\n <div class=\"br-unit-inner br-inactive\"></div>\r\n</ng-template>\r\n", styles: ["*{box-sizing:border-box}:host{--_br-font-size: var(--br-font-size, 16px);--_br-gap: var(--br-gap, 0);--_br-active-color: var(--br-active-color, #EDB867);--_br-inactive-color: var(--br-inactive-color, #D2D2D2);--_br-effect-scale: var(--br-effect-scale, 1.5);--_br-effect-duration: var(--br-effect-duration, .4s);--_br-effect-ease: var(--br-effect-ease, ease-out)}.br{position:relative}.br-units{display:flex;flex-wrap:nowrap;gap:var(--_br-gap)}.br-unit{font-size:var(--_br-font-size);cursor:pointer;-webkit-font-smoothing:antialiased;text-rendering:auto}.br-unit-inner{position:relative}.br-readonly .br-unit,.br-disabled .br-unit{cursor:default;pointer-events:none}.br-unit-clone{position:absolute;pointer-events:none;transform-origin:center;animation:scale-fade-out var(--_br-effect-duration) var(--_br-effect-ease)}@keyframes scale-fade-out{0%{transform:scale(1);opacity:1}to{transform:scale(var(--_br-effect-scale));opacity:0}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.1", ngImport: i0, type: BarRating, decorators: [{
type: Component,
args: [{ selector: 'bar-rating', providers: [RATING_VALUE_ACCESSOR, RATING_VALUE_VALIDATOR], imports: [NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"br br-{{ customInActiveRating() ? '' : theme() }}\"\r\n [class.br-readonly]=\"readonly()\"\r\n [class.br-disabled]=\"disabled\"\r\n role=\"button\"\r\n [attr.tabindex]=\"disabled || readonly() ? -1 : tabIndex()\"\r\n (focusout)=\"onTouched()\"\r\n (click)=\"handleClick($event)\"\r\n (keydown)=\"handleKeydown($event)\">\r\n\r\n <div class=\"br-units\"\r\n (mouseleave)=\"hoveredIndex.set(0)\">\r\n @for (unit of contexts(); let i = $index; track i) {\r\n @let value = i + 1;\r\n <div class=\"br-unit\"\r\n [attr.data-value]=\"value\"\r\n (mouseenter)=\"hoveredIndex.set(value)\">\r\n @switch (unit) {\r\n @case (UNITS.fraction) {\r\n <ng-template [ngTemplateOutlet]=\"customFractionRating()?.template || fractionTemplate\"/>\r\n }\r\n @case (UNITS.inactive) {\r\n <ng-template [ngTemplateOutlet]=\"customInActiveRating()?.template || inactiveTemplate\"/>\r\n }\r\n @default {\r\n <ng-template [ngTemplateOutlet]=\"customActiveRating()?.template || activeTemplate\"/>\r\n }\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n @if (showText()) {\r\n <div class=\"br-text\">{{ ratingText() }}</div>\r\n }\r\n</div>\r\n\r\n<ng-template #fractionTemplate>\r\n <div class=\"br-unit-inner br-fraction\"></div>\r\n</ng-template>\r\n\r\n<ng-template #activeTemplate>\r\n <div class=\"br-unit-inner br-active\"></div>\r\n</ng-template>\r\n\r\n<ng-template #inactiveTemplate>\r\n <div class=\"br-unit-inner br-inactive\"></div>\r\n</ng-template>\r\n", styles: ["*{box-sizing:border-box}:host{--_br-font-size: var(--br-font-size, 16px);--_br-gap: var(--br-gap, 0);--_br-active-color: var(--br-active-color, #EDB867);--_br-inactive-color: var(--br-inactive-color, #D2D2D2);--_br-effect-scale: var(--br-effect-scale, 1.5);--_br-effect-duration: var(--br-effect-duration, .4s);--_br-effect-ease: var(--br-effect-ease, ease-out)}.br{position:relative}.br-units{display:flex;flex-wrap:nowrap;gap:var(--_br-gap)}.br-unit{font-size:var(--_br-font-size);cursor:pointer;-webkit-font-smoothing:antialiased;text-rendering:auto}.br-unit-inner{position:relative}.br-readonly .br-unit,.br-disabled .br-unit{cursor:default;pointer-events:none}.br-unit-clone{position:absolute;pointer-events:none;transform-origin:center;animation:scale-fade-out var(--_br-effect-duration) var(--_br-effect-ease)}@keyframes scale-fade-out{0%{transform:scale(1);opacity:1}to{transform:scale(var(--_br-effect-scale));opacity:0}}\n"] }]
}] });
class BarRatingEffect {
constructor() {
this.barRating = inject(BarRating, {
host: true,
self: true
});
// Save the original handleClick method
const originalHandleClick = this.barRating.handleClick.bind(this.barRating);
// Override the handleClick method
this.barRating.handleClick = (event) => {
// Call the original handleClick logic
originalHandleClick(event);
// Add custom effect logic
this.handleClickWithEffect(event);
};
}
handleClickWithEffect(event) {
const target = event.target;
const unitElement = target.closest('.br-unit');
const clone = unitElement.cloneNode(true);
const rect = unitElement.getBoundingClientRect();
const parentRect = event.currentTarget.getBoundingClientRect();
clone.classList.add('br-unit-clone');
clone.style.left = `${rect.left - parentRect.left}px`;
clone.style.top = `${rect.top - parentRect.top}px`;
clone.style.width = `${rect.width}px`;
clone.style.height = `${rect.height}px`;
const parentElement = event.currentTarget;
parentElement.appendChild(clone);
clone.addEventListener('animationend', () => {
parentElement.removeChild(clone);
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.1", ngImport: i0, type: BarRatingEffect, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.1.1", type: BarRatingEffect, isStandalone: true, selector: "bar-rating[effect]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.1", ngImport: i0, type: BarRatingEffect, decorators: [{
type: Directive,
args: [{
selector: 'bar-rating[effect]',
}]
}], ctorParameters: () => [] });
class BarRatingModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.1", ngImport: i0, type: BarRatingModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.1.1", ngImport: i0, type: BarRatingModule, imports: [BarRating,
BarRatingEffect,
ActiveRating,
InactiveRating,
FractionRating], exports: [BarRating,
BarRatingEffect,
ActiveRating,
InactiveRating,
FractionRating] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.1.1", ngImport: i0, type: BarRatingModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.1", ngImport: i0, type: BarRatingModule, decorators: [{
type: NgModule,
args: [{
imports: [
BarRating,
BarRatingEffect,
ActiveRating,
InactiveRating,
FractionRating
],
exports: [
BarRating,
BarRatingEffect,
ActiveRating,
InactiveRating,
FractionRating
]
}]
}] });
/**
* Generated bundle index. Do not edit.
*/
export { ActiveRating, BAR_RATING_OPTIONS, BarRating, BarRatingEffect, BarRatingModule, FractionRating, InactiveRating, provideBarRatingOptions };
//# sourceMappingURL=ngx-bar-rating.mjs.map