@ionic/core
Version:
Base components for Ionic
1,126 lines (1,125 loc) • 45.1 kB
JavaScript
/*!
* (C) Ionic http://ionicframework.com - MIT License
*/
import { Host, h } from "@stencil/core";
import { findClosestIonContent, disableContentScrollY, resetContentScrollY } from "../../utils/content/index";
import { inheritAriaAttributes, clamp, debounceEvent, renderHiddenInput, isSafeNumber } from "../../utils/helpers";
import { printIonWarning } from "../../utils/logging/index";
import { isRTL } from "../../utils/rtl/index";
import { createColorClasses, hostContext } from "../../utils/theme";
import { getIonMode } from "../../global/ionic-global";
import { roundToMaxDecimalPlaces } from "../../utils/floating-point";
// TODO(FW-2832): types
/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
*
* @slot label - The label text to associate with the range. Use the "labelPlacement" property to control where the label is placed relative to the range.
* @slot start - Content is placed to the left of the range slider in LTR, and to the right in RTL.
* @slot end - Content is placed to the right of the range slider in LTR, and to the left in RTL.
*
* @part tick - An inactive tick mark.
* @part tick-active - An active tick mark.
* @part pin - The counter that appears above a knob.
* @part knob - The handle that is used to drag the range.
* @part bar - The inactive part of the bar.
* @part bar-active - The active part of the bar.
* @part label - The label text describing the range.
*/
export class Range {
constructor() {
this.rangeId = `ion-r-${rangeIds++}`;
this.didLoad = false;
this.noUpdate = false;
this.hasFocus = false;
this.inheritedAttributes = {};
this.contentEl = null;
this.initialContentScrollY = true;
/**
* Compares two RangeValue inputs to determine if they are different.
*
* @param newVal - The new value.
* @param oldVal - The old value.
* @returns `true` if the values are different, `false` otherwise.
*/
this.compareValues = (newVal, oldVal) => {
if (typeof newVal === 'object' && typeof oldVal === 'object') {
return newVal.lower !== oldVal.lower || newVal.upper !== oldVal.upper;
}
return newVal !== oldVal;
};
this.clampBounds = (value) => {
return clamp(this.min, value, this.max);
};
this.ensureValueInBounds = (value) => {
if (this.dualKnobs) {
return {
lower: this.clampBounds(value.lower),
upper: this.clampBounds(value.upper),
};
}
else {
return this.clampBounds(value);
}
};
this.setupGesture = async () => {
const rangeSlider = this.rangeSlider;
if (rangeSlider) {
this.gesture = (await import('../../utils/gesture')).createGesture({
el: rangeSlider,
gestureName: 'range',
gesturePriority: 100,
/**
* Provide a threshold since the drag movement
* might be a user scrolling the view.
* If this is true, then the range
* should not move.
*/
threshold: 10,
onStart: () => this.onStart(),
onMove: (ev) => this.onMove(ev),
onEnd: (ev) => this.onEnd(ev),
});
this.gesture.enable(!this.disabled);
}
};
this.handleKeyboard = (knob, isIncrease) => {
const { ensureValueInBounds } = this;
let step = this.step;
step = step > 0 ? step : 1;
step = step / (this.max - this.min);
if (!isIncrease) {
step *= -1;
}
if (knob === 'A') {
this.ratioA = clamp(0, this.ratioA + step, 1);
}
else {
this.ratioB = clamp(0, this.ratioB + step, 1);
}
this.ionKnobMoveStart.emit({ value: ensureValueInBounds(this.value) });
this.updateValue();
this.emitValueChange();
this.ionKnobMoveEnd.emit({ value: ensureValueInBounds(this.value) });
};
this.onBlur = () => {
if (this.hasFocus) {
this.hasFocus = false;
this.ionBlur.emit();
}
};
this.onFocus = () => {
if (!this.hasFocus) {
this.hasFocus = true;
this.ionFocus.emit();
}
};
this.ratioA = 0;
this.ratioB = 0;
this.pressedKnob = undefined;
this.color = undefined;
this.debounce = undefined;
this.name = this.rangeId;
this.label = undefined;
this.dualKnobs = false;
this.min = 0;
this.max = 100;
this.pin = false;
this.pinFormatter = (value) => Math.round(value);
this.snaps = false;
this.step = 1;
this.ticks = true;
this.activeBarStart = undefined;
this.disabled = false;
this.value = 0;
this.labelPlacement = 'start';
}
debounceChanged() {
const { ionInput, debounce, originalIonInput } = this;
/**
* If debounce is undefined, we have to manually revert the ionInput emitter in case
* debounce used to be set to a number. Otherwise, the event would stay debounced.
*/
this.ionInput = debounce === undefined ? originalIonInput !== null && originalIonInput !== void 0 ? originalIonInput : ionInput : debounceEvent(ionInput, debounce);
}
minChanged(newValue) {
if (!isSafeNumber(newValue)) {
this.min = 0;
}
if (!this.noUpdate) {
this.updateRatio();
}
}
maxChanged(newValue) {
if (!isSafeNumber(newValue)) {
this.max = 100;
}
if (!this.noUpdate) {
this.updateRatio();
}
}
stepChanged(newValue) {
if (!isSafeNumber(newValue)) {
this.step = 1;
}
}
activeBarStartChanged() {
const { activeBarStart } = this;
if (activeBarStart !== undefined) {
if (activeBarStart > this.max) {
printIonWarning(`Range: The value of activeBarStart (${activeBarStart}) is greater than the max (${this.max}). Valid values are greater than or equal to the min value and less than or equal to the max value.`, this.el);
this.activeBarStart = this.max;
}
else if (activeBarStart < this.min) {
printIonWarning(`Range: The value of activeBarStart (${activeBarStart}) is less than the min (${this.min}). Valid values are greater than or equal to the min value and less than or equal to the max value.`, this.el);
this.activeBarStart = this.min;
}
}
}
disabledChanged() {
if (this.gesture) {
this.gesture.enable(!this.disabled);
}
}
valueChanged(newValue, oldValue) {
const valuesChanged = this.compareValues(newValue, oldValue);
if (valuesChanged) {
this.ionInput.emit({ value: this.value });
}
if (!this.noUpdate) {
this.updateRatio();
}
}
componentWillLoad() {
/**
* If user has custom ID set then we should
* not assign the default incrementing ID.
*/
if (this.el.hasAttribute('id')) {
this.rangeId = this.el.getAttribute('id');
}
this.inheritedAttributes = inheritAriaAttributes(this.el);
// If min, max, or step are not safe, set them to 0, 100, and 1, respectively.
// Each watch does this, but not before the initial load.
this.min = isSafeNumber(this.min) ? this.min : 0;
this.max = isSafeNumber(this.max) ? this.max : 100;
this.step = isSafeNumber(this.step) ? this.step : 1;
}
componentDidLoad() {
this.originalIonInput = this.ionInput;
this.setupGesture();
this.updateRatio();
this.didLoad = true;
}
connectedCallback() {
var _a;
this.updateRatio();
this.debounceChanged();
this.disabledChanged();
this.activeBarStartChanged();
/**
* If we have not yet rendered
* ion-range, then rangeSlider is not defined.
* But if we are moving ion-range via appendChild,
* then rangeSlider will be defined.
*/
if (this.didLoad) {
this.setupGesture();
}
const ionContent = findClosestIonContent(this.el);
this.contentEl = (_a = ionContent === null || ionContent === void 0 ? void 0 : ionContent.querySelector('.ion-content-scroll-host')) !== null && _a !== void 0 ? _a : ionContent;
}
disconnectedCallback() {
if (this.gesture) {
this.gesture.destroy();
this.gesture = undefined;
}
}
getValue() {
var _a;
const value = (_a = this.value) !== null && _a !== void 0 ? _a : 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;
}
}
/**
* Emits an `ionChange` event.
*
* This API should be called for user committed changes.
* This API should not be used for external value changes.
*/
emitValueChange() {
this.value = this.ensureValueInBounds(this.value);
this.ionChange.emit({ value: this.value });
}
/**
* The value should be updated on touch end or
* when the component is being dragged.
* This follows the native behavior of mobile devices.
*
* For example: When the user lifts their finger from the
* screen after tapping the bar or dragging the bar or knob.
*/
onStart() {
this.ionKnobMoveStart.emit({ value: this.ensureValueInBounds(this.value) });
}
/**
* The value should be updated while dragging the
* bar or knob.
*
* While the user is dragging, the view
* should not scroll. This is to prevent the user from
* feeling disoriented while dragging.
*
* The user can scroll on the view if the knob or
* bar is not being dragged.
*
* @param detail The details of the gesture event.
*/
onMove(detail) {
const { contentEl, pressedKnob } = this;
const currentX = detail.currentX;
/**
* Since the user is dragging on the bar or knob, the view should not scroll.
*
* This only needs to be done once.
*/
if (contentEl && this.pressedKnob === undefined) {
this.initialContentScrollY = disableContentScrollY(contentEl);
}
/**
* The `pressedKnob` can be undefined if the user just
* started dragging the knob.
*
* This is necessary to determine which knob the user is dragging,
* especially when it's a dual knob.
* Plus, it determines when to apply certain styles.
*
* This only needs to be done once since the knob won't change
* while the user is dragging.
*/
if (pressedKnob === undefined) {
this.setPressedKnob(currentX);
}
this.update(currentX);
}
/**
* The value should be updated on touch end:
* - When the user lifts their finger from the screen after
* tapping the bar.
*
* @param detail The details of the gesture or mouse event.
*/
onEnd(detail) {
var _a;
const { contentEl, initialContentScrollY } = this;
const currentX = (_a = detail.currentX) !== null && _a !== void 0 ? _a : detail.clientX;
/**
* The `pressedKnob` can be undefined if the user never
* dragged the knob. They just tapped on the bar.
*
* This is necessary to determine which knob the user is changing,
* especially when it's a dual knob.
* Plus, it determines when to apply certain styles.
*/
if (this.pressedKnob === undefined) {
this.setPressedKnob(currentX);
}
/**
* The user is no longer dragging the bar or
* knob (if they were dragging it).
*
* The user can now scroll on the view in the next gesture event.
*/
if (contentEl && this.pressedKnob !== undefined) {
resetContentScrollY(contentEl, initialContentScrollY);
}
// update the active knob's position
this.update(currentX);
/**
* Reset the pressed knob to undefined since the user
* may start dragging a different knob in the next gesture event.
*/
this.pressedKnob = undefined;
this.emitValueChange();
this.ionKnobMoveEnd.emit({ value: this.ensureValueInBounds(this.value) });
}
update(currentX) {
// figure out where the pointer is currently at
// update the knob being interacted with
const rect = this.rect;
let ratio = clamp(0, (currentX - rect.left) / rect.width, 1);
if (isRTL(this.el)) {
ratio = 1 - ratio;
}
if (this.snaps) {
// snaps the ratio to the current value
ratio = valueToRatio(ratioToValue(ratio, this.min, this.max, this.step), this.min, this.max);
}
// update which knob is pressed
if (this.pressedKnob === 'A') {
this.ratioA = ratio;
}
else {
this.ratioB = ratio;
}
// Update input value
this.updateValue();
}
setPressedKnob(currentX) {
const rect = (this.rect = this.rangeSlider.getBoundingClientRect());
// figure out which knob they started closer to
let ratio = clamp(0, (currentX - rect.left) / rect.width, 1);
if (isRTL(this.el)) {
ratio = 1 - ratio;
}
this.pressedKnob = !this.dualKnobs || Math.abs(this.ratioA - ratio) < Math.abs(this.ratioB - ratio) ? 'A' : 'B';
this.setFocus(this.pressedKnob);
}
get valA() {
return ratioToValue(this.ratioA, this.min, this.max, this.step);
}
get valB() {
return ratioToValue(this.ratioB, this.min, this.max, this.step);
}
get ratioLower() {
if (this.dualKnobs) {
return Math.min(this.ratioA, this.ratioB);
}
const { activeBarStart } = this;
if (activeBarStart == null) {
return 0;
}
return valueToRatio(activeBarStart, this.min, this.max);
}
get ratioUpper() {
if (this.dualKnobs) {
return Math.max(this.ratioA, this.ratioB);
}
return this.ratioA;
}
updateRatio() {
const value = this.getValue();
const { min, max } = this;
if (this.dualKnobs) {
this.ratioA = valueToRatio(value.lower, min, max);
this.ratioB = valueToRatio(value.upper, min, max);
}
else {
this.ratioA = valueToRatio(value, min, max);
}
}
updateValue() {
this.noUpdate = true;
const { valA, valB } = this;
this.value = !this.dualKnobs
? valA
: {
lower: Math.min(valA, valB),
upper: Math.max(valA, valB),
};
this.noUpdate = false;
}
setFocus(knob) {
if (this.el.shadowRoot) {
const knobEl = this.el.shadowRoot.querySelector(knob === 'A' ? '.range-knob-a' : '.range-knob-b');
if (knobEl) {
knobEl.focus();
}
}
}
/**
* Returns true if content was passed to the "start" slot
*/
get hasStartSlotContent() {
return this.el.querySelector('[slot="start"]') !== null;
}
/**
* Returns true if content was passed to the "end" slot
*/
get hasEndSlotContent() {
return this.el.querySelector('[slot="end"]') !== null;
}
get hasLabel() {
return this.label !== undefined || this.el.querySelector('[slot="label"]') !== null;
}
renderRangeSlider() {
var _a;
const { min, max, step, handleKeyboard, pressedKnob, disabled, pin, ratioLower, ratioUpper, pinFormatter, inheritedAttributes, } = this;
let barStart = `${ratioLower * 100}%`;
let barEnd = `${100 - ratioUpper * 100}%`;
const rtl = isRTL(this.el);
const start = rtl ? 'right' : 'left';
const end = rtl ? 'left' : 'right';
const tickStyle = (tick) => {
return {
[start]: tick[start],
};
};
if (this.dualKnobs === false) {
/**
* When the value is less than the activeBarStart or the min value,
* the knob will display at the start of the active bar.
*/
if (this.valA < ((_a = this.activeBarStart) !== null && _a !== void 0 ? _a : this.min)) {
/**
* Sets the bar positions relative to the upper and lower limits.
* Converts the ratio values into percentages, used as offsets for left/right styles.
*
* The ratioUpper refers to the knob position on the bar.
* The ratioLower refers to the end position of the active bar (the value).
*/
barStart = `${ratioUpper * 100}%`;
barEnd = `${100 - ratioLower * 100}%`;
}
else {
/**
* Otherwise, the knob will display at the end of the active bar.
*
* The ratioLower refers to the start position of the active bar (the value).
* The ratioUpper refers to the knob position on the bar.
*/
barStart = `${ratioLower * 100}%`;
barEnd = `${100 - ratioUpper * 100}%`;
}
}
const barStyle = {
[start]: barStart,
[end]: barEnd,
};
const ticks = [];
if (this.snaps && this.ticks) {
for (let value = min; value <= max; value += step) {
const ratio = valueToRatio(value, min, max);
const ratioMin = Math.min(ratioLower, ratioUpper);
const ratioMax = Math.max(ratioLower, ratioUpper);
const tick = {
ratio,
/**
* Sets the tick mark as active when the tick is between the min bounds and the knob.
* When using activeBarStart, the tick mark will be active between the knob and activeBarStart.
*/
active: ratio >= ratioMin && ratio <= ratioMax,
};
tick[start] = `${ratio * 100}%`;
ticks.push(tick);
}
}
return (h("div", { class: "range-slider", ref: (rangeEl) => (this.rangeSlider = rangeEl),
/**
* Since the gesture has a threshold, the value
* won't change until the user has dragged past
* the threshold. This is to prevent the range
* from moving when the user is scrolling.
*
* This results in the value not being updated
* and the event emitters not being triggered
* if the user taps on the range. This is why
* we need to listen for the "pointerUp" event.
*/
onPointerUp: (ev) => {
/**
* If the user drags the knob on the web
* version (does not occur on mobile),
* the "pointerUp" event will be triggered
* along with the gesture's events.
* This leads to duplicate events.
*
* By checking if the pressedKnob is undefined,
* we can determine if the "pointerUp" event was
* triggered by a tap or a drag. If it was
* dragged, the pressedKnob will be defined.
*/
if (this.pressedKnob === undefined) {
this.onStart();
this.onEnd(ev);
}
} }, ticks.map((tick) => (h("div", { style: tickStyle(tick), role: "presentation", class: {
'range-tick': true,
'range-tick-active': tick.active,
}, part: tick.active ? 'tick-active' : 'tick' }))), h("div", { class: "range-bar-container" }, h("div", { class: "range-bar", role: "presentation", part: "bar" }), h("div", { class: {
'range-bar': true,
'range-bar-active': true,
'has-ticks': ticks.length > 0,
}, role: "presentation", style: barStyle, part: "bar-active" })), renderKnob(rtl, {
knob: 'A',
pressed: pressedKnob === 'A',
value: this.valA,
ratio: this.ratioA,
pin,
pinFormatter,
disabled,
handleKeyboard,
min,
max,
inheritedAttributes,
}), this.dualKnobs &&
renderKnob(rtl, {
knob: 'B',
pressed: pressedKnob === 'B',
value: this.valB,
ratio: this.ratioB,
pin,
pinFormatter,
disabled,
handleKeyboard,
min,
max,
inheritedAttributes,
})));
}
render() {
const { disabled, el, hasLabel, rangeId, pin, pressedKnob, labelPlacement, label } = this;
const inItem = hostContext('ion-item', el);
/**
* If there is no start content then the knob at
* the min value will be cut off by the item margin.
*/
const hasStartContent = (hasLabel && (labelPlacement === 'start' || labelPlacement === 'fixed')) || this.hasStartSlotContent;
const needsStartAdjustment = inItem && !hasStartContent;
/**
* If there is no end content then the knob at
* the max value will be cut off by the item margin.
*/
const hasEndContent = (hasLabel && labelPlacement === 'end') || this.hasEndSlotContent;
const needsEndAdjustment = inItem && !hasEndContent;
const mode = getIonMode(this);
renderHiddenInput(true, el, this.name, JSON.stringify(this.getValue()), disabled);
return (h(Host, { key: 'e97cb7eab877eb1624429b4a79107130c6809cf5', onFocusin: this.onFocus, onFocusout: this.onBlur, id: rangeId, class: createColorClasses(this.color, {
[mode]: true,
'in-item': inItem,
'range-disabled': disabled,
'range-pressed': pressedKnob !== undefined,
'range-has-pin': pin,
[`range-label-placement-${labelPlacement}`]: true,
'range-item-start-adjustment': needsStartAdjustment,
'range-item-end-adjustment': needsEndAdjustment,
}) }, h("label", { key: 'a43e9859f74f83460439efefccb5fbb9f387c9ee', class: "range-wrapper", id: "range-label" }, h("div", { key: '75352a30f30dbd0228c6138eb4324a5c021dbb48', class: {
'label-text-wrapper': true,
'label-text-wrapper-hidden': !hasLabel,
}, part: "label" }, label !== undefined ? h("div", { class: "label-text" }, label) : h("slot", { name: "label" })), h("div", { key: '6a3e147c3e5d938bb2b50522a290f6fdfcf40f05', class: "native-wrapper" }, h("slot", { key: '6627236eac9f711fa9c27879a017dd994e65811e', name: "start" }), this.renderRangeSlider(), h("slot", { key: '6af3bbadacd036bc7cd30732227f76d7c64117fb', name: "end" })))));
}
static get is() { return "ion-range"; }
static get encapsulation() { return "shadow"; }
static get originalStyleUrls() {
return {
"ios": ["range.ios.scss"],
"md": ["range.md.scss"]
};
}
static get styleUrls() {
return {
"ios": ["range.ios.css"],
"md": ["range.md.css"]
};
}
static get properties() {
return {
"color": {
"type": "string",
"mutable": false,
"complexType": {
"original": "Color",
"resolved": "\"danger\" | \"dark\" | \"light\" | \"medium\" | \"primary\" | \"secondary\" | \"success\" | \"tertiary\" | \"warning\" | string & Record<never, never> | undefined",
"references": {
"Color": {
"location": "import",
"path": "../../interface",
"id": "src/interface.d.ts::Color"
}
}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "The color to use from your application's color palette.\nDefault options are: `\"primary\"`, `\"secondary\"`, `\"tertiary\"`, `\"success\"`, `\"warning\"`, `\"danger\"`, `\"light\"`, `\"medium\"`, and `\"dark\"`.\nFor more information on colors, see [theming](/docs/theming/basics)."
},
"attribute": "color",
"reflect": true
},
"debounce": {
"type": "number",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "How long, in milliseconds, to wait to trigger the\n`ionInput` event after each change in the range value."
},
"attribute": "debounce",
"reflect": false
},
"name": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "The name of the control, which is submitted with the form data."
},
"attribute": "name",
"reflect": false,
"defaultValue": "this.rangeId"
},
"label": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "The text to display as the control's label. Use this over the `label` slot if\nyou only need plain text. The `label` property will take priority over the\n`label` slot if both are used."
},
"attribute": "label",
"reflect": false
},
"dualKnobs": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Show two knobs."
},
"attribute": "dual-knobs",
"reflect": false,
"defaultValue": "false"
},
"min": {
"type": "number",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Minimum integer value of the range."
},
"attribute": "min",
"reflect": false,
"defaultValue": "0"
},
"max": {
"type": "number",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Maximum integer value of the range."
},
"attribute": "max",
"reflect": false,
"defaultValue": "100"
},
"pin": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "If `true`, a pin with integer value is shown when the knob\nis pressed."
},
"attribute": "pin",
"reflect": false,
"defaultValue": "false"
},
"pinFormatter": {
"type": "unknown",
"mutable": false,
"complexType": {
"original": "PinFormatter",
"resolved": "(value: number) => string | number",
"references": {
"PinFormatter": {
"location": "import",
"path": "./range-interface",
"id": "src/components/range/range-interface.ts::PinFormatter"
}
}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "A callback used to format the pin text.\nBy default the pin text is set to `Math.round(value)`.\n\nSee https://ionicframework.com/docs/troubleshooting/runtime#accessing-this\nif you need to access `this` from within the callback."
},
"defaultValue": "(value: number): number => Math.round(value)"
},
"snaps": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "If `true`, the knob snaps to tick marks evenly spaced based\non the step property value."
},
"attribute": "snaps",
"reflect": false,
"defaultValue": "false"
},
"step": {
"type": "number",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Specifies the value granularity."
},
"attribute": "step",
"reflect": false,
"defaultValue": "1"
},
"ticks": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "If `true`, tick marks are displayed based on the step value.\nOnly applies when `snaps` is `true`."
},
"attribute": "ticks",
"reflect": false,
"defaultValue": "true"
},
"activeBarStart": {
"type": "number",
"mutable": true,
"complexType": {
"original": "number",
"resolved": "number | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "The start position of the range active bar. This feature is only available with a single knob (dualKnobs=\"false\").\nValid values are greater than or equal to the min value and less than or equal to the max value."
},
"attribute": "active-bar-start",
"reflect": false
},
"disabled": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "If `true`, the user cannot interact with the range."
},
"attribute": "disabled",
"reflect": false,
"defaultValue": "false"
},
"value": {
"type": "number",
"mutable": true,
"complexType": {
"original": "RangeValue",
"resolved": "number | { lower: number; upper: number; }",
"references": {
"RangeValue": {
"location": "import",
"path": "./range-interface",
"id": "src/components/range/range-interface.ts::RangeValue"
}
}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "the value of the range."
},
"attribute": "value",
"reflect": false,
"defaultValue": "0"
},
"labelPlacement": {
"type": "string",
"mutable": false,
"complexType": {
"original": "'start' | 'end' | 'fixed' | 'stacked'",
"resolved": "\"end\" | \"fixed\" | \"stacked\" | \"start\"",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Where to place the label relative to the range.\n`\"start\"`: The label will appear to the left of the range in LTR and to the right in RTL.\n`\"end\"`: The label will appear to the right of the range in LTR and to the left in RTL.\n`\"fixed\"`: The label has the same behavior as `\"start\"` except it also has a fixed width. Long text will be truncated with ellipses (\"...\").\n`\"stacked\"`: The label will appear above the range regardless of the direction."
},
"attribute": "label-placement",
"reflect": false,
"defaultValue": "'start'"
}
};
}
static get states() {
return {
"ratioA": {},
"ratioB": {},
"pressedKnob": {}
};
}
static get events() {
return [{
"method": "ionChange",
"name": "ionChange",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "The `ionChange` event is fired for `<ion-range>` elements when the user\nmodifies the element's value:\n- When the user releases the knob after dragging;\n- When the user moves the knob with keyboard arrows\n\nThis event will not emit when programmatically setting the `value` property."
},
"complexType": {
"original": "RangeChangeEventDetail",
"resolved": "RangeChangeEventDetail",
"references": {
"RangeChangeEventDetail": {
"location": "import",
"path": "./range-interface",
"id": "src/components/range/range-interface.ts::RangeChangeEventDetail"
}
}
}
}, {
"method": "ionInput",
"name": "ionInput",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "The `ionInput` event is fired for `<ion-range>` elements when the value\nis modified. Unlike `ionChange`, `ionInput` is fired continuously\nwhile the user is dragging the knob."
},
"complexType": {
"original": "RangeChangeEventDetail",
"resolved": "RangeChangeEventDetail",
"references": {
"RangeChangeEventDetail": {
"location": "import",
"path": "./range-interface",
"id": "src/components/range/range-interface.ts::RangeChangeEventDetail"
}
}
}
}, {
"method": "ionFocus",
"name": "ionFocus",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Emitted when the range has focus."
},
"complexType": {
"original": "void",
"resolved": "void",
"references": {}
}
}, {
"method": "ionBlur",
"name": "ionBlur",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Emitted when the range loses focus."
},
"complexType": {
"original": "void",
"resolved": "void",
"references": {}
}
}, {
"method": "ionKnobMoveStart",
"name": "ionKnobMoveStart",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Emitted when the user starts moving the range knob, whether through\nmouse drag, touch gesture, or keyboard interaction."
},
"complexType": {
"original": "RangeKnobMoveStartEventDetail",
"resolved": "RangeKnobMoveStartEventDetail",
"references": {
"RangeKnobMoveStartEventDetail": {
"location": "import",
"path": "./range-interface",
"id": "src/components/range/range-interface.ts::RangeKnobMoveStartEventDetail"
}
}
}
}, {
"method": "ionKnobMoveEnd",
"name": "ionKnobMoveEnd",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Emitted when the user finishes moving the range knob, whether through\nmouse drag, touch gesture, or keyboard interaction."
},
"complexType": {
"original": "RangeKnobMoveEndEventDetail",
"resolved": "RangeKnobMoveEndEventDetail",
"references": {
"RangeKnobMoveEndEventDetail": {
"location": "import",
"path": "./range-interface",
"id": "src/components/range/range-interface.ts::RangeKnobMoveEndEventDetail"
}
}
}
}];
}
static get elementRef() { return "el"; }
static get watchers() {
return [{
"propName": "debounce",
"methodName": "debounceChanged"
}, {
"propName": "min",
"methodName": "minChanged"
}, {
"propName": "max",
"methodName": "maxChanged"
}, {
"propName": "step",
"methodName": "stepChanged"
}, {
"propName": "activeBarStart",
"methodName": "activeBarStartChanged"
}, {
"propName": "disabled",
"methodName": "disabledChanged"
}, {
"propName": "value",
"methodName": "valueChanged"
}];
}
}
const renderKnob = (rtl, { knob, value, ratio, min, max, disabled, pressed, pin, handleKeyboard, pinFormatter, inheritedAttributes }) => {
const start = rtl ? 'right' : 'left';
const knobStyle = () => {
const style = {};
style[start] = `${ratio * 100}%`;
return style;
};
// The aria label should be preferred over visible text if both are specified
const ariaLabel = inheritedAttributes['aria-label'];
return (h("div", { onKeyDown: (ev) => {
const key = ev.key;
if (key === 'ArrowLeft' || key === 'ArrowDown') {
handleKeyboard(knob, false);
ev.preventDefault();
ev.stopPropagation();
}
else if (key === 'ArrowRight' || key === 'ArrowUp') {
handleKeyboard(knob, true);
ev.preventDefault();
ev.stopPropagation();
}
}, class: {
'range-knob-handle': true,
'range-knob-a': knob === 'A',
'range-knob-b': knob === 'B',
'range-knob-pressed': pressed,
'range-knob-min': value === min,
'range-knob-max': value === max,
'ion-activatable': true,
'ion-focusable': true,
}, style: knobStyle(), role: "slider", tabindex: disabled ? -1 : 0, "aria-label": ariaLabel !== undefined ? ariaLabel : null, "aria-labelledby": ariaLabel === undefined ? 'range-label' : null, "aria-valuemin": min, "aria-valuemax": max, "aria-disabled": disabled ? 'true' : null, "aria-valuenow": value }, pin && (h("div", { class: "range-pin", role: "presentation", part: "pin" }, pinFormatter(value))), h("div", { class: "range-knob", role: "presentation", part: "knob" })));
};
const ratioToValue = (ratio, min, max, step) => {
let value = (max - min) * ratio;
if (step > 0) {
// round to nearest multiple of step, then add min
value = Math.round(value / step) * step + min;
}
const clampedValue = clamp(min, value, max);
return roundToMaxDecimalPlaces(clampedValue, min, max, step);
};
const valueToRatio = (value, min, max) => {
return clamp(0, (value - min) / (max - min), 1);
};
let rangeIds = 0;