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. 4 5 cr.define('options', function() { 6 7 var Preferences = options.Preferences; 8 9 /** 10 * Allows an element to be disabled for several reasons. 11 * The element is disabled if at least one reason is true, and the reasons 12 * can be set separately. 13 * @private 14 * @param {!HTMLElement} el The element to update. 15 * @param {string} reason The reason for disabling the element. 16 * @param {boolean} disabled Whether the element should be disabled or enabled 17 * for the given |reason|. 18 */ 19 function updateDisabledState_(el, reason, disabled) { 20 if (!el.disabledReasons) 21 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; 26 } 27 if (!el.disabled) { 28 // If the element is not disabled, there should be no reason, except for 29 // 'other'. 30 delete el.disabledReasons.other; 31 if (Object.keys(el.disabledReasons).length > 0) 32 console.error('Element is not disabled but should be'); 33 } 34 if (disabled) { 35 el.disabledReasons[reason] = true; 36 } else { 37 delete el.disabledReasons[reason]; 38 } 39 el.disabled = Object.keys(el.disabledReasons).length > 0; 40 } 41 42 ///////////////////////////////////////////////////////////////////////////// 43 // PrefInputElement class: 44 45 // Define a constructor that uses an input element as its underlying element. 46 var PrefInputElement = cr.ui.define('input'); 47 48 PrefInputElement.prototype = { 49 // Set up the prototype chain 50 __proto__: HTMLInputElement.prototype, 51 52 /** 53 * Initialization function for the cr.ui framework. 54 */ 55 decorate: function() { 56 var self = this; 57 58 // Listen for user events. 59 this.addEventListener('change', this.handleChange_.bind(this)); 60 61 // Listen for pref changes. 62 Preferences.getInstance().addEventListener(this.pref, function(event) { 63 if (event.value.uncommitted && !self.dialogPref) 64 return; 65 self.updateStateFromPref_(event); 66 updateDisabledState_(self, 'notUserModifiable', event.value.disabled); 67 self.controlledBy = event.value.controlledBy; 68 }); 69 }, 70 71 /** 72 * Handle changes to the input element's state made by the user. If a custom 73 * change handler does not suppress it, a default handler is invoked that 74 * updates the associated pref. 75 * @param {Event} event Change event. 76 * @private 77 */ 78 handleChange_: function(event) { 79 if (!this.customChangeHandler(event)) 80 this.updatePrefFromState_(); 81 }, 82 83 /** 84 * Update the input element's state when the associated pref changes. 85 * @param {Event} event Pref change event. 86 * @private 87 */ 88 updateStateFromPref_: function(event) { 89 this.value = event.value.value; 90 }, 91 92 /** 93 * See |updateDisabledState_| above. 94 */ 95 setDisabled: function(reason, disabled) { 96 updateDisabledState_(this, reason, disabled); 97 }, 98 99 /** 100 * Custom change handler that is invoked first when the user makes changes 101 * to the input element's state. If it returns false, a default handler is 102 * invoked next that updates the associated pref. If it returns true, the 103 * default handler is suppressed (i.e., this works like stopPropagation or 104 * cancelBubble). 105 * @param {Event} event Input element change event. 106 */ 107 customChangeHandler: function(event) { 108 return false; 109 }, 110 }; 111 112 /** 113 * The name of the associated preference. 114 * @type {string} 115 */ 116 cr.defineProperty(PrefInputElement, 'pref', cr.PropertyKind.ATTR); 117 118 /** 119 * The data type of the associated preference, only relevant for derived 120 * classes that support different data types. 121 * @type {string} 122 */ 123 cr.defineProperty(PrefInputElement, 'dataType', cr.PropertyKind.ATTR); 124 125 /** 126 * Whether this input element is part of a dialog. If so, changes take effect 127 * in the settings UI immediately but are only actually committed when the 128 * user confirms the dialog. If the user cancels the dialog instead, the 129 * changes are rolled back in the settings UI and never committed. 130 * @type {boolean} 131 */ 132 cr.defineProperty(PrefInputElement, 'dialogPref', cr.PropertyKind.BOOL_ATTR); 133 134 /** 135 * Whether the associated preference is controlled by a source other than the 136 * user's setting (can be 'policy', 'extension', 'recommended' or unset). 137 * @type {string} 138 */ 139 cr.defineProperty(PrefInputElement, 'controlledBy', cr.PropertyKind.ATTR); 140 141 /** 142 * The user metric string. 143 * @type {string} 144 */ 145 cr.defineProperty(PrefInputElement, 'metric', cr.PropertyKind.ATTR); 146 147 ///////////////////////////////////////////////////////////////////////////// 148 // PrefCheckbox class: 149 150 // Define a constructor that uses an input element as its underlying element. 151 var PrefCheckbox = cr.ui.define('input'); 152 153 PrefCheckbox.prototype = { 154 // Set up the prototype chain 155 __proto__: PrefInputElement.prototype, 156 157 /** 158 * Initialization function for the cr.ui framework. 159 */ 160 decorate: function() { 161 PrefInputElement.prototype.decorate.call(this); 162 this.type = 'checkbox'; 163 164 // Consider a checked dialog checkbox as a 'suggestion' which is committed 165 // once the user confirms the dialog. 166 if (this.dialogPref && this.checked) 167 this.updatePrefFromState_(); 168 }, 169 170 /** 171 * Update the associated pref when when the user makes changes to the 172 * checkbox state. 173 * @private 174 */ 175 updatePrefFromState_: function() { 176 var value = this.inverted_pref ? !this.checked : this.checked; 177 Preferences.setBooleanPref(this.pref, value, 178 !this.dialogPref, this.metric); 179 }, 180 181 /** 182 * Update the checkbox state when the associated pref changes. 183 * @param {Event} event Pref change event. 184 * @private 185 */ 186 updateStateFromPref_: function(event) { 187 var value = Boolean(event.value.value); 188 this.checked = this.inverted_pref ? !value : value; 189 }, 190 }; 191 192 /** 193 * Whether the mapping between checkbox state and associated pref is inverted. 194 * @type {boolean} 195 */ 196 cr.defineProperty(PrefCheckbox, 'inverted_pref', cr.PropertyKind.BOOL_ATTR); 197 198 ///////////////////////////////////////////////////////////////////////////// 199 // PrefNumber class: 200 201 // Define a constructor that uses an input element as its underlying element. 202 var PrefNumber = cr.ui.define('input'); 203 204 PrefNumber.prototype = { 205 // Set up the prototype chain 206 __proto__: PrefInputElement.prototype, 207 208 /** 209 * Initialization function for the cr.ui framework. 210 */ 211 decorate: function() { 212 PrefInputElement.prototype.decorate.call(this); 213 this.type = 'number'; 214 }, 215 216 /** 217 * Update the associated pref when when the user inputs a number. 218 * @private 219 */ 220 updatePrefFromState_: function() { 221 if (this.validity.valid) { 222 Preferences.setIntegerPref(this.pref, this.value, 223 !this.dialogPref, this.metric); 224 } 225 }, 226 }; 227 228 ///////////////////////////////////////////////////////////////////////////// 229 // PrefRadio class: 230 231 //Define a constructor that uses an input element as its underlying element. 232 var PrefRadio = cr.ui.define('input'); 233 234 PrefRadio.prototype = { 235 // Set up the prototype chain 236 __proto__: PrefInputElement.prototype, 237 238 /** 239 * Initialization function for the cr.ui framework. 240 */ 241 decorate: function() { 242 PrefInputElement.prototype.decorate.call(this); 243 this.type = 'radio'; 244 }, 245 246 /** 247 * Update the associated pref when when the user selects the radio button. 248 * @private 249 */ 250 updatePrefFromState_: function() { 251 if (this.value == 'true' || this.value == 'false') { 252 Preferences.setBooleanPref(this.pref, 253 this.value == String(this.checked), 254 !this.dialogPref, this.metric); 255 } else { 256 Preferences.setIntegerPref(this.pref, this.value, 257 !this.dialogPref, this.metric); 258 } 259 }, 260 261 /** 262 * Update the radio button state when the associated pref changes. 263 * @param {Event} event Pref change event. 264 * @private 265 */ 266 updateStateFromPref_: function(event) { 267 this.checked = this.value == String(event.value.value); 268 }, 269 }; 270 271 ///////////////////////////////////////////////////////////////////////////// 272 // PrefRange class: 273 274 // Define a constructor that uses an input element as its underlying element. 275 var PrefRange = cr.ui.define('input'); 276 277 PrefRange.prototype = { 278 // Set up the prototype chain 279 __proto__: PrefInputElement.prototype, 280 281 /** 282 * The map from slider position to corresponding pref value. 283 */ 284 valueMap: undefined, 285 286 /** 287 * Initialization function for the cr.ui framework. 288 */ 289 decorate: function() { 290 PrefInputElement.prototype.decorate.call(this); 291 this.type = 'range'; 292 293 // Listen for user events. 294 // TODO(jhawkins): Add onmousewheel handling once the associated WK bug is 295 // fixed. 296 // https://bugs.webkit.org/show_bug.cgi?id=52256 297 this.addEventListener('keyup', this.handleRelease_.bind(this)); 298 this.addEventListener('mouseup', this.handleRelease_.bind(this)); 299 }, 300 301 /** 302 * Update the associated pref when when the user releases the slider. 303 * @private 304 */ 305 updatePrefFromState_: function() { 306 Preferences.setIntegerPref(this.pref, this.mapPositionToPref(this.value), 307 !this.dialogPref, this.metric); 308 }, 309 310 /** 311 * Ignore changes to the slider position made by the user while the slider 312 * has not been released. 313 * @private 314 */ 315 handleChange_: function() { 316 }, 317 318 /** 319 * Handle changes to the slider position made by the user when the slider is 320 * released. If a custom change handler does not suppress it, a default 321 * handler is invoked that updates the associated pref. 322 * @param {Event} event Change event. 323 * @private 324 */ 325 handleRelease_: function(event) { 326 if (!this.customChangeHandler(event)) 327 this.updatePrefFromState_(); 328 }, 329 330 /** 331 * Update the slider position when the associated pref changes. 332 * @param {Event} event Pref change event. 333 * @private 334 */ 335 updateStateFromPref_: function(event) { 336 var value = event.value.value; 337 this.value = this.valueMap ? this.valueMap.indexOf(value) : value; 338 }, 339 340 /** 341 * Map slider position to the range of values provided by the client, 342 * represented by |valueMap|. 343 * @param {number} position The slider position to map. 344 */ 345 mapPositionToPref: function(position) { 346 return this.valueMap ? this.valueMap[position] : position; 347 }, 348 }; 349 350 ///////////////////////////////////////////////////////////////////////////// 351 // PrefSelect class: 352 353 // Define a constructor that uses a select element as its underlying element. 354 var PrefSelect = cr.ui.define('select'); 355 356 PrefSelect.prototype = { 357 // Set up the prototype chain 358 __proto__: PrefInputElement.prototype, 359 360 /** 361 * Update the associated pref when when the user selects an item. 362 * @private 363 */ 364 updatePrefFromState_: function() { 365 var value = this.options[this.selectedIndex].value; 366 switch (this.dataType) { 367 case 'number': 368 Preferences.setIntegerPref(this.pref, value, 369 !this.dialogPref, this.metric); 370 break; 371 case 'double': 372 Preferences.setDoublePref(this.pref, value, 373 !this.dialogPref, this.metric); 374 break; 375 case 'boolean': 376 Preferences.setBooleanPref(this.pref, value == 'true', 377 !this.dialogPref, this.metric); 378 break; 379 case 'string': 380 Preferences.setStringPref(this.pref, value, 381 !this.dialogPref, this.metric); 382 break; 383 default: 384 console.error('Unknown data type for <select> UI element: ' + 385 this.dataType); 386 } 387 }, 388 389 /** 390 * Update the selected item when the associated pref changes. 391 * @param {Event} event Pref change event. 392 * @private 393 */ 394 updateStateFromPref_: function(event) { 395 // Make sure the value is a string, because the value is stored as a 396 // string in the HTMLOptionElement. 397 value = String(event.value.value); 398 399 var found = false; 400 for (var i = 0; i < this.options.length; i++) { 401 if (this.options[i].value == value) { 402 this.selectedIndex = i; 403 found = true; 404 } 405 } 406 407 // Item not found, select first item. 408 if (!found) 409 this.selectedIndex = 0; 410 411 // The "onchange" event automatically fires when the user makes a manual 412 // change. It should never be fired for a programmatic change. However, 413 // these two lines were here already and it is hard to tell who may be 414 // relying on them. 415 if (this.onchange) 416 this.onchange(event); 417 }, 418 }; 419 420 ///////////////////////////////////////////////////////////////////////////// 421 // PrefTextField class: 422 423 // Define a constructor that uses an input element as its underlying element. 424 var PrefTextField = cr.ui.define('input'); 425 426 PrefTextField.prototype = { 427 // Set up the prototype chain 428 __proto__: PrefInputElement.prototype, 429 430 /** 431 * Initialization function for the cr.ui framework. 432 */ 433 decorate: function() { 434 PrefInputElement.prototype.decorate.call(this); 435 var self = this; 436 437 // Listen for user events. 438 window.addEventListener('unload', function() { 439 if (document.activeElement == self) 440 self.blur(); 441 }); 442 }, 443 444 /** 445 * Update the associated pref when when the user inputs text. 446 * @private 447 */ 448 updatePrefFromState_: function(event) { 449 switch (this.dataType) { 450 case 'number': 451 Preferences.setIntegerPref(this.pref, this.value, 452 !this.dialogPref, this.metric); 453 break; 454 case 'double': 455 Preferences.setDoublePref(this.pref, this.value, 456 !this.dialogPref, this.metric); 457 break; 458 case 'url': 459 Preferences.setURLPref(this.pref, this.value, 460 !this.dialogPref, this.metric); 461 break; 462 default: 463 Preferences.setStringPref(this.pref, this.value, 464 !this.dialogPref, this.metric); 465 break; 466 } 467 }, 468 }; 469 470 ///////////////////////////////////////////////////////////////////////////// 471 // PrefPortNumber class: 472 473 // Define a constructor that uses an input element as its underlying element. 474 var PrefPortNumber = cr.ui.define('input'); 475 476 PrefPortNumber.prototype = { 477 // Set up the prototype chain 478 __proto__: PrefTextField.prototype, 479 480 /** 481 * Initialization function for the cr.ui framework. 482 */ 483 decorate: function() { 484 var self = this; 485 self.type = 'text'; 486 self.dataType = 'number'; 487 PrefTextField.prototype.decorate.call(this); 488 self.oninput = function() { 489 // Note that using <input type="number"> is insufficient to restrict 490 // the input as it allows negative numbers and does not limit the 491 // number of charactes typed even if a range is set. Furthermore, 492 // it sometimes produces strange repaint artifacts. 493 var filtered = self.value.replace(/[^0-9]/g, ''); 494 if (filtered != self.value) 495 self.value = filtered; 496 }; 497 } 498 }; 499 500 ///////////////////////////////////////////////////////////////////////////// 501 // PrefButton class: 502 503 // Define a constructor that uses a button element as its underlying element. 504 var PrefButton = cr.ui.define('button'); 505 506 PrefButton.prototype = { 507 // Set up the prototype chain 508 __proto__: HTMLButtonElement.prototype, 509 510 /** 511 * Initialization function for the cr.ui framework. 512 */ 513 decorate: function() { 514 var self = this; 515 516 // Listen for pref changes. 517 // This element behaves like a normal button and does not affect the 518 // underlying preference; it just becomes disabled when the preference is 519 // managed, and its value is false. This is useful for buttons that should 520 // be disabled when the underlying Boolean preference is set to false by a 521 // policy or extension. 522 Preferences.getInstance().addEventListener(this.pref, function(event) { 523 updateDisabledState_(self, 'notUserModifiable', 524 event.value.disabled && !event.value.value); 525 self.controlledBy = event.value.controlledBy; 526 }); 527 }, 528 529 /** 530 * See |updateDisabledState_| above. 531 */ 532 setDisabled: function(reason, disabled) { 533 updateDisabledState_(this, reason, disabled); 534 }, 535 }; 536 537 /** 538 * The name of the associated preference. 539 * @type {string} 540 */ 541 cr.defineProperty(PrefButton, 'pref', cr.PropertyKind.ATTR); 542 543 /** 544 * Whether the associated preference is controlled by a source other than the 545 * user's setting (can be 'policy', 'extension', 'recommended' or unset). 546 * @type {string} 547 */ 548 cr.defineProperty(PrefButton, 'controlledBy', cr.PropertyKind.ATTR); 549 550 // Export 551 return { 552 PrefCheckbox: PrefCheckbox, 553 PrefNumber: PrefNumber, 554 PrefRadio: PrefRadio, 555 PrefRange: PrefRange, 556 PrefSelect: PrefSelect, 557 PrefTextField: PrefTextField, 558 PrefPortNumber: PrefPortNumber, 559 PrefButton: PrefButton 560 }; 561 562 }); 563