import {
Component,
ChangeDetectionStrategy,
Input,
Output,
EventEmitter,
OnInit,
TemplateRef,
OnChanges,
SimpleChanges,
ContentChild,
forwardRef,
ChangeDetectorRef
} from '@angular/core';
import {NgbRatingConfig} from './rating-config';
import {toString, getValueInRange} from '../util/util';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
enum Key {
End = 35,
Home = 36,
ArrowLeft = 37,
ArrowUp = 38,
ArrowRight = 39,
ArrowDown = 40
}
/**
* Context for the custom star display template
*/
export interface StarTemplateContext {
/**
* Star fill percentage. An integer value between 0 and 100
*/
fill: number;
/**
* Index of the star.
*/
index: number;
}
const NGB_RATING_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NgbRating),
multi: true
};
/**
* Rating directive that will take care of visualising a star rating bar.
*/
@Component({
selector: 'ngb-rating',
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'class': 'd-inline-flex',
'tabindex': '0',
'role': 'slider',
'aria-valuemin': '0',
'[attr.aria-valuemax]': 'max',
'[attr.aria-valuenow]': 'nextRate',
'[attr.aria-valuetext]': 'ariaValueText()',
'[attr.aria-disabled]': 'readonly ? true : null',
'(blur)': 'handleBlur()',
'(keydown)': 'handleKeyDown($event)',
'(mouseleave)': 'reset()'
},
template: `
{{ fill === 100 ? '★' : '☆' }}
({{ index < nextRate ? '*' : ' ' }})
`,
providers: [NGB_RATING_VALUE_ACCESSOR]
})
export class NgbRating implements ControlValueAccessor,
OnInit, OnChanges {
contexts: StarTemplateContext[] = [];
disabled = false;
nextRate: number;
/**
* Maximal rating that can be given using this widget.
*/
@Input() max: number;
/**
* Current rating. Can be a decimal value like 3.75
*/
@Input() rate: number;
/**
* A flag indicating if rating can be updated.
*/
@Input() readonly: boolean;
/**
* A flag indicating if rating can be reset to 0 on mouse click
*/
@Input() resettable: boolean;
/**
* A template to override star display.
* Alternatively put a as the only child of element
*/
@Input() @ContentChild(TemplateRef) starTemplate: TemplateRef;
/**
* An event fired when a user is hovering over a given rating.
* Event's payload equals to the rating being hovered over.
*/
@Output() hover = new EventEmitter();
/**
* An event fired when a user stops hovering over a given rating.
* Event's payload equals to the rating of the last item being hovered over.
*/
@Output() leave = new EventEmitter();
/**
* An event fired when a user selects a new rating.
* Event's payload equals to the newly selected rating.
*/
@Output() rateChange = new EventEmitter(true);
onChange = (_: any) => {};
onTouched = () => {};
constructor(config: NgbRatingConfig, private _changeDetectorRef: ChangeDetectorRef) {
this.max = config.max;
this.readonly = config.readonly;
}
ariaValueText() { return `${this.nextRate} out of ${this.max}`; }
enter(value: number): void {
if (!this.readonly && !this.disabled) {
this._updateState(value);
}
this.hover.emit(value);
}
handleBlur() { this.onTouched(); }
handleClick(value: number) { this.update(this.resettable && this.rate === value ? 0 : value); }
handleKeyDown(event: KeyboardEvent) {
if (Key[toString(event.which)]) {
event.preventDefault();
switch (event.which) {
case Key.ArrowDown:
case Key.ArrowLeft:
this.update(this.rate - 1);
break;
case Key.ArrowUp:
case Key.ArrowRight:
this.update(this.rate + 1);
break;
case Key.Home:
this.update(0);
break;
case Key.End:
this.update(this.max);
break;
}
}
}
ngOnChanges(changes: SimpleChanges) {
if (changes['rate']) {
this.update(this.rate);
}
}
ngOnInit(): void {
this.contexts = Array.from({length: this.max}, (v, k) => ({fill: 0, index: k}));
this._updateState(this.rate);
}
registerOnChange(fn: (value: any) => any): void { this.onChange = fn; }
registerOnTouched(fn: () => any): void { this.onTouched = fn; }
reset(): void {
this.leave.emit(this.nextRate);
this._updateState(this.rate);
}
setDisabledState(isDisabled: boolean) { this.disabled = isDisabled; }
update(value: number, internalChange = true): void {
const newRate = getValueInRange(value, this.max, 0);
if (!this.readonly && !this.disabled && this.rate !== newRate) {
this.rate = newRate;
this.rateChange.emit(this.rate);
}
if (internalChange) {
this.onChange(this.rate);
this.onTouched();
}
this._updateState(this.rate);
}
writeValue(value) {
this.update(value, false);
this._changeDetectorRef.markForCheck();
}
private _getFillValue(index: number): number {
const diff = this.nextRate - index;
if (diff >= 1) {
return 100;
}
if (diff < 1 && diff > 0) {
return Number.parseInt((diff * 100).toFixed(2));
}
return 0;
}
private _updateState(nextValue: number) {
this.nextRate = nextValue;
this.contexts.forEach((context, index) => context.fill = this._getFillValue(index));
}
}