Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(634)

Side by Side Diff: chrome/browser/resources/settings/device_page/night_light_slider.js

Issue 2915753003: [Night Light] CL5: Schedule Settings (Closed)
Patch Set: Steven's comments Created 3 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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 });
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698