123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. import { ChangeDetectorRef, Component, ElementRef, Input, Optional, Renderer, ViewChild, ViewEncapsulation } from '@angular/core';
  2. import { NG_VALUE_ACCESSOR } from '@angular/forms';
  3. import { clamp, isTrueProperty } from '../../util/util';
  4. import { Config } from '../../config/config';
  5. import { DomController } from '../../platform/dom-controller';
  6. import { Form } from '../../util/form';
  7. import { Haptic } from '../../tap-click/haptic';
  8. import { BaseInput } from '../../util/base-input';
  9. import { Item } from '../item/item';
  10. import { Platform } from '../../platform/platform';
  11. import { pointerCoord } from '../../util/dom';
  12. import { UIEventManager } from '../../gestures/ui-event-manager';
  13. /**
  14. * @name Range
  15. * @description
  16. * The Range slider lets users select from a range of values by moving
  17. * the slider knob. It can accept dual knobs, but by default one knob
  18. * controls the value of the range.
  19. *
  20. * ### Range Labels
  21. * Labels can be placed on either side of the range by adding the
  22. * `range-left` or `range-right` property to the element. The element
  23. * doesn't have to be an `ion-label`, it can be added to any element
  24. * to place it to the left or right of the range. See [usage](#usage)
  25. * below for examples.
  26. *
  27. *
  28. * ### Minimum and Maximum Values
  29. * Minimum and maximum values can be passed to the range through the `min`
  30. * and `max` properties, respectively. By default, the range sets the `min`
  31. * to `0` and the `max` to `100`.
  32. *
  33. *
  34. * ### Steps and Snaps
  35. * The `step` property specifies the value granularity of the range's value.
  36. * It can be useful to set the `step` when the value isn't in increments of `1`.
  37. * Setting the `step` property will show tick marks on the range for each step.
  38. * The `snaps` property can be set to automatically move the knob to the nearest
  39. * tick mark based on the step property value.
  40. *
  41. *
  42. * ### Dual Knobs
  43. * Setting the `dualKnobs` property to `true` on the range component will
  44. * enable two knobs on the range. If the range has two knobs, the value will
  45. * be an object containing two properties: `lower` and `upper`.
  46. *
  47. *
  48. * @usage
  49. * ```html
  50. * <ion-list>
  51. * <ion-item>
  52. * <ion-range [(ngModel)]="singleValue" color="danger" pin="true"></ion-range>
  53. * </ion-item>
  54. *
  55. * <ion-item>
  56. * <ion-range min="-200" max="200" [(ngModel)]="saturation" color="secondary">
  57. * <ion-label range-left>-200</ion-label>
  58. * <ion-label range-right>200</ion-label>
  59. * </ion-range>
  60. * </ion-item>
  61. *
  62. * <ion-item>
  63. * <ion-range min="20" max="80" step="2" [(ngModel)]="brightness">
  64. * <ion-icon small range-left name="sunny"></ion-icon>
  65. * <ion-icon range-right name="sunny"></ion-icon>
  66. * </ion-range>
  67. * </ion-item>
  68. *
  69. * <ion-item>
  70. * <ion-label>step=100, snaps, {{singleValue4}}</ion-label>
  71. * <ion-range min="1000" max="2000" step="100" snaps="true" color="secondary" [(ngModel)]="singleValue4"></ion-range>
  72. * </ion-item>
  73. *
  74. * <ion-item>
  75. * <ion-label>dual, step=3, snaps, {{dualValue2 | json}}</ion-label>
  76. * <ion-range dualKnobs="true" [(ngModel)]="dualValue2" min="21" max="72" step="3" snaps="true"></ion-range>
  77. * </ion-item>
  78. * </ion-list>
  79. * ```
  80. *
  81. *
  82. * @demo /docs/demos/src/range/
  83. */
  84. export class Range extends BaseInput {
  85. constructor(form, _haptic, item, config, _plt, elementRef, renderer, _dom, _cd) {
  86. super(config, elementRef, renderer, 'range', 0, form, item, null);
  87. this._haptic = _haptic;
  88. this._plt = _plt;
  89. this._dom = _dom;
  90. this._cd = _cd;
  91. this._min = 0;
  92. this._max = 100;
  93. this._step = 1;
  94. this._valA = 0;
  95. this._valB = 0;
  96. this._ratioA = 0;
  97. this._ratioB = 0;
  98. this._events = new UIEventManager(_plt);
  99. }
  100. /**
  101. * @input {number} Minimum integer value of the range. Defaults to `0`.
  102. */
  103. get min() {
  104. return this._min;
  105. }
  106. set min(val) {
  107. val = Math.round(val);
  108. if (!isNaN(val)) {
  109. this._min = val;
  110. this._inputUpdated();
  111. }
  112. }
  113. /**
  114. * @input {number} Maximum integer value of the range. Defaults to `100`.
  115. */
  116. get max() {
  117. return this._max;
  118. }
  119. set max(val) {
  120. val = Math.round(val);
  121. if (!isNaN(val)) {
  122. this._max = val;
  123. this._inputUpdated();
  124. }
  125. }
  126. /**
  127. * @input {number} Specifies the value granularity. Defaults to `1`.
  128. */
  129. get step() {
  130. return this._step;
  131. }
  132. set step(val) {
  133. val = Math.round(val);
  134. if (!isNaN(val) && val > 0) {
  135. this._step = val;
  136. }
  137. }
  138. /**
  139. * @input {boolean} If true, the knob snaps to tick marks evenly spaced based
  140. * on the step property value. Defaults to `false`.
  141. */
  142. get snaps() {
  143. return this._snaps;
  144. }
  145. set snaps(val) {
  146. this._snaps = isTrueProperty(val);
  147. }
  148. /**
  149. * @input {boolean} If true, a pin with integer value is shown when the knob
  150. * is pressed. Defaults to `false`.
  151. */
  152. get pin() {
  153. return this._pin;
  154. }
  155. set pin(val) {
  156. this._pin = isTrueProperty(val);
  157. }
  158. /**
  159. * @input {number} How long, in milliseconds, to wait to trigger the
  160. * `ionChange` event after each change in the range value. Default `0`.
  161. */
  162. get debounce() {
  163. return this._debouncer.wait;
  164. }
  165. set debounce(val) {
  166. this._debouncer.wait = val;
  167. }
  168. /**
  169. * @input {boolean} Show two knobs. Defaults to `false`.
  170. */
  171. get dualKnobs() {
  172. return this._dual;
  173. }
  174. set dualKnobs(val) {
  175. this._dual = isTrueProperty(val);
  176. }
  177. /**
  178. * Returns the ratio of the knob's is current location, which is a number
  179. * between `0` and `1`. If two knobs are used, this property represents
  180. * the lower value.
  181. */
  182. get ratio() {
  183. if (this._dual) {
  184. return Math.min(this._ratioA, this._ratioB);
  185. }
  186. return this._ratioA;
  187. }
  188. /**
  189. * Returns the ratio of the upper value's is current location, which is
  190. * a number between `0` and `1`. If there is only one knob, then this
  191. * will return `null`.
  192. */
  193. get ratioUpper() {
  194. if (this._dual) {
  195. return Math.max(this._ratioA, this._ratioB);
  196. }
  197. return null;
  198. }
  199. /**
  200. * @hidden
  201. */
  202. ngAfterContentInit() {
  203. this._initialize();
  204. // add touchstart/mousedown listeners
  205. this._events.pointerEvents({
  206. element: this._slider.nativeElement,
  207. pointerDown: this._pointerDown.bind(this),
  208. pointerMove: this._pointerMove.bind(this),
  209. pointerUp: this._pointerUp.bind(this),
  210. zone: true
  211. });
  212. // build all the ticks if there are any to show
  213. this._createTicks();
  214. }
  215. /** @internal */
  216. _pointerDown(ev) {
  217. // TODO: we could stop listening for events instead of checking this._disabled.
  218. // since there are a lot of events involved, this solution is
  219. // enough for the moment
  220. if (this._disabled) {
  221. return false;
  222. }
  223. // trigger ionFocus event
  224. this._fireFocus();
  225. // prevent default so scrolling does not happen
  226. ev.preventDefault();
  227. ev.stopPropagation();
  228. // get the start coordinates
  229. const current = pointerCoord(ev);
  230. // get the full dimensions of the slider element
  231. const rect = this._rect = this._plt.getElementBoundingClientRect(this._slider.nativeElement);
  232. // figure out which knob they started closer to
  233. const ratio = clamp(0, (current.x - rect.left) / (rect.width), 1);
  234. this._activeB = this._dual && (Math.abs(ratio - this._ratioA) > Math.abs(ratio - this._ratioB));
  235. // update the active knob's position
  236. this._update(current, rect, true);
  237. // trigger a haptic start
  238. this._haptic.gestureSelectionStart();
  239. // return true so the pointer events
  240. // know everything's still valid
  241. return true;
  242. }
  243. /** @internal */
  244. _pointerMove(ev) {
  245. if (this._disabled) {
  246. return;
  247. }
  248. // prevent default so scrolling does not happen
  249. ev.preventDefault();
  250. ev.stopPropagation();
  251. // update the active knob's position
  252. const hasChanged = this._update(pointerCoord(ev), this._rect, true);
  253. if (hasChanged && this._snaps) {
  254. // trigger a haptic selection changed event
  255. // if this is a snap range
  256. this._haptic.gestureSelectionChanged();
  257. }
  258. }
  259. /** @internal */
  260. _pointerUp(ev) {
  261. if (this._disabled) {
  262. return;
  263. }
  264. // prevent default so scrolling does not happen
  265. ev.preventDefault();
  266. ev.stopPropagation();
  267. // update the active knob's position
  268. this._update(pointerCoord(ev), this._rect, false);
  269. // trigger a haptic end
  270. this._haptic.gestureSelectionEnd();
  271. // trigger ionBlur event
  272. this._fireBlur();
  273. }
  274. /** @internal */
  275. _update(current, rect, isPressed) {
  276. // figure out where the pointer is currently at
  277. // update the knob being interacted with
  278. let ratio = clamp(0, (current.x - rect.left) / (rect.width), 1);
  279. let val = this._ratioToValue(ratio);
  280. if (this._snaps) {
  281. // snaps the ratio to the current value
  282. ratio = this._valueToRatio(val);
  283. }
  284. // update which knob is pressed
  285. this._pressed = isPressed;
  286. let valChanged = false;
  287. if (this._activeB) {
  288. // when the pointer down started it was determined
  289. // that knob B was the one they were interacting with
  290. this._pressedB = isPressed;
  291. this._pressedA = false;
  292. this._ratioB = ratio;
  293. valChanged = val === this._valB;
  294. this._valB = val;
  295. }
  296. else {
  297. // interacting with knob A
  298. this._pressedA = isPressed;
  299. this._pressedB = false;
  300. this._ratioA = ratio;
  301. valChanged = val === this._valA;
  302. this._valA = val;
  303. }
  304. this._updateBar();
  305. if (valChanged) {
  306. return false;
  307. }
  308. // value has been updated
  309. let value;
  310. if (this._dual) {
  311. // dual knobs have an lower and upper value
  312. value = {
  313. lower: Math.min(this._valA, this._valB),
  314. upper: Math.max(this._valA, this._valB)
  315. };
  316. (void 0) /* console.debug */;
  317. }
  318. else {
  319. // single knob only has one value
  320. value = this._valA;
  321. (void 0) /* console.debug */;
  322. }
  323. // Update input value
  324. this.value = value;
  325. return true;
  326. }
  327. /** @internal */
  328. _updateBar() {
  329. const ratioA = this._ratioA;
  330. const ratioB = this._ratioB;
  331. if (this._dual) {
  332. this._barL = `${(Math.min(ratioA, ratioB) * 100)}%`;
  333. this._barR = `${100 - (Math.max(ratioA, ratioB) * 100)}%`;
  334. }
  335. else {
  336. this._barL = '';
  337. this._barR = `${100 - (ratioA * 100)}%`;
  338. }
  339. this._updateTicks();
  340. }
  341. /** @internal */
  342. _createTicks() {
  343. if (this._snaps) {
  344. this._dom.write(() => {
  345. // TODO: Fix to not use RAF
  346. this._ticks = [];
  347. for (var value = this._min; value <= this._max; value += this._step) {
  348. var ratio = this._valueToRatio(value);
  349. this._ticks.push({
  350. ratio: ratio,
  351. left: `${ratio * 100}%`,
  352. });
  353. }
  354. this._updateTicks();
  355. });
  356. }
  357. }
  358. /** @internal */
  359. _updateTicks() {
  360. const ticks = this._ticks;
  361. const ratio = this.ratio;
  362. if (this._snaps && ticks) {
  363. if (this._dual) {
  364. var upperRatio = this.ratioUpper;
  365. ticks.forEach(t => {
  366. t.active = (t.ratio >= ratio && t.ratio <= upperRatio);
  367. });
  368. }
  369. else {
  370. ticks.forEach(t => {
  371. t.active = (t.ratio <= ratio);
  372. });
  373. }
  374. }
  375. }
  376. /** @hidden */
  377. _keyChg(isIncrease, isKnobB) {
  378. const step = this._step;
  379. if (isKnobB) {
  380. if (isIncrease) {
  381. this._valB += step;
  382. }
  383. else {
  384. this._valB -= step;
  385. }
  386. this._valB = clamp(this._min, this._valB, this._max);
  387. this._ratioB = this._valueToRatio(this._valB);
  388. }
  389. else {
  390. if (isIncrease) {
  391. this._valA += step;
  392. }
  393. else {
  394. this._valA -= step;
  395. }
  396. this._valA = clamp(this._min, this._valA, this._max);
  397. this._ratioA = this._valueToRatio(this._valA);
  398. }
  399. this._updateBar();
  400. }
  401. /** @internal */
  402. _ratioToValue(ratio) {
  403. ratio = Math.round(((this._max - this._min) * ratio));
  404. ratio = Math.round(ratio / this._step) * this._step + this._min;
  405. return clamp(this._min, ratio, this._max);
  406. }
  407. /** @internal */
  408. _valueToRatio(value) {
  409. value = Math.round((value - this._min) / this._step) * this._step;
  410. value = value / (this._max - this._min);
  411. return clamp(0, value, 1);
  412. }
  413. _inputNormalize(val) {
  414. if (this._dual) {
  415. return val;
  416. }
  417. else {
  418. val = parseFloat(val);
  419. return isNaN(val) ? undefined : val;
  420. }
  421. }
  422. /**
  423. * @hidden
  424. */
  425. _inputUpdated() {
  426. const val = this.value;
  427. if (this._dual) {
  428. this._valA = val.lower;
  429. this._valB = val.upper;
  430. this._ratioA = this._valueToRatio(val.lower);
  431. this._ratioB = this._valueToRatio(val.upper);
  432. }
  433. else {
  434. this._valA = val;
  435. this._ratioA = this._valueToRatio(val);
  436. }
  437. this._updateBar();
  438. this._cd.detectChanges();
  439. }
  440. /**
  441. * @hidden
  442. */
  443. ngOnDestroy() {
  444. super.ngOnDestroy();
  445. this._events.destroy();
  446. }
  447. }
  448. Range.decorators = [
  449. { type: Component, args: [{
  450. selector: 'ion-range',
  451. template: '<ng-content select="[range-left]"></ng-content>' +
  452. '<div class="range-slider" #slider>' +
  453. '<div class="range-tick" *ngFor="let t of _ticks" [style.left]="t.left" [class.range-tick-active]="t.active" role="presentation"></div>' +
  454. '<div class="range-bar" role="presentation"></div>' +
  455. '<div class="range-bar range-bar-active" [style.left]="_barL" [style.right]="_barR" #bar role="presentation"></div>' +
  456. '<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]="_labelId"></div>' +
  457. '<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]="_labelId" *ngIf="_dual"></div>' +
  458. '</div>' +
  459. '<ng-content select="[range-right]"></ng-content>',
  460. host: {
  461. '[class.range-disabled]': '_disabled',
  462. '[class.range-pressed]': '_pressed',
  463. '[class.range-has-pin]': '_pin'
  464. },
  465. providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: Range, multi: true }],
  466. encapsulation: ViewEncapsulation.None,
  467. },] },
  468. ];
  469. /** @nocollapse */
  470. Range.ctorParameters = () => [
  471. { type: Form, },
  472. { type: Haptic, },
  473. { type: Item, decorators: [{ type: Optional },] },
  474. { type: Config, },
  475. { type: Platform, },
  476. { type: ElementRef, },
  477. { type: Renderer, },
  478. { type: DomController, },
  479. { type: ChangeDetectorRef, },
  480. ];
  481. Range.propDecorators = {
  482. '_slider': [{ type: ViewChild, args: ['slider',] },],
  483. 'min': [{ type: Input },],
  484. 'max': [{ type: Input },],
  485. 'step': [{ type: Input },],
  486. 'snaps': [{ type: Input },],
  487. 'pin': [{ type: Input },],
  488. 'debounce': [{ type: Input },],
  489. 'dualKnobs': [{ type: Input },],
  490. };
  491. //# sourceMappingURL=range.js.map