diff --git a/core/src/components.d.ts b/core/src/components.d.ts
index f9d2db1251..4e670eb252 100644
--- a/core/src/components.d.ts
+++ b/core/src/components.d.ts
@@ -42,6 +42,7 @@ import {
GestureConfig,
GestureDetail,
InputChangeEvent,
+ Knob,
LoadingOptions,
Menu,
MenuChangeEventDetail,
@@ -4494,14 +4495,14 @@ declare global {
namespace StencilComponents {
interface IonRangeKnob {
'disabled': boolean;
- 'knob': string;
+ 'knob': Knob;
'labelId': string;
'max': number;
'min': number;
'pin': boolean;
'pressed': boolean;
'ratio': number;
- 'val': number;
+ 'value': number;
}
}
@@ -4525,7 +4526,7 @@ declare global {
namespace JSXElements {
export interface IonRangeKnobAttributes extends HTMLAttributes {
'disabled'?: boolean;
- 'knob'?: string;
+ 'knob'?: Knob;
'labelId'?: string;
'max'?: number;
'min'?: number;
@@ -4534,7 +4535,7 @@ declare global {
'pin'?: boolean;
'pressed'?: boolean;
'ratio'?: number;
- 'val'?: number;
+ 'value'?: number;
}
}
}
@@ -4580,14 +4581,6 @@ declare global {
* If true, a pin with integer value is shown when the knob is pressed. Defaults to `false`.
*/
'pin': boolean;
- /**
- * 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.
- */
- 'ratio': () => number;
- /**
- * 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`.
- */
- 'ratioUpper': () => number | null;
/**
* If true, the knob snaps to tick marks evenly spaced based on the step property value. Defaults to `false`.
*/
diff --git a/core/src/components/range-knob/range-knob.tsx b/core/src/components/range-knob/range-knob.tsx
index b946555a14..4531d0fd2f 100644
--- a/core/src/components/range-knob/range-knob.tsx
+++ b/core/src/components/range-knob/range-knob.tsx
@@ -1,19 +1,20 @@
import { Component, Event, EventEmitter, Listen, Prop } from '@stencil/core';
+import { Knob } from '../../interface';
@Component({
tag: `ion-range-knob`
})
export class RangeKnob {
- @Prop() pressed = false;
- @Prop() pin = false;
+ @Prop() pressed!: boolean;
+ @Prop() pin!: boolean;
@Prop() min!: number;
@Prop() max!: number;
- @Prop() val!: number;
- @Prop() disabled = false;
- @Prop() labelId!: string;
- @Prop() knob!: string;
+ @Prop() value!: number;
@Prop() ratio!: number;
+ @Prop() disabled!: boolean;
+ @Prop() labelId!: string;
+ @Prop() knob!: Knob;
@Event() ionIncrease!: EventEmitter;
@Event() ionDecrease!: EventEmitter;
@@ -25,6 +26,7 @@ export class RangeKnob {
this.ionDecrease.emit({isIncrease: false, knob: this.knob});
ev.preventDefault();
ev.stopPropagation();
+
} else if (keyCode === KEY_RIGHT || keyCode === KEY_UP) {
this.ionIncrease.emit({isIncrease: true, knob: this.knob});
ev.preventDefault();
@@ -32,34 +34,33 @@ export class RangeKnob {
}
}
- leftPos(val: number) {
- return `${val * 100}%`;
- }
-
hostData() {
+ const {value, min, max} = this;
+ const pos = this.ratio * 100;
return {
class: {
+ 'range-knob-handle': true,
'range-knob-pressed': this.pressed,
- 'range-knob-min': this.val === this.min || this.val === undefined,
- 'range-knob-max': this.val === this.max
+ 'range-knob-min': value === min,
+ 'range-knob-max': value === max
},
style: {
- 'left': this.leftPos(this.ratio)
+ 'left': `${pos}%`
},
'role': 'slider',
'tabindex': this.disabled ? -1 : 0,
- 'aria-valuemin': this.min,
- 'aria-valuemax': this.max,
+ 'aria-valuemin': min,
+ 'aria-valuemax': max,
'aria-disabled': this.disabled,
'aria-labelledby': this.labelId,
- 'aria-valuenow': this.val
+ 'aria-valuenow': value
};
}
render() {
if (this.pin) {
return [
-
{this.val}
,
+ {Math.round(this.value)}
,
];
}
diff --git a/core/src/components/range-knob/readme.md b/core/src/components/range-knob/readme.md
index 44746eb90c..fb819358ab 100644
--- a/core/src/components/range-knob/readme.md
+++ b/core/src/components/range-knob/readme.md
@@ -14,7 +14,7 @@ boolean
#### knob
-string
+number
#### labelId
@@ -47,7 +47,7 @@ boolean
number
-#### val
+#### value
number
@@ -61,7 +61,7 @@ boolean
#### knob
-string
+number
#### label-id
@@ -94,7 +94,7 @@ boolean
number
-#### val
+#### value
number
diff --git a/core/src/components/range/range-interface.ts b/core/src/components/range/range-interface.ts
new file mode 100644
index 0000000000..443ac0b6cf
--- /dev/null
+++ b/core/src/components/range/range-interface.ts
@@ -0,0 +1,13 @@
+
+export const enum Knob {
+ None,
+ A,
+ B
+}
+
+export type RangeValue = number | {lower: number, upper: number};
+
+export interface RangeEventDetail extends Event {
+ isIncrease: boolean;
+ knob: Knob;
+}
diff --git a/core/src/components/range/range.tsx b/core/src/components/range/range.tsx
index f8ca2e1836..7e76bb29d1 100644
--- a/core/src/components/range/range.tsx
+++ b/core/src/components/range/range.tsx
@@ -1,12 +1,7 @@
-import { Component, Element, Event, EventEmitter, Listen, Method, Prop, State, Watch } from '@stencil/core';
+import { Component, Element, Event, EventEmitter, Listen, Prop, State, Watch } from '@stencil/core';
import { BaseInput, GestureDetail, Mode, RangeInputChangeEvent, StyleEvent } from '../../interface';
import { clamp, debounceEvent, deferEvent } from '../../utils/helpers';
-
-export interface Tick {
- ratio: number | (() => number);
- left: string;
- active?: boolean;
-}
+import { Knob, RangeEventDetail, RangeValue } from './range-interface';
@Component({
tag: 'ion-range',
@@ -20,24 +15,15 @@ export interface Tick {
})
export class Range implements BaseInput {
- hasFocus = false;
+ private noUpdate = false;
+ private rect!: ClientRect;
+ private hasFocus = false;
- @Element() el!: HTMLElement;
-
- @State() barL!: string;
- @State() barR!: string;
- @State() valA = 0;
- @State() valB = 0;
- @State() ratioA = 0;
- @State() ratioB = 0;
- @State() ticks: Tick[] = [];
- @State() activeB = false;
- @State() rect!: ClientRect;
-
- @State() pressed = false;
- @State() pressedA = false;
- @State() pressedB = false;
+ @Element() el!: HTMLStencilElement;
+ @State() private ratioA = 0;
+ @State() private ratioB = 0;
+ @State() private pressedKnob = Knob.None;
/**
* The color to use from your Sass `$colors` map.
@@ -74,16 +60,16 @@ export class Range implements BaseInput {
*/
@Prop() dualKnobs = false;
- /**
- * Maximum integer value of the range. Defaults to `100`.
- */
- @Prop() max = 100;
-
/**
* Minimum integer value of the range. Defaults to `0`.
*/
@Prop() min = 0;
+ /**
+ * Maximum integer value of the range. Defaults to `100`.
+ */
+ @Prop() max = 100;
+
/**
* If true, a pin with integer value is shown when the knob
* is pressed. Defaults to `false`.
@@ -113,11 +99,12 @@ export class Range implements BaseInput {
/**
* the value of the range.
*/
- @Prop({ mutable: true }) value: any;
+ @Prop({ mutable: true }) value: any = 0;
@Watch('value')
- protected valueChanged(value: any) {
- this.inputUpdated();
- this.emitStyle();
+ protected valueChanged(value: RangeValue) {
+ if (!this.noUpdate) {
+ this.updateRatio();
+ }
this.ionChange.emit({value});
}
@@ -146,12 +133,45 @@ export class Range implements BaseInput {
componentWillLoad() {
this.ionStyle = deferEvent(this.ionStyle);
- this.inputUpdated();
- this.createTicks();
+ this.updateRatio();
this.debounceChanged();
this.emitStyle();
}
+ @Listen('ionIncrease')
+ @Listen('ionDecrease')
+ keyChng(ev: CustomEvent) {
+ let step = this.step;
+ step = step > 0 ? step : 1;
+ step = step / (this.max - this.min);
+ if (!ev.detail.isIncrease) {
+ step *= -1;
+ }
+ if (ev.detail.knob === Knob.A) {
+ this.ratioA += step;
+ } else {
+ this.ratioB += step;
+ }
+ }
+
+ private getValue(): RangeValue {
+ const value = this.value || 0;
+ if (this.dualKnobs) {
+ if (typeof value === 'object') {
+ return value;
+ }
+ return {
+ lower: 0,
+ upper: value,
+ };
+ } else {
+ if (typeof value === 'object') {
+ return value.upper;
+ }
+ return value;
+ }
+ }
+
private emitStyle() {
this.ionStyle.emit({
'range-disabled': this.disabled
@@ -174,244 +194,130 @@ export class Range implements BaseInput {
}
}
- private inputUpdated() {
- const val = this.value;
- if (this.dualKnobs) {
- 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();
+ private onDragStart(detail: GestureDetail) {
+ this.fireFocus();
+
+ const el = this.el.querySelector('.range-slider')!;
+ const rect = this.rect = el.getBoundingClientRect() as any;
+ const currentX = detail.currentX;
+
+ // figure out which knob they started closer to
+ const ratio = clamp(0, (currentX - rect.left) / rect.width, 1);
+ this.pressedKnob = (!this.dualKnobs || (Math.abs(this.ratioA - ratio) < Math.abs(this.ratioB - ratio)))
+ ? Knob.A
+ : Knob.B;
+
+ // update the active knob's position
+ this.update(currentX);
}
- private updateBar() {
- const ratioA = this.ratioA;
- const ratioB = this.ratioB;
-
- if (this.dualKnobs) {
- 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();
+ private onDragMove(detail: GestureDetail) {
+ this.update(detail.currentX);
}
- private createTicks() {
- if (this.snaps) {
- for (let value = this.min; value <= this.max; value += this.step) {
- const ratio = this.valueToRatio(value);
- this.ticks.push({
- ratio,
- left: `${ratio * 100}%`
- });
- }
- this.updateTicks();
- }
+ private onDragEnd(detail: GestureDetail) {
+ this.update(detail.currentX);
+ this.pressedKnob = Knob.None;
+ this.fireBlur();
}
- private updateTicks() {
- const ticks = this.ticks;
- const ratio = this.ratio;
- if (this.snaps && ticks) {
- if (this.dualKnobs) {
- const upperRatio = this.ratioUpper()!;
-
- ticks.forEach(t => {
- t.active = t.ratio >= ratio && t.ratio <= upperRatio;
- });
- } else {
- ticks.forEach(t => {
- t.active = t.ratio <= ratio;
- });
- }
- }
- }
-
- private valueToRatio(value: number) {
- value = Math.round((value - this.min) / this.step) * this.step;
- value = value / (this.max - this.min);
- return clamp(0, value, 1);
- }
-
- private ratioToValue(ratio: number) {
- 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);
- }
-
- // private inputNormalize(val: any): any {
- // if (this.dualKnobs) {
- // return val;
- // } else {
- // val = parseFloat(val);
- // return isNaN(val) ? undefined : val;
- // }
- // }
-
- private update(current: { x: number; y: number }, rect: ClientRect, isPressed: boolean) {
+ private update(currentX: number) {
// figure out where the pointer is currently at
// update the knob being interacted with
- let ratio = clamp(0, (current.x - rect.left) / rect.width, 1);
- const val = this.ratioToValue(ratio);
-
+ const rect = this.rect;
+ let ratio = clamp(0, (currentX - rect.left) / rect.width, 1);
if (this.snaps) {
// snaps the ratio to the current value
- ratio = this.valueToRatio(val);
+ const value = ratioToValue(ratio, this.min, this.max, this.step);
+ ratio = valueToRatio(value, this.min, this.max);
}
+
// update which knob is pressed
- this.pressed = isPressed;
- let valChanged = false;
- 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;
- valChanged = val === this.valB;
- this.valB = val;
- } else {
- // interacting with knob A
- this.pressedA = isPressed;
- this.pressedB = false;
+ if (this.pressedKnob === Knob.A) {
this.ratioA = ratio;
- valChanged = val === this.valA;
- this.valA = val;
- }
-
- this.updateBar();
- if (valChanged) {
- return false;
- }
-
- // value has been updated
- let value;
- if (this.dualKnobs) {
- // dual knobs have an lower and upper value
- value = {
- lower: Math.min(this.valA, this.valB),
- upper: Math.max(this.valA, this.valB)
- };
-
} else {
- // single knob only has one value
- value = this.valA;
+ this.ratioB = ratio;
}
// Update input value
- this.value = value;
-
- return true;
+ this.updateValue();
}
- /**
- * 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.
- */
- @Method()
- ratio(): number {
+ private get valA() {
+ return ratioToValue(this.ratioA, this.min, this.max, this.step);
+ }
+
+ private get valB() {
+ return ratioToValue(this.ratioB, this.min, this.max, this.step);
+ }
+
+ private get ratioLower() {
if (this.dualKnobs) {
return Math.min(this.ratioA, this.ratioB);
}
+ return 0;
+ }
+
+ private get ratioUpper() {
+ if (this.dualKnobs) {
+ return Math.max(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`.
- */
- @Method()
- ratioUpper() {
+ private updateRatio() {
+ const value = this.getValue() as any;
+ const {min, max} = this;
if (this.dualKnobs) {
- return Math.max(this.ratioA, this.ratioB);
- }
- return null;
- }
-
- @Listen('ionIncrease, ionDecrease')
- keyChng(ev: RangeEvent) {
- const step = this.step;
- if (ev.detail.knob === 'knobB') {
- if (ev.detail.isIncrease) {
- this.valB += step;
- } else {
- this.valB -= step;
- }
- this.valB = clamp(this.min, this.valB, this.max);
- this.ratioB = this.valueToRatio(this.valB);
+ this.ratioA = valueToRatio(value.lower, min, max);
+ this.ratioB = valueToRatio(value.upper, min, max);
} else {
- if (ev.detail.isIncrease) {
- this.valA += step;
- } else {
- this.valA -= step;
- }
- this.valA = clamp(this.min, this.valA, this.max);
- this.ratioA = this.valueToRatio(this.valA);
+ this.ratioA = valueToRatio(value, min, max);
}
- this.updateBar();
}
- private onDragStart(detail: GestureDetail) {
- if (this.disabled) return false;
- this.fireFocus();
+ private updateValue() {
+ this.noUpdate = true;
- const current = { x: detail.currentX, y: detail.currentY };
- const el = this.el.querySelector('.range-slider')!;
- const rect = el.getBoundingClientRect();
- this.rect = rect;
+ const {valA, valB} = this;
+ this.value = (!this.dualKnobs)
+ ? valA
+ : {
+ lower: Math.min(valA, valB),
+ upper: Math.max(valA, valB)
+ };
- // figure out which knob they started closer to
- const ratio = clamp(0, (current.x - rect.left) / rect.width, 1);
- this.activeB =
- this.dualKnobs &&
- Math.abs(ratio - this.ratioA) > Math.abs(ratio - this.ratioB);
-
- // update the active knob's position
- this.update(current, rect, true);
-
- // return true so the pointer events
- // know everything's still valid
- return true;
- }
-
- private onDragEnd(detail: GestureDetail) {
- if (this.disabled) {
- return;
- }
- // update the active knob's position
- this.update({ x: detail.currentX, y: detail.currentY }, this.rect, false);
- // trigger ionBlur event
- this.fireBlur();
- }
-
- private onDragMove(detail: GestureDetail) {
- if (this.disabled) {
- return;
- }
- const current = { x: detail.currentX, y: detail.currentY };
- // update the active knob's position
- this.update(current, this.rect, true);
+ this.noUpdate = false;
}
hostData() {
return {
class: {
'range-disabled': this.disabled,
- 'range-pressed': this.pressed,
+ 'range-pressed': this.pressedKnob !== Knob.None,
'range-has-pin': this.pin
}
};
}
render() {
+ const {min, max, step, ratioLower, ratioUpper} = this;
+
+ const barL = `${ratioLower * 100}%`;
+ const barR = `${100 - ratioUpper * 100}%`;
+
+ const ticks = [];
+ if (this.snaps) {
+ for (let value = min; value <= max; value += step) {
+ const ratio = valueToRatio(value, min, max);
+ ticks.push({
+ ratio,
+ active: ratio >= ratioLower && ratio <= ratioUpper,
+ left: `${ratio * 100}%`
+ });
+ }
+ }
+
return [
,
- {this.ticks.map(t =>
+ {ticks.map(t =>
+ class={{
+ 'range-tick': true,
+ 'range-tick-active': t.active
+ }}/>
)}
@@ -439,33 +347,28 @@ export class Range implements BaseInput {
class="range-bar range-bar-active"
role="presentation"
style={{
- left: this.barL,
- right: this.barR
+ left: barL,
+ right: barR
}}
/>
+ min={min}
+ max={max}/>
- {this.dualKnobs
- ?
- : null}
+ { this.dualKnobs &&
+
}
,
@@ -473,9 +376,15 @@ export class Range implements BaseInput {
}
}
-export interface RangeEvent extends Event {
- detail: {
- isIncrease: boolean,
- knob: string
- };
+
+export function ratioToValue(ratio: number, min: number, max: number, step: number): number {
+ let value = ((max - min) * ratio);
+ if (step > 0) {
+ value = Math.round(value / step) * step + min;
+ }
+ return clamp(min, value, max);
+}
+
+export function valueToRatio(value: number, min: number, max: number): number {
+ return clamp(0, (value - min) / (max - min), 1);
}
diff --git a/core/src/components/range/readme.md b/core/src/components/range/readme.md
index df2208a6aa..727d2ee28a 100644
--- a/core/src/components/range/readme.md
+++ b/core/src/components/range/readme.md
@@ -254,22 +254,6 @@ Emitted when the range has focus.
Emitted when the styles change.
-## Methods
-
-#### ratio()
-
-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.
-
-
-#### ratioUpper()
-
-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`.
-
-
----------------------------------------------
diff --git a/core/src/components/range/test/basic/index.html b/core/src/components/range/test/basic/index.html
index 2e2efee262..de2ecec8e2 100644
--- a/core/src/components/range/test/basic/index.html
+++ b/core/src/components/range/test/basic/index.html
@@ -26,10 +26,10 @@
-
+
-
+
@@ -48,7 +48,7 @@
Dynamic Value
-
+
@@ -103,21 +103,32 @@