import { Component, OnInit, ViewChild, ElementRef, AfterViewInit, ViewEncapsulation, Renderer2, Input, Output, EventEmitter } from '@angular/core'; import * as d3 from 'd3'; import noUiSlider from 'nouislider'; import wNumb from 'wnumb'; import { gradeColors } from '../../../styles/colors'; type NumberPair = [number, number]; @Component({ selector: 'onguard-mos-range-slider', templateUrl: './mos-range-slider.component.html', styleUrls: ['./mos-range-slider.component.scss'], encapsulation: ViewEncapsulation.None }) export class MosRangeSliderComponent implements OnInit, AfterViewInit { @Input() range: [number, number] = [0, 5]; @Input() selection: [number, number] = [0, 5]; @Output() selectionChange = new EventEmitter<[number, number]>(); @Input() pipValues: number[]; @Input() colors = [gradeColors.bad, gradeColors.poor, gradeColors.fair, gradeColors.good, gradeColors.excellent]; @Input() step = 0.1; @Input() focusAlpha = '55'; @Input() formatting = { to: (value: number) => value.toFixed(1), from: (formattedValue: string) => Number(formattedValue) }; private gradeColorScale: d3.ScaleLinear; private transparentColorScale: d3.ScaleLinear; public style: {}; @ViewChild('slider') sliderElement: ElementRef; constructor(private renderer: Renderer2) { } ngOnInit(): void { // Default pipValues to range if (!this.pipValues || !this.pipValues.length) { this.pipValues = this.range; } this.setGradients(); } ngAfterViewInit(): void { // Create Slider const slider = noUiSlider.create(this.sliderElement.nativeElement, { start: this.range, range: { min: this.range[0], max: this.range[1] }, format: this.formatting, step: this.step, behaviour: 'drag-tap', connect: [true, true, true], tooltips: [true, true], pips: { density: 100, mode: 'values', values: this.pipValues } }) // Attach event handlers slider.on('update', this.updateHandle); slider.on('start', this.startDrag); slider.on('end', this.endDrag); // Set Gradient this.renderer.setStyle(this.sliderElement.nativeElement, 'background-image', `linear-gradient(to right, ${this.colors.join(', ')})`) // Set up connector events to style handles const connector = this.sliderElement.nativeElement.querySelector('.noUi-draggable'); connector.addEventListener('mouseover', () => { if (!this.isDragging()){ // add class to handles const handleElements = this.getHandles(); handleElements.forEach(handle => handle.classList.add('connector-hover')); } }); connector.addEventListener('mouseout', () => { if (!this.isDragging()){ // remove class from handles const handleElements = this.getHandles(); handleElements.forEach(handle => handle.classList.remove('connector-hover')); } }); } private updateHandle = (values: NumberPair, handle: number, _unencodedValues: number[]) => { const handleElement = this.getHandle(handle); const tooltipElement = handleElement.querySelector('.noUi-tooltip'); // Set handle Color this.renderer.setStyle(handleElement, '--box-shadow-color', this.transparentColorScale(values[handle]), 2); this.renderer.setStyle(handleElement, 'background', this.gradeColorScale(values[handle])); // Hide tooltip if on boundary if (this.pipValues.includes(Number(values[handle]))) { this.renderer.setStyle(tooltipElement, 'display', 'none'); } else { this.renderer.setStyle(tooltipElement, 'display', 'block'); } } private startDrag = (_values: NumberPair, handle: number, _unencodedValues: number[]) => { const handleElement = this.getHandle(handle); handleElement.classList.add('noUi-active'); handleElement.classList.remove('connector-hover'); } private endDrag = (_values: NumberPair, handle: number, unencodedValues: number[]) => { // Update values this.selection = unencodedValues as [number, number]; this.selectionChange.emit(this.selection); // Update Styles const handleElement = this.getHandle(handle); handleElement.classList.remove('noUi-active'); } private setGradients = () => { const gradientStep = this.range[1] / (this.colors.length - 1); const domain = this.colors.map((_color, index) => index * gradientStep); this.gradeColorScale = d3.scaleLinear() .domain(domain) .range(this.colors); this.transparentColorScale = d3.scaleLinear() .domain(domain) .range(this.colors.map(color => color + this.focusAlpha)); } private isDragging = (): boolean => { return this.sliderElement.nativeElement.classList.contains('noUi-state-drag'); } private getHandle = (index: number): Element => { return this.sliderElement.nativeElement.querySelector(`.noUi-handle[data-handle='${index}']`); } private getHandles = (): NodeListOf => { return this.sliderElement.nativeElement.querySelectorAll('.noUi-handle'); } }