1 // Copyright (c) 2012 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.
5 cr.define('options', function() {
6 var Preferences = options.Preferences;
9 * Allows an element to be disabled for several reasons.
10 * The element is disabled if at least one reason is true, and the reasons
11 * can be set separately.
13 * @param {!HTMLElement} el The element to update.
14 * @param {string} reason The reason for disabling the element.
15 * @param {boolean} disabled Whether the element should be disabled or enabled
16 * for the given |reason|.
18 function updateDisabledState(el, reason, disabled) {
19 if (!el.disabledReasons)
20 el.disabledReasons = {};
22 if (el.disabled && (Object.keys(el.disabledReasons).length == 0)) {
23 // The element has been previously disabled without a reason, so we add
24 // one to keep it disabled.
25 el.disabledReasons.other = true;
29 // If the element is not disabled, there should be no reason, except for
31 delete el.disabledReasons.other;
32 if (Object.keys(el.disabledReasons).length > 0)
33 console.error('Element is not disabled but should be');
37 el.disabledReasons[reason] = true;
39 delete el.disabledReasons[reason];
41 el.disabled = Object.keys(el.disabledReasons).length > 0;
44 /////////////////////////////////////////////////////////////////////////////
45 // PrefInputElement class:
48 * Define a constructor that uses an input element as its underlying element.
50 * @extends {HTMLInputElement}
52 var PrefInputElement = cr.ui.define('input');
54 PrefInputElement.prototype = {
55 // Set up the prototype chain
56 __proto__: HTMLInputElement.prototype,
59 * Initialization function for the cr.ui framework.
61 decorate: function() {
64 // Listen for user events.
65 this.addEventListener('change', this.handleChange.bind(this));
67 // Listen for pref changes.
68 Preferences.getInstance().addEventListener(this.pref, function(event) {
69 if (event.value.uncommitted && !self.dialogPref)
71 self.updateStateFromPref(event);
72 updateDisabledState(self, 'notUserModifiable', event.value.disabled);
73 self.controlledBy = event.value.controlledBy;
78 * Handle changes to the input element's state made by the user. If a custom
79 * change handler does not suppress it, a default handler is invoked that
80 * updates the associated pref.
81 * @param {Event} event Change event.
84 handleChange: function(event) {
85 if (!this.customChangeHandler(event))
86 this.updatePrefFromState();
90 * Handles changes to the pref. If a custom change handler does not suppress
91 * it, a default handler is invoked that updates the input element's state.
92 * @param {Event} event Pref change event.
95 updateStateFromPref: function(event) {
96 if (!this.customPrefChangeHandler(event))
97 this.value = event.value.value;
101 * See |updateDisabledState| above.
103 setDisabled: function(reason, disabled) {
104 updateDisabledState(this, reason, disabled);
108 * Custom change handler that is invoked first when the user makes changes
109 * to the input element's state. If it returns false, a default handler is
110 * invoked next that updates the associated pref. If it returns true, the
111 * default handler is suppressed (i.e., this works like stopPropagation or
113 * @param {Event} event Input element change event.
115 customChangeHandler: function(event) {
120 * Custom change handler that is invoked first when the preference
121 * associated with the input element changes. If it returns false, a default
122 * handler is invoked next that updates the input element. If it returns
123 * true, the default handler is suppressed.
124 * @param {Event} event Input element change event.
126 customPrefChangeHandler: function(event) {
132 * The name of the associated preference.
134 cr.defineProperty(PrefInputElement, 'pref', cr.PropertyKind.ATTR);
137 * The data type of the associated preference, only relevant for derived
138 * classes that support different data types.
140 cr.defineProperty(PrefInputElement, 'dataType', cr.PropertyKind.ATTR);
143 * Whether this input element is part of a dialog. If so, changes take effect
144 * in the settings UI immediately but are only actually committed when the
145 * user confirms the dialog. If the user cancels the dialog instead, the
146 * changes are rolled back in the settings UI and never committed.
148 cr.defineProperty(PrefInputElement, 'dialogPref', cr.PropertyKind.BOOL_ATTR);
151 * Whether the associated preference is controlled by a source other than the
152 * user's setting (can be 'policy', 'extension', 'recommended' or unset).
154 cr.defineProperty(PrefInputElement, 'controlledBy', cr.PropertyKind.ATTR);
157 * The user metric string.
159 cr.defineProperty(PrefInputElement, 'metric', cr.PropertyKind.ATTR);
161 /////////////////////////////////////////////////////////////////////////////
162 // PrefCheckbox class:
165 * Define a constructor that uses an input element as its underlying element.
167 * @extends {options.PrefInputElement}
169 var PrefCheckbox = cr.ui.define('input');
171 PrefCheckbox.prototype = {
172 // Set up the prototype chain
173 __proto__: PrefInputElement.prototype,
176 * Initialization function for the cr.ui framework.
178 decorate: function() {
179 PrefInputElement.prototype.decorate.call(this);
180 this.type = 'checkbox';
182 // Consider a checked dialog checkbox as a 'suggestion' which is committed
183 // once the user confirms the dialog.
184 if (this.dialogPref && this.checked)
185 this.updatePrefFromState();
189 * Update the associated pref when when the user makes changes to the
193 updatePrefFromState: function() {
194 var value = this.inverted_pref ? !this.checked : this.checked;
195 Preferences.setBooleanPref(this.pref, value,
196 !this.dialogPref, this.metric);
200 updateStateFromPref: function(event) {
201 if (!this.customPrefChangeHandler(event))
202 this.defaultPrefChangeHandler(event);
206 * @param {Event} event A pref change event.
208 defaultPrefChangeHandler: function(event) {
209 var value = Boolean(event.value.value);
210 this.checked = this.inverted_pref ? !value : value;
215 * Whether the mapping between checkbox state and associated pref is inverted.
217 cr.defineProperty(PrefCheckbox, 'inverted_pref', cr.PropertyKind.BOOL_ATTR);
219 /////////////////////////////////////////////////////////////////////////////
222 // Define a constructor that uses an input element as its underlying element.
223 var PrefNumber = cr.ui.define('input');
225 PrefNumber.prototype = {
226 // Set up the prototype chain
227 __proto__: PrefInputElement.prototype,
230 * Initialization function for the cr.ui framework.
232 decorate: function() {
233 PrefInputElement.prototype.decorate.call(this);
234 this.type = 'number';
238 * Update the associated pref when the user inputs a number.
241 updatePrefFromState: function() {
242 if (this.validity.valid) {
243 Preferences.setIntegerPref(this.pref, this.value,
244 !this.dialogPref, this.metric);
249 /////////////////////////////////////////////////////////////////////////////
252 //Define a constructor that uses an input element as its underlying element.
253 var PrefRadio = cr.ui.define('input');
255 PrefRadio.prototype = {
256 // Set up the prototype chain
257 __proto__: PrefInputElement.prototype,
260 * Initialization function for the cr.ui framework.
262 decorate: function() {
263 PrefInputElement.prototype.decorate.call(this);
268 * Update the associated pref when when the user selects the radio button.
271 updatePrefFromState: function() {
272 if (this.value == 'true' || this.value == 'false') {
273 Preferences.setBooleanPref(this.pref,
274 this.value == String(this.checked),
275 !this.dialogPref, this.metric);
277 Preferences.setIntegerPref(this.pref, this.value,
278 !this.dialogPref, this.metric);
283 updateStateFromPref: function(event) {
284 if (!this.customPrefChangeHandler(event))
285 this.checked = this.value == String(event.value.value);
289 /////////////////////////////////////////////////////////////////////////////
293 * Define a constructor that uses an input element as its underlying element.
295 * @extends {options.PrefInputElement}
297 var PrefRange = cr.ui.define('input');
299 PrefRange.prototype = {
300 // Set up the prototype chain
301 __proto__: PrefInputElement.prototype,
304 * The map from slider position to corresponding pref value.
309 * Initialization function for the cr.ui framework.
311 decorate: function() {
312 PrefInputElement.prototype.decorate.call(this);
315 // Listen for user events.
316 // TODO(jhawkins): Add onmousewheel handling once the associated WK bug is
318 // https://bugs.webkit.org/show_bug.cgi?id=52256
319 this.addEventListener('keyup', this.handleRelease_.bind(this));
320 this.addEventListener('mouseup', this.handleRelease_.bind(this));
321 this.addEventListener('touchcancel', this.handleRelease_.bind(this));
322 this.addEventListener('touchend', this.handleRelease_.bind(this));
326 * Update the associated pref when when the user releases the slider.
329 updatePrefFromState: function() {
330 Preferences.setIntegerPref(
332 this.mapPositionToPref(parseInt(this.value, 10)),
338 handleChange: function() {
339 // Ignore changes to the slider position made by the user while the slider
340 // has not been released.
344 * Handle changes to the slider position made by the user when the slider is
345 * released. If a custom change handler does not suppress it, a default
346 * handler is invoked that updates the associated pref.
347 * @param {Event} event Change event.
350 handleRelease_: function(event) {
351 if (!this.customChangeHandler(event))
352 this.updatePrefFromState();
356 * Handles changes to the pref associated with the slider. If a custom
357 * change handler does not suppress it, a default handler is invoked that
358 * updates the slider position.
361 updateStateFromPref: function(event) {
362 if (this.customPrefChangeHandler(event))
364 var value = event.value.value;
365 this.value = this.valueMap ? this.valueMap.indexOf(value) : value;
369 * Map slider position to the range of values provided by the client,
370 * represented by |valueMap|.
371 * @param {number} position The slider position to map.
373 mapPositionToPref: function(position) {
374 return this.valueMap ? this.valueMap[position] : position;
378 /////////////////////////////////////////////////////////////////////////////
381 // Define a constructor that uses a select element as its underlying element.
382 var PrefSelect = cr.ui.define('select');
384 PrefSelect.prototype = {
385 // Set up the prototype chain
386 __proto__: PrefInputElement.prototype,
389 * Update the associated pref when when the user selects an item.
392 updatePrefFromState: function() {
393 var value = this.options[this.selectedIndex].value;
394 switch (this.dataType) {
396 Preferences.setIntegerPref(this.pref, value,
397 !this.dialogPref, this.metric);
400 Preferences.setDoublePref(this.pref, value,
401 !this.dialogPref, this.metric);
404 Preferences.setBooleanPref(this.pref, value == 'true',
405 !this.dialogPref, this.metric);
408 Preferences.setStringPref(this.pref, value,
409 !this.dialogPref, this.metric);
412 console.error('Unknown data type for <select> UI element: ' +
418 updateStateFromPref: function(event) {
419 if (this.customPrefChangeHandler(event))
422 // Make sure the value is a string, because the value is stored as a
423 // string in the HTMLOptionElement.
424 var value = String(event.value.value);
427 for (var i = 0; i < this.options.length; i++) {
428 if (this.options[i].value == value) {
429 this.selectedIndex = i;
434 // Item not found, select first item.
436 this.selectedIndex = 0;
438 // The "onchange" event automatically fires when the user makes a manual
439 // change. It should never be fired for a programmatic change. However,
440 // these two lines were here already and it is hard to tell who may be
443 this.onchange(event);
447 /////////////////////////////////////////////////////////////////////////////
448 // PrefTextField class:
450 // Define a constructor that uses an input element as its underlying element.
451 var PrefTextField = cr.ui.define('input');
453 PrefTextField.prototype = {
454 // Set up the prototype chain
455 __proto__: PrefInputElement.prototype,
458 * Initialization function for the cr.ui framework.
460 decorate: function() {
461 PrefInputElement.prototype.decorate.call(this);
464 // Listen for user events.
465 window.addEventListener('unload', function() {
466 if (document.activeElement == self)
472 * Update the associated pref when when the user inputs text.
475 updatePrefFromState: function(event) {
476 switch (this.dataType) {
478 Preferences.setIntegerPref(this.pref, this.value,
479 !this.dialogPref, this.metric);
482 Preferences.setDoublePref(this.pref, this.value,
483 !this.dialogPref, this.metric);
486 Preferences.setURLPref(this.pref, this.value,
487 !this.dialogPref, this.metric);
490 Preferences.setStringPref(this.pref, this.value,
491 !this.dialogPref, this.metric);
497 /////////////////////////////////////////////////////////////////////////////
498 // PrefPortNumber class:
500 // Define a constructor that uses an input element as its underlying element.
501 var PrefPortNumber = cr.ui.define('input');
503 PrefPortNumber.prototype = {
504 // Set up the prototype chain
505 __proto__: PrefTextField.prototype,
508 * Initialization function for the cr.ui framework.
510 decorate: function() {
513 self.dataType = 'number';
514 PrefTextField.prototype.decorate.call(this);
515 self.oninput = function() {
516 // Note that using <input type="number"> is insufficient to restrict
517 // the input as it allows negative numbers and does not limit the
518 // number of charactes typed even if a range is set. Furthermore,
519 // it sometimes produces strange repaint artifacts.
520 var filtered = self.value.replace(/[^0-9]/g, '');
521 if (filtered != self.value)
522 self.value = filtered;
527 /////////////////////////////////////////////////////////////////////////////
530 // Define a constructor that uses a button element as its underlying element.
531 var PrefButton = cr.ui.define('button');
533 PrefButton.prototype = {
534 // Set up the prototype chain
535 __proto__: HTMLButtonElement.prototype,
538 * Initialization function for the cr.ui framework.
540 decorate: function() {
543 // Listen for pref changes.
544 // This element behaves like a normal button and does not affect the
545 // underlying preference; it just becomes disabled when the preference is
546 // managed, and its value is false. This is useful for buttons that should
547 // be disabled when the underlying Boolean preference is set to false by a
548 // policy or extension.
549 Preferences.getInstance().addEventListener(this.pref, function(event) {
550 updateDisabledState(self, 'notUserModifiable',
551 event.value.disabled && !event.value.value);
552 self.controlledBy = event.value.controlledBy;
557 * See |updateDisabledState| above.
559 setDisabled: function(reason, disabled) {
560 updateDisabledState(this, reason, disabled);
565 * The name of the associated preference.
567 cr.defineProperty(PrefButton, 'pref', cr.PropertyKind.ATTR);
570 * Whether the associated preference is controlled by a source other than the
571 * user's setting (can be 'policy', 'extension', 'recommended' or unset).
573 cr.defineProperty(PrefButton, 'controlledBy', cr.PropertyKind.ATTR);
577 PrefCheckbox: PrefCheckbox,
578 PrefInputElement: PrefInputElement,
579 PrefNumber: PrefNumber,
580 PrefRadio: PrefRadio,
581 PrefRange: PrefRange,
582 PrefSelect: PrefSelect,
583 PrefTextField: PrefTextField,
584 PrefPortNumber: PrefPortNumber,
585 PrefButton: PrefButton