 Chromium Code Reviews
 Chromium Code Reviews Issue 2915753003:
  [Night Light] CL5: Schedule Settings  (Closed)
    
  
    Issue 2915753003:
  [Night Light] CL5: Schedule Settings  (Closed) 
  | OLD | NEW | 
|---|---|
| (Empty) | |
| 1 // Copyright 2017 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 /** | |
| 6 * @fileoverview | |
| 7 * night-light-slider is used to set the custom automatic schedule of the | |
| 8 * Night Light feature, so that users can set their desired start and end | |
| 9 * times. | |
| 10 */ | |
| 11 | |
| 12 /** @const */ var TOTAL_MINUTES_PER_DAY = 24 * 60; | |
| 13 /** @const */ var OFFSET_MINUTES_6PM = 18 * 60; | |
| 14 /** @const */ var MIN_KNOBS_DISTANCE_MINUTES = 30; | |
| 15 | |
| 16 Polymer({ | |
| 17 is: 'night-light-slider', | |
| 18 | |
| 19 behaviors: [ | |
| 20 I18nBehavior, | |
| 21 PrefsBehavior, | |
| 22 Polymer.IronA11yKeysBehavior, | |
| 23 ], | |
| 24 | |
| 25 properties: { | |
| 26 /** | |
| 27 * The object currently being dragged. Either the start or end knobs. | |
| 28 * @type {?Object} | |
| 29 * @private | |
| 30 */ | |
| 31 dragObject_: { | |
| 32 type: Object, | |
| 33 value: null, | |
| 34 }, | |
| 35 | |
| 36 /** | |
| 37 * The start knob time as a string to be shown on the start label bubble. | |
| 38 * @private | |
| 39 */ | |
| 40 startTime_: { | |
| 41 type: String, | |
| 42 }, | |
| 43 | |
| 44 /** | |
| 45 * The end knob time as a string to be shown on the end label bubble. | |
| 46 * @private | |
| 47 */ | |
| 48 endTime_: { | |
| 49 type: String, | |
| 50 }, | |
| 51 }, | |
| 52 | |
| 53 observers: [ | |
| 54 'customTimesChanged_(prefs.ash.night_light.custom_start_time.*, ' + | |
| 55 'prefs.ash.night_light.custom_end_time.*)', | |
| 56 ], | |
| 57 | |
| 58 keyBindings: { | |
| 59 'left': 'onLeftKey_', | |
| 60 'right': 'onRightKey_', | |
| 61 }, | |
| 62 | |
| 63 ready: function() { | |
| 64 // Build the legend markers. | |
| 65 var markersContainer = this.$.markersContainer; | |
| 66 var width = markersContainer.offsetWidth; | |
| 67 var num = 24; | |
| 
stevenjb
2017/06/09 22:51:45
nit: use a constant above, e.g. HOURS_PER_DAY
 
afakhry
2017/06/12 18:07:08
Done.
 | |
| 68 for (var i = 0; i <= num; ++i) { | |
| 69 var marker = document.createElement('div'); | |
| 70 marker.className = 'markers'; | |
| 71 markersContainer.appendChild(marker); | |
| 72 marker.style.left = (i * 100 / num) + '%'; | |
| 73 } | |
| 74 this.async(function() { | |
| 75 // Read the initial prefs values and refresh the slider. | |
| 76 this.customTimesChanged_(); | |
| 77 }); | |
| 78 }, | |
| 79 | |
| 80 /** | |
| 81 * Expands or un-expands the knob being dragged along with its corresponding | |
| 82 * label bubble. | |
| 83 * @param {boolean} expand True to expand, and false to un-expand. | |
| 84 * @private | |
| 85 */ | |
| 86 setExpanded_: function(expand) { | |
| 87 var knob = this.$.startKnob; | |
| 88 var label = this.$.startLabel; | |
| 89 if (this.dragObject_ == this.$.endKnob) { | |
| 90 knob = this.$.endKnob; | |
| 91 label = this.$.endLabel; | |
| 92 } | |
| 93 | |
| 94 if (expand) { | |
| 95 knob.classList.add('expanded-knob'); | |
| 96 label.classList.add('expanded-knob'); | |
| 97 } else { | |
| 98 knob.classList.remove('expanded-knob'); | |
| 99 label.classList.remove('expanded-knob'); | |
| 100 } | |
| 
stevenjb
2017/06/09 22:51:45
nit: knob.classList.toggle('expanded-knob', expand
 
afakhry
2017/06/12 18:07:08
Interesting! Thanks! Done.
 | |
| 101 }, | |
| 102 | |
| 103 /** | |
| 104 * If one of the two knobs is focused, this function blurs it. | |
| 105 * @private | |
| 106 */ | |
| 107 blurAnyFocusedKnob_: function() { | |
| 108 var activeElement = this.shadowRoot.activeElement; | |
| 109 if (activeElement == this.$.startKnob || activeElement == this.$.endKnob) | |
| 110 activeElement.blur(); | |
| 111 }, | |
| 112 | |
| 113 /** | |
| 114 * Start dragging the target knob. | |
| 115 * @private | |
| 116 */ | |
| 117 drag_: function(event) { | |
| 
stevenjb
2017/06/09 22:51:45
nit: startDrag_
 
afakhry
2017/06/12 18:07:08
Done.
 | |
| 118 event.preventDefault(); | |
| 119 this.dragObject_ = event.target; | |
| 120 this.setExpanded_(true); | |
| 121 | |
| 122 // Focus is only given to the knobs by means of keyboard tab navigations. | |
| 123 // When we start dragging, we don't want to see any focus halos around any | |
| 124 // knob. | |
| 125 this.blurAnyFocusedKnob_(); | |
| 126 | |
| 127 // However, our night-light-slider element must get the focus. | |
| 128 this.focus(); | |
| 129 }, | |
| 130 | |
| 131 /** | |
| 132 * Continues dragging the selected knob if any. | |
| 133 * @private | |
| 134 */ | |
| 135 continueDrag_: function(event) { | |
| 136 if (!this.dragObject_) | |
| 137 return; | |
| 138 | |
| 139 event.stopPropagation(); | |
| 140 switch (event.detail.state) { | |
| 141 case 'start': | |
| 142 this.drag_(event); | |
| 143 break; | |
| 144 case 'track': | |
| 145 this.doKnobTracking_(event); | |
| 146 break; | |
| 147 case 'end': | |
| 148 this.drop_(event); | |
| 149 break; | |
| 150 } | |
| 151 }, | |
| 152 | |
| 153 /** | |
| 154 * Updates the knob's corresponding pref value in response to dragging, which | |
| 155 * will in turn update the location of the knob and its corresponding label | |
| 156 * bubble and its text contents. | |
| 157 * @private | |
| 158 */ | |
| 159 doKnobTracking_: function(event) { | |
| 160 var deltaRatio = Math.abs(event.detail.ddx) / this.$.sliderBar.offsetWidth; | |
| 161 var deltaMinutes = Math.floor(deltaRatio * TOTAL_MINUTES_PER_DAY); | |
| 162 if (deltaMinutes <= 0) | |
| 163 return; | |
| 164 | |
| 165 var knobPref = 'ash.night_light.custom_end_time'; | |
| 166 if (this.dragObject_ == this.$.startKnob) | |
| 167 knobPref = 'ash.night_light.custom_start_time'; | |
| 
stevenjb
2017/06/09 22:51:45
Trinary operator would be more clear:
var knobPre
 
afakhry
2017/06/12 18:07:08
Done.
 | |
| 168 | |
| 169 if (event.detail.ddx > 0) { | |
| 170 // Increment the knob's pref by the amount of deltaMinutes. | |
| 171 this.incrementPref_(knobPref, deltaMinutes); | |
| 172 } else { | |
| 173 // Decrement the knob's pref by the amount of deltaMinutes. | |
| 174 this.decrementPref_(knobPref, deltaMinutes); | |
| 175 } | |
| 176 }, | |
| 177 | |
| 178 /** | |
| 179 * Ends the dragging. | |
| 180 * @private | |
| 181 */ | |
| 182 drop_: function(event) { | |
| 
stevenjb
2017/06/09 22:51:45
endDrag_
 
afakhry
2017/06/12 18:07:08
Done.
 | |
| 183 event.preventDefault(); | |
| 184 this.setExpanded_(false); | |
| 185 this.dragObject_ = null; | |
| 186 }, | |
| 187 | |
| 188 /** | |
| 189 * Gets the given knob's offset ratio with respect to its parent element | |
| 190 * (which is the slider bar). | |
| 191 * @param {HTMLDivElement} knob Either one of the two knobs. | |
| 192 * @return {number} | |
| 193 * @private | |
| 194 */ | |
| 195 getKnobRatio_: function(knob) { | |
| 196 return parseFloat(knob.style.left) / this.$.sliderBar.offsetWidth; | |
| 197 }, | |
| 198 | |
| 199 /** | |
| 200 * Pads the given number num with leading zeros such that its length as a | |
| 
stevenjb
2017/06/09 22:51:45
|num|
 
afakhry
2017/06/12 18:07:08
Done.
 | |
| 201 * string is 2. | |
| 202 * @param {number} num | |
| 203 * @return {string} | |
| 204 * @private | |
| 205 */ | |
| 206 pad2_: function(num) { | |
| 207 var s = String(num); | |
| 208 if (s.length == 2) | |
| 209 return s; | |
| 210 | |
| 211 return '0' + s; | |
| 212 }, | |
| 213 | |
| 214 /** | |
| 215 * Converts the offsetMinutes value (which the number of minutes since 00:00) | |
| 216 * to its string representation in the format 6:30 PM. | |
| 217 * @param {number} offsetMinutes The time of day represented as the number of | |
| 218 * minutes from 00:00. | |
| 219 * @return {string} | |
| 220 * @private | |
| 221 */ | |
| 222 offsetMinutesToTimeString_: function(offsetMinutes) { | |
| 223 var hour = Math.floor(offsetMinutes / 60); | |
| 224 var amPm = hour >= 12 ? ' PM' : ' AM'; | |
| 
stevenjb
2017/06/09 22:51:45
See previous comment about localization
 
afakhry
2017/06/12 18:07:08
Done. I added a TODO.
 | |
| 225 hour %= 12; | |
| 226 hour = hour == 0 ? 12 : hour; | |
| 227 var minute = Math.floor(offsetMinutes % 60); | |
| 228 return hour + ':' + this.pad2_(minute) + amPm; | |
| 229 }, | |
| 230 | |
| 231 /** | |
| 232 * Handles changes in the start and end times prefs. | |
| 233 * @private | |
| 234 */ | |
| 235 customTimesChanged_: function() { | |
| 236 var startOffsetMinutes = /** @type {number} */( | |
| 237 this.getPref('ash.night_light.custom_start_time').value); | |
| 238 this.startTime_ = this.offsetMinutesToTimeString_(startOffsetMinutes); | |
| 239 this.updateKnobLeft_(this.$.startKnob, startOffsetMinutes); | |
| 240 var endOffsetMinutes = /** @type {number} */( | |
| 241 this.getPref('ash.night_light.custom_end_time').value); | |
| 242 this.endTime_ = this.offsetMinutesToTimeString_(endOffsetMinutes); | |
| 243 this.updateKnobLeft_(this.$.endKnob, endOffsetMinutes); | |
| 244 this.refresh_(); | |
| 245 }, | |
| 246 | |
| 247 /** | |
| 248 * Updates the absolute left coordinate of the given knob based on the time it | |
| 249 * represents given as an offsetMinutes value. | |
| 250 * @param {HTMLDivElement} knob | |
| 251 * @param {number} offsetMinutes | |
| 252 * @private | |
| 253 */ | |
| 254 updateKnobLeft_: function(knob, offsetMinutes) { | |
| 255 var offsetAfter6pm = | |
| 256 (offsetMinutes + TOTAL_MINUTES_PER_DAY - OFFSET_MINUTES_6PM) % | |
| 257 TOTAL_MINUTES_PER_DAY; | |
| 258 var ratio = offsetAfter6pm / TOTAL_MINUTES_PER_DAY; | |
| 259 | |
| 260 if (ratio == 0) { | |
| 261 // If the ratio is 0, then there are two possibilities: | |
| 262 // - The knob time is 6:00 PM on the left side of the slider. | |
| 263 // - The knob time is 6:00 PM on the right side of the slider. | |
| 264 // We need to check the current knob offset ratio to determine which case | |
| 265 // it is. | |
| 266 var currentKnobRatio = this.getKnobRatio_(knob); | |
| 267 ratio = currentKnobRatio > 0.5 ? 1.0 : 0.0; | |
| 268 } | |
| 269 | |
| 270 knob.style.left = (ratio * this.$.sliderBar.offsetWidth) + 'px'; | |
| 271 }, | |
| 272 | |
| 273 /** | |
| 274 * Refreshes elements of the slider other than the knobs (the label bubbles, | |
| 275 * and the progress bar). | |
| 276 * @private | |
| 277 */ | |
| 278 refresh_: function() { | |
| 279 var endKnob = this.$.endKnob; | |
| 280 var startKnob = this.$.startKnob; | |
| 281 var startProgress = this.$.startProgress; | |
| 282 var endProgress = this.$.endProgress; | |
| 283 var startLabel = this.$.startLabel; | |
| 284 var endLabel = this.$.endLabel; | |
| 285 | |
| 286 // The label bubbles have the same left coordinates as their corresponding | |
| 287 // knobs. | |
| 288 startLabel.style.left = startKnob.style.left; | |
| 289 endLabel.style.left = endKnob.style.left; | |
| 290 | |
| 291 // The end progress bar starts from either the start knob or the start of | |
| 292 // the slider (whichever is to its left) and ends at the end knob. | |
| 293 var endProgressLeft = startKnob.offsetLeft >= endKnob.offsetLeft | |
| 294 ? '0px' : startKnob.style.left; | |
| 295 endProgress.style.left = endProgressLeft; | |
| 296 endProgress.style.width = | |
| 297 (parseFloat(endKnob.style.left) - parseFloat(endProgressLeft)) + 'px'; | |
| 298 | |
| 299 // The start progress bar starts at the start knob, and ends at either the | |
| 300 // end knob or the end of the slider (whichever is to its right). | |
| 301 var startProgressRight = endKnob.offsetLeft < startKnob.offsetLeft | |
| 302 ? this.$.sliderBar.offsetWidth | |
| 303 : endKnob.style.left; | |
| 304 startProgress.style.left = startKnob.style.left; | |
| 305 startProgress.style.width = | |
| 306 (parseFloat(startProgressRight) - parseFloat(startKnob.style.left)) + | |
| 307 'px'; | |
| 308 | |
| 309 this.fixLabelsOverlapIfAny_(); | |
| 310 }, | |
| 311 | |
| 312 /** | |
| 313 * If the label bubbles overlap, this function fixes them by moving the end | |
| 314 * label up a little. | |
| 315 * @private | |
| 316 */ | |
| 317 fixLabelsOverlapIfAny_: function() { | |
| 318 var startLabel = this.$.startLabel; | |
| 319 var endLabel = this.$.endLabel; | |
| 320 var distance = Math.abs(parseFloat(startLabel.style.left) - | |
| 321 parseFloat(endLabel.style.left)); | |
| 322 if (distance <= startLabel.offsetWidth) { | |
| 323 // Shift the end label up so that it doesn't overlap with the start label. | |
| 324 endLabel.classList.add('end-label-overlap'); | |
| 325 } else { | |
| 326 endLabel.classList.remove('end-label-overlap'); | |
| 327 } | |
| 328 }, | |
| 329 | |
| 330 /** | |
| 331 * Given the prefPath that corresponds to one knob time, it gets the value of | |
| 332 * the pref that corresponds to the other knob. | |
| 333 * @param {string} prefPath | |
| 334 * @return {number} | |
| 335 * @private | |
| 336 */ | |
| 337 getOtherKnobPrefValue_: function(prefPath) { | |
| 338 if (prefPath == 'ash.night_light.custom_start_time') | |
| 339 return /** @type {number} */ ( | |
| 340 this.getPref('ash.night_light.custom_end_time').value); | |
| 
stevenjb
2017/06/09 22:51:45
{}
 
afakhry
2017/06/12 18:07:08
Done.
 | |
| 341 | |
| 342 return /** @type {number} */ ( | |
| 343 this.getPref('ash.night_light.custom_start_time').value); | |
| 344 }, | |
| 345 | |
| 346 /** | |
| 347 * Increments the value of the pref whose path is given by prefPath by the | |
| 348 * amount given in increment. | |
| 349 * @param {string} prefPath | |
| 350 * @param {number} increment | |
| 351 * @private | |
| 352 */ | |
| 353 incrementPref_: function(prefPath, increment) { | |
| 354 var value = this.getPref(prefPath).value + increment; | |
| 355 | |
| 356 var otherValue = this.getOtherKnobPrefValue_(prefPath); | |
| 357 if (otherValue > value && | |
| 358 ((otherValue - value) < MIN_KNOBS_DISTANCE_MINUTES)) { | |
| 359 // We are incrementing the minutes offset moving towards the other knob. | |
| 360 // We have a minimum 30 minutes overlap threshold. Move this knob to the | |
| 361 // other side of the other knob. | |
| 362 // | |
| 363 // Was: | |
| 364 // ------ (+) --- 29 MIN --- + ------->> | |
| 365 // | |
| 366 // Now: | |
| 367 // ------ + --- 30 MIN --- (+) ------->> | |
| 368 // | |
| 369 // (+) ==> Knob being moved. | |
| 370 value = otherValue + MIN_KNOBS_DISTANCE_MINUTES; | |
| 371 } | |
| 372 | |
| 373 // The knobs are allowed to wrap around. | |
| 374 this.setPrefValue(prefPath, (value % TOTAL_MINUTES_PER_DAY)); | |
| 375 }, | |
| 376 | |
| 377 /** | |
| 378 * Decrements the value of the pref whose path is given by prefPath by the | |
| 379 * amount given in decrement. | |
| 380 * @param {string} prefPath | |
| 381 * @param {number} decrement | |
| 382 * @private | |
| 383 */ | |
| 384 decrementPref_: function(prefPath, decrement) { | |
| 385 var value = /** @type {number} */(this.getPref(prefPath).value) - decrement; | |
| 386 | |
| 387 var otherValue = this.getOtherKnobPrefValue_(prefPath); | |
| 388 if (value > otherValue && | |
| 389 ((value - otherValue) < MIN_KNOBS_DISTANCE_MINUTES)) { | |
| 390 // We are decrementing the minutes offset moving towards the other knob. | |
| 391 // We have a minimum 30 minutes overlap threshold. Move this knob to the | |
| 392 // other side of the other knob. | |
| 393 // | |
| 394 // Was: | |
| 395 // <<------ + --- 29 MIN --- (+) ------- | |
| 396 // | |
| 397 // Now: | |
| 398 // <<------ (+) --- 30 MIN --- + ------ | |
| 399 // | |
| 400 // (+) ==> Knob being moved. | |
| 401 value = otherValue - MIN_KNOBS_DISTANCE_MINUTES; | |
| 402 } | |
| 403 | |
| 404 // The knobs are allowed to wrap around. | |
| 405 if (value < 0) | |
| 406 value += TOTAL_MINUTES_PER_DAY; | |
| 407 | |
| 408 this.setPrefValue(prefPath, Math.abs(value) % TOTAL_MINUTES_PER_DAY); | |
| 409 }, | |
| 410 | |
| 411 /** | |
| 412 * Gets the pref path of the currently focused knob. Returns null if no knob | |
| 413 * is currently focused. | |
| 414 * @return {?string} | |
| 415 * @private | |
| 416 */ | |
| 417 getFocusedKnobPrefPathIfAny_: function() { | |
| 418 var focusedElement = this.shadowRoot.activeElement; | |
| 419 if (focusedElement == this.$.startKnob) | |
| 420 return 'ash.night_light.custom_start_time'; | |
| 421 | |
| 422 if (focusedElement == this.$.endKnob) | |
| 423 return 'ash.night_light.custom_end_time'; | |
| 424 | |
| 425 return null; | |
| 426 }, | |
| 427 | |
| 428 /** | |
| 429 * Handles the 'left' key event. | |
| 430 * @private | |
| 431 */ | |
| 432 onLeftKey_: function(e) { | |
| 433 e.preventDefault(); | |
| 434 var knobPref = this.getFocusedKnobPrefPathIfAny_(); | |
| 435 if (!knobPref) | |
| 436 return; | |
| 437 | |
| 438 this.decrementPref_(knobPref, 1); | |
| 439 }, | |
| 440 | |
| 441 /** | |
| 442 * Handles the 'right' key event. | |
| 443 * @private | |
| 444 */ | |
| 445 onRightKey_: function(e) { | |
| 446 e.preventDefault(); | |
| 447 var knobPref = this.getFocusedKnobPrefPathIfAny_(); | |
| 448 if (!knobPref) | |
| 449 return; | |
| 450 | |
| 451 this.incrementPref_(knobPref, 1); | |
| 452 }, | |
| 453 }); | |
| OLD | NEW |