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 165 /** 166 * Update the associated pref when when the user makes changes to the 167 * checkbox state. 168 * @private 169 */ 170 updatePrefFromState_: function() { 171 var value = this.inverted_pref ? !this.checked : this.checked; 172 Preferences.setBooleanPref(this.pref, value, 173 !this.dialogPref, this.metric); 174 }, 175 176 /** 177 * Update the checkbox state when the associated pref changes. 178 * @param {Event} event Pref change event. 179 * @private 180 */ 181 updateStateFromPref_: function(event) { 182 var value = Boolean(event.value.value); 183 this.checked = this.inverted_pref ? !value : value; 184 }, 185 }; 186 187 /** 188 * Whether the mapping between checkbox state and associated pref is inverted. 189 * @type {boolean} 190 */ 191 cr.defineProperty(PrefCheckbox, 'inverted_pref', cr.PropertyKind.BOOL_ATTR); 192 193 ///////////////////////////////////////////////////////////////////////////// 194 // PrefNumber class: 195 196 // Define a constructor that uses an input element as its underlying element. 197 var PrefNumber = cr.ui.define('input'); 198 199 PrefNumber.prototype = { 200 // Set up the prototype chain 201 __proto__: PrefInputElement.prototype, 202 203 /** 204 * Initialization function for the cr.ui framework. 205 */ 206 decorate: function() { 207 PrefInputElement.prototype.decorate.call(this); 208 this.type = 'number'; 209 }, 210 211 /** 212 * Update the associated pref when when the user inputs a number. 213 * @private 214 */ 215 updatePrefFromState_: function() { 216 if (this.validity.valid) { 217 Preferences.setIntegerPref(this.pref, this.value, 218 !this.dialogPref, this.metric); 219 } 220 }, 221 }; 222 223 ///////////////////////////////////////////////////////////////////////////// 224 // PrefRadio class: 225 226 //Define a constructor that uses an input element as its underlying element. 227 var PrefRadio = cr.ui.define('input'); 228 229 PrefRadio.prototype = { 230 // Set up the prototype chain 231 __proto__: PrefInputElement.prototype, 232 233 /** 234 * Initialization function for the cr.ui framework. 235 */ 236 decorate: function() { 237 PrefInputElement.prototype.decorate.call(this); 238 this.type = 'radio'; 239 }, 240 241 /** 242 * Update the associated pref when when the user selects the radio button. 243 * @private 244 */ 245 updatePrefFromState_: function() { 246 if (this.value == 'true' || this.value == 'false') { 247 Preferences.setBooleanPref(this.pref, 248 this.value == String(this.checked), 249 !this.dialogPref, this.metric); 250 } else { 251 Preferences.setIntegerPref(this.pref, this.value, 252 !this.dialogPref, this.metric); 253 } 254 }, 255 256 /** 257 * Update the radio button state when the associated pref changes. 258 * @param {Event} event Pref change event. 259 * @private 260 */ 261 updateStateFromPref_: function(event) { 262 this.checked = this.value == String(event.value.value); 263 }, 264 }; 265 266 ///////////////////////////////////////////////////////////////////////////// 267 // PrefRange class: 268 269 // Define a constructor that uses an input element as its underlying element. 270 var PrefRange = cr.ui.define('input'); 271 272 PrefRange.prototype = { 273 // Set up the prototype chain 274 __proto__: PrefInputElement.prototype, 275 276 /** 277 * The map from slider position to corresponding pref value. 278 */ 279 valueMap: undefined, 280 281 /** 282 * Initialization function for the cr.ui framework. 283 */ 284 decorate: function() { 285 PrefInputElement.prototype.decorate.call(this); 286 this.type = 'range'; 287 288 // Listen for user events. 289 // TODO(jhawkins): Add onmousewheel handling once the associated WK bug is 290 // fixed. 291 // https://bugs.webkit.org/show_bug.cgi?id=52256 292 this.addEventListener('keyup', this.handleRelease_.bind(this)); 293 this.addEventListener('mouseup', this.handleRelease_.bind(this)); 294 }, 295 296 /** 297 * Update the associated pref when when the user releases the slider. 298 * @private 299 */ 300 updatePrefFromState_: function() { 301 Preferences.setIntegerPref(this.pref, this.mapPositionToPref(this.value), 302 !this.dialogPref, this.metric); 303 }, 304 305 /** 306 * Ignore changes to the slider position made by the user while the slider 307 * has not been released. 308 * @private 309 */ 310 handleChange_: function() { 311 }, 312 313 /** 314 * Handle changes to the slider position made by the user when the slider is 315 * released. If a custom change handler does not suppress it, a default 316 * handler is invoked that updates the associated pref. 317 * @param {Event} event Change event. 318 * @private 319 */ 320 handleRelease_: function(event) { 321 if (!this.customChangeHandler(event)) 322 this.updatePrefFromState_(); 323 }, 324 325 /** 326 * Update the slider position when the associated pref changes. 327 * @param {Event} event Pref change event. 328 * @private 329 */ 330 updateStateFromPref_: function(event) { 331 var value = event.value.value; 332 this.value = this.valueMap ? this.valueMap.indexOf(value) : value; 333 }, 334 335 /** 336 * Map slider position to the range of values provided by the client, 337 * represented by |valueMap|. 338 * @param {number} position The slider position to map. 339 */ 340 mapPositionToPref: function(position) { 341 return this.valueMap ? this.valueMap[position] : position; 342 }, 343 }; 344 345 ///////////////////////////////////////////////////////////////////////////// 346 // PrefSelect class: 347 348 // Define a constructor that uses a select element as its underlying element. 349 var PrefSelect = cr.ui.define('select'); 350 351 PrefSelect.prototype = { 352 // Set up the prototype chain 353 __proto__: PrefInputElement.prototype, 354 355 /** 356 * Update the associated pref when when the user selects an item. 357 * @private 358 */ 359 updatePrefFromState_: function() { 360 var value = this.options[this.selectedIndex].value; 361 switch (this.dataType) { 362 case 'number': 363 Preferences.setIntegerPref(this.pref, value, 364 !this.dialogPref, this.metric); 365 break; 366 case 'double': 367 Preferences.setDoublePref(this.pref, value, 368 !this.dialogPref, this.metric); 369 break; 370 case 'boolean': 371 Preferences.setBooleanPref(this.pref, value == 'true', 372 !this.dialogPref, this.metric); 373 break; 374 case 'string': 375 Preferences.setStringPref(this.pref, value, 376 !this.dialogPref, this.metric); 377 break; 378 default: 379 console.error('Unknown data type for <select> UI element: ' + 380 this.dataType); 381 } 382 }, 383 384 /** 385 * Update the selected item when the associated pref changes. 386 * @param {Event} event Pref change event. 387 * @private 388 */ 389 updateStateFromPref_: function(event) { 390 // Make sure the value is a string, because the value is stored as a 391 // string in the HTMLOptionElement. 392 value = String(event.value.value); 393 394 var found = false; 395 for (var i = 0; i < this.options.length; i++) { 396 if (this.options[i].value == value) { 397 this.selectedIndex = i; 398 found = true; 399 } 400 } 401 402 // Item not found, select first item. 403 if (!found) 404 this.selectedIndex = 0; 405 406 // The "onchange" event automatically fires when the user makes a manual 407 // change. It should never be fired for a programmatic change. However, 408 // these two lines were here already and it is hard to tell who may be 409 // relying on them. 410 if (this.onchange) 411 this.onchange(event); 412 }, 413 }; 414 415 ///////////////////////////////////////////////////////////////////////////// 416 // PrefTextField class: 417 418 // Define a constructor that uses an input element as its underlying element. 419 var PrefTextField = cr.ui.define('input'); 420 421 PrefTextField.prototype = { 422 // Set up the prototype chain 423 __proto__: PrefInputElement.prototype, 424 425 /** 426 * Initialization function for the cr.ui framework. 427 */ 428 decorate: function() { 429 PrefInputElement.prototype.decorate.call(this); 430 var self = this; 431 432 // Listen for user events. 433 window.addEventListener('unload', function() { 434 if (document.activeElement == self) 435 self.blur(); 436 }); 437 }, 438 439 /** 440 * Update the associated pref when when the user inputs text. 441 * @private 442 */ 443 updatePrefFromState_: function(event) { 444 switch (this.dataType) { 445 case 'number': 446 Preferences.setIntegerPref(this.pref, this.value, 447 !this.dialogPref, this.metric); 448 break; 449 case 'double': 450 Preferences.setDoublePref(this.pref, this.value, 451 !this.dialogPref, this.metric); 452 break; 453 case 'url': 454 Preferences.setURLPref(this.pref, this.value, 455 !this.dialogPref, this.metric); 456 break; 457 default: 458 Preferences.setStringPref(this.pref, this.value, 459 !this.dialogPref, this.metric); 460 break; 461 } 462 }, 463 }; 464 465 ///////////////////////////////////////////////////////////////////////////// 466 // PrefPortNumber class: 467 468 // Define a constructor that uses an input element as its underlying element. 469 var PrefPortNumber = cr.ui.define('input'); 470 471 PrefPortNumber.prototype = { 472 // Set up the prototype chain 473 __proto__: PrefTextField.prototype, 474 475 /** 476 * Initialization function for the cr.ui framework. 477 */ 478 decorate: function() { 479 var self = this; 480 self.type = 'text'; 481 self.dataType = 'number'; 482 PrefTextField.prototype.decorate.call(this); 483 self.oninput = function() { 484 // Note that using <input type="number"> is insufficient to restrict 485 // the input as it allows negative numbers and does not limit the 486 // number of charactes typed even if a range is set. Furthermore, 487 // it sometimes produces strange repaint artifacts. 488 var filtered = self.value.replace(/[^0-9]/g, ''); 489 if (filtered != self.value) 490 self.value = filtered; 491 }; 492 } 493 }; 494 495 ///////////////////////////////////////////////////////////////////////////// 496 // PrefButton class: 497 498 // Define a constructor that uses a button element as its underlying element. 499 var PrefButton = cr.ui.define('button'); 500 501 PrefButton.prototype = { 502 // Set up the prototype chain 503 __proto__: HTMLButtonElement.prototype, 504 505 /** 506 * Initialization function for the cr.ui framework. 507 */ 508 decorate: function() { 509 var self = this; 510 511 // Listen for pref changes. 512 // This element behaves like a normal button and does not affect the 513 // underlying preference; it just becomes disabled when the preference is 514 // managed, and its value is false. This is useful for buttons that should 515 // be disabled when the underlying Boolean preference is set to false by a 516 // policy or extension. 517 Preferences.getInstance().addEventListener(this.pref, function(event) { 518 updateDisabledState_(self, 'notUserModifiable', 519 event.value.disabled && !event.value.value); 520 self.controlledBy = event.value.controlledBy; 521 }); 522 }, 523 524 /** 525 * See |updateDisabledState_| above. 526 */ 527 setDisabled: function(reason, disabled) { 528 updateDisabledState_(this, reason, disabled); 529 }, 530 }; 531 532 /** 533 * The name of the associated preference. 534 * @type {string} 535 */ 536 cr.defineProperty(PrefButton, 'pref', cr.PropertyKind.ATTR); 537 538 /** 539 * Whether the associated preference is controlled by a source other than the 540 * user's setting (can be 'policy', 'extension', 'recommended' or unset). 541 * @type {string} 542 */ 543 cr.defineProperty(PrefButton, 'controlledBy', cr.PropertyKind.ATTR); 544 545 // Export 546 return { 547 PrefCheckbox: PrefCheckbox, 548 PrefNumber: PrefNumber, 549 PrefRadio: PrefRadio, 550 PrefRange: PrefRange, 551 PrefSelect: PrefSelect, 552 PrefTextField: PrefTextField, 553 PrefPortNumber: PrefPortNumber, 554 PrefButton: PrefButton 555 }; 556 557 }); 558