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