| 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; |
| 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 } |
| 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) { |
| 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'; |
| 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) { |
| 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 |
| 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'; |
| 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); |
| 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 |