@@ -29,6 +29,9 @@ import 'package:angular_components/utils/browser/dom_service/dom_service.dart';
2929 templateUrl: 'material_slider.html' ,
3030 styleUrls: ['material_slider.scss.css' ],
3131 changeDetection: ChangeDetectionStrategy .OnPush ,
32+ directives: [
33+ NgIf ,
34+ ],
3235 // TODO(google): Change to `Visibility.local` to reduce code size.
3336 visibility: Visibility .all,
3437)
@@ -48,18 +51,53 @@ class MaterialSliderComponent implements AfterChanges, HasDisabled {
4851 @Input ()
4952 bool disabled = false ;
5053
51- /// The current value of the input element.
54+ bool _isTwoSided = false ;
55+
56+ /// True if the slider is 2 sided.
57+ bool get isTwoSided => _isTwoSided;
58+ @Input ()
59+ set isTwoSided (bool isTwoSided) {
60+ _isTwoSided = isTwoSided;
61+ }
62+
63+ /// The current value of the input [value] element.
5264 ///
53- /// Must be between [min] and [max] , inclusive, and a multiple of [step] .
65+ /// When [isTwoSided] is true, then this represents the current value of the
66+ /// right slider knob. Must be between [min] and [max] , inclusive, a multiple
67+ /// of [step] , and greater than [leftValue] .
5468 @Input ()
5569 num value = 0 ;
5670
5771 final _changeController = StreamController <num >.broadcast (sync : true );
5872
59- /// Publishes events when the value of the input is changed by the user.
73+ /// Publishes events when the value of the [value] input is changed by the
74+ /// user.
6075 @Output ()
6176 Stream <num > get valueChange => _changeController.stream;
6277
78+ num _leftValue = 0 ;
79+
80+ /// The current value of the [leftValue] input in a 2 sided slider, defaults
81+ /// to 0.
82+ ///
83+ /// When [isTwoSided] is true, then this represents the current value of the
84+ /// left slider knob. Must be between [min] and [max] , inclusive, a multiple
85+ /// of [step] and less than or equal to [value] .
86+ num get leftValue => isTwoSided ? _leftValue : min;
87+ @Input ()
88+ set leftValue (int val) {
89+ if (isTwoSided) {
90+ _leftValue = val;
91+ }
92+ }
93+
94+ final _leftChangeController = StreamController <num >.broadcast (sync : true );
95+
96+ /// Publishes events when the value of the [leftValue] input is changed
97+ /// by the user in a 2 sided slider.
98+ @Output ()
99+ Stream <num > get leftValueChange => _leftChangeController.stream;
100+
63101 /// The minimum progress value.
64102 ///
65103 /// Defaults to 0, must be strictly smaller than max.
@@ -78,9 +116,13 @@ class MaterialSliderComponent implements AfterChanges, HasDisabled {
78116 @Input ()
79117 num step = 1 ;
80118
81- /// The current progress of the input in percent.
119+ /// The current progress of the [value] input in percent.
82120 double get progressPercent => (100.0 * (value - min) / (max - min));
83121
122+ /// The current progress of the [leftValue] input in percent.
123+ double get leftProgressPercent =>
124+ isTwoSided ? (100.0 * (leftValue - min) / (max - min)) : 0 ;
125+
84126 /// Verifies that the input values of this control are consistent.
85127 @override
86128 void ngAfterChanges () {
@@ -95,6 +137,19 @@ class MaterialSliderComponent implements AfterChanges, HasDisabled {
95137 message: 'Failed assertion: ${value } <= ${max }' );
96138 checkArgument (_divisible (value - min, step),
97139 message: 'Failed assertion: (${value } - ${min }) % ${step } ~ 0.' );
140+
141+ if (isTwoSided) {
142+ checkArgument (leftValue <= value,
143+ message: 'Failed assertion: ${leftValue } <= ${value }' );
144+ checkArgument (leftValue >= min,
145+ message: 'Failed assertion: ${leftValue } >= ${min }' );
146+ // Redundant check but done for consistency.
147+ checkArgument (leftValue <= max,
148+ message: 'Failed assertion: ${leftValue } <= ${max }' );
149+ checkArgument (_divisible (leftValue - min, step),
150+ message:
151+ 'Failed assertion: (${leftValue } - ${min }) % ${step } ~ 0.' );
152+ }
98153 return true ;
99154 }());
100155 }
@@ -117,6 +172,12 @@ class MaterialSliderComponent implements AfterChanges, HasDisabled {
117172 /// Whether the current user locale is RTL.
118173 bool get isRtl => Bidi .isRtlLanguage (Intl .defaultLocale ?? '' );
119174
175+ /// True if mouse click event on left knob of a 2 sided slider.
176+ bool isLeftKnobSelected = false ;
177+
178+ /// True if mouse click event on right knob.
179+ bool isRightKnobSelected = false ;
180+
120181 /// Updates the current value to reflect the given slider position, if needed.
121182 void _setValueToMousePosition (int position) {
122183 _domService.scheduleRead (() {
@@ -127,25 +188,36 @@ class MaterialSliderComponent implements AfterChanges, HasDisabled {
127188 final fractionOfTrackLtr = (position - containerLeft) / containerWidth;
128189 final fractionOfTrack =
129190 isRtl ? 1.0 - fractionOfTrackLtr : fractionOfTrackLtr;
130-
131191 final scaledValue = (fractionOfTrack * (max - min));
132192 final halfStep = step / 2 ;
133193 // Clamp to the closest step value.
134194 final unboundedValue = min +
135195 (scaledValue ~ / step) * step +
136196 (scaledValue.remainder (step) > halfStep ? step : 0 );
137197 final newValue = math.max (min, math.min (max, unboundedValue));
138- if (newValue != value) {
139- value = newValue;
140- _changeController.add (value);
198+ // Adjust left knob in 2 sided slider
199+ if (isLeftKnobSelected ||
200+ (newValue < leftValue && ! isRightKnobSelected)) {
201+ if (newValue != leftValue) {
202+ // Prevent left knob value from being greater than right knob value
203+ leftValue = _getValidLeftValue (value, newValue);
204+ _leftChangeController.add (leftValue);
205+ }
206+ } else {
207+ // Adjust right knob in 1 or 2 sided slider.
208+ if (newValue != value) {
209+ // Prevent right knob value from being less than left knob value
210+ value = _getValidRightValue (leftValue, newValue);
211+ _changeController.add (value);
212+ }
141213 }
142214 });
143215 }
144216
145- /// Whether the user is currently dragging the slider knob.
217+ /// Whether the user is currently dragging either slider knob.
146218 bool isDragging = false ;
147219
148- /// Handles mouse down events on the slider knob or the slider track.
220+ /// Handles mouse down events on either slider knob or the slider track.
149221 void mouseDown (MouseEvent event) {
150222 if (disabled) return ;
151223 if (event.button != 0 ) return ;
@@ -160,12 +232,14 @@ class MaterialSliderComponent implements AfterChanges, HasDisabled {
160232 document.onMouseUp.take (1 ).listen ((event) {
161233 event.preventDefault ();
162234 mouseMoveSubscription.cancel ();
235+ isLeftKnobSelected = false ;
236+ isRightKnobSelected = false ;
163237 isDragging = false ;
164238 _changeDetector.markForCheck ();
165239 });
166240 }
167241
168- /// Handles touch start events on the slider knob.
242+ /// Handles touch start events on either slider knob.
169243 void touchStart (TouchEvent event) {
170244 if (disabled) return ;
171245 event.preventDefault ();
@@ -181,36 +255,56 @@ class MaterialSliderComponent implements AfterChanges, HasDisabled {
181255 document.onTouchEnd.take (1 ).listen ((event) {
182256 event.preventDefault ();
183257 touchMoveSubscription.cancel ();
258+ isLeftKnobSelected = false ;
259+ isRightKnobSelected = false ;
184260 isDragging = false ;
185261 _changeDetector.markForCheck ();
186262 });
187263 }
188264
189- /// Handles key press events on the slider knob.
190- void knobKeyDown (KeyboardEvent event) {
265+ /// Handles key press events on either slider knob.
266+ ///
267+ /// [isLeftKnob] true indicates that the event ocurred on the left knob.
268+ void knobKeyDown (KeyboardEvent event, {bool isLeftKnobPressed = false }) {
191269 if (disabled) return ;
192- var newValue = value;
270+ var currValue = isLeftKnobPressed ? leftValue : value;
271+ var newValue = currValue;
193272 final bigStepSize = ((max - min) / 10.0 ).ceil ();
194273 final sign = isRtl ? - 1 : 1 ;
195274 switch (event.keyCode) {
196275 case KeyCode .DOWN :
197276 case KeyCode .LEFT :
198- newValue = math.max (min, math.min (max, value - step * sign));
277+ newValue = math.max (min, math.min (max, currValue - step * sign));
199278 break ;
200279 case KeyCode .UP :
201280 case KeyCode .RIGHT :
202- newValue = math.max (min, math.min (max, value + step * sign));
281+ newValue = math.max (min, math.min (max, currValue + step * sign));
203282 break ;
204283 case KeyCode .PAGE_UP :
205- newValue = math.max (min, math.min (max, value + step * bigStepSize));
284+ newValue = math.max (min, math.min (max, currValue + step * bigStepSize));
206285 break ;
207286 case KeyCode .PAGE_DOWN :
208- newValue = math.max (min, math.min (max, value - step * bigStepSize));
287+ newValue = math.max (min, math.min (max, currValue - step * bigStepSize));
209288 break ;
210289 }
211- if (newValue != value) {
212- value = newValue;
290+ if (isLeftKnobPressed) {
291+ if (newValue != leftValue) {
292+ leftValue = _getValidLeftValue (value, newValue);
293+ _leftChangeController.add (leftValue);
294+ }
295+ } else if (newValue != value) {
296+ value = _getValidRightValue (leftValue, newValue);
213297 _changeController.add (value);
214298 }
215299 }
300+
301+ /// Returns a value that is valid for right knob depending on language
302+ /// direction.
303+ num _getValidRightValue (double valA, double valB, {isRtl = false }) =>
304+ isRtl ? math.min (valA, valB) : math.max (valA, valB);
305+
306+ /// Returns a value that is valid for left knob depending on language
307+ /// direction.
308+ num _getValidLeftValue (double valA, double valB, {isRtl = false }) =>
309+ isRtl ? math.max (valA, valB) : math.min (valA, valB);
216310}
0 commit comments