' +
+ '',
+ host: {
+ '[class.range-knob-pressed]': 'pressed',
+ '[style.left]': '_x',
+ '[style.top]': '_y',
+ '[style.transform]': '_trns',
+ '[attr.aria-valuenow]': '_val',
+ '[attr.aria-valuemin]': 'range.min',
+ '[attr.aria-valuemax]': 'range.max',
+ 'role': 'slider',
+ 'tabindex': '0'
+ }
+})
+export class RangeKnob {
+ private _ratio: number;
+ private _val: number;
+ private _x: string;
+ pressed: boolean;
+
+ @Input() upper: boolean;
+
+ constructor(@Inject(forwardRef(() => Range)) private range: Range) {}
+
+ get ratio(): number {
+ return this._ratio;
+ }
+ set ratio(ratio: number) {
+ this._ratio = clamp(0, ratio, 1);
+ this._val = this.range.ratioToValue(this._ratio);
+
+ if (this.range.snaps) {
+ this._ratio = this.range.valueToRatio(this._val);
+ }
+ }
+
+ get value(): number {
+ return this._val;
+ }
+ set value(val: number) {
+ if (isString(val)) {
+ val = Math.round(val);
+ }
+ if (isNumber(val) && !isNaN(val)) {
+ this._ratio = this.range.valueToRatio(val);
+ this._val = this.range.ratioToValue(this._ratio);
+ }
+ }
+
+ position() {
+ this._x = `${this._ratio * 100}%`;
+ }
+
+ ngOnInit() {
+ if (isPresent(this.range.value)) {
+ // we already have a value
+ if (this.range.dualKnobs) {
+ // we have a value and there are two knobs
+ if (this.upper) {
+ // this is the upper knob
+ this.value = this.range.value.upper;
+
+ } else {
+ // this is the lower knob
+ this.value = this.range.value.lower;
+ }
+
+ } else {
+ // we have a value and there is only one knob
+ this.value = this.range.value;
+ }
+
+ } else {
+ // we do not have a value so set defaults
+ this.ratio = ((this.range.dualKnobs && this.upper) ? 1 : 0);
+ }
+
+ this.position();
+ }
+
+}
+
+
+/**
+ * @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.
+ *
+ *
+ * ### 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
+ *
+ *
+ *
+ *
+ *
+ *
+ * -200
+ *
+ * 200
+ *
+ *
+ *
+ * step=2, {{singleValue3}}
+ *
+ *
+ *
+ *
+ * step=100, snaps, {{singleValue4}}
+ *
+ *
+ *
+ *
+ * dual, step=3, snaps, {{dualValue2 | json}}
+ *
+ *
+ *
+ * ```
+ *
+ *
+ * @demo /docs/v2/demos/range/
+ */
+@Component({
+ selector: 'ion-range',
+ template:
+ '
' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '
',
+ host: {
+ '[class.range-disabled]': '_disabled',
+ '[class.range-pressed]': '_pressed',
+ '[class.range-has-pin]': '_pin'
+ },
+ directives: [RangeKnob],
+ providers: [RANGE_VALUE_ACCESSOR],
+ encapsulation: ViewEncapsulation.None,
+})
+export class Range {
+ private _dual: boolean = false;
+ private _pin: boolean;
+ private _disabled: boolean = false;
+ private _pressed: boolean;
+ private _labelId: string;
+ private _fn: Function;
+
+ private _active: RangeKnob;
+ private _start: Coordinates = null;
+ private _rect: ClientRect;
+ private _ticks: any[];
+ private _barL: string;
+ private _barR: string;
+
+ private _min: number = 0;
+ private _max: number = 100;
+ private _step: number = 1;
+ private _snaps: boolean = false;
+ private _removes: Function[] = [];
+ private _mouseRemove: Function;
+
+ /**
+ * @private
+ */
+ value: any;
+
+ @ViewChild('bar') private _bar: ElementRef;
+ @ViewChild('slider') private _slider: ElementRef;
+ @ViewChildren(RangeKnob) private _knobs: QueryList;
+
+ /**
+ * @private
+ */
+ id: string;
+
+ /**
+ * @input {number} Minimum integer value of the range. Defaults to `0`.
+ */
+ @Input()
+ get min(): number {
+ return this._min;
+ }
+ set min(val: number) {
+ val = Math.round(val);
+ if (!isNaN(val)) {
+ this._min = val;
+ }
+ }
+
+ /**
+ * @input {number} Maximum integer value of the range. Defaults to `100`.
+ */
+ @Input()
+ get max(): number {
+ return this._max;
+ }
+ set max(val: number) {
+ val = Math.round(val);
+ if (!isNaN(val)) {
+ this._max = val;
+ }
+ }
+
+ /**
+ * @input {number} Specifies the value granularity. Defaults to `1`.
+ */
+ @Input()
+ get step(): number {
+ return this._step;
+ }
+ set step(val: number) {
+ val = Math.round(val);
+ if (!isNaN(val) && val > 0) {
+ this._step = val;
+ }
+ }
+
+ /**
+ * @input {number} If true, the knob snaps to tick marks evenly spaced based on the step property value. Defaults to `false`.
+ */
+ @Input()
+ get snaps(): boolean {
+ return this._snaps;
+ }
+ set snaps(val: boolean) {
+ this._snaps = isTrueProperty(val);
+ }
+
+ /**
+ * @input {number} If true, a pin with integer value is shown when the knob is pressed. Defaults to `false`.
+ */
+ @Input()
+ get pin(): boolean {
+ return this._pin;
+ }
+ set pin(val: boolean) {
+ this._pin = isTrueProperty(val);
+ }
+
+ /**
+ * @input {boolean} Show two knobs. Defaults to `false`.
+ */
+ @Input()
+ get dualKnobs(): boolean {
+ return this._dual;
+ }
+ set dualKnobs(val: boolean) {
+ this._dual = isTrueProperty(val);
+ }
+
+ /**
+ * @output {Range} Expression to evaluate when the range value changes.
+ */
+ @Output() ionChange: EventEmitter = new EventEmitter();
+
+
+ constructor(
+ private _form: Form,
+ @Optional() private _item: Item,
+ private _renderer: Renderer
+ ) {
+ _form.register(this);
+
+ if (_item) {
+ this.id = 'rng-' + _item.registerInput('range');
+ this._labelId = 'lbl-' + _item.id;
+ _item.setCssClass('item-range', true);
+ }
+ }
+
+ /**
+ * @private
+ */
+ ngAfterViewInit() {
+ let barL = '';
+ let barR = '';
+
+ let firstRatio = this._knobs.first.ratio;
+
+ if (this._dual) {
+ let lastRatio = this._knobs.last.ratio;
+ barL = `${(Math.min(firstRatio, lastRatio) * 100)}%`;
+ barR = `${100 - (Math.max(firstRatio, lastRatio) * 100)}%`;
+
+ } else {
+ barR = `${100 - (firstRatio * 100)}%`;
+ }
+
+ this._renderer.setElementStyle(this._bar.nativeElement, 'left', barL);
+ this._renderer.setElementStyle(this._bar.nativeElement, 'right', barR);
+
+ this.createTicks();
+
+ // add touchstart/mousedown listeners
+ this._renderer.listen(this._slider.nativeElement, 'touchstart', this.pointerDown.bind(this));
+ this._mouseRemove = this._renderer.listen(this._slider.nativeElement, 'mousedown', this.pointerDown.bind(this));
+ }
+
+ /**
+ * @private
+ */
+ pointerDown(ev: UIEvent) {
+ console.debug(`range, ${ev.type}`);
+
+ // prevent default so scrolling does not happen
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ if (ev.type === 'touchstart') {
+ // if this was a touchstart, then let's remove the mousedown
+ this._mouseRemove && this._mouseRemove();
+ }
+
+ // get the start coordinates
+ this._start = pointerCoord(ev);
+
+ // get the full dimensions of the slider element
+ let rect: ClientRect = this._rect = this._slider.nativeElement.getBoundingClientRect();
+
+ // figure out the offset
+ // the start of the pointer could actually
+ // have been left or right of the slider bar
+ if (this._start.x < rect.left) {
+ rect.xOffset = (this._start.x - rect.left);
+
+ } else if (this._start.x > rect.right) {
+ rect.xOffset = (this._start.x - rect.right);
+
+ } else {
+ rect.xOffset = 0;
+ }
+
+ // figure out which knob we're interacting with
+ this.setActiveKnob(this._start, rect);
+
+ // update the ratio for the active knob
+ this.updateKnob(this._start, rect);
+
+ // ensure past listeners have been removed
+ this.clearListeners();
+
+ // update the active knob's position
+ this._active.position();
+ this._pressed = this._active.pressed = true;
+
+ // add a move listener depending on touch/mouse
+ let renderer = this._renderer;
+ let removes = this._removes;
+
+ if (ev.type === 'touchstart') {
+ removes.push(renderer.listen(this._slider.nativeElement, 'touchmove', this.pointerMove.bind(this)));
+ removes.push(renderer.listen(this._slider.nativeElement, 'touchend', this.pointerUp.bind(this)));
+
+ } else {
+ removes.push(renderer.listenGlobal('body', 'mousemove', this.pointerMove.bind(this)));
+ removes.push(renderer.listenGlobal('body', 'mouseup', this.pointerUp.bind(this)));
+ }
+ }
+
+ /**
+ * @private
+ */
+ pointerMove(ev: UIEvent) {
+ console.debug(`range, ${ev.type}`);
+
+ // prevent default so scrolling does not happen
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ if (this._start !== null && this._active !== null) {
+ // only use pointer move if it's a valid pointer
+ // and we already have start coordinates
+
+ // update the ratio for the active knob
+ this.updateKnob(pointerCoord(ev), this._rect);
+
+ // update the active knob's position
+ this._active.position();
+ this._pressed = this._active.pressed = true;
+
+ } else {
+ // ensure listeners have been removed
+ this.clearListeners();
+ }
+ }
+
+ /**
+ * @private
+ */
+ pointerUp(ev: UIEvent) {
+ console.debug(`range, ${ev.type}`);
+
+ // prevent default so scrolling does not happen
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ // update the ratio for the active knob
+ this.updateKnob(pointerCoord(ev), this._rect);
+
+ // update the active knob's position
+ this._active.position();
+
+ // clear the start coordinates and active knob
+ this._start = this._active = null;
+
+ // ensure listeners have been removed
+ this.clearListeners();
+ }
+
+ /**
+ * @private
+ */
+ clearListeners() {
+ this._pressed = this._knobs.first.pressed = this._knobs.last.pressed = false;
+
+ for (var i = 0; i < this._removes.length; i++) {
+ this._removes[i]();
+ }
+ this._removes.length = 0;
+ }
+
+ /**
+ * @private
+ */
+ setActiveKnob(current: Coordinates, rect: ClientRect) {
+ // figure out which knob is the closest one to the pointer
+ let ratio = (current.x - rect.left) / (rect.width);
+
+ if (this._dual && Math.abs(ratio - this._knobs.first.ratio) > Math.abs(ratio - this._knobs.last.ratio)) {
+ this._active = this._knobs.last;
+
+ } else {
+ this._active = this._knobs.first;
+ }
+ }
+
+ /**
+ * @private
+ */
+ updateKnob(current: Coordinates, rect: ClientRect) {
+ // figure out where the pointer is currently at
+ // update the knob being interacted with
+ if (this._active) {
+ let oldVal = this._active.value;
+ this._active.ratio = (current.x - rect.left) / (rect.width);
+ let newVal = this._active.value;
+
+ if (oldVal !== newVal) {
+ // value has been updated
+ if (this._dual) {
+ this.value = {
+ lower: Math.min(this._knobs.first.value, this._knobs.last.value),
+ upper: Math.max(this._knobs.first.value, this._knobs.last.value),
+ };
+
+ } else {
+ this.value = newVal;
+ }
+
+ this.onChange(this.value);
+
+ this.ionChange.emit(this);
+ }
+
+ this.updateBar();
+ }
+ }
+
+ /**
+ * @private
+ */
+ updateBar() {
+ let firstRatio = this._knobs.first.ratio;
+
+ if (this._dual) {
+ let lastRatio = this._knobs.last.ratio;
+ this._barL = `${(Math.min(firstRatio, lastRatio) * 100)}%`;
+ this._barR = `${100 - (Math.max(firstRatio, lastRatio) * 100)}%`;
+
+ } else {
+ this._barL = '';
+ this._barR = `${100 - (firstRatio * 100)}%`;
+ }
+
+ this.updateTicks();
+ }
+
+ /**
+ * @private
+ */
+ createTicks() {
+ if (this._snaps) {
+ this._ticks = [];
+ for (var value = this._min; value <= this._max; value += this._step) {
+ var ratio = this.valueToRatio(value);
+ this._ticks.push({
+ ratio: ratio,
+ left: `${ratio * 100}%`,
+ });
+ }
+ this.updateTicks();
+
+ } else {
+ this._ticks = null;
+ }
+ }
+
+ /**
+ * @private
+ */
+ updateTicks() {
+ if (this._snaps) {
+ let ratio = this.ratio;
+ if (this._dual) {
+ let upperRatio = this.ratioUpper;
+
+ this._ticks.forEach(t => {
+ t.active = (t.ratio >= ratio && t.ratio <= upperRatio);
+ });
+
+ } else {
+ this._ticks.forEach(t => {
+ t.active = (t.ratio <= ratio);
+ });
+ }
+ }
+ }
+
+ /**
+ * @private
+ */
+ ratioToValue(ratio: number) {
+ ratio = Math.round(((this._max - this._min) * ratio) + this._min);
+ return Math.round(ratio / this._step) * this._step;
+ }
+
+ /**
+ * @private
+ */
+ valueToRatio(value: number) {
+ value = Math.round(clamp(this._min, value, this._max) / this._step) * this._step;
+ return (value - this._min) / (this._max - this._min);
+ }
+
+ /**
+ * @private
+ */
+ writeValue(val: any) {
+ if (isPresent(val)) {
+ let knobs = this._knobs;
+ this.value = val;
+
+ if (this._knobs) {
+ if (this._dual) {
+ knobs.first.value = val.lower;
+ knobs.last.value = val.upper;
+ knobs.last.position();
+
+ } else {
+ knobs.first.value = val;
+ }
+ knobs.first.position();
+ this.updateBar();
+ }
+ }
+ }
+
+ /**
+ * @private
+ */
+ registerOnChange(fn: Function): void {
+ this._fn = fn;
+ this.onChange = (val: any) => {
+ fn(val);
+ this.onTouched();
+ };
+ }
+
+ /**
+ * @private
+ */
+ registerOnTouched(fn) { this.onTouched = fn; }
+
+ /**
+ * @input {boolean} Whether or not the range is disabled. Defaults to `false`.
+ */
+ @Input()
+ get disabled(): boolean {
+ return this._disabled;
+ }
+ set disabled(val: boolean) {
+ this._disabled = isTrueProperty(val);
+ this._item && this._item.setCssClass('item-range-disabled', this._disabled);
+ }
+
+ /**
+ * 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.
+ */
+ get ratio(): number {
+ if (this._dual) {
+ return Math.min(this._knobs.first.ratio, this._knobs.last.ratio);
+ }
+ return this._knobs.first.ratio;
+ }
+
+ /**
+ * 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`.
+ */
+ get ratioUpper(): number {
+ if (this._dual) {
+ return Math.max(this._knobs.first.ratio, this._knobs.last.ratio);
+ }
+ return null;
+ }
+
+ /**
+ * @private
+ */
+ onChange(val: any) {
+ // used when this input does not have an ngModel or ngControl
+ this.onTouched();
+ }
+
+ /**
+ * @private
+ */
+ onTouched() {}
+
+ /**
+ * @private
+ */
+ ngOnDestroy() {
+ this._form.deregister(this);
+ this.clearListeners();
+ }
+}
+
+
+export interface ClientRect {
+ top?: number;
+ right?: number;
+ bottom?: number;
+ left?: number;
+ width?: number;
+ height?: number;
+ xOffset?: number;
+ yOffset?: number;
+}
+
+export interface Coordinates {
+ x?: number;
+ y?: number;
+}
diff --git a/src/components/range/range.wp.scss b/src/components/range/range.wp.scss
new file mode 100644
index 0000000000..edc8a3b689
--- /dev/null
+++ b/src/components/range/range.wp.scss
@@ -0,0 +1,187 @@
+@import "../../globals.wp";
+
+// Windows Range
+// --------------------------------------------------
+
+$range-wp-padding-vertical: 8px !default;
+$range-wp-padding-horizontal: 8px !default;
+
+$range-wp-slider-height: 42px !default;
+
+$range-wp-hit-width: 42px !default;
+$range-wp-hit-height: $range-wp-slider-height !default;
+
+$range-wp-bar-height: 2px !default;
+$range-wp-bar-background-color: #bdbdbd !default;
+$range-wp-bar-active-background-color: color($colors-wp, primary) !default;
+
+$range-wp-knob-width: 8px !default;
+$range-wp-knob-height: $range-wp-knob-width * 3 !default;
+$range-wp-knob-background-color: $range-wp-bar-active-background-color !default;
+$range-wp-knob-border-radius: $range-wp-knob-width / 2 !default;
+
+$range-wp-tick-width: $range-wp-bar-height !default;
+$range-wp-tick-height: $range-wp-tick-width * 3 !default;
+$range-wp-tick-border-radius: $range-wp-knob-width / 2 !default;
+$range-wp-tick-background-color: $range-wp-bar-background-color !default;
+$range-wp-tick-active-background-color: $range-wp-bar-active-background-color !default;
+
+$range-wp-pin-background-color: $range-wp-bar-active-background-color !default;
+$range-wp-pin-color: color-contrast($colors-wp, $range-wp-bar-active-background-color) !default;
+$range-wp-pin-font-size: 12px !default;
+$range-wp-pin-padding: 8px !default;
+
+
+.item-range .item-inner {
+ overflow: visible;
+}
+
+.item-range .input-wrapper {
+ overflow: visible;
+
+ flex-direction: column;
+}
+
+.item-range ion-range {
+ width: 100%;
+}
+
+ion-range {
+ position: relative;
+ display: block;
+
+ padding: $range-wp-padding-vertical $range-wp-padding-horizontal;
+}
+
+.range-has-pin {
+ padding-top: $range-wp-padding-vertical + $range-wp-pin-font-size + $range-wp-pin-padding;
+}
+
+.range-slider {
+ position: relative;
+
+ height: $range-wp-slider-height;
+
+ cursor: pointer;
+}
+
+.range-bar {
+ position: absolute;
+ top: ($range-wp-slider-height / 2);
+ left: 0;
+
+ width: 100%;
+ height: $range-wp-bar-height;
+
+ background: $range-wp-bar-background-color;
+
+ pointer-events: none;
+}
+
+.range-pressed .range-bar-active {
+ will-change: left, right;
+}
+
+.range-pressed .range-knob-handle {
+ will-change: left;
+}
+
+.range-bar-active {
+ bottom: 0;
+
+ width: auto;
+
+ background: $range-wp-bar-active-background-color;
+}
+
+.range-knob-handle {
+ position: absolute;
+ top: ($range-wp-slider-height / 2);
+ left: 0%;
+
+ margin-top: -($range-wp-hit-height / 2);
+ margin-left: -($range-wp-hit-width / 2);
+
+ width: $range-wp-hit-width;
+ height: $range-wp-hit-height;
+
+ text-align: center;
+}
+
+.range-knob {
+ position: absolute;
+ top: ($range-wp-hit-height / 2) - ($range-wp-knob-height / 2) + ($range-wp-bar-height / 2);
+ left: ($range-wp-hit-width / 2) - ($range-wp-knob-width / 2);
+
+ width: $range-wp-knob-width;
+ height: $range-wp-knob-height;
+
+ border-radius: $range-wp-knob-border-radius;
+
+ background: $range-wp-knob-background-color;
+
+ pointer-events: none;
+}
+
+.range-tick {
+ position: absolute;
+ top: ($range-wp-hit-height / 2) - ($range-wp-tick-height / 2) + ($range-wp-bar-height / 2);
+
+ margin-left: ($range-wp-tick-width / 2) * -1;
+
+ width: $range-wp-tick-width;
+ height: $range-wp-tick-height;
+
+ border-radius: $range-wp-tick-border-radius;
+
+ background: $range-wp-tick-background-color;
+
+ pointer-events: none;
+}
+
+.range-tick-active {
+ background: $range-wp-tick-active-background-color;
+}
+
+.range-pin {
+ position: relative;
+ top: -24px;
+ display: inline-block;
+
+ padding: $range-wp-pin-padding;
+
+ min-width: 28px;
+
+ border-radius: 50px;
+
+ font-size: $range-wp-pin-font-size;
+
+ text-align: center;
+
+ color: $range-wp-pin-color;
+
+ background: $range-wp-pin-background-color;
+
+ transform: translate3d(0, 28px, 0) scale(.01);
+ transition: transform 120ms ease;
+}
+
+.range-knob-pressed .range-pin {
+ transform: translate3d(0, 0, 0) scale(1);
+}
+
+// Generate Windows Range Colors
+// --------------------------------------------------
+
+@each $color-name, $color-base, $color-contrast in get-colors($colors-wp) {
+
+ ion-range[#{$color-name}] {
+ .range-bar-active,
+ .range-tick-active,
+ .range-knob,
+ .range-pin {
+ background: $color-base;
+ }
+ }
+
+}
diff --git a/src/components/range/test/basic/e2e.ts b/src/components/range/test/basic/e2e.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/components/range/test/basic/index.ts b/src/components/range/test/basic/index.ts
new file mode 100644
index 0000000000..bb1fa45247
--- /dev/null
+++ b/src/components/range/test/basic/index.ts
@@ -0,0 +1,27 @@
+import {App, Page} from '../../../../../src';
+
+
+@Page({
+ templateUrl: 'page1.html'
+})
+class Page1 {
+ singleValue: number;
+ singleValue2: number = 150;
+ singleValue3: number = 64;
+ singleValue4: number = 1300;
+ dualValue: any;
+ dualValue2 = {lower: 33, upper: 60};
+
+ rangeChange(ev) {
+ console.log(`range, change, ratio: ${ev.ratio}, value: ${ev.value}`);
+ }
+
+}
+
+
+@App({
+ templateUrl: 'main.html'
+})
+class E2EApp {
+ rootPage = Page1;
+}
diff --git a/src/components/range/test/basic/main.html b/src/components/range/test/basic/main.html
new file mode 100644
index 0000000000..4f1fff2076
--- /dev/null
+++ b/src/components/range/test/basic/main.html
@@ -0,0 +1,133 @@
+
+
+
+ Left Menu
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Right Menu
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/range/test/basic/page1.html b/src/components/range/test/basic/page1.html
new file mode 100644
index 0000000000..5943831148
--- /dev/null
+++ b/src/components/range/test/basic/page1.html
@@ -0,0 +1,89 @@
+
+
+
+
+
+ Range
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ no init value, default min/max, {{singleValue}}
+
+
+
+
+