-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRangeSelector.js
More file actions
535 lines (506 loc) · 20.1 KB
/
RangeSelector.js
File metadata and controls
535 lines (506 loc) · 20.1 KB
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
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
/*
The RangeSelector class allows the user to specify regions of activity in a
timeline representing the total duration of a given lesson. One RangeSelector
is created for each Standard chosen in the log. When more than one Standard is
chosen, they will appear stacked above one another, allowing the user to
visualize and indicate the overlap of multiple Standards during the lesson
period. The granularity of the RangeSelector (that is, the number of buttons
comprising it), is set by the 'increment' property, which represents the %
added to the total each time a button is pressed. Thus, the default increment
of 5 results in splitting the timeline into 20 buttons, each button representing
5% of the total.
*/
Ext.define('iPad2.view.component.RangeSelector', {
extend: 'Ext.SegmentedButton',
alias: 'widget.rangeselector',
xtype: 'rangeselector',
requires: [
'Ext.Button',
'Ext.util.Region'
],
config: {
layout: {
type: 'hbox',
pack: 'center',
align: 'stretchmax'
},
cls: 'o-rangeselector',//Important for CSS styling
allowMultiple: true,//Do not change
increment: 5,//This determines the granularity (number of buttons) of the RangeSelector. For example, default value of 5 results in 20 buttons, each worth 5% of total
area: {},//This stores the Ext.util.Region associated with the area of the screen the RangeSelector occupies when drawn in the browser
items: [],//This stores the buttons
state: { //This stores state variables used by the touchstart, touchmove, and touchend event handlers
touchInProgress: false, //Set to true while the user's finger remains on the screen after touching the RangeSelector, false otherwise
touched: null, //Set to the button currently targeted by the touch
lastTouched: null, //Set to the button previously targeted by the touch
direction: null, //Set to the direction the touch is currently heading ('right' or 'left')
lastDirection: null, //Set to the direction the touch was previously heading ('right' or 'left')
reversed: false //Set to true when direction!=lastDirection, false otherwise
},
regions: '',//This stores the region data, each button is represented by either a 0 or 1 in the string
total: 0,//This stores the total % currently indicated on the slider
totalPercentCmp: '',//This stores the itemId of the component used to display the total % (established via config when RangeSelector is instantiated)
totalMinutesCmp: '',//This stores the itemId of the component used to display the total minutes (established via config when RangeSelector is instantiated)
durationCmp: '',//This stores the itemId of the component used to enter the lesson duration time in minutes (elsewhere in log page)
listeners: {
/*
touchstart - This function establishes the location of the touch, locates the button associated with that location, initializes the state
variables for this touch, and toggles the target button if found. Note that buttons are looked up by the pageX value of the touch event
*/
touchstart: {
element: 'element',//Important - SegmentedButton doesn't normally support this event, so it must be set to listen on the DOM element instead
fn: function(e){
var button=this.getButtonByTouch(e.pageX),
state=this.getState();
state.position=e.pageX
if(button){
state.touchInProgress=true;
state.touched=button.index;
state.lastTouched=null;
state.direction=null;
state.lastDirection=null;
state.reversed=false;
this.toggleButtonByIndex(state.touched);
//console.log('touchstart over button ' + button.index);
//console.log('touchstart: '+this.getPressedIndices().toString());
}
}
},
/*
touchmove - This function locates the button associated with the current touch location, updates the state
variables for this touch, and toggles the target button if found. There is additional logic to handle changes
in direction of the touch and to deal with problems stemming from events not firing during rapid changes in
touch position, which used to result in gaps as the user quickly moved his/her finger across the RangeSelector.
Note that buttons are looked up by the pageX value of the touch event.
*/
touchmove: {
element: 'element',//Important - SegmentedButton doesn't normally support this event, so it must be set to listen on the DOM element instead
fn: function(e){
var button=this.getButtonByTouch(e.pageX),
state=this.getState();
if(button && state.touchInProgress && state.touched!=button.index){//Confirms that the touch has moved over a new button
//Update state variables
state.lastTouched=state.touched
state.touched=button.index;
state.lastDirection=state.direction;
state.direction=(state.lastTouched<state.touched)?'right':'left';
state.reversed=(state.lastDirection!=null && state.direction!=state.lastDirection)?true:false;
var lastButton=this.getButtonByIndex(state.lastTouched);
/*this block helps deal with buggy/missing touchmove events*/
if(Math.abs(state.touched-state.lastTouched)>1){//A gap is present any time the
var gapStart=(state.direction=='right')?state.lastTouched+1:state.touched+1,
gapEnd=(state.direction=='right')?state.touched-1:state.lastTouched-1,
i=gapStart;
for(;i<=gapEnd;i++){
if(this.isPressed(lastButton)){
this.activateButtonByIndex(i);
}
else{
this.deactivateButtonByIndex(i);
}
//console.log('skipped button '+ i);
}
}
if(!state.reversed){
if(this.isPressed(lastButton)==this.isPressed(button)){
state.touchInProgress=false;//If the current touch is activating buttons the touch will be terminated upon encountered another region of activated buttons and vice versa
}
else{
this.toggleButtonByIndex(state.touched);
this.recordTotal();//Update and display the new total
//console.log('touchmove over button ' + button.index + '|pageX: ' + e.pageX + ', pageY: ' + e.pageY);
}
}
else{//If the direction is reversed, the previously touched button must be toggled back to it's previous state (reversal undoes the action)
this.toggleButtonByIndex(state.lastTouched);
this.toggleButtonByIndex(state.touched);
this.recordTotal();//Update and display the new total
//console.log('touchmove over button ' + button.index + '|pageX: ' + e.pageX + ', pageY: ' + e.pageY);
}
//console.log('touchmove over button ' + button.index);
//console.log('touchmove: '+this.getPressedIndices().toString());
}
}
},
/*
touchend - This function resets the state variables to their default values at the end of a touch interaction
and then updates the region information to reflect the new state of the RangeSelector.
*/
touchend: {
element: 'element',//Important - SegmentedButton doesn't normally support this event, so it must be set to listen on the DOM element instead
fn: function(e){
var button=this.getButtonByTouch(e.pageX),
state=this.getState();
state.touchInProgress=false;
state.touched=null;
state.lastTouched=null;
state.direction=null,
state.lastDirection=null,
state.reverse=false;
this.recordRegions();
//console.log('touchend over button ' + button.index);
//console.log('touchend: '+this.getPressedIndices().toString());
}
},
/*
tap - This function handles toggling single buttons when tapped/clicked. This allows for fine tuning of the
regions.
*/
tap: {
element: 'element',//Important - SegmentedButton doesn't normally support this event, so it must be set to listen on the DOM element instead
fn: function(e){
var button=this.getButtonByTouch(e.pageX);
if(button){
this.toggleButtonByIndex(button.index);
//console.log('tap on button '+button.index);
//console.log('tap: '+this.getPressedIndices().toString());
}
}
},
/*
longpress - This function toggles between all 'on' or all 'off'. If targeted button is 'on' all will be turned 'off' and vice versa.
*/
longpress: {
element: 'element',//Important - SegmentedButton doesn't normally support this event, so it must be set to listen on the DOM element instead
fn: function(e){
var button=this.getButtonByTouch(e.pageX);
if(button){
var buttons=this.getItems().items,
buttonCount=buttons.length,
task=this.isPressed(button)?'activate':'deactivate',//This is a little funky/reversed, because isPressed changes on 'mousedown'
pressedIndicesNew = [],
i=0;
if(task=='activate'){
for(;i<buttonCount;i++){
pressedIndicesNew.push(i);
}
}
this.setPressedButtons(pressedIndicesNew);
//console.log('longpress on button '+button.index);
}
}
},
/*
resize - This function updates the RangeSelector's knowledge of it's size/position with respect to the screen after it's DOM element
is resized as a result of either the browser window changing size or the layout changing. Also updates the area property of each
button.
*/
resize: {
buffer: 100, //buffer reduces calls to this function as resize is taking place, all that matters is final size in 'resting' state
fn: function(){
var buttons=this.getItems().items,
buttonCount=buttons.length,
i=0;
for(;i<buttonCount;i++){
buttons[i].area=Ext.util.Region.getRegion(buttons[i].element);
}
this.setArea(Ext.util.Region.getRegion(this.element));
//console.log('selector resized', this.element.dom.scrollWidth);
//console.log(this.area.toString());
}
},
/*
painted - This function initializes the values of the related components (totalPercentCmp and totalMinutesCmp), not called until it
is safe to attempt to access those other components.
*/
painted: {
fn: function(){
this.displayTotalPercent();
this.displayTotalMinutes();
}
}
}
},
/*
initialize - This function initializes the RangeSelector using values specified in the config object. SegmentedButton constructor
must be called first [this.callParent()].
*/
initialize: function(){
this.callParent(); //Important - takes care of establishing everything associated with the parent class (SegmentedButton)
//Establish number of buttons given increment setting
var increment=this.getIncrement(),//default of 5 is given in class definition config above
numButtons=(100/increment),
regions='',
i=0;
//Create and initialize buttons
for(;i<numButtons;i++){
var button=Ext.create('Ext.Button', {//There are a number of custom properties being added to the button class. In the future, an extension of Ext.Button might be nice
index: i,//custom index used to identify buttons
text: ' ',
width: increment+'%',//set size of each button based on increment
area: {}, //This stores the Ext.util.Region associated with the area of the screen the button occupies when drawn in the browser
pressed: false //default to not pressed
});
this.add(button);
regions+='0';//build empty region string for default empty state
}
this.setRegions(this.config.regions || regions);//Pull in region data is available, otherwise initialize to empty state.
this.setTotalPercentCmp(this.config.totalPercentCmp);
this.setTotalMinutesCmp(this.config.totalMinutesCmp);
this.setDurationCmp(this.config.durationCmp);
this.initializeRegions();//Now that the buttons are in place, set them to 'on' or 'off' according to region data
//console.log(this.getItems().items);
},
/*
getButtonByTouch - This function uses the touch position (pageX) to look up which button corresponds to that position
by iterating over the button collection and checking if the touch position falls within each button's area. Returns
the button object when found. Additional logic is in place to account for touches going beyond the RangeSelector's
area, in which case either the first or last button is returned depending on which side of the RangeSelector the touch
has gone out of bounds on.
*/
getButtonByTouch: function(pageX){
var buttons=this.getItems().items,
buttonCount=buttons.length,
button,
rangeSelectorArea=this.getArea(),
i=0;
for(;i<buttonCount;i++){
if(!buttons[i].area.isOutOfBoundX(pageX)){
button=buttons[i];
break;
}
}
if(!button){
if(rangeSelectorArea.isOutOfBoundX(pageX)){
if(rangeSelectorArea.getOutOfBoundOffsetX(pageX)>0){
button=buttons[0];
}
else{
button=buttons[buttonCount-1];
}
//console.log(rangeSelectorArea.getOutOfBoundOffsetX(pageX));
}
}
return button;
},
/*
getButtonByIndex - This function uses a button index to look up and return a button. Uses SegmentedButton's getAt function
*/
getButtonByIndex: function(index){
return this.getAt(index);
},
/*
getPressedIndices - This function returns an array of indices corresponding to the buttons that are currently pressed.
Uses SegmentedButton's getPressedButtons function to retrieve an array of button objects which is iterated over to
collect those buttons' index properties into an array.
*/
getPressedIndices: function(){
var pressedButtons=this.getPressedButtons(),
pressedButtonCount=pressedButtons.length,
pressedIndices=[],
i=0;
for(;i<pressedButtonCount;i++){
pressedIndices.push(pressedButtons[i].index);
}
return pressedIndices;
},
/*
toggleButtonByIndex - This function toggles the state of the button identified by the index argument. Uses SegmentedButton's
setPressedButtons function.
*/
toggleButtonByIndex: function(index){
var button=this.getItems().items[index],
pressedIndicesOld=this.getPressedIndices(),
pressedIndicesNew=[],
wasPressed=this.isPressed(button);
if(wasPressed){
var i=0, pressedButtonCountOld=pressedIndicesOld.length;
for(;i<pressedButtonCountOld;i++){
if(pressedIndicesOld[i]!=index){
pressedIndicesNew.push(pressedIndicesOld[i]);
}
}
}
else{
pressedIndicesNew=pressedIndicesOld;
pressedIndicesNew.push(index);
}
this.setPressedButtons(pressedIndicesNew);
//console.log('toggled by index: '+index);
//console.log('selected: '+this.getPressedIndices().toString());
},
/*
activateButtonByIndex - This function turns on the button identified by the index argument or does nothing if that button is
already turned on. Uses SegmentedButton's setPressedButtons function.
*/
activateButtonByIndex: function(index){
var button=this.getItems().items[index],
pressedIndicesOld=this.getPressedIndices(),
pressedIndicesNew=[],
wasPressed=this.isPressed(button);
if(wasPressed){
return;
}
else{
pressedIndicesNew=pressedIndicesOld;
pressedIndicesNew.push(index);
}
this.setPressedButtons(pressedIndicesNew);
//console.log('toggled by index: '+index);
//console.log('selected: '+this.getPressedIndices().toString());
},
/*
deactivateButtonByIndex - This function turns off the button identified by the index argument or does nothing if that button is
already turned off. Uses SegmentedButton's setPressedButtons function.
*/
deactivateButtonByIndex: function(index){
var button=this.getItems().items[index],
pressedIndicesOld=this.getPressedIndices(),
pressedIndicesNew=[],
wasPressed=this.isPressed(button);
if(wasPressed){
var i=0, pressedButtonCountOld=pressedIndicesOld.length;
for(;i<pressedButtonCountOld;i++){
if(pressedIndicesOld[i]!=index){
pressedIndicesNew.push(pressedIndicesOld[i]);
}
}
}
else{
return;
}
this.setPressedButtons(pressedIndicesNew);
//console.log('toggled by index: '+index);
//console.log('selected: '+this.getPressedIndices().toString());
},
/*
initializeRegions - This function is responsible for using the region data (if available) retrieved from the server to initialize
the state of each button when the RangeSelector is first displayed. It also establishes the initial value for RangeSelector.total
based on the region data. Uses SegmentedButton's setPressedButtons function. Code written to handle previous, object-style region
data has been commented out and replaced by code that handles string representation of the region data
*/
initializeRegions: function(){
/*var regions=this.getRegions(),
regionCount=regions.length,
initialPressedIndices=[],
i=0,
j=0,
total=0;
for(;i<regionCount;i++){
for(j=regions[i].start;j<=regions[i].end;j++){
initialPressedIndices.push(j);
}
total+=regions[i].percentage;
}*/
var regions=this.getRegions(),
buttonCount=regions.length,
increment=this.getIncrement(),
initialPressedIndices=[],
total=0,
i=0;
for(;i<buttonCount;i++){
if(regions.charAt(i)=='1'){
initialPressedIndices.push(i);
total+=increment;
}
}
this.setPressedButtons(initialPressedIndices);
this.setTotal(total);
//console.log(this.getTotal());
},
/*
recordRegions - This function updates the region data to match the current state of the buttons in the RangeSelector. Total is
tallied and displayed as well. Code written to handle previous, object-style region data has been commented out and replaced by
code that handles string representation of the region data.
*/
recordRegions: function(){/*
var buttons=this.getItems().items,
buttonCount=buttons.length,
tempRegions=[],
tempRegion=null,
i=0,
total=0;
for(;i<buttonCount;i++){
if(this.isPressed(buttons[i])){
if(tempRegion){
tempRegion.end=i;
if(i==(buttonCount-1)){//edge-case solution.
tempRegion.percentage=((tempRegion.end-tempRegion.start+1)/buttonCount)*100;
total+=tempRegion.percentage;
tempRegions.push(tempRegion);
tempRegion=null
}
}
else{
tempRegion=new Object();
tempRegion.id=tempRegions.length;
tempRegion.start=i;
tempRegion.end=i;
}
}
else{
if(tempRegion){
tempRegion.percentage=((tempRegion.end-tempRegion.start+1)/buttonCount)*100;
total+=tempRegion.percentage;
tempRegions.push(tempRegion);
tempRegion=null;
}
}
}*/
var buttons=this.getItems().items,
buttonCount=buttons.length,
increment=this.getIncrement(),
tempRegions='',
i=0,
total=0;
for(;i<buttonCount;i++){
if(this.isPressed(buttons[i])){
tempRegions+='1';
total+=increment;
}
else{
tempRegions+='0';
}
}
this.setRegions(tempRegions);
this.setTotal(total);
this.displayTotalPercent();
this.displayTotalMinutes();
//console.log('regions: '+this.regions.toString())
//console.log(this.total);
},
/*
recordTotal - This function tallies the total % selected in the RangeSelector and updates RangeSelector.totalPercentage
and displays the updated total value. Then calls function to display total minutes given the % and the lesson duration
retrieved from Lesson Duration input on the page.
*/
recordTotal: function(){
var buttons=this.getItems().items,
buttonCount=buttons.length,
increment=this.getIncrement(),
i=0,
total=0;
for(;i<buttonCount;i++){
if(this.isPressed(buttons[i])){
total+=increment;
}
}
this.setTotal(total);
this.displayTotalPercent();
this.displayTotalMinutes();
},
/*
displayTotalPercentage - This function updates the displayed total percentage value. Looks up the total % display component using
id fed into config at initialization.
*/
displayTotalPercent: function(){
var outString=Math.round(this.getTotal())+'%';
Ext.ComponentQuery.query('#'+this.getTotalPercentCmp())[0].setValue(outString);
},
/*
displayTotalPercentage - This function updates the displayed total percentage value. Looks up the total % display component using
id fed into config at initialization.
*/
displayTotalMinutes: function(){
var totalMinutes=0,
totalPercent=this.getTotal(),
duration=Ext.ComponentQuery.query('#'+this.getDurationCmp())[0].getValue(),
totalMinutes=(totalPercent/100)*duration,
outString='';
if(Math.round(totalMinutes)!=totalMinutes){
outString='~';
}
outString+=Math.round(totalMinutes)+' minutes';
Ext.ComponentQuery.query('#'+this.getTotalMinutesCmp())[0].setValue(outString);
}
});