UNPKG

ionic-angular

Version:

A powerful framework for building mobile and progressive web apps with JavaScript and Angular 2

743 lines 22.8 kB
import { ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, Input, Optional, Output, Renderer, ViewChild, ViewEncapsulation } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { clamp, isPresent, isTrueProperty } from '../../util/util'; import { Config } from '../../config/config'; import { DomController } from '../../platform/dom-controller'; import { Form } from '../../util/form'; import { Haptic } from '../../tap-click/haptic'; import { Ion } from '../ion'; import { Item } from '../item/item'; import { Platform } from '../../platform/platform'; import { pointerCoord } from '../../util/dom'; import { TimeoutDebouncer } from '../../util/debouncer'; import { UIEventManager } from '../../gestures/ui-event-manager'; export const /** @type {?} */ RANGE_VALUE_ACCESSOR = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => Range), multi: true }; /** * \@name Range * \@description * The Range slider lets users select from a range of values by moving * the slider knob. It can accept dual knobs, but by default one knob * controls the value of the range. * * ### Range Labels * Labels can be placed on either side of the range by adding the * `range-left` or `range-right` property to the element. The element * doesn't have to be an `ion-label`, it can be added to any element * to place it to the left or right of the range. See [usage](#usage) * below for examples. * * * ### Minimum and Maximum Values * Minimum and maximum values can be passed to the range through the `min` * and `max` properties, respectively. By default, the range sets the `min` * to `0` and the `max` to `100`. * * * ### Steps and Snaps * The `step` property specifies the value granularity of the range's value. * It can be useful to set the `step` when the value isn't in increments of `1`. * Setting the `step` property will show tick marks on the range for each step. * The `snaps` property can be set to automatically move the knob to the nearest * tick mark based on the step property value. * * * ### Dual Knobs * Setting the `dualKnobs` property to `true` on the range component will * enable two knobs on the range. If the range has two knobs, the value will * be an object containing two properties: `lower` and `upper`. * * * \@usage * ```html * <ion-list> * <ion-item> * <ion-range [(ngModel)]="singleValue" color="danger" pin="true"></ion-range> * </ion-item> * * <ion-item> * <ion-range min="-200" max="200" [(ngModel)]="saturation" color="secondary"> * <ion-label range-left>-200</ion-label> * <ion-label range-right>200</ion-label> * </ion-range> * </ion-item> * * <ion-item> * <ion-range min="20" max="80" step="2" [(ngModel)]="brightness"> * <ion-icon small range-left name="sunny"></ion-icon> * <ion-icon range-right name="sunny"></ion-icon> * </ion-range> * </ion-item> * * <ion-item> * <ion-label>step=100, snaps, {{singleValue4}}</ion-label> * <ion-range min="1000" max="2000" step="100" snaps="true" color="secondary" [(ngModel)]="singleValue4"></ion-range> * </ion-item> * * <ion-item> * <ion-label>dual, step=3, snaps, {{dualValue2 | json}}</ion-label> * <ion-range dualKnobs="true" [(ngModel)]="dualValue2" min="21" max="72" step="3" snaps="true"></ion-range> * </ion-item> * </ion-list> * ``` * * * \@demo /docs/v2/demos/src/range/ */ export class Range extends Ion { /** * @param {?} _form * @param {?} _haptic * @param {?} _item * @param {?} config * @param {?} _plt * @param {?} elementRef * @param {?} renderer * @param {?} _dom * @param {?} _cd */ constructor(_form, _haptic, _item, config, _plt, elementRef, renderer, _dom, _cd) { super(config, elementRef, renderer, 'range'); this._form = _form; this._haptic = _haptic; this._item = _item; this._plt = _plt; this._dom = _dom; this._cd = _cd; this._disabled = false; this._min = 0; this._max = 100; this._step = 1; this._valA = 0; this._valB = 0; this._ratioA = 0; this._ratioB = 0; this._debouncer = new TimeoutDebouncer(0); /** * @output {Range} Emitted when the range value changes. */ this.ionChange = new EventEmitter(); this._events = new UIEventManager(_plt); _form.register(this); if (_item) { this.id = 'rng-' + _item.registerInput('range'); this._lblId = 'lbl-' + _item.id; _item.setElementClass('item-range', true); } } /** * \@input {string} The color to use from your Sass `$colors` map. * Default options are: `"primary"`, `"secondary"`, `"danger"`, `"light"`, and `"dark"`. * For more information, see [Theming your App](/docs/v2/theming/theming-your-app). * @param {?} val * @return {?} */ set color(val) { this._setColor(val); } /** * \@input {string} The mode determines which platform styles to use. * Possible values are: `"ios"`, `"md"`, or `"wp"`. * For more information, see [Platform Styles](/docs/v2/theming/platform-specific-styles). * @param {?} val * @return {?} */ set mode(val) { this._setMode(val); } /** * \@input {number} Minimum integer value of the range. Defaults to `0`. * @return {?} */ get min() { return this._min; } /** * @param {?} val * @return {?} */ set min(val) { val = Math.round(val); if (!isNaN(val)) { this._min = val; } } /** * \@input {number} Maximum integer value of the range. Defaults to `100`. * @return {?} */ get max() { return this._max; } /** * @param {?} val * @return {?} */ set max(val) { val = Math.round(val); if (!isNaN(val)) { this._max = val; } } /** * \@input {number} Specifies the value granularity. Defaults to `1`. * @return {?} */ get step() { return this._step; } /** * @param {?} val * @return {?} */ set step(val) { val = Math.round(val); if (!isNaN(val) && val > 0) { this._step = val; } } /** * \@input {boolean} If true, the knob snaps to tick marks evenly spaced based * on the step property value. Defaults to `false`. * @return {?} */ get snaps() { return this._snaps; } /** * @param {?} val * @return {?} */ set snaps(val) { this._snaps = isTrueProperty(val); } /** * \@input {boolean} If true, a pin with integer value is shown when the knob * is pressed. Defaults to `false`. * @return {?} */ get pin() { return this._pin; } /** * @param {?} val * @return {?} */ set pin(val) { this._pin = isTrueProperty(val); } /** * \@input {number} How long, in milliseconds, to wait to trigger the * `ionChange` event after each change in the range value. Default `0`. * @return {?} */ get debounce() { return this._debouncer.wait; } /** * @param {?} val * @return {?} */ set debounce(val) { this._debouncer.wait = val; } /** * \@input {boolean} Show two knobs. Defaults to `false`. * @return {?} */ get dualKnobs() { return this._dual; } /** * @param {?} val * @return {?} */ set dualKnobs(val) { this._dual = isTrueProperty(val); } /** * \@input {boolean} If true, the user cannot interact with this element. * @return {?} */ get disabled() { return this._disabled; } /** * @param {?} val * @return {?} */ set disabled(val) { this._disabled = val = isTrueProperty(val); const /** @type {?} */ item = this._item; item && item.setElementClass('item-range-disabled', val); } /** * Returns the ratio of the knob's is current location, which is a number * between `0` and `1`. If two knobs are used, this property represents * the lower value. * @return {?} */ get ratio() { if (this._dual) { return Math.min(this._ratioA, this._ratioB); } return this._ratioA; } /** * Returns the ratio of the upper value's is current location, which is * a number between `0` and `1`. If there is only one knob, then this * will return `null`. * @return {?} */ get ratioUpper() { if (this._dual) { return Math.max(this._ratioA, this._ratioB); } return null; } /** * @return {?} */ ngAfterViewInit() { // add touchstart/mousedown listeners this._events.pointerEvents({ element: this._slider.nativeElement, pointerDown: this._pointerDown.bind(this), pointerMove: this._pointerMove.bind(this), pointerUp: this._pointerUp.bind(this), zone: true }); // build all the ticks if there are any to show this._createTicks(); } /** * \@internal * @param {?} ev * @return {?} */ _pointerDown(ev) { // TODO: we could stop listening for events instead of checking this._disabled. // since there are a lot of events involved, this solution is // enough for the moment if (this._disabled) { return false; } // prevent default so scrolling does not happen ev.preventDefault(); ev.stopPropagation(); // get the start coordinates const /** @type {?} */ current = pointerCoord(ev); // get the full dimensions of the slider element const /** @type {?} */ rect = this._rect = this._plt.getElementBoundingClientRect(this._slider.nativeElement); // figure out which knob they started closer to const /** @type {?} */ ratio = clamp(0, (current.x - rect.left) / (rect.width), 1); this._activeB = (Math.abs(ratio - this._ratioA) > Math.abs(ratio - this._ratioB)); // update the active knob's position this._update(current, rect, true); // trigger a haptic start this._haptic.gestureSelectionStart(); // return true so the pointer events // know everything's still valid return true; } /** * \@internal * @param {?} ev * @return {?} */ _pointerMove(ev) { if (!this._disabled) { // prevent default so scrolling does not happen ev.preventDefault(); ev.stopPropagation(); // update the active knob's position const /** @type {?} */ hasChanged = this._update(pointerCoord(ev), this._rect, true); if (hasChanged && this._snaps) { // trigger a haptic selection changed event // if this is a snap range this._haptic.gestureSelectionChanged(); } } } /** * \@internal * @param {?} ev * @return {?} */ _pointerUp(ev) { if (!this._disabled) { // prevent default so scrolling does not happen ev.preventDefault(); ev.stopPropagation(); // update the active knob's position this._update(pointerCoord(ev), this._rect, false); // trigger a haptic end this._haptic.gestureSelectionEnd(); } } /** * \@internal * @param {?} current * @param {?} rect * @param {?} isPressed * @return {?} */ _update(current, rect, isPressed) { // figure out where the pointer is currently at // update the knob being interacted with let /** @type {?} */ ratio = clamp(0, (current.x - rect.left) / (rect.width), 1); let /** @type {?} */ val = this._ratioToValue(ratio); if (this._snaps) { // snaps the ratio to the current value ratio = this._valueToRatio(val); } // update which knob is pressed this._pressed = isPressed; if (this._activeB) { // when the pointer down started it was determined // that knob B was the one they were interacting with this._pressedB = isPressed; this._pressedA = false; this._ratioB = ratio; if (val === this._valB) { // hasn't changed return false; } this._valB = val; } else { // interacting with knob A this._pressedA = isPressed; this._pressedB = false; this._ratioA = ratio; if (val === this._valA) { // hasn't changed return false; } this._valA = val; } // value has been updated if (this._dual) { // dual knobs have an lower and upper value if (!this.value) { // ensure we're always updating the same object this.value = {}; } this.value.lower = Math.min(this._valA, this._valB); this.value.upper = Math.max(this._valA, this._valB); (void 0) /* console.debug */; } else { // single knob only has one value this.value = this._valA; (void 0) /* console.debug */; } this._debouncer.debounce(() => { this.onChange(this.value); this.ionChange.emit(this); }); this._updateBar(); return true; } /** * \@internal * @return {?} */ _updateBar() { const /** @type {?} */ ratioA = this._ratioA; const /** @type {?} */ ratioB = this._ratioB; if (this._dual) { this._barL = `${(Math.min(ratioA, ratioB) * 100)}%`; this._barR = `${100 - (Math.max(ratioA, ratioB) * 100)}%`; } else { this._barL = ''; this._barR = `${100 - (ratioA * 100)}%`; } this._updateTicks(); } /** * \@internal * @return {?} */ _createTicks() { if (this._snaps) { this._dom.write(() => { // TODO: Fix to not use RAF this._ticks = []; for (var /** @type {?} */ value = this._min; value <= this._max; value += this._step) { var /** @type {?} */ ratio = this._valueToRatio(value); this._ticks.push({ ratio: ratio, left: `${ratio * 100}%`, }); } this._updateTicks(); }); } } /** * \@internal * @return {?} */ _updateTicks() { const /** @type {?} */ ticks = this._ticks; const /** @type {?} */ ratio = this.ratio; if (this._snaps && ticks) { if (this._dual) { var /** @type {?} */ upperRatio = this.ratioUpper; ticks.forEach(t => { t.active = (t.ratio >= ratio && t.ratio <= upperRatio); }); } else { ticks.forEach(t => { t.active = (t.ratio <= ratio); }); } } } /** * @param {?} isIncrease * @param {?} isKnobB * @return {?} */ _keyChg(isIncrease, isKnobB) { const /** @type {?} */ step = this._step; if (isKnobB) { if (isIncrease) { this._valB += step; } else { this._valB -= step; } this._valB = clamp(this._min, this._valB, this._max); this._ratioB = this._valueToRatio(this._valB); } else { if (isIncrease) { this._valA += step; } else { this._valA -= step; } this._valA = clamp(this._min, this._valA, this._max); this._ratioA = this._valueToRatio(this._valA); } this._updateBar(); } /** * \@internal * @param {?} ratio * @return {?} */ _ratioToValue(ratio) { ratio = Math.round(((this._max - this._min) * ratio)); ratio = Math.round(ratio / this._step) * this._step + this._min; return clamp(this._min, ratio, this._max); } /** * \@internal * @param {?} value * @return {?} */ _valueToRatio(value) { value = Math.round((value - this._min) / this._step) * this._step; value = value / (this._max - this._min); return clamp(0, value, 1); } /** * @param {?} val * @return {?} */ writeValue(val) { if (isPresent(val)) { this.value = val; if (this._dual) { this._valA = val.lower; this._valB = val.upper; this._ratioA = this._valueToRatio(val.lower); this._ratioB = this._valueToRatio(val.upper); } else { this._valA = val; this._ratioA = this._valueToRatio(val); } this._updateBar(); } } /** * @param {?} fn * @return {?} */ registerOnChange(fn) { this._fn = fn; this.onChange = (val) => { fn(val); this.onTouched(); }; } /** * @param {?} fn * @return {?} */ registerOnTouched(fn) { this.onTouched = fn; } /** * @param {?} val * @return {?} */ onChange(val) { // used when this input does not have an ngModel or formControlName this.onTouched(); this._cd.detectChanges(); } /** * @return {?} */ onTouched() { } /** * @param {?} isDisabled * @return {?} */ setDisabledState(isDisabled) { this.disabled = isDisabled; } /** * @return {?} */ ngOnDestroy() { this._form.deregister(this); this._events.destroy(); } } Range.decorators = [ { type: Component, args: [{ selector: 'ion-range', template: '<ng-content select="[range-left]"></ng-content>' + '<div class="range-slider" #slider>' + '<div class="range-tick" *ngFor="let t of _ticks" [style.left]="t.left" [class.range-tick-active]="t.active" role="presentation"></div>' + '<div class="range-bar" role="presentation"></div>' + '<div class="range-bar range-bar-active" [style.left]="_barL" [style.right]="_barR" #bar role="presentation"></div>' + '<div class="range-knob-handle" (ionIncrease)="_keyChg(true, false)" (ionDecrease)="_keyChg(false, false)" [ratio]="_ratioA" [val]="_valA" [pin]="_pin" [pressed]="_pressedA" [min]="_min" [max]="_max" [disabled]="_disabled" [labelId]="_lblId"></div>' + '<div class="range-knob-handle" (ionIncrease)="_keyChg(true, true)" (ionDecrease)="_keyChg(false, true)" [ratio]="_ratioB" [val]="_valB" [pin]="_pin" [pressed]="_pressedB" [min]="_min" [max]="_max" [disabled]="_disabled" [labelId]="_lblId" *ngIf="_dual"></div>' + '</div>' + '<ng-content select="[range-right]"></ng-content>', host: { '[class.range-disabled]': '_disabled', '[class.range-pressed]': '_pressed', '[class.range-has-pin]': '_pin' }, providers: [RANGE_VALUE_ACCESSOR], encapsulation: ViewEncapsulation.None, },] }, ]; /** @nocollapse */ Range.ctorParameters = () => [ { type: Form, }, { type: Haptic, }, { type: Item, decorators: [{ type: Optional },] }, { type: Config, }, { type: Platform, }, { type: ElementRef, }, { type: Renderer, }, { type: DomController, }, { type: ChangeDetectorRef, }, ]; Range.propDecorators = { '_slider': [{ type: ViewChild, args: ['slider',] },], 'color': [{ type: Input },], 'mode': [{ type: Input },], 'min': [{ type: Input },], 'max': [{ type: Input },], 'step': [{ type: Input },], 'snaps': [{ type: Input },], 'pin': [{ type: Input },], 'debounce': [{ type: Input },], 'dualKnobs': [{ type: Input },], 'disabled': [{ type: Input },], 'ionChange': [{ type: Output },], }; function Range_tsickle_Closure_declarations() { /** @type {?} */ Range.decorators; /** * @nocollapse * @type {?} */ Range.ctorParameters; /** @type {?} */ Range.propDecorators; /** @type {?} */ Range.prototype._dual; /** @type {?} */ Range.prototype._pin; /** @type {?} */ Range.prototype._disabled; /** @type {?} */ Range.prototype._pressed; /** @type {?} */ Range.prototype._lblId; /** @type {?} */ Range.prototype._fn; /** @type {?} */ Range.prototype._activeB; /** @type {?} */ Range.prototype._rect; /** @type {?} */ Range.prototype._ticks; /** @type {?} */ Range.prototype._min; /** @type {?} */ Range.prototype._max; /** @type {?} */ Range.prototype._step; /** @type {?} */ Range.prototype._snaps; /** @type {?} */ Range.prototype._valA; /** @type {?} */ Range.prototype._valB; /** @type {?} */ Range.prototype._ratioA; /** @type {?} */ Range.prototype._ratioB; /** @type {?} */ Range.prototype._pressedA; /** @type {?} */ Range.prototype._pressedB; /** @type {?} */ Range.prototype._barL; /** @type {?} */ Range.prototype._barR; /** @type {?} */ Range.prototype._debouncer; /** @type {?} */ Range.prototype._events; /** @type {?} */ Range.prototype._slider; /** @type {?} */ Range.prototype.value; /** @type {?} */ Range.prototype.id; /** * \@output {Range} Emitted when the range value changes. * @type {?} */ Range.prototype.ionChange; /** @type {?} */ Range.prototype._form; /** @type {?} */ Range.prototype._haptic; /** @type {?} */ Range.prototype._item; /** @type {?} */ Range.prototype._plt; /** @type {?} */ Range.prototype._dom; /** @type {?} */ Range.prototype._cd; } //# sourceMappingURL=range.js.map