-
Notifications
You must be signed in to change notification settings - Fork 12
/
Slider.js
476 lines (386 loc) · 17.9 KB
/
Slider.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
// Copyright 2013-2020, University of Colorado Boulder
/**
* Slider, with support for horizontal and vertical orientations. By default, the slider is constructed in the
* horizontal orientation, then adjusted if the vertical orientation was specified.
*
* Note: This type was originally named HSlider, renamed in https://github.com/phetsims/sun/issues/380.
*
* @author Chris Malley (PixelZoom, Inc.)
*/
import Property from '../../axon/js/Property.js';
import Dimension2 from '../../dot/js/Dimension2.js';
import Range from '../../dot/js/Range.js';
import Utils from '../../dot/js/Utils.js';
import Shape from '../../kite/js/Shape.js';
import assertMutuallyExclusiveOptions from '../../phet-core/js/assertMutuallyExclusiveOptions.js';
import InstanceRegistry from '../../phet-core/js/documentation/InstanceRegistry.js';
import merge from '../../phet-core/js/merge.js';
import Orientation from '../../phet-core/js/Orientation.js';
import FocusHighlightFromNode from '../../scenery/js/accessibility/FocusHighlightFromNode.js';
import DragListener from '../../scenery/js/listeners/DragListener.js';
import Node from '../../scenery/js/nodes/Node.js';
import Path from '../../scenery/js/nodes/Path.js';
import PhetioObject from '../../tandem/js/PhetioObject.js';
import Tandem from '../../tandem/js/Tandem.js';
import BooleanIO from '../../tandem/js/types/BooleanIO.js';
import IOType from '../../tandem/js/types/IOType.js';
import VoidIO from '../../tandem/js/types/VoidIO.js';
import AccessibleSlider from './accessibility/AccessibleSlider.js';
import DefaultSliderTrack from './DefaultSliderTrack.js';
import EnabledNode from './EnabledNode.js';
import SliderThumb from './SliderThumb.js';
import sun from './sun.js';
import SunConstants from './SunConstants.js';
// constants
const VERTICAL_ROTATION = -Math.PI / 2;
class Slider extends Node {
/**
* @param {Property.<number>} valueProperty
* @param {Range} range
* @param {Object} [options]
* @mixes AccessibleSlider
* @mixes EnabledNode
*/
constructor( valueProperty, range, options ) {
// Guard against mutually exclusive options before defaults are filled in.
assert && assertMutuallyExclusiveOptions( options, [ 'thumbNode' ], [
'thumbSize', 'thumbFill', 'thumbFillHighlighted', 'thumbStroke', 'thumbLineWidth', 'thumbCenterLineStroke',
'thumbTouchAreaXDilation', 'thumbTouchAreaYDilation', 'thumbMouseAreaXDilation', 'thumbMouseAreaYDilation'
] );
assert && assertMutuallyExclusiveOptions( options, [ 'trackNode' ], [
'trackSize', 'trackFillEnabled', 'trackFillDisabled', 'trackStroke', 'trackLineWidth', 'trackCornerRadius' ] );
options = merge( {
orientation: Orientation.HORIZONTAL, // {Orientation}
// {SliderTrack} optional track, replaces the default.
// Client is responsible for highlighting, disable and pointer areas.
trackNode: null,
// track - options to create a SliderTrack if trackNode not supplied
trackSize: new Dimension2( 100, 5 ),
trackFillEnabled: 'white',
trackFillDisabled: 'gray',
trackStroke: 'black',
trackLineWidth: 1,
trackCornerRadius: 0,
// {Node} optional thumb, replaces the default.
// Client is responsible for highlighting, disabling and pointer areas.
// The thumb is positioned based on its center and hence can have its origin anywhere
// Note for PhET-IO: This thumbNode should be instrumented. The thumb's dragListener is instrumented underneath
// this thumbNode.
thumbNode: null,
// Options for the default thumb, ignored if thumbNode is set
thumbSize: new Dimension2( 17, 34 ),
thumbFill: 'rgb(50,145,184)',
thumbFillHighlighted: 'rgb(71,207,255)',
thumbStroke: 'black',
thumbLineWidth: 1,
thumbCenterLineStroke: 'white',
thumbTouchAreaXDilation: 11,
thumbTouchAreaYDilation: 11,
thumbMouseAreaXDilation: 0,
thumbMouseAreaYDilation: 0,
// Applied to default or supplied thumb
thumbYOffset: 0, // center of the thumb is vertically offset by this amount from the center of the track
// ticks - if adding an option here, make sure it ends up in this.tickOptions
tickLabelSpacing: 6,
majorTickLength: 25,
majorTickStroke: 'black',
majorTickLineWidth: 1,
minorTickLength: 10,
minorTickStroke: 'black',
minorTickLineWidth: 1,
// other
cursor: 'pointer',
startDrag: _.noop, // called when a drag sequence starts
drag: _.noop, // called at the end of a drag event, after the valueProperty changes
endDrag: _.noop, // called when a drag sequence ends
constrainValue: _.identity, // called before valueProperty is set
enabledRangeProperty: null, // {Property.<Range>|null} determine the portion of range that is enabled
disabledOpacity: SunConstants.DISABLED_OPACITY, // opacity applied to the entire Slider when disabled
// phet-io
tandem: Tandem.REQUIRED,
phetioType: Slider.SliderIO,
phetioComponentOptions: null, // filled in below with PhetioObject.mergePhetioComponentOptions()
// {Property.<number>|null} - if provided, create a LinkedElement for this PhET-iO instrumented Property, instead
// of using the passed in Property. This option was created to support passing DynamicProperty or "wrapping"
// Property that are "implementation details" to the PhET-iO API, and still support having a LinkedElement that
// points to the underlying model Property.
phetioLinkedProperty: null
}, options );
// Extra logic to prevent providing two conflicting options to EnabledNode
if ( !options.enabledProperty ) {
options = merge( {
// EnabledNode
enabledPropertyOptions: {
phetioFeatured: true
}
}, options );
}
assert && assert( range instanceof Range, 'range must be of type Range:' + range );
assert && assert( Orientation.includes( options.orientation ),
'invalid orientation: ' + options.orientation );
PhetioObject.mergePhetioComponentOptions( {
visibleProperty: { phetioFeatured: true }
}, options );
super();
// @private {Orientation}
this.orientation = options.orientation;
const ownsEnabledRangeProperty = !options.enabledRangeProperty;
// controls the portion of the slider that is enabled
options.enabledRangeProperty = options.enabledRangeProperty || new Property( range, {
valueType: Range,
isValidValue: value => ( value.min >= range.min && value.max <= range.max ),
tandem: options.tandem.createTandem( 'enabledRangeProperty' ),
phetioType: Property.PropertyIO( Range.RangeIO ),
phetioDocumentation: 'Sliders support two ranges: the outer range which specifies the min and max of the track and ' +
'the enabledRangeProperty, which determines how low and high the thumb can be dragged within the track.'
} );
// @public {Property.<Range>|null}
this.enabledRangeProperty = options.enabledRangeProperty;
// @private {Object} - options needed by prototype functions that add ticks
this.tickOptions = _.pick( options, 'tickLabelSpacing',
'majorTickLength', 'majorTickStroke', 'majorTickLineWidth',
'minorTickLength', 'minorTickStroke', 'minorTickLineWidth' );
const sliderParts = [];
// @private {Node} ticks are added to these parents, so they are behind the knob
this.majorTicksParent = new Node();
this.minorTicksParent = new Node();
sliderParts.push( this.majorTicksParent );
sliderParts.push( this.minorTicksParent );
// @private {Node} track
this.track = options.trackNode || new DefaultSliderTrack( valueProperty, range, {
// propagate options that are specific to SliderTrack
size: options.trackSize,
fillEnabled: options.trackFillEnabled,
fillDisabled: options.trackFillDisabled,
stroke: options.trackStroke,
lineWidth: options.trackLineWidth,
cornerRadius: options.trackCornerRadius,
startDrag: options.startDrag,
drag: options.drag,
endDrag: options.endDrag,
constrainValue: options.constrainValue,
enabledRangeProperty: this.enabledRangeProperty,
// phet-io
tandem: options.tandem.createTandem( 'track' )
} );
// Position the track horizontally
this.track.centerX = this.track.valueToPosition( ( range.max + range.min ) / 2 );
// The thumb of the slider
const thumb = options.thumbNode || new SliderThumb( {
// propagate options that are specific to SliderThumb
size: options.thumbSize,
fill: options.thumbFill,
fillHighlighted: options.thumbFillHighlighted,
stroke: options.thumbStroke,
lineWidth: options.thumbLineWidth,
centerLineStroke: options.thumbCenterLineStroke,
tandem: options.tandem.createTandem( 'thumb' )
} );
// Dilate the local bounds horizontally so that it extends beyond where the thumb can reach. This prevents layout
// asymmetry when the slider thumb is off the edges of the track. See https://github.com/phetsims/sun/issues/282
this.track.localBounds = this.track.localBounds.dilatedX( thumb.width / 2 );
// Add the track
sliderParts.push( this.track );
// Position the thumb vertically.
thumb.setCenterY( this.track.centerY + options.thumbYOffset );
sliderParts.push( thumb );
// Wrap all of the slider parts in a Node, and set the orientation of that Node.
// This allows us to still decorate the Slider with additional children.
// See https://github.com/phetsims/sun/issues/406
const sliderPartsNode = new Node( { children: sliderParts } );
if ( options.orientation === Orientation.VERTICAL ) {
sliderPartsNode.rotation = VERTICAL_ROTATION;
}
this.addChild( sliderPartsNode );
// touchArea for the default thumb. If a custom thumb is provided, the client is responsible for its touchArea.
if ( !options.thumbNode && ( options.thumbTouchAreaXDilation || options.thumbTouchAreaYDilation ) ) {
thumb.touchArea = thumb.localBounds.dilatedXY( options.thumbTouchAreaXDilation, options.thumbTouchAreaYDilation );
}
// mouseArea for the default thumb. If a custom thumb is provided, the client is responsible for its mouseArea.
if ( !options.thumbNode && ( options.thumbMouseAreaXDilation || options.thumbMouseAreaYDilation ) ) {
thumb.mouseArea = thumb.localBounds.dilatedXY( options.thumbMouseAreaXDilation, options.thumbMouseAreaYDilation );
}
// update value when thumb is dragged
let clickXOffset = 0; // x-offset between initial click and thumb's origin
const thumbDragListener = new DragListener( {
// Deviate from the variable name because we will nest this tandem under the thumb directly
tandem: thumb.tandem.createTandem( 'dragListener' ),
start: ( event, listener ) => {
if ( this.enabledProperty.get() ) {
options.startDrag( event );
const transform = listener.pressedTrail.subtrailTo( sliderPartsNode ).getTransform();
// Determine the offset relative to the center of the thumb
clickXOffset = transform.inversePosition2( event.pointer.point ).x - thumb.centerX;
}
},
drag: ( event, listener ) => {
if ( this.enabledProperty.get() ) {
const transform = listener.pressedTrail.subtrailTo( sliderPartsNode ).getTransform(); // we only want the transform to our parent
const x = transform.inversePosition2( event.pointer.point ).x - clickXOffset;
const newValue = this.track.valueToPosition.inverse( x );
const valueInRange = this.enabledRangeProperty.get().constrainValue( newValue );
valueProperty.set( options.constrainValue( valueInRange ) );
// after valueProperty is set so listener can use the new value
options.drag( event );
}
},
end: event => {
if ( this.enabledProperty.get() ) {
options.endDrag( event );
}
}
} );
thumb.addInputListener( thumbDragListener );
// update thumb position when value changes
const valueObserver = value => {
thumb.centerX = this.track.valueToPosition( value );
};
valueProperty.link( valueObserver ); // must be unlinked in disposeSlider
// when the enabled range changes, the value to position linear function must change as well
const enabledRangeObserver = function( enabledRange ) {
// clamp the value to the enabled range if it changes
valueProperty.set( Utils.clamp( valueProperty.value, enabledRange.min, enabledRange.max ) );
};
this.enabledRangeProperty.link( enabledRangeObserver ); // needs to be unlinked in dispose function
this.mutate( options );
// must initialize after the Slider is instrumented
this.initializeEnabledNode( options );
// @private {function} - Called by dispose
this.disposeSlider = () => {
thumb.dispose && thumb.dispose(); // in case a custom thumb is provided via options.thumbNode that doesn't implement dispose
this.track.dispose();
this.disposeAccessibleSlider();
this.disposeEnabledNode();
valueProperty.unlink( valueObserver );
ownsEnabledRangeProperty && this.enabledRangeProperty.dispose();
thumbDragListener.dispose();
};
// pdom - custom focus highlight that surrounds and moves with the thumb
this.focusHighlight = new FocusHighlightFromNode( thumb );
assert && assert( !options.ariaOrientation, 'Slider sets its own ariaOrientation' );
this.initializeAccessibleSlider( valueProperty, this.enabledRangeProperty, this.enabledProperty,
merge( { ariaOrientation: options.orientation }, options ) );
assert && Tandem.VALIDATION && assert( !options.phetioLinkedProperty || options.phetioLinkedProperty.isPhetioInstrumented(),
'If provided, phetioLinkedProperty should be PhET-iO instrumented' );
this.addLinkedElement( options.phetioLinkedProperty || valueProperty, {
tandem: options.tandem.createTandem( 'valueProperty' )
} );
// support for binder documentation, stripped out in builds and only runs when ?binder is specified
assert && phet.chipper.queryParameters.binder && InstanceRegistry.registerDataURL( 'sun', 'Slider', this );
}
get enabledRange() { return this.getEnabledRange(); }
set enabledRange( range ) { this.setEnabledRange( range ); }
get majorTicksVisible() { return this.getMajorTicksVisible(); }
set majorTicksVisible( value ) { this.setMajorTicksVisible( value ); }
get minorTicksVisible() { return this.getMinorTicksVisible(); }
set minorTicksVisible( value ) { this.setMinorTicksVisible( value ); }
/**
* @public
* @override
*/
dispose() {
this.disposeSlider();
super.dispose();
}
/**
* Adds a major tick mark.
* @param {number} value
* @param {Node} [label] optional
* @public
*/
addMajorTick( value, label ) {
this.addTick( this.majorTicksParent, value, label,
this.tickOptions.majorTickLength, this.tickOptions.majorTickStroke, this.tickOptions.majorTickLineWidth );
}
/**
* Adds a minor tick mark.
* @param {number} value
* @param {Node} [label] optional
* @public
*/
addMinorTick( value, label ) {
this.addTick( this.minorTicksParent, value, label,
this.tickOptions.minorTickLength, this.tickOptions.minorTickStroke, this.tickOptions.minorTickLineWidth );
}
/*
* Adds a tick mark above the track.
* @param {Node} parent
* @param {number} value
* @param {Node} [label] optional
* @param {number} length
* @param {number} stroke
* @param {number} lineWidth
* @private
*/
addTick( parent, value, label, length, stroke, lineWidth ) {
const labelX = this.track.valueToPosition( value );
// ticks
const tick = new Path( new Shape()
.moveTo( labelX, this.track.top )
.lineTo( labelX, this.track.top - length ),
{ stroke: stroke, lineWidth: lineWidth } );
parent.addChild( tick );
// label
if ( label ) {
// For a vertical slider, rotate labels opposite the rotation of the slider, so that they appear as expected.
if ( this.orientation === Orientation.VERTICAL ) {
label.rotation = -VERTICAL_ROTATION;
}
parent.addChild( label );
label.centerX = tick.centerX;
label.bottom = tick.top - this.tickOptions.tickLabelSpacing;
label.pickable = false;
}
}
// @public
setEnabledRange( enabledRange ) { this.enabledRangeProperty.value = enabledRange; }
// @public
getEnabledRange() { return this.enabledRangeProperty.value; }
// @public - Sets visibility of major ticks.
setMajorTicksVisible( visible ) {
this.majorTicksParent.visible = visible;
}
// @public - Gets visibility of major ticks.
getMajorTicksVisible() {
return this.majorTicksParent.visible;
}
// @public - Sets visibility of minor ticks.
setMinorTicksVisible( visible ) {
this.minorTicksParent.visible = visible;
}
// @public - Gets visibility of minor ticks.
getMinorTicksVisible() {
return this.minorTicksParent.visible;
}
}
// mix accessibility into Slider
AccessibleSlider.mixInto( Slider );
// mix enabledProperty into Slider
EnabledNode.mixInto( Slider );
Slider.SliderIO = new IOType( 'SliderIO', {
valueType: Slider,
documentation: 'A traditional slider component, with a knob and possibly tick marks',
supertype: Node.NodeIO,
methods: {
setMajorTicksVisible: {
returnType: VoidIO,
parameterTypes: [ BooleanIO ],
implementation: function( visible ) {
this.setMajorTicksVisible( visible );
},
documentation: 'Set whether the major tick marks should be shown',
invocableForReadOnlyElements: false
},
setMinorTicksVisible: {
returnType: VoidIO,
parameterTypes: [ BooleanIO ],
implementation: function( visible ) {
this.setMinorTicksVisible( visible );
},
documentation: 'Set whether the minor tick marks should be shown',
invocableForReadOnlyElements: false
}
}
} );
sun.register( 'Slider', Slider );
export default Slider;