swimlane/ngx-ui

View on GitHub
projects/swimlane/ngx-ui/src/lib/components/slider/slider.component.ts

Summary

Maintainability
B
4 hrs
Test Coverage
import {
  Component,
  Input,
  Output,
  EventEmitter,
  OnInit,
  HostListener,
  forwardRef,
  ViewEncapsulation,
  ChangeDetectionStrategy,
  ChangeDetectorRef
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

let nextId = 0;

const SLIDER_VALUE_ACCESSOR: any = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => SliderComponent),
  multi: true
};

const edge = window.navigator.userAgent.indexOf('Edge') > -1;

@Component({
  selector: 'ngx-slider',
  exportAs: 'ngxSlider',
  templateUrl: './slider.component.html',
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrls: ['./slider.component.scss'],
  providers: [SLIDER_VALUE_ACCESSOR],
  host: {
    class: 'ngx-slider',
    '[class.filled]': 'filled',
    '[class.multiple]': 'multiple',
    '[class.disabled]': 'disabled',
    '[class.active]': 'active',
    '[class.vertical]': 'isVertical',
    '[class.horizontal]': 'isHorizontal'
  }
})
export class SliderComponent implements ControlValueAccessor, OnInit {
  @Input() id = `range-${++nextId}`;
  @Input() min = 0;
  @Input() max = 100;
  @Input() step = 1;
  @Input() orientation = 'horizontal';
  @Input() filled = false;
  @Input() multiple = false;
  @Input() disabled = false;
  @Input() showTicks = false;
  @Input() tickStep: number;
  @Output() change = new EventEmitter();

  _values = [0];
  _percents = [0];
  _thumbs: any[] = [];
  _fill: any;
  _ticks = [];
  _active = [];
  active: boolean;

  get value() {
    if (!this._values) return 0;
    if (this.multiple) return [...this._values].sort((a, b) => a - b).join(',');
    return this._values[0];
  }

  set value(val: any) {
    val = ('' + val).split(',');
    if (String(val) !== String(this._values)) {
      this.setValues(val);
      this.onChangeCallback(this._values);

      this.change.emit({
        value: this._values,
        percent: this.percent
      });

      this.cdr.markForCheck();
    }
  }

  get percent(): string {
    const pct = this._percents;
    if (this.multiple) return pct.join(',');
    return '' + pct[0];
  }

  get isHorizontal(): boolean {
    return this.orientation === 'horizontal';
  }

  get isVertical(): boolean {
    return this.orientation === 'vertical';
  }

  constructor(private cdr: ChangeDetectorRef) {}

  ngOnInit(): void {
    if (this.showTicks) {
      this._ticks = this.getTicks();
    }
    this.setValues([0]);
  }

  setValues(values: number[]) {
    this._values = values;
    this._percents = values
      .map(v => Math.max(this.min, Math.min(this.max, v)))
      .map(v => Math.round((100 * (v - this.min)) / (this.max - this.min)));

    this._thumbs = this._percents.map(p => {
      return {
        left: `calc(${p}% - ${p / 100}em)`
      };
    });

    if (this.filled) {
      this._fill = this.getFill();
    }

    if (this.showTicks) {
      this._ticks = this.getTicks();
    }
  }

  setActive(index: number, active: boolean) {
    this._active[index] = active;
  }

  setValue(val: number, index: number) {
    if (this._values[index] !== val) {
      this._values[index] = val;
      this.setValues(this._values);
      this.onChangeCallback(this.value);

      this.change.emit({
        value: this.value,
        percent: this.percent
      });
    }
  }

  getCount(): any {
    const idxs = [];
    const step = this.tickStep || this.step;

    let i = this.min;
    while (i <= this.max) {
      idxs.push(i);
      i += step;
    }

    return idxs;
  }

  getTicks(): any {
    return this.getCount().map(p => {
      return {
        left: `calc(${p}% - ${p / 100 - 0.5}em)`
      };
    });
  }

  getFill(): any {
    if (this.filled) {
      const percentMin = this.multiple ? Math.min(...this._percents) : 0;
      const percentMax = this.multiple ? Math.max(...this._percents) : this._percents[0];
      const width = percentMax - percentMin;

      if (edge && this.multiple) {
        return {
          left: `calc(${percentMin}% - ${percentMin / 100 - 0.5}em)`,
          'background-size': `calc(${width}% - ${width / 100}em) 100%`
        };
      }
      return {
        left: `${percentMin}%`,
        'background-size': `${width}% 100%`
      };
    }
  }

  @HostListener('mousedown', ['$event'])
  onMouseDown(event): void {
    event.stopPropagation();
    this.active = true;
  }

  @HostListener('mouseup', ['$event'])
  onMouseUp(event): void {
    event.stopPropagation();
    this.active = false;
  }

  onChange(event): void {
    event.stopPropagation();

    this.change.emit({
      value: this.value,
      percent: this.percent
    });
  }

  writeValue(val): void {
    val = val ? String(val).split(',') : ['0'];
    if (String(val) !== String(this._values)) {
      this.setValues(val.map(v => +v));
      this.cdr.markForCheck();
    }
  }

  registerOnChange(fn: any): void {
    this.onChangeCallback = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouchedCallback = fn;
  }

  trackIndex(index) {
    return index;
  }

  onChangeCallback: (_: any) => void = () => {
    // placeholder
  };

  onTouchedCallback: (_: any) => void = () => {
    // placeholder
  };
}