diff --git a/projects/supernova/src/lib/components/range-slider/range-slider.component.ts b/projects/supernova/src/lib/components/range-slider/range-slider.component.ts
index 4b145f79..325fe40e 100644
--- a/projects/supernova/src/lib/components/range-slider/range-slider.component.ts
+++ b/projects/supernova/src/lib/components/range-slider/range-slider.component.ts
@@ -1,124 +1,26 @@
-import { FocusMonitor, FocusOrigin } from '@angular/cdk/a11y';
-import { Directionality } from '@angular/cdk/bidi';
-import { BooleanInput, coerceBooleanProperty, coerceNumberProperty, NumberInput } from '@angular/cdk/coercion';
-import {
- DOWN_ARROW,
- END,
- HOME,
- LEFT_ARROW,
- PAGE_DOWN,
- PAGE_UP,
- RIGHT_ARROW,
- UP_ARROW,
- hasModifierKey,
-} from '@angular/cdk/keycodes';
-import {
- Attribute,
- ChangeDetectionStrategy,
- ChangeDetectorRef,
- Component,
- ElementRef,
- EventEmitter,
- forwardRef,
- Inject,
- Input,
- OnDestroy,
- Optional,
- Output,
- ViewChild,
- ViewEncapsulation,
- NgZone,
- AfterViewInit,
- HostBinding,
-} from '@angular/core';
-import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+import { coerceNumberProperty } from '@angular/cdk/coercion';
+import { Component, forwardRef, Input } from '@angular/core';
-import { ANIMATION_MODULE_TYPE } from '@angular/platform-browser/animations';
-import { normalizePassiveListenerOptions } from '@angular/cdk/platform';
-import { DOCUMENT } from '@angular/common';
-import { Subscription } from 'rxjs';
-import { mixinTabIndex, mixinDisabled } from '../../mixins';
-import { SliderColor } from './range-slider.config';
+import { SliderBase } from '../slider-base/slider-base.component';
+import { NG_VALUE_ACCESSOR } from '@angular/forms';
-const activeEventOptions = normalizePassiveListenerOptions({ passive: false });
-
-/**
- * Provider Expression that allows mat-slider to register as a ControlValueAccessor.
- * This allows it to support [(ngModel)] and [formControl].
- * @docs-private
- */
-export const RANGE_SLIDER_VALUE_ACCESSOR: any = {
+export const SLIDER_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RangeSlider),
multi: true,
};
-/** A simple change event emitted by the Slider component. */
-export class RangeSliderChange {
- source: RangeSlider;
- value: [number, number] | null;
-}
-
-const _SliderBase = mixinTabIndex(
- mixinDisabled(
- class {
- constructor(public _elementRef: ElementRef) {}
- }
- )
-);
-
/**
* Allows users to select from a range of values by moving the slider thumb. It is similar in
* behavior to the native `` element.
*/
@Component({
+ providers: [SLIDER_VALUE_ACCESSOR],
selector: 'sn-range-slider',
exportAs: 'snRangeSlider',
- providers: [RANGE_SLIDER_VALUE_ACCESSOR],
- host: {
- '(focus)': '_onFocus()',
- '(blur)': '_onBlur()',
- '(keyup)': '_onKeyup()',
- '(keydown)': '_onKeydown($event)',
- '(mouseenter)': '_onMouseenter()',
-
- // On Safari starting to slide temporarily triggers text selection mode which
- // show the wrong cursor. We prevent it by stopping the `selectstart` event.
- '(selectstart)': '$event.preventDefault()',
- class: 'sn-slider sn-focus-indicator',
- role: 'slider',
- '[tabIndex]': 'tabIndex',
- '[attr.aria-disabled]': 'disabled',
- '[attr.aria-valuemax]': 'max',
- '[attr.aria-valuemin]': 'min',
- '[attr.aria-valuenow]': 'value',
- '[attr.aria-orientation]': 'vertical ? "vertical" : "horizontal"',
- '[class.sn-slider-disabled]': 'disabled',
- '[class.sn-slider-horizontal]': '!vertical',
- '[class.sn-slider-axis-inverted]': '_shouldInvertAxis()',
- '[class.sn-slider-invert-mouse-coords]': '_shouldInvertMouseCoords()',
- '[class.sn-slider-vertical]': 'vertical',
- '[class.sn-slider-sliding]': '_isSliding',
- '[class.sn-slider-focused-start]': '_focusIndex === 0',
- '[class.sn-slider-focused-end]': '_focusIndex === 1',
- },
templateUrl: 'range-slider.component.html',
- inputs: ['disabled', 'tabIndex'],
- encapsulation: ViewEncapsulation.None,
- changeDetection: ChangeDetectionStrategy.OnPush,
})
-// TODO extend from another class. Both this and Slider component
-export class RangeSlider extends _SliderBase implements ControlValueAccessor, OnDestroy, AfterViewInit {
- /** Whether the slider is inverted. */
- @Input()
- get invert(): boolean {
- return this._invert;
- }
- set invert(value: boolean) {
- this._invert = coerceBooleanProperty(value);
- }
- private _invert = false;
-
+export class RangeSlider extends SliderBase<[number, number]> {
/** The maximum value that the slider can have. */
@Input()
get max(): number {
@@ -126,12 +28,12 @@ export class RangeSlider extends _SliderBase implements ControlValueAccessor, On
}
set max(v: number) {
this._max = coerceNumberProperty(v, this._max);
- this._percent[1] = this._calculatePercentage(this._value?.[1]);
+ this._percent[1] = this._calculatePercentage(this.value[1]);
// Since this also modifies the percentage, we need to let the change detection know.
this._changeDetectorRef.markForCheck();
}
- private _max: number = 100;
+ protected _max: number = 100;
/** The minimum value that the slider can have. */
@Input()
@@ -140,50 +42,24 @@ export class RangeSlider extends _SliderBase implements ControlValueAccessor, On
}
set min(v: number) {
this._min = coerceNumberProperty(v, this._min);
- this._percent[0] = this._calculatePercentage(this._value?.[0]);
+ this._percent[0] = this._calculatePercentage(this.value[0]);
// Since this also modifies the percentage, we need to let the change detection know.
this._changeDetectorRef.markForCheck();
}
- private _min: number = 0;
-
- /** The values at which the thumb will snap. */
- @Input()
- get step(): number {
- return this._step;
- }
- set step(v: number) {
- this._step = coerceNumberProperty(v, this._step);
-
- if (this._step % 1 !== 0) {
- this._roundToDecimal = this._step.toString().split('.').pop()!.length;
- }
-
- // Since this could modify the label, we need to notify the change detection.
- this._changeDetectorRef.markForCheck();
- }
- private _step: number = 1;
-
- /** Whether or not to show the thumb label. */
- @Input()
- get thumbLabel(): boolean {
- return this._thumbLabel;
- }
- set thumbLabel(value: boolean) {
- this._thumbLabel = coerceBooleanProperty(value);
- }
- private _thumbLabel: boolean = true;
+ protected _min: number = 0;
/** Value of the slider. */
@Input()
get value(): [number, number] {
- // If the value needs to be read and it is still uninitialized, initialize it to the min.
+ // If the value needs to be read, and it is still uninitialized, initialize it to the min.
if (this._value === null) {
this.value = [this._min, this._max];
}
return this._value as [number, number];
}
set value(v: [number, number]) {
+ v = v || [this.min, this.max];
if (v[0] !== this._value?.[0] || v[1] !== this._value?.[1]) {
let value: [number, number] = [coerceNumberProperty(v[0], 0), coerceNumberProperty(v[1], 0)];
@@ -200,52 +76,7 @@ export class RangeSlider extends _SliderBase implements ControlValueAccessor, On
this._changeDetectorRef.markForCheck();
}
}
- private _value: [number, number] | null = null;
-
- /**
- * Function that will be used to format the value before it is displayed
- * in the thumb label. Can be used to format very large number in order
- * for them to fit into the slider thumb.
- */
- @Input() displayWith: (value: number) => string | number;
-
- /** Whether the slider is vertical. */
- @Input()
- get vertical(): boolean {
- return this._vertical;
- }
- set vertical(value: boolean) {
- this._vertical = coerceBooleanProperty(value);
- }
- private _vertical = false;
-
- @Input()
- get color(): SliderColor {
- return this._color || 'primary';
- }
- set color(newValue: SliderColor) {
- this._color = newValue;
- }
- private _color: SliderColor;
-
- @HostBinding('class') get switchClasses() {
- return {
- [`sn-slider-${this.color}`]: this.color,
- };
- }
-
- /** Event emitted when the slider value has changed. */
- @Output() readonly change: EventEmitter = new EventEmitter();
-
- /** Event emitted when the slider thumb moves. */
- @Output() readonly input: EventEmitter = new EventEmitter();
-
- /**
- * Emits when the raw value of the slider changes. This is here primarily
- * to facilitate the two-way binding for the `value` input.
- * @docs-private
- */
- @Output() readonly valueChange: EventEmitter<[number, number] | null> = new EventEmitter<[number, number] | null>();
+ protected _value: [number, number] | null = null;
/** The value to be used for display purposes. */
displayValue(focusIndex: 0 | 1): string | number {
@@ -273,19 +104,6 @@ export class RangeSlider extends _SliderBase implements ControlValueAccessor, On
return this.displayValue(1);
}
- /** set focus to the host element */
- focus(options?: FocusOptions) {
- this._focusHostElement(options);
- }
-
- /** blur the host element */
- blur() {
- this._blurHostElement();
- }
-
- /** onTouch function registered via registerOnTouch (ControlValueAccessor). */
- onTouched: () => any = () => {};
-
/** The percentage of the slider that coincides with the value. */
get percent(): [number, number] {
const min = this._clamp(this._percent[0], 0, this._percent[1]);
@@ -294,28 +112,7 @@ export class RangeSlider extends _SliderBase implements ControlValueAccessor, On
}
private _percent: [number, number] = [0, 1];
- _focusIndex: 0 | 1 = 0;
-
- /**
- * Whether or not the thumb is sliding and what the user is using to slide it with.
- * Used to determine if there should be a transition for the thumb and fill track.
- */
- _isSliding: 'keyboard' | 'pointer' | null = null;
-
- /**
- * Whether or not the slider is active (clicked or sliding).
- */
- _isActive: boolean = false;
-
- /**
- * Whether the axis of the slider is inverted.
- * (i.e. whether moving the thumb in the positive x or y direction decreases the slider's value).
- */
- _shouldInvertAxis() {
- // Standard non-inverted mode for a vertical slider should be dragging the thumb from bottom to
- // top. However from a y-axis standpoint this is inverted.
- return this.vertical ? !this.invert : this.invert;
- }
+ protected _focusIndex: 0 | 1 = 0;
/** CSS styles for the track fill element. */
_getTrackFillStyles(): { [key: string]: string } {
@@ -348,304 +145,11 @@ export class RangeSlider extends _SliderBase implements ControlValueAccessor, On
};
}
- /** The dimensions of the slider. */
- private _sliderDimensions: ClientRect | null = null;
-
- private _controlValueAccessorChangeFn: (value: any) => void = () => {};
-
- /** Decimal places to round to, based on the step amount. */
- private _roundToDecimal: number;
-
- /** Subscription to the Directionality change EventEmitter. */
- private _dirChangeSubscription = Subscription.EMPTY;
-
/** The value of the slider when the slide start event fires. */
- private _valueOnSlideStart: [number, number] | null;
-
- /** Reference to the inner slider wrapper element. */
- @ViewChild('sliderWrapper') private _sliderWrapper: ElementRef;
- /**
- * Whether mouse events should be converted to a slider position by calculating their distance
- * from the right or bottom edge of the slider as opposed to the top or left.
- */
- _shouldInvertMouseCoords() {
- const shouldInvertAxis = this._shouldInvertAxis();
- return this._getDirection() == 'rtl' && !this.vertical ? !shouldInvertAxis : shouldInvertAxis;
- }
-
- /** The language direction for this slider element. */
- private _getDirection() {
- return this._dir && this._dir.value == 'rtl' ? 'rtl' : 'ltr';
- }
-
- /** Keeps track of the last pointer event that was captured by the slider. */
- private _lastPointerEvent: MouseEvent | TouchEvent | null;
-
- /** Used to subscribe to global move and end events */
- protected _document: Document;
-
- /**
- * Identifier used to attribute a touch event to a particular slider.
- * Will be undefined if one of the following conditions is true:
- * - The user isn't dragging using a touch device.
- * - The browser doesn't support `Touch.identifier`.
- * - Dragging hasn't started yet.
- */
- private _touchId: number | undefined;
-
- constructor(
- elementRef: ElementRef,
- private _focusMonitor: FocusMonitor,
- private _changeDetectorRef: ChangeDetectorRef,
- @Optional() private _dir: Directionality,
- @Attribute('tabindex') tabIndex: string,
- private _ngZone: NgZone,
- @Inject(DOCUMENT) _document: any,
- @Optional() @Inject(ANIMATION_MODULE_TYPE) public _animationMode?: string
- ) {
- super(elementRef);
- this._document = _document;
- this.tabIndex = parseInt(tabIndex) || 0;
-
- _ngZone.runOutsideAngular(() => {
- const element = elementRef.nativeElement;
- element.addEventListener('mousedown', this._pointerDown, activeEventOptions);
- element.addEventListener('touchstart', this._pointerDown, activeEventOptions);
- });
- }
-
- ngAfterViewInit() {
- this._focusMonitor.monitor(this._elementRef, true).subscribe((origin: FocusOrigin) => {
- this._isActive = !!origin && origin !== 'keyboard';
- this._changeDetectorRef.detectChanges();
- });
- if (this._dir) {
- this._dirChangeSubscription = this._dir.change.subscribe(() => {
- this._changeDetectorRef.markForCheck();
- });
- }
- }
-
- ngOnDestroy() {
- const element = this._elementRef.nativeElement;
- element.removeEventListener('mousedown', this._pointerDown, activeEventOptions);
- element.removeEventListener('touchstart', this._pointerDown, activeEventOptions);
- this._lastPointerEvent = null;
- this._removeGlobalEvents();
- this._focusMonitor.stopMonitoring(this._elementRef);
- this._dirChangeSubscription.unsubscribe();
- }
-
- _onMouseenter() {
- if (this.disabled) {
- return;
- }
-
- // We save the dimensions of the slider here so we can use them to update the spacing of the
- // ticks and determine where on the slider click and slide events happen.
- this._sliderDimensions = this._getSliderDimensions();
- }
-
- _onFocus() {
- // We save the dimensions of the slider here so we can use them to update the spacing of the
- // ticks and determine where on the slider click and slide events happen.
- this._sliderDimensions = this._getSliderDimensions();
- }
-
- _onBlur() {
- this.onTouched();
- }
-
- _onKeydown(event: KeyboardEvent) {
- if (this.disabled || hasModifierKey(event) || (this._isSliding && this._isSliding !== 'keyboard')) {
- return;
- }
-
- const oldValue = this.value;
-
- switch (event.keyCode) {
- case PAGE_UP:
- this._increment(10);
- break;
- case PAGE_DOWN:
- this._increment(-10);
- break;
- case END:
- this._increment(this.max - this.min);
- break;
- case HOME:
- this._increment(this.max - this.min);
- break;
- case LEFT_ARROW:
- this._increment(this._getDirection() == 'rtl' || this.invert ? 1 : -1);
- break;
- case UP_ARROW:
- this._increment(1);
- break;
- case RIGHT_ARROW:
- this._increment(this._getDirection() == 'rtl' || this.invert ? -1 : 1);
- break;
- case DOWN_ARROW:
- this._increment(-1);
- break;
- default:
- // Return if the key is not one that we explicitly handle to avoid calling preventDefault on
- // it.
- return;
- }
-
- if (oldValue != this.value) {
- this._emitInputEvent();
- this._emitChangeEvent();
- }
-
- this._isSliding = 'keyboard';
- event.preventDefault();
- }
-
- _onKeyup() {
- if (this._isSliding === 'keyboard') {
- this._isSliding = null;
- }
- }
-
- /** Called when the user has put their pointer down on the slider. */
- private _pointerDown = (event: TouchEvent | MouseEvent) => {
- // Don't do anything if the slider is disabled or the
- // user is using anything other than the main mouse button.
- if (this.disabled || this._isSliding || (!isTouchEvent(event) && event.button !== 0)) {
- return;
- }
-
- this._ngZone.run(() => {
- this._touchId = isTouchEvent(event) ? getTouchIdForSlider(event, this._elementRef.nativeElement) : undefined;
- const pointerPosition = getPointerPositionOnPage(event, this._touchId);
-
- if (pointerPosition) {
- const oldValue = this.value;
- this._isSliding = 'pointer';
- this._lastPointerEvent = event;
- event.preventDefault();
- this._focusHostElement();
- this._onMouseenter(); // Simulate mouseenter in case this is a mobile device.
- this._bindGlobalEvents(event);
- this._focusHostElement();
- this._updateValueFromPosition(pointerPosition, true);
- this._valueOnSlideStart = oldValue;
-
- // Emit a change and input event if the value changed.
- if (oldValue != this.value) {
- this._emitInputEvent();
- }
- }
- });
- };
-
- /**
- * Called when the user has moved their pointer after
- * starting to drag. Bound on the document level.
- */
- private _pointerMove = (event: TouchEvent | MouseEvent) => {
- if (this._isSliding === 'pointer') {
- const pointerPosition = getPointerPositionOnPage(event, this._touchId);
-
- if (pointerPosition) {
- // Prevent the slide from selecting anything else.
- event.preventDefault();
- const oldValue = this.value;
- this._lastPointerEvent = event;
- this._updateValueFromPosition(pointerPosition, false);
-
- // Native range elements always emit `input` events when the value changed while sliding.
- if (oldValue != this.value) {
- this._emitInputEvent();
- }
- }
- }
- };
-
- /** Called when the user has lifted their pointer. Bound on the document level. */
- private _pointerUp = (event: TouchEvent | MouseEvent) => {
- if (this._isSliding === 'pointer') {
- if (
- !isTouchEvent(event) ||
- typeof this._touchId !== 'number' ||
- // Note that we use `changedTouches`, rather than `touches` because it
- // seems like in most cases `touches` is empty for `touchend` events.
- findMatchingTouch(event.changedTouches, this._touchId)
- ) {
- event.preventDefault();
- this._removeGlobalEvents();
- this._isSliding = null;
- this._touchId = undefined;
-
- if (this._valueOnSlideStart != this.value && !this.disabled) {
- this._emitChangeEvent();
- }
-
- this._valueOnSlideStart = this._lastPointerEvent = null;
- }
- }
- };
-
- /** Called when the window has lost focus. */
- private _windowBlur = () => {
- // If the window is blurred while dragging we need to stop dragging because the
- // browser won't dispatch the `mouseup` and `touchend` events anymore.
- if (this._lastPointerEvent) {
- this._pointerUp(this._lastPointerEvent);
- }
- };
-
- /** Use defaultView of injected document if available or fallback to global window reference */
- private _getWindow(): Window {
- return this._document.defaultView || window;
- }
-
- /**
- * Binds our global move and end events. They're bound at the document level and only while
- * dragging so that the user doesn't have to keep their pointer exactly over the slider
- * as they're swiping across the screen.
- */
- private _bindGlobalEvents(triggerEvent: TouchEvent | MouseEvent) {
- // Note that we bind the events to the `document`, because it allows us to capture
- // drag cancel events where the user's pointer is outside the browser window.
- const document = this._document;
- const isTouch = isTouchEvent(triggerEvent);
- const moveEventName = isTouch ? 'touchmove' : 'mousemove';
- const endEventName = isTouch ? 'touchend' : 'mouseup';
- document.addEventListener(moveEventName, this._pointerMove, activeEventOptions);
- document.addEventListener(endEventName, this._pointerUp, activeEventOptions);
-
- if (isTouch) {
- document.addEventListener('touchcancel', this._pointerUp, activeEventOptions);
- }
-
- const window = this._getWindow();
-
- if (typeof window !== 'undefined' && window) {
- window.addEventListener('blur', this._windowBlur);
- }
- }
-
- /** Removes any global event listeners that we may have added. */
- private _removeGlobalEvents() {
- const document = this._document;
- document.removeEventListener('mousemove', this._pointerMove, activeEventOptions);
- document.removeEventListener('mouseup', this._pointerUp, activeEventOptions);
- document.removeEventListener('touchmove', this._pointerMove, activeEventOptions);
- document.removeEventListener('touchend', this._pointerUp, activeEventOptions);
- document.removeEventListener('touchcancel', this._pointerUp, activeEventOptions);
-
- const window = this._getWindow();
-
- if (typeof window !== 'undefined' && window) {
- window.removeEventListener('blur', this._windowBlur);
- }
- }
+ protected _valueOnSlideStart: [number, number] | null;
/** Increments the slider by the given number of steps (negative number decrements). */
- private _increment(numSteps: number) {
+ protected _increment(numSteps: number) {
const rangeValue: [number, number] = [...this.value];
rangeValue[this._focusIndex] = this._focusIndex ? this.max : this.min;
rangeValue[this._focusIndex] = this._clamp((this.value[this._focusIndex] || 0) + this.step * numSteps, ...rangeValue);
@@ -653,7 +157,7 @@ export class RangeSlider extends _SliderBase implements ControlValueAccessor, On
}
/** Calculate the new value from the new physical location. The value will always be snapped. */
- private _updateValueFromPosition(pos: { x: number; y: number }, setFocusIndex: boolean) {
+ protected _updateValueFromPosition(pos: { x: number; y: number }, setFocusIndex = false) {
if (!this._sliderDimensions) {
return;
}
@@ -699,161 +203,4 @@ export class RangeSlider extends _SliderBase implements ControlValueAccessor, On
rangeValue[this._focusIndex] = value;
this.value = rangeValue;
}
-
- /** Emits a change event if the current value is different from the last emitted value. */
- private _emitChangeEvent() {
- this._controlValueAccessorChangeFn(this.value);
- this.valueChange.emit(this.value);
- this.change.emit(this._createChangeEvent());
- }
-
- /** Emits an input event when the current value is different from the last emitted value. */
- private _emitInputEvent() {
- this.input.emit(this._createChangeEvent());
- }
-
- /** Creates a slider change object from the specified value. */
- private _createChangeEvent(value = this.value): RangeSliderChange {
- let event = new RangeSliderChange();
-
- event.source = this;
- event.value = value;
-
- return event;
- }
-
- /** Calculates the percentage of the slider that a value is. */
- private _calculatePercentage(value: number | null) {
- return ((value || 0) - this.min) / (this.max - this.min);
- }
-
- /** Calculates the value a percentage of the slider corresponds to. */
- private _calculateValue(percentage: number) {
- return this.min + percentage * (this.max - this.min);
- }
-
- /** Return a number between two numbers. */
- private _clamp(value: number, min = 0, max = 1) {
- return Math.max(min, Math.min(value, max));
- }
-
- /**
- * Get the bounding client rect of the slider track element.
- * The track is used rather than the native element to ignore the extra space that the thumb can
- * take up.
- */
- private _getSliderDimensions() {
- return this._sliderWrapper ? this._sliderWrapper.nativeElement.getBoundingClientRect() : null;
- }
-
- /**
- * Focuses the native element.
- * Currently only used to allow a blur event to fire but will be used with keyboard input later.
- */
- private _focusHostElement(options?: FocusOptions) {
- this._elementRef.nativeElement.focus(options);
- }
-
- /** Blurs the native element. */
- private _blurHostElement() {
- this._elementRef.nativeElement.blur();
- }
-
- /**
- * Sets the model value. Implemented as part of ControlValueAccessor.
- * @param value
- */
- writeValue(value: any) {
- this.value = value;
- }
-
- /**
- * Registers a callback to be triggered when the value has changed.
- * Implemented as part of ControlValueAccessor.
- * @param fn Callback to be registered.
- */
- registerOnChange(fn: (value: any) => void) {
- this._controlValueAccessorChangeFn = fn;
- }
-
- /**
- * Registers a callback to be triggered when the component is touched.
- * Implemented as part of ControlValueAccessor.
- * @param fn Callback to be registered.
- */
- registerOnTouched(fn: any) {
- this.onTouched = fn;
- }
-
- /**
- * Sets whether the component should be disabled.
- * Implemented as part of ControlValueAccessor.
- * @param isDisabled
- */
- setDisabledState(isDisabled: boolean) {
- this.disabled = isDisabled;
- }
-
- static ngAcceptInputType_invert: BooleanInput;
- static ngAcceptInputType_max: NumberInput;
- static ngAcceptInputType_min: NumberInput;
- static ngAcceptInputType_step: NumberInput;
- static ngAcceptInputType_thumbLabel: BooleanInput;
- static ngAcceptInputType_value: NumberInput;
- static ngAcceptInputType_vertical: BooleanInput;
- static ngAcceptInputType_disabled: BooleanInput;
- static ngAcceptInputType_tabIndex: NumberInput;
-}
-
-/** Returns whether an event is a touch event. */
-function isTouchEvent(event: MouseEvent | TouchEvent): event is TouchEvent {
- // This function is called for every pixel that the user has dragged so we need it to be
- // as fast as possible. Since we only bind mouse events and touch events, we can assume
- // that if the event's name starts with `t`, it's a touch event.
- return event.type[0] === 't';
-}
-
-/** Gets the coordinates of a touch or mouse event relative to the viewport. */
-function getPointerPositionOnPage(event: MouseEvent | TouchEvent, id: number | undefined) {
- let point: { clientX: number; clientY: number } | undefined;
-
- if (isTouchEvent(event)) {
- // The `identifier` could be undefined if the browser doesn't support `TouchEvent.identifier`.
- // If that's the case, attribute the first touch to all active sliders. This should still cover
- // the most common case while only breaking multi-touch.
- if (typeof id === 'number') {
- point = findMatchingTouch(event.touches, id) || findMatchingTouch(event.changedTouches, id);
- } else {
- // `touches` will be empty for start/end events so we have to fall back to `changedTouches`.
- point = event.touches[0] || event.changedTouches[0];
- }
- } else {
- point = event;
- }
-
- return point ? { x: point.clientX, y: point.clientY } : undefined;
-}
-
-/** Finds a `Touch` with a specific ID in a `TouchList`. */
-function findMatchingTouch(touches: TouchList, id: number): Touch | undefined {
- for (let i = 0; i < touches.length; i++) {
- if (touches[i].identifier === id) {
- return touches[i];
- }
- }
-
- return undefined;
-}
-
-/** Gets the unique ID of a touch that matches a specific slider. */
-function getTouchIdForSlider(event: TouchEvent, sliderHost: HTMLElement): number | undefined {
- for (let i = 0; i < event.touches.length; i++) {
- const target = event.touches[i].target as HTMLElement;
-
- if (sliderHost === target || sliderHost.contains(target)) {
- return event.touches[i].identifier;
- }
- }
-
- return undefined;
}
diff --git a/projects/supernova/src/lib/components/range-slider/renge-slider.stories.ts b/projects/supernova/src/lib/components/range-slider/renge-slider.stories.ts
index 4d33727b..2dc907c5 100644
--- a/projects/supernova/src/lib/components/range-slider/renge-slider.stories.ts
+++ b/projects/supernova/src/lib/components/range-slider/renge-slider.stories.ts
@@ -4,7 +4,7 @@ import { Component, Input } from '@angular/core';
import { RangeSliderModule } from './range-slider.module';
import { CheckboxModule } from '../checkbox';
-import { RangeSliderChange } from './range-slider.component';
+import { ISliderChange } from '../slider-base/slider-change.model';
@Component({
selector: 'range-slider-example',
@@ -58,7 +58,7 @@ class RangeSliderComponent {
public output: [number, number] | null;
- change(e: RangeSliderChange): void {
+ change(e: ISliderChange<[number, number]>): void {
this.output = e.value;
}
diff --git a/projects/supernova/src/lib/components/slider-base/slider-base.component.ts b/projects/supernova/src/lib/components/slider-base/slider-base.component.ts
new file mode 100644
index 00000000..26f14038
--- /dev/null
+++ b/projects/supernova/src/lib/components/slider-base/slider-base.component.ts
@@ -0,0 +1,658 @@
+import { FocusMonitor, FocusOrigin } from '@angular/cdk/a11y';
+import { Directionality } from '@angular/cdk/bidi';
+import { coerceBooleanProperty, coerceNumberProperty } from '@angular/cdk/coercion';
+import {
+ DOWN_ARROW,
+ END,
+ hasModifierKey,
+ HOME,
+ LEFT_ARROW,
+ PAGE_DOWN,
+ PAGE_UP,
+ RIGHT_ARROW,
+ UP_ARROW,
+} from '@angular/cdk/keycodes';
+import {
+ Attribute,
+ ChangeDetectorRef,
+ Component,
+ EventEmitter,
+ ElementRef,
+ HostBinding,
+ Inject,
+ Input,
+ NgZone,
+ OnInit,
+ Optional,
+ ViewChild,
+ OnDestroy,
+ AfterViewInit,
+ ViewEncapsulation,
+ ChangeDetectionStrategy,
+ Output,
+} from '@angular/core';
+import { ControlValueAccessor } from '@angular/forms';
+
+import { ANIMATION_MODULE_TYPE } from '@angular/platform-browser/animations';
+import { normalizePassiveListenerOptions } from '@angular/cdk/platform';
+import { DOCUMENT } from '@angular/common';
+import { Subscription } from 'rxjs';
+import { mixinDisabled, mixinTabIndex } from '../../mixins';
+import { SliderColor } from './slider-base.config';
+import { ISliderChange } from './slider-change.model';
+
+const activeEventOptions = normalizePassiveListenerOptions({ passive: false });
+
+const _SliderBase = mixinTabIndex(
+ mixinDisabled(
+ class {
+ constructor(public _elementRef: ElementRef) {}
+ }
+ )
+);
+
+@Component({
+ host: {
+ '(focus)': '_onFocus()',
+ '(blur)': '_onBlur()',
+ '(keyup)': '_onKeyup()',
+ '(keydown)': '_onKeydown($event)',
+ '(mouseenter)': '_onMouseenter()',
+
+ // On Safari starting to slide temporarily triggers text selection mode which
+ // show the wrong cursor. We prevent it by stopping the `selectstart` event.
+ '(selectstart)': '$event.preventDefault()',
+ class: 'sn-slider sn-focus-indicator',
+ role: 'slider',
+ '[tabIndex]': 'tabIndex',
+ '[attr.aria-disabled]': 'disabled',
+ '[attr.aria-valuemax]': 'max',
+ '[attr.aria-valuemin]': 'min',
+ '[attr.aria-valuenow]': 'value',
+
+ '[attr.aria-valuetext]': 'valueText == null ? displayValue : valueText',
+ '[attr.aria-orientation]': 'vertical ? "vertical" : "horizontal"',
+ '[class.sn-slider-disabled]': 'disabled',
+ '[class.sn-slider-horizontal]': '!vertical',
+ '[class.sn-slider-axis-inverted]': '_shouldInvertAxis()',
+ '[class.sn-slider-invert-mouse-coords]': '_shouldInvertMouseCoords()',
+ '[class.sn-slider-vertical]': 'vertical',
+ '[class.sn-slider-sliding]': '_isSliding',
+ '[class.sn-slider-focused-start]': '_focusIndex === 0',
+ '[class.sn-slider-focused-end]': '_focusIndex === 1',
+ },
+ template: ``,
+ inputs: ['disabled', 'tabIndex'],
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export abstract class SliderBase extends _SliderBase implements OnInit, ControlValueAccessor, OnDestroy, AfterViewInit {
+ protected abstract _value: T;
+ protected abstract min: number;
+ protected abstract max: number;
+ protected abstract _updateValueFromPosition(pos: { x: number; y: number }, setFocusIndex?: boolean): void;
+ protected abstract _increment(numSteps: number): void;
+ protected abstract _valueOnSlideStart: T | null;
+ abstract value: T;
+ /** Whether the slider is inverted. */
+ @Input()
+ get invert(): boolean {
+ return this._invert;
+ }
+ set invert(value: boolean) {
+ this._invert = coerceBooleanProperty(value);
+ }
+ private _invert = false;
+
+ /** The values at which the thumb will snap. */
+ @Input()
+ get step(): number {
+ return this._step;
+ }
+ set step(v: number) {
+ this._step = coerceNumberProperty(v, this._step);
+
+ if (this._step % 1 !== 0) {
+ this._roundToDecimal = this._step.toString().split('.').pop()!.length;
+ }
+
+ // Since this could modify the label, we need to notify the change detection.
+ this._changeDetectorRef.markForCheck();
+ }
+ private _step: number = 1;
+
+ /** Whether or not to show the thumb label. */
+ @Input()
+ get thumbLabel(): boolean {
+ return this._thumbLabel;
+ }
+ set thumbLabel(value: boolean) {
+ this._thumbLabel = coerceBooleanProperty(value);
+ }
+ private _thumbLabel: boolean = true;
+
+ /**
+ * Function that will be used to format the value before it is displayed
+ * in the thumb label. Can be used to format very large number in order
+ * for them to fit into the slider thumb.
+ */
+ @Input() displayWith: (value: number) => string | number;
+
+ /** Text corresponding to the slider's value. Used primarily for improved accessibility. */
+ @Input() valueText: string;
+
+ /** Whether the slider is vertical. */
+ @Input()
+ get vertical(): boolean {
+ return this._vertical;
+ }
+ set vertical(value: boolean) {
+ this._vertical = coerceBooleanProperty(value);
+ }
+ private _vertical = false;
+
+ @Input()
+ get color(): SliderColor {
+ return this._color || 'primary';
+ }
+ set color(newValue: SliderColor) {
+ this._color = newValue;
+ }
+ private _color: SliderColor;
+
+ /** Event emitted when the slider value has changed. */
+ @Output() readonly change: EventEmitter> = new EventEmitter>();
+
+ /** Event emitted when the slider thumb moves. */
+ @Output() readonly input: EventEmitter> = new EventEmitter>();
+
+ /**
+ * Emits when the raw value of the slider changes. This is here primarily
+ * to facilitate the two-way binding for the `value` input.
+ * @docs-private
+ */
+ @Output() readonly valueChange: EventEmitter = new EventEmitter();
+
+ @HostBinding('class') get switchClasses() {
+ return {
+ [`sn-slider-${this.color}`]: this.color,
+ };
+ }
+
+ /** set focus to the host element */
+ focus(options?: FocusOptions) {
+ this._focusHostElement(options);
+ }
+
+ /** blur the host element */
+ blur() {
+ this._blurHostElement();
+ }
+
+ /** onTouch function registered via registerOnTouch (ControlValueAccessor). */
+ onTouched: () => any = () => {};
+
+ /**
+ * Whether or not the thumb is sliding and what the user is using to slide it with.
+ * Used to determine if there should be a transition for the thumb and fill track.
+ */
+ protected _isSliding: 'keyboard' | 'pointer' | null = null;
+
+ /**
+ * Whether or not the slider is active (clicked or sliding).
+ */
+ protected _isActive: boolean = false;
+
+ /**
+ * Whether the axis of the slider is inverted.
+ * (i.e. whether moving the thumb in the positive x or y direction decreases the slider's value).
+ */
+ protected _shouldInvertAxis() {
+ // Standard non-inverted mode for a vertical slider should be dragging the thumb from bottom to
+ // top. However from a y-axis standpoint this is inverted.
+ return this.vertical ? !this.invert : this.invert;
+ }
+
+ /** The dimensions of the slider. */
+ protected _sliderDimensions: ClientRect | null = null;
+
+ private _controlValueAccessorChangeFn: (value: any) => void = () => {};
+
+ /** Decimal places to round to, based on the step amount. */
+ protected _roundToDecimal: number;
+
+ /** Subscription to the Directionality change EventEmitter. */
+ private _dirChangeSubscription = Subscription.EMPTY;
+
+ /** Reference to the inner slider wrapper element. */
+ @ViewChild('sliderWrapper') private _sliderWrapper: ElementRef;
+
+ ngAfterViewInit() {
+ this._focusMonitor.monitor(this._elementRef, true).subscribe((origin: FocusOrigin) => {
+ this._isActive = !!origin && origin !== 'keyboard';
+ this._changeDetectorRef.detectChanges();
+ });
+ if (this._dir) {
+ this._dirChangeSubscription = this._dir.change.subscribe(() => {
+ this._changeDetectorRef.markForCheck();
+ });
+ }
+ }
+
+ ngOnDestroy() {
+ const element = this._elementRef.nativeElement;
+ element.removeEventListener('mousedown', this._pointerDown, activeEventOptions);
+ element.removeEventListener('touchstart', this._pointerDown, activeEventOptions);
+ this._lastPointerEvent = null;
+ this._removeGlobalEvents();
+ this._focusMonitor.stopMonitoring(this._elementRef);
+ this._dirChangeSubscription.unsubscribe();
+ }
+
+ private _onMouseenter() {
+ if (this.disabled) {
+ return;
+ }
+
+ // We save the dimensions of the slider here so we can use them to update the spacing of the
+ // ticks and determine where on the slider click and slide events happen.
+ this._sliderDimensions = this._getSliderDimensions();
+ }
+
+ private _onFocus() {
+ // We save the dimensions of the slider here so we can use them to update the spacing of the
+ // ticks and determine where on the slider click and slide events happen.
+ this._sliderDimensions = this._getSliderDimensions();
+ }
+
+ private _onBlur() {
+ this.onTouched();
+ }
+
+ /**
+ * Whether mouse events should be converted to a slider position by calculating their distance
+ * from the right or bottom edge of the slider as opposed to the top or left.
+ */
+ protected _shouldInvertMouseCoords() {
+ const shouldInvertAxis = this._shouldInvertAxis();
+ return this._getDirection() == 'rtl' && !this.vertical ? !shouldInvertAxis : shouldInvertAxis;
+ }
+
+ /** The language direction for this slider element. */
+ protected _getDirection() {
+ return this._dir && this._dir.value == 'rtl' ? 'rtl' : 'ltr';
+ }
+
+ /** Keeps track of the last pointer event that was captured by the slider. */
+ protected _lastPointerEvent: MouseEvent | TouchEvent | null;
+
+ /** Used to subscribe to global move and end events */
+ protected _document: Document;
+
+ /**
+ * Identifier used to attribute a touch event to a particular slider.
+ * Will be undefined if one of the following conditions is true:
+ * - The user isn't dragging using a touch device.
+ * - The browser doesn't support `Touch.identifier`.
+ * - Dragging hasn't started yet.
+ */
+ protected _touchId: number | undefined;
+
+ /** Returns whether an event is a touch event. */
+ static isTouchEvent(event: MouseEvent | TouchEvent): event is TouchEvent {
+ // This function is called for every pixel that the user has dragged so we need it to be
+ // as fast as possible. Since we only bind mouse events and touch events, we can assume
+ // that if the event's name starts with `t`, it's a touch event.
+ return event.type[0] === 't';
+ }
+
+ /** Gets the coordinates of a touch or mouse event relative to the viewport. */
+ static getPointerPositionOnPage(event: MouseEvent | TouchEvent, id: number | undefined) {
+ let point: { clientX: number; clientY: number } | undefined;
+
+ if (this.isTouchEvent(event)) {
+ // The `identifier` could be undefined if the browser doesn't support `TouchEvent.identifier`.
+ // If that's the case, attribute the first touch to all active sliders. This should still cover
+ // the most common case while only breaking multi-touch.
+ if (typeof id === 'number') {
+ point = this.findMatchingTouch(event.touches, id) || this.findMatchingTouch(event.changedTouches, id);
+ } else {
+ // `touches` will be empty for start/end events so we have to fall back to `changedTouches`.
+ point = event.touches[0] || event.changedTouches[0];
+ }
+ } else {
+ point = event;
+ }
+
+ return point ? { x: point.clientX, y: point.clientY } : undefined;
+ }
+
+ /** Finds a `Touch` with a specific ID in a `TouchList`. */
+ static findMatchingTouch(touches: TouchList, id: number): Touch | undefined {
+ for (let i = 0; i < touches.length; i++) {
+ if (touches[i].identifier === id) {
+ return touches[i];
+ }
+ }
+
+ return undefined;
+ }
+
+ /** Gets the unique ID of a touch that matches a specific slider. */
+ static getTouchIdForSlider(event: TouchEvent, sliderHost: HTMLElement): number | undefined {
+ for (let i = 0; i < event.touches.length; i++) {
+ const target = event.touches[i].target as HTMLElement;
+
+ if (sliderHost === target || sliderHost.contains(target)) {
+ return event.touches[i].identifier;
+ }
+ }
+
+ return undefined;
+ }
+
+ constructor(
+ private elementRef: ElementRef,
+ private _focusMonitor: FocusMonitor,
+ protected _changeDetectorRef: ChangeDetectorRef,
+ @Optional() private _dir: Directionality,
+ @Attribute('tabindex') tabIndex: string,
+ protected _ngZone: NgZone,
+ @Inject(DOCUMENT) _document: any,
+ @Optional() @Inject(ANIMATION_MODULE_TYPE) public _animationMode?: string
+ ) {
+ super(elementRef);
+ this._document = _document;
+ this.tabIndex = parseInt(tabIndex) || 0;
+ }
+
+ ngOnInit(): void {
+ this._ngZone.runOutsideAngular(() => {
+ const element = this.elementRef.nativeElement;
+ element.addEventListener('mousedown', this._pointerDown, activeEventOptions);
+ element.addEventListener('touchstart', this._pointerDown, activeEventOptions);
+ });
+ }
+
+ private _onKeydown(event: KeyboardEvent) {
+ if (this.disabled || hasModifierKey(event) || (this._isSliding && this._isSliding !== 'keyboard')) {
+ return;
+ }
+
+ const oldValue = this.value;
+
+ switch (event.keyCode) {
+ case PAGE_UP:
+ this._increment(10);
+ break;
+ case PAGE_DOWN:
+ this._increment(-10);
+ break;
+ case END:
+ this._increment(this.max - this.min);
+ break;
+ case HOME:
+ this._increment(this.max - this.min);
+ break;
+ case LEFT_ARROW:
+ this._increment(this._getDirection() == 'rtl' || this.invert ? 1 : -1);
+ break;
+ case UP_ARROW:
+ this._increment(1);
+ break;
+ case RIGHT_ARROW:
+ this._increment(this._getDirection() == 'rtl' || this.invert ? -1 : 1);
+ break;
+ case DOWN_ARROW:
+ this._increment(-1);
+ break;
+ default:
+ // Return if the key is not one that we explicitly handle to avoid calling preventDefault on
+ // it.
+ return;
+ }
+
+ if (oldValue != this.value) {
+ this._emitInputEvent();
+ this._emitChangeEvent();
+ }
+
+ this._isSliding = 'keyboard';
+ event.preventDefault();
+ }
+
+ private _onKeyup() {
+ if (this._isSliding === 'keyboard') {
+ this._isSliding = null;
+ }
+ }
+
+ /** Called when the user has put their pointer down on the slider. */
+ private _pointerDown = (event: TouchEvent | MouseEvent) => {
+ // Don't do anything if the slider is disabled or the
+ // user is using anything other than the main mouse button.
+ if (this.disabled || this._isSliding || (!SliderBase.isTouchEvent(event) && event.button !== 0)) {
+ return;
+ }
+
+ this._ngZone.run(() => {
+ this._touchId = SliderBase.isTouchEvent(event)
+ ? SliderBase.getTouchIdForSlider(event, this._elementRef.nativeElement)
+ : undefined;
+ const pointerPosition = SliderBase.getPointerPositionOnPage(event, this._touchId);
+
+ if (pointerPosition) {
+ const oldValue = this.value;
+ this._isSliding = 'pointer';
+ this._lastPointerEvent = event;
+ event.preventDefault();
+ this._focusHostElement();
+ this._onMouseenter(); // Simulate mouseenter in case this is a mobile device.
+ this._bindGlobalEvents(event);
+ this._focusHostElement();
+ this._updateValueFromPosition(pointerPosition, true);
+ this._valueOnSlideStart = oldValue;
+
+ // Emit a change and input event if the value changed.
+ if (oldValue != this.value) {
+ this._emitInputEvent();
+ }
+ }
+ });
+ };
+
+ /**
+ * Called when the user has moved their pointer after
+ * starting to drag. Bound on the document level.
+ */
+ private _pointerMove = (event: TouchEvent | MouseEvent) => {
+ if (this._isSliding === 'pointer') {
+ const pointerPosition = SliderBase.getPointerPositionOnPage(event, this._touchId);
+
+ if (pointerPosition) {
+ // Prevent the slide from selecting anything else.
+ event.preventDefault();
+ const oldValue = this.value;
+ this._lastPointerEvent = event;
+ this._updateValueFromPosition(pointerPosition);
+
+ // Native range elements always emit `input` events when the value changed while sliding.
+ if (oldValue != this.value) {
+ this._emitInputEvent();
+ }
+ }
+ }
+ };
+
+ /** Called when the user has lifted their pointer. Bound on the document level. */
+ private _pointerUp = (event: TouchEvent | MouseEvent) => {
+ if (this._isSliding === 'pointer') {
+ if (
+ !SliderBase.isTouchEvent(event) ||
+ typeof this._touchId !== 'number' ||
+ // Note that we use `changedTouches`, rather than `touches` because it
+ // seems like in most cases `touches` is empty for `touchend` events.
+ SliderBase.findMatchingTouch(event.changedTouches, this._touchId)
+ ) {
+ event.preventDefault();
+ this._removeGlobalEvents();
+ this._isSliding = null;
+ this._touchId = undefined;
+
+ if (this._valueOnSlideStart != this.value && !this.disabled) {
+ this._emitChangeEvent();
+ }
+
+ this._valueOnSlideStart = this._lastPointerEvent = null;
+ }
+ }
+ };
+
+ /** Called when the window has lost focus. */
+ private _windowBlur = () => {
+ // If the window is blurred while dragging we need to stop dragging because the
+ // browser won't dispatch the `mouseup` and `touchend` events anymore.
+ if (this._lastPointerEvent) {
+ this._pointerUp(this._lastPointerEvent);
+ }
+ };
+
+ /** Use defaultView of injected document if available or fallback to global window reference */
+ private _getWindow(): Window {
+ return this._document.defaultView || window;
+ }
+
+ /**
+ * Binds our global move and end events. They're bound at the document level and only while
+ * dragging so that the user doesn't have to keep their pointer exactly over the slider
+ * as they're swiping across the screen.
+ */
+ private _bindGlobalEvents(triggerEvent: TouchEvent | MouseEvent) {
+ // Note that we bind the events to the `document`, because it allows us to capture
+ // drag cancel events where the user's pointer is outside the browser window.
+ const document = this._document;
+ const isTouch = SliderBase.isTouchEvent(triggerEvent);
+ const moveEventName = isTouch ? 'touchmove' : 'mousemove';
+ const endEventName = isTouch ? 'touchend' : 'mouseup';
+ document.addEventListener(moveEventName, this._pointerMove, activeEventOptions);
+ document.addEventListener(endEventName, this._pointerUp, activeEventOptions);
+
+ if (isTouch) {
+ document.addEventListener('touchcancel', this._pointerUp, activeEventOptions);
+ }
+
+ const window = this._getWindow();
+
+ if (typeof window !== 'undefined' && window) {
+ window.addEventListener('blur', this._windowBlur);
+ }
+ }
+
+ /** Removes any global event listeners that we may have added. */
+ private _removeGlobalEvents() {
+ const document = this._document;
+ document.removeEventListener('mousemove', this._pointerMove, activeEventOptions);
+ document.removeEventListener('mouseup', this._pointerUp, activeEventOptions);
+ document.removeEventListener('touchmove', this._pointerMove, activeEventOptions);
+ document.removeEventListener('touchend', this._pointerUp, activeEventOptions);
+ document.removeEventListener('touchcancel', this._pointerUp, activeEventOptions);
+
+ const window = this._getWindow();
+
+ if (typeof window !== 'undefined' && window) {
+ window.removeEventListener('blur', this._windowBlur);
+ }
+ }
+
+ /** Emits a change event if the current value is different from the last emitted value. */
+ private _emitChangeEvent() {
+ this._controlValueAccessorChangeFn(this.value);
+ this.valueChange.emit(this.value);
+ this.change.emit(this._createChangeEvent());
+ }
+
+ /** Emits an input event when the current value is different from the last emitted value. */
+ private _emitInputEvent() {
+ this.input.emit(this._createChangeEvent());
+ }
+
+ /** Calculates the percentage of the slider that a value is. */
+ protected _calculatePercentage(value: number | null) {
+ return ((value || 0) - this.min) / (this.max - this.min);
+ }
+
+ /** Calculates the value a percentage of the slider corresponds to. */
+ protected _calculateValue(percentage: number) {
+ return this.min + percentage * (this.max - this.min);
+ }
+
+ /** Return a number between two numbers. */
+ protected _clamp(value: number, min = 0, max = 1) {
+ return Math.max(min, Math.min(value, max));
+ }
+
+ /**
+ * Get the bounding client rect of the slider track element.
+ * The track is used rather than the native element to ignore the extra space that the thumb can
+ * take up.
+ */
+ private _getSliderDimensions() {
+ return this._sliderWrapper ? this._sliderWrapper.nativeElement.getBoundingClientRect() : null;
+ }
+
+ /**
+ * Focuses the native element.
+ * Currently only used to allow a blur event to fire but will be used with keyboard input later.
+ */
+ private _focusHostElement(options?: FocusOptions) {
+ this._elementRef.nativeElement.focus(options);
+ }
+
+ /** Blurs the native element. */
+ private _blurHostElement() {
+ this._elementRef.nativeElement.blur();
+ }
+
+ /**
+ * Sets the model value. Implemented as part of ControlValueAccessor.
+ * @param value
+ */
+ writeValue(value: any) {
+ this.value = value;
+ }
+
+ /**
+ * Registers a callback to be triggered when the value has changed.
+ * Implemented as part of ControlValueAccessor.
+ * @param fn Callback to be registered.
+ */
+ registerOnChange(fn: (value: any) => void) {
+ this._controlValueAccessorChangeFn = fn;
+ }
+
+ /**
+ * Registers a callback to be triggered when the component is touched.
+ * Implemented as part of ControlValueAccessor.
+ * @param fn Callback to be registered.
+ */
+ registerOnTouched(fn: any) {
+ this.onTouched = fn;
+ }
+
+ /**
+ * Sets whether the component should be disabled.
+ * Implemented as part of ControlValueAccessor.
+ * @param isDisabled
+ */
+ setDisabledState(isDisabled: boolean) {
+ this.disabled = isDisabled;
+ }
+
+ /** Creates a slider change object from the specified value. */
+ protected _createChangeEvent(value = this.value): ISliderChange {
+ return {
+ source: this,
+ value,
+ };
+ }
+}
diff --git a/projects/supernova/src/lib/components/slider/slider.config.ts b/projects/supernova/src/lib/components/slider-base/slider-base.config.ts
similarity index 100%
rename from projects/supernova/src/lib/components/slider/slider.config.ts
rename to projects/supernova/src/lib/components/slider-base/slider-base.config.ts
diff --git a/projects/supernova/src/lib/components/slider-base/slider-change.model.ts b/projects/supernova/src/lib/components/slider-base/slider-change.model.ts
new file mode 100644
index 00000000..284315d1
--- /dev/null
+++ b/projects/supernova/src/lib/components/slider-base/slider-change.model.ts
@@ -0,0 +1,4 @@
+export interface ISliderChange {
+ source: any;
+ value: T;
+}
diff --git a/projects/supernova/src/lib/components/slider/slider.component.ts b/projects/supernova/src/lib/components/slider/slider.component.ts
index 775b5812..b1359486 100644
--- a/projects/supernova/src/lib/components/slider/slider.component.ts
+++ b/projects/supernova/src/lib/components/slider/slider.component.ts
@@ -1,46 +1,8 @@
-import { FocusMonitor, FocusOrigin } from '@angular/cdk/a11y';
-import { Directionality } from '@angular/cdk/bidi';
-import { BooleanInput, coerceBooleanProperty, coerceNumberProperty, NumberInput } from '@angular/cdk/coercion';
-import {
- DOWN_ARROW,
- END,
- HOME,
- LEFT_ARROW,
- PAGE_DOWN,
- PAGE_UP,
- RIGHT_ARROW,
- UP_ARROW,
- hasModifierKey,
-} from '@angular/cdk/keycodes';
-import {
- Attribute,
- ChangeDetectionStrategy,
- ChangeDetectorRef,
- Component,
- ElementRef,
- EventEmitter,
- forwardRef,
- Inject,
- Input,
- OnDestroy,
- Optional,
- Output,
- ViewChild,
- ViewEncapsulation,
- NgZone,
- AfterViewInit,
- HostBinding,
-} from '@angular/core';
-import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
-
-import { ANIMATION_MODULE_TYPE } from '@angular/platform-browser/animations';
-import { normalizePassiveListenerOptions } from '@angular/cdk/platform';
-import { DOCUMENT } from '@angular/common';
-import { Subscription } from 'rxjs';
-import { mixinTabIndex, mixinDisabled } from '../../mixins';
-import { SliderColor } from './slider.config';
-
-const activeEventOptions = normalizePassiveListenerOptions({ passive: false });
+import { coerceNumberProperty } from '@angular/cdk/coercion';
+import { Component, forwardRef, Input } from '@angular/core';
+
+import { SliderBase } from '../slider-base/slider-base.component';
+import { NG_VALUE_ACCESSOR } from '@angular/forms';
/** The thumb gap size for a disabled slider. */
const DISABLED_THUMB_GAP = 7;
@@ -51,83 +13,23 @@ const MIN_VALUE_NONACTIVE_THUMB_GAP = 7;
/** The thumb gap size for an active slider at its minimum value. */
const MIN_VALUE_ACTIVE_THUMB_GAP = 8;
-/**
- * Provider Expression that allows mat-slider to register as a ControlValueAccessor.
- * This allows it to support [(ngModel)] and [formControl].
- * @docs-private
- */
export const SLIDER_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => Slider),
multi: true,
};
-/** A simple change event emitted by the Slider component. */
-export class SliderChange {
- source: Slider;
- value: number | null;
-}
-
-const _SliderBase = mixinTabIndex(
- mixinDisabled(
- class {
- constructor(public _elementRef: ElementRef) {}
- }
- )
-);
-
/**
* Allows users to select from a range of values by moving the slider thumb. It is similar in
* behavior to the native `` element.
*/
@Component({
+ providers: [SLIDER_VALUE_ACCESSOR],
selector: 'sn-slider',
exportAs: 'snSlider',
- providers: [SLIDER_VALUE_ACCESSOR],
- host: {
- '(focus)': '_onFocus()',
- '(blur)': '_onBlur()',
- '(keyup)': '_onKeyup()',
- '(keydown)': '_onKeydown($event)',
- '(mouseenter)': '_onMouseenter()',
-
- // On Safari starting to slide temporarily triggers text selection mode which
- // show the wrong cursor. We prevent it by stopping the `selectstart` event.
- '(selectstart)': '$event.preventDefault()',
- class: 'sn-slider sn-focus-indicator',
- role: 'slider',
- '[tabIndex]': 'tabIndex',
- '[attr.aria-disabled]': 'disabled',
- '[attr.aria-valuemax]': 'max',
- '[attr.aria-valuemin]': 'min',
- '[attr.aria-valuenow]': 'value',
-
- '[attr.aria-valuetext]': 'valueText == null ? displayValue : valueText',
- '[attr.aria-orientation]': 'vertical ? "vertical" : "horizontal"',
- '[class.sn-slider-disabled]': 'disabled',
- '[class.sn-slider-horizontal]': '!vertical',
- '[class.sn-slider-axis-inverted]': '_shouldInvertAxis()',
- '[class.sn-slider-invert-mouse-coords]': '_shouldInvertMouseCoords()',
- '[class.sn-slider-vertical]': 'vertical',
- '[class.sn-slider-sliding]': '_isSliding',
- '[class.sn-slider-min-value]': '_isMinValue()',
- },
templateUrl: 'slider.component.html',
- inputs: ['disabled', 'tabIndex'],
- encapsulation: ViewEncapsulation.None,
- changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class Slider extends _SliderBase implements ControlValueAccessor, OnDestroy, AfterViewInit {
- /** Whether the slider is inverted. */
- @Input()
- get invert(): boolean {
- return this._invert;
- }
- set invert(value: boolean) {
- this._invert = coerceBooleanProperty(value);
- }
- private _invert = false;
-
+export class Slider extends SliderBase {
/** The maximum value that the slider can have. */
@Input()
get max(): number {
@@ -140,7 +42,7 @@ export class Slider extends _SliderBase implements ControlValueAccessor, OnDestr
// Since this also modifies the percentage, we need to let the change detection know.
this._changeDetectorRef.markForCheck();
}
- private _max: number = 100;
+ protected _max: number = 100;
/** The minimum value that the slider can have. */
@Input()
@@ -154,39 +56,12 @@ export class Slider extends _SliderBase implements ControlValueAccessor, OnDestr
// Since this also modifies the percentage, we need to let the change detection know.
this._changeDetectorRef.markForCheck();
}
- private _min: number = 0;
-
- /** The values at which the thumb will snap. */
- @Input()
- get step(): number {
- return this._step;
- }
- set step(v: number) {
- this._step = coerceNumberProperty(v, this._step);
-
- if (this._step % 1 !== 0) {
- this._roundToDecimal = this._step.toString().split('.').pop()!.length;
- }
-
- // Since this could modify the label, we need to notify the change detection.
- this._changeDetectorRef.markForCheck();
- }
- private _step: number = 1;
-
- /** Whether or not to show the thumb label. */
- @Input()
- get thumbLabel(): boolean {
- return this._thumbLabel;
- }
- set thumbLabel(value: boolean) {
- this._thumbLabel = coerceBooleanProperty(value);
- }
- private _thumbLabel: boolean = true;
+ protected _min: number = 0;
/** Value of the slider. */
@Input()
get value(): number {
- // If the value needs to be read and it is still uninitialized, initialize it to the min.
+ // If the value needs to be read, and it is still uninitialized, initialize it to the min.
if (this._value === null) {
this.value = this._min;
}
@@ -209,55 +84,7 @@ export class Slider extends _SliderBase implements ControlValueAccessor, OnDestr
this._changeDetectorRef.markForCheck();
}
}
- private _value: number | null = null;
-
- /**
- * Function that will be used to format the value before it is displayed
- * in the thumb label. Can be used to format very large number in order
- * for them to fit into the slider thumb.
- */
- @Input() displayWith: (value: number) => string | number;
-
- /** Text corresponding to the slider's value. Used primarily for improved accessibility. */
- @Input() valueText: string;
-
- /** Whether the slider is vertical. */
- @Input()
- get vertical(): boolean {
- return this._vertical;
- }
- set vertical(value: boolean) {
- this._vertical = coerceBooleanProperty(value);
- }
- private _vertical = false;
-
- @Input()
- get color(): SliderColor {
- return this._color || 'primary';
- }
- set color(newValue: SliderColor) {
- this._color = newValue;
- }
- private _color: SliderColor;
-
- @HostBinding('class') get switchClasses() {
- return {
- [`sn-slider-${this.color}`]: this.color,
- };
- }
-
- /** Event emitted when the slider value has changed. */
- @Output() readonly change: EventEmitter = new EventEmitter();
-
- /** Event emitted when the slider thumb moves. */
- @Output() readonly input: EventEmitter = new EventEmitter();
-
- /**
- * Emits when the raw value of the slider changes. This is here primarily
- * to facilitate the two-way binding for the `value` input.
- * @docs-private
- */
- @Output() readonly valueChange: EventEmitter = new EventEmitter();
+ protected _value: number | null = null;
/** The value to be used for display purposes. */
get displayValue(): string | number {
@@ -277,46 +104,12 @@ export class Slider extends _SliderBase implements ControlValueAccessor, OnDestr
return this.value || 0;
}
- /** set focus to the host element */
- focus(options?: FocusOptions) {
- this._focusHostElement(options);
- }
-
- /** blur the host element */
- blur() {
- this._blurHostElement();
- }
-
- /** onTouch function registered via registerOnTouch (ControlValueAccessor). */
- onTouched: () => any = () => {};
-
/** The percentage of the slider that coincides with the value. */
get percent(): number {
return this._clamp(this._percent);
}
private _percent: number = 0;
- /**
- * Whether or not the thumb is sliding and what the user is using to slide it with.
- * Used to determine if there should be a transition for the thumb and fill track.
- */
- _isSliding: 'keyboard' | 'pointer' | null = null;
-
- /**
- * Whether or not the slider is active (clicked or sliding).
- */
- _isActive: boolean = false;
-
- /**
- * Whether the axis of the slider is inverted.
- * (i.e. whether moving the thumb in the positive x or y direction decreases the slider's value).
- */
- _shouldInvertAxis() {
- // Standard non-inverted mode for a vertical slider should be dragging the thumb from bottom to
- // top. However from a y-axis standpoint this is inverted.
- return this.vertical ? !this.invert : this.invert;
- }
-
/** Whether the slider is at its minimum value. */
_isMinValue() {
return this.percent === 0;
@@ -378,309 +171,16 @@ export class Slider extends _SliderBase implements ControlValueAccessor, OnDestr
};
}
- /** The dimensions of the slider. */
- private _sliderDimensions: ClientRect | null = null;
-
- private _controlValueAccessorChangeFn: (value: any) => void = () => {};
-
- /** Decimal places to round to, based on the step amount. */
- private _roundToDecimal: number;
-
- /** Subscription to the Directionality change EventEmitter. */
- private _dirChangeSubscription = Subscription.EMPTY;
-
/** The value of the slider when the slide start event fires. */
- private _valueOnSlideStart: number | null;
-
- /** Reference to the inner slider wrapper element. */
- @ViewChild('sliderWrapper') private _sliderWrapper: ElementRef;
- /**
- * Whether mouse events should be converted to a slider position by calculating their distance
- * from the right or bottom edge of the slider as opposed to the top or left.
- */
- _shouldInvertMouseCoords() {
- const shouldInvertAxis = this._shouldInvertAxis();
- return this._getDirection() == 'rtl' && !this.vertical ? !shouldInvertAxis : shouldInvertAxis;
- }
-
- /** The language direction for this slider element. */
- private _getDirection() {
- return this._dir && this._dir.value == 'rtl' ? 'rtl' : 'ltr';
- }
-
- /** Keeps track of the last pointer event that was captured by the slider. */
- private _lastPointerEvent: MouseEvent | TouchEvent | null;
-
- /** Used to subscribe to global move and end events */
- protected _document: Document;
-
- /**
- * Identifier used to attribute a touch event to a particular slider.
- * Will be undefined if one of the following conditions is true:
- * - The user isn't dragging using a touch device.
- * - The browser doesn't support `Touch.identifier`.
- * - Dragging hasn't started yet.
- */
- private _touchId: number | undefined;
-
- constructor(
- elementRef: ElementRef,
- private _focusMonitor: FocusMonitor,
- private _changeDetectorRef: ChangeDetectorRef,
- @Optional() private _dir: Directionality,
- @Attribute('tabindex') tabIndex: string,
- private _ngZone: NgZone,
- @Inject(DOCUMENT) _document: any,
- @Optional() @Inject(ANIMATION_MODULE_TYPE) public _animationMode?: string
- ) {
- super(elementRef);
- this._document = _document;
- this.tabIndex = parseInt(tabIndex) || 0;
-
- _ngZone.runOutsideAngular(() => {
- const element = elementRef.nativeElement;
- element.addEventListener('mousedown', this._pointerDown, activeEventOptions);
- element.addEventListener('touchstart', this._pointerDown, activeEventOptions);
- });
- }
-
- ngAfterViewInit() {
- this._focusMonitor.monitor(this._elementRef, true).subscribe((origin: FocusOrigin) => {
- this._isActive = !!origin && origin !== 'keyboard';
- this._changeDetectorRef.detectChanges();
- });
- if (this._dir) {
- this._dirChangeSubscription = this._dir.change.subscribe(() => {
- this._changeDetectorRef.markForCheck();
- });
- }
- }
-
- ngOnDestroy() {
- const element = this._elementRef.nativeElement;
- element.removeEventListener('mousedown', this._pointerDown, activeEventOptions);
- element.removeEventListener('touchstart', this._pointerDown, activeEventOptions);
- this._lastPointerEvent = null;
- this._removeGlobalEvents();
- this._focusMonitor.stopMonitoring(this._elementRef);
- this._dirChangeSubscription.unsubscribe();
- }
-
- _onMouseenter() {
- if (this.disabled) {
- return;
- }
-
- // We save the dimensions of the slider here so we can use them to update the spacing of the
- // ticks and determine where on the slider click and slide events happen.
- this._sliderDimensions = this._getSliderDimensions();
- }
-
- _onFocus() {
- // We save the dimensions of the slider here so we can use them to update the spacing of the
- // ticks and determine where on the slider click and slide events happen.
- this._sliderDimensions = this._getSliderDimensions();
- }
-
- _onBlur() {
- this.onTouched();
- }
-
- _onKeydown(event: KeyboardEvent) {
- if (this.disabled || hasModifierKey(event) || (this._isSliding && this._isSliding !== 'keyboard')) {
- return;
- }
-
- const oldValue = this.value;
-
- switch (event.keyCode) {
- case PAGE_UP:
- this._increment(10);
- break;
- case PAGE_DOWN:
- this._increment(-10);
- break;
- case END:
- this.value = this.max;
- break;
- case HOME:
- this.value = this.min;
- break;
- case LEFT_ARROW:
- this._increment(this._getDirection() == 'rtl' || this.invert ? 1 : -1);
- break;
- case UP_ARROW:
- this._increment(1);
- break;
- case RIGHT_ARROW:
- this._increment(this._getDirection() == 'rtl' || this.invert ? -1 : 1);
- break;
- case DOWN_ARROW:
- this._increment(-1);
- break;
- default:
- // Return if the key is not one that we explicitly handle to avoid calling preventDefault on
- // it.
- return;
- }
-
- if (oldValue != this.value) {
- this._emitInputEvent();
- this._emitChangeEvent();
- }
-
- this._isSliding = 'keyboard';
- event.preventDefault();
- }
-
- _onKeyup() {
- if (this._isSliding === 'keyboard') {
- this._isSliding = null;
- }
- }
-
- /** Called when the user has put their pointer down on the slider. */
- private _pointerDown = (event: TouchEvent | MouseEvent) => {
- // Don't do anything if the slider is disabled or the
- // user is using anything other than the main mouse button.
- if (this.disabled || this._isSliding || (!isTouchEvent(event) && event.button !== 0)) {
- return;
- }
-
- this._ngZone.run(() => {
- this._touchId = isTouchEvent(event) ? getTouchIdForSlider(event, this._elementRef.nativeElement) : undefined;
- const pointerPosition = getPointerPositionOnPage(event, this._touchId);
-
- if (pointerPosition) {
- const oldValue = this.value;
- this._isSliding = 'pointer';
- this._lastPointerEvent = event;
- event.preventDefault();
- this._focusHostElement();
- this._onMouseenter(); // Simulate mouseenter in case this is a mobile device.
- this._bindGlobalEvents(event);
- this._focusHostElement();
- this._updateValueFromPosition(pointerPosition);
- this._valueOnSlideStart = oldValue;
-
- // Emit a change and input event if the value changed.
- if (oldValue != this.value) {
- this._emitInputEvent();
- }
- }
- });
- };
-
- /**
- * Called when the user has moved their pointer after
- * starting to drag. Bound on the document level.
- */
- private _pointerMove = (event: TouchEvent | MouseEvent) => {
- if (this._isSliding === 'pointer') {
- const pointerPosition = getPointerPositionOnPage(event, this._touchId);
-
- if (pointerPosition) {
- // Prevent the slide from selecting anything else.
- event.preventDefault();
- const oldValue = this.value;
- this._lastPointerEvent = event;
- this._updateValueFromPosition(pointerPosition);
-
- // Native range elements always emit `input` events when the value changed while sliding.
- if (oldValue != this.value) {
- this._emitInputEvent();
- }
- }
- }
- };
-
- /** Called when the user has lifted their pointer. Bound on the document level. */
- private _pointerUp = (event: TouchEvent | MouseEvent) => {
- if (this._isSliding === 'pointer') {
- if (
- !isTouchEvent(event) ||
- typeof this._touchId !== 'number' ||
- // Note that we use `changedTouches`, rather than `touches` because it
- // seems like in most cases `touches` is empty for `touchend` events.
- findMatchingTouch(event.changedTouches, this._touchId)
- ) {
- event.preventDefault();
- this._removeGlobalEvents();
- this._isSliding = null;
- this._touchId = undefined;
-
- if (this._valueOnSlideStart != this.value && !this.disabled) {
- this._emitChangeEvent();
- }
-
- this._valueOnSlideStart = this._lastPointerEvent = null;
- }
- }
- };
-
- /** Called when the window has lost focus. */
- private _windowBlur = () => {
- // If the window is blurred while dragging we need to stop dragging because the
- // browser won't dispatch the `mouseup` and `touchend` events anymore.
- if (this._lastPointerEvent) {
- this._pointerUp(this._lastPointerEvent);
- }
- };
-
- /** Use defaultView of injected document if available or fallback to global window reference */
- private _getWindow(): Window {
- return this._document.defaultView || window;
- }
-
- /**
- * Binds our global move and end events. They're bound at the document level and only while
- * dragging so that the user doesn't have to keep their pointer exactly over the slider
- * as they're swiping across the screen.
- */
- private _bindGlobalEvents(triggerEvent: TouchEvent | MouseEvent) {
- // Note that we bind the events to the `document`, because it allows us to capture
- // drag cancel events where the user's pointer is outside the browser window.
- const document = this._document;
- const isTouch = isTouchEvent(triggerEvent);
- const moveEventName = isTouch ? 'touchmove' : 'mousemove';
- const endEventName = isTouch ? 'touchend' : 'mouseup';
- document.addEventListener(moveEventName, this._pointerMove, activeEventOptions);
- document.addEventListener(endEventName, this._pointerUp, activeEventOptions);
-
- if (isTouch) {
- document.addEventListener('touchcancel', this._pointerUp, activeEventOptions);
- }
-
- const window = this._getWindow();
-
- if (typeof window !== 'undefined' && window) {
- window.addEventListener('blur', this._windowBlur);
- }
- }
-
- /** Removes any global event listeners that we may have added. */
- private _removeGlobalEvents() {
- const document = this._document;
- document.removeEventListener('mousemove', this._pointerMove, activeEventOptions);
- document.removeEventListener('mouseup', this._pointerUp, activeEventOptions);
- document.removeEventListener('touchmove', this._pointerMove, activeEventOptions);
- document.removeEventListener('touchend', this._pointerUp, activeEventOptions);
- document.removeEventListener('touchcancel', this._pointerUp, activeEventOptions);
-
- const window = this._getWindow();
-
- if (typeof window !== 'undefined' && window) {
- window.removeEventListener('blur', this._windowBlur);
- }
- }
+ protected _valueOnSlideStart: number | null;
/** Increments the slider by the given number of steps (negative number decrements). */
- private _increment(numSteps: number) {
+ protected _increment(numSteps: number) {
this.value = this._clamp((this.value || 0) + this.step * numSteps, this.min, this.max);
}
/** Calculate the new value from the new physical location. The value will always be snapped. */
- private _updateValueFromPosition(pos: { x: number; y: number }) {
+ protected _updateValueFromPosition(pos: { x: number; y: number }) {
if (!this._sliderDimensions) {
return;
}
@@ -714,161 +214,4 @@ export class Slider extends _SliderBase implements ControlValueAccessor, OnDestr
this.value = this._clamp(closestValue, this.min, this.max);
}
}
-
- /** Emits a change event if the current value is different from the last emitted value. */
- private _emitChangeEvent() {
- this._controlValueAccessorChangeFn(this.value);
- this.valueChange.emit(this.value);
- this.change.emit(this._createChangeEvent());
- }
-
- /** Emits an input event when the current value is different from the last emitted value. */
- private _emitInputEvent() {
- this.input.emit(this._createChangeEvent());
- }
-
- /** Creates a slider change object from the specified value. */
- private _createChangeEvent(value = this.value): SliderChange {
- let event = new SliderChange();
-
- event.source = this;
- event.value = value;
-
- return event;
- }
-
- /** Calculates the percentage of the slider that a value is. */
- private _calculatePercentage(value: number | null) {
- return ((value || 0) - this.min) / (this.max - this.min);
- }
-
- /** Calculates the value a percentage of the slider corresponds to. */
- private _calculateValue(percentage: number) {
- return this.min + percentage * (this.max - this.min);
- }
-
- /** Return a number between two numbers. */
- private _clamp(value: number, min = 0, max = 1) {
- return Math.max(min, Math.min(value, max));
- }
-
- /**
- * Get the bounding client rect of the slider track element.
- * The track is used rather than the native element to ignore the extra space that the thumb can
- * take up.
- */
- private _getSliderDimensions() {
- return this._sliderWrapper ? this._sliderWrapper.nativeElement.getBoundingClientRect() : null;
- }
-
- /**
- * Focuses the native element.
- * Currently only used to allow a blur event to fire but will be used with keyboard input later.
- */
- private _focusHostElement(options?: FocusOptions) {
- this._elementRef.nativeElement.focus(options);
- }
-
- /** Blurs the native element. */
- private _blurHostElement() {
- this._elementRef.nativeElement.blur();
- }
-
- /**
- * Sets the model value. Implemented as part of ControlValueAccessor.
- * @param value
- */
- writeValue(value: any) {
- this.value = value;
- }
-
- /**
- * Registers a callback to be triggered when the value has changed.
- * Implemented as part of ControlValueAccessor.
- * @param fn Callback to be registered.
- */
- registerOnChange(fn: (value: any) => void) {
- this._controlValueAccessorChangeFn = fn;
- }
-
- /**
- * Registers a callback to be triggered when the component is touched.
- * Implemented as part of ControlValueAccessor.
- * @param fn Callback to be registered.
- */
- registerOnTouched(fn: any) {
- this.onTouched = fn;
- }
-
- /**
- * Sets whether the component should be disabled.
- * Implemented as part of ControlValueAccessor.
- * @param isDisabled
- */
- setDisabledState(isDisabled: boolean) {
- this.disabled = isDisabled;
- }
-
- static ngAcceptInputType_invert: BooleanInput;
- static ngAcceptInputType_max: NumberInput;
- static ngAcceptInputType_min: NumberInput;
- static ngAcceptInputType_step: NumberInput;
- static ngAcceptInputType_thumbLabel: BooleanInput;
- static ngAcceptInputType_value: NumberInput;
- static ngAcceptInputType_vertical: BooleanInput;
- static ngAcceptInputType_disabled: BooleanInput;
- static ngAcceptInputType_tabIndex: NumberInput;
-}
-
-/** Returns whether an event is a touch event. */
-function isTouchEvent(event: MouseEvent | TouchEvent): event is TouchEvent {
- // This function is called for every pixel that the user has dragged so we need it to be
- // as fast as possible. Since we only bind mouse events and touch events, we can assume
- // that if the event's name starts with `t`, it's a touch event.
- return event.type[0] === 't';
-}
-
-/** Gets the coordinates of a touch or mouse event relative to the viewport. */
-function getPointerPositionOnPage(event: MouseEvent | TouchEvent, id: number | undefined) {
- let point: { clientX: number; clientY: number } | undefined;
-
- if (isTouchEvent(event)) {
- // The `identifier` could be undefined if the browser doesn't support `TouchEvent.identifier`.
- // If that's the case, attribute the first touch to all active sliders. This should still cover
- // the most common case while only breaking multi-touch.
- if (typeof id === 'number') {
- point = findMatchingTouch(event.touches, id) || findMatchingTouch(event.changedTouches, id);
- } else {
- // `touches` will be empty for start/end events so we have to fall back to `changedTouches`.
- point = event.touches[0] || event.changedTouches[0];
- }
- } else {
- point = event;
- }
-
- return point ? { x: point.clientX, y: point.clientY } : undefined;
-}
-
-/** Finds a `Touch` with a specific ID in a `TouchList`. */
-function findMatchingTouch(touches: TouchList, id: number): Touch | undefined {
- for (let i = 0; i < touches.length; i++) {
- if (touches[i].identifier === id) {
- return touches[i];
- }
- }
-
- return undefined;
-}
-
-/** Gets the unique ID of a touch that matches a specific slider. */
-function getTouchIdForSlider(event: TouchEvent, sliderHost: HTMLElement): number | undefined {
- for (let i = 0; i < event.touches.length; i++) {
- const target = event.touches[i].target as HTMLElement;
-
- if (sliderHost === target || sliderHost.contains(target)) {
- return event.touches[i].identifier;
- }
- }
-
- return undefined;
}
diff --git a/projects/supernova/src/lib/components/slider/slider.stories.ts b/projects/supernova/src/lib/components/slider/slider.stories.ts
index ed02ce3e..8dd6f120 100644
--- a/projects/supernova/src/lib/components/slider/slider.stories.ts
+++ b/projects/supernova/src/lib/components/slider/slider.stories.ts
@@ -4,7 +4,7 @@ import { Component, Input } from '@angular/core';
import { SliderModule } from './slider.module';
import { CheckboxModule } from '../checkbox';
-import { SliderChange } from './slider.component';
+import { ISliderChange } from '../slider-base/slider-change.model';
@Component({
selector: 'slider-example',
@@ -60,7 +60,7 @@ class SliderComponent {
public output: number | null;
- change(e: SliderChange): void {
+ change(e: ISliderChange): void {
this.output = e.value;
}