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