1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 3 // Copyright: ©2006-2011 Strobe Inc. and contributors. 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 8 sc_require('panes/panel'); 9 sc_require('views/button'); 10 11 /** 12 Passed to delegate when alert pane is dismissed by pressing button 1 13 14 @static 15 @type String 16 @default 'button1' 17 */ 18 SC.BUTTON1_STATUS = 'button1'; 19 20 /** 21 Passed to delegate when alert pane is dismissed by pressing button 2 22 23 @static 24 @type String 25 @default 'button2' 26 */ 27 SC.BUTTON2_STATUS = 'button2'; 28 29 /** 30 Passed to delegate when alert pane is dismissed by pressing button 3 31 32 @static 33 @type String 34 @default 'button3' 35 */ 36 SC.BUTTON3_STATUS = 'button3'; 37 38 /** @class 39 Displays a preformatted modal alert pane. 40 41 Alert panes are a simple way to provide modal messaging that otherwise 42 blocks the user's interaction with your application. Alert panes are 43 useful for showing important error messages and confirmation dialogs. They 44 provide a substantially better user experience than using the OS-level alert 45 dialogs. 46 47 ## Displaying an Alert Pane 48 49 The easiest way to display an alert pane is to use one of the various 50 class methods defined on `SC.AlertPane`, passing the message and an optional 51 detailed description and caption. 52 53 There are four variations of this method can you can invoke: 54 55 - `warn({})` -- displays an alert pane with a warning icon to the left. 56 - `error()` -- displays an alert with an error icon. 57 - `info()` -- displays an alert with an info icon. 58 - `plain()` -- displays an alert with no icon. 59 - `show()` -- displays an alert with the icon class you specify. 60 61 Each method takes a single argument: a hash of options. These options include: 62 63 - `message` -- The alert's title message. 64 - `description` -- A longer description of the alert, displayed below the title 65 in a smaller font. 66 - `caption` -- A third layer of alert text, displayed below the description in 67 an even-smaller font. 68 - `icon` -- This is set for you automatically unless you call `show`. You may 69 specify any icon class you wish. The icon is displayed at the alert pane's 70 left. 71 - `themeName` -- A button theme that is applied to each button. The default is 72 `capsule`. 73 - `delegate` -- A delegate to be notified when the user reacts to your pane. See 74 "Responding to User Actions" below. 75 - `buttons` -- An array of up to three hashes used to customize the alert's buttons. 76 See "Customizing Buttons" below. 77 78 ## Responding to User Actions 79 80 Often, you may wish to be notified when the user has dismissed to your alert. You 81 have two options: you may specify a delegate in the options hash, or you may 82 customize each button with a target & action. 83 84 If you specify a delegate, it must implement a method with the following signature: 85 `alertPaneDidDismiss(pane, buttonKey)`. When the user dismisses your alert, this 86 method will be called with the pane instance and a key indicating which button was 87 pressed (one of either `SC.BUTTON1_STATUS`, `SC.BUTTON2_STATUS` or `SC.BUTTON3_STATUS`). 88 89 If you specify a target/action for a button (see "Customizing Buttons" below) and the 90 user dismisses the alert with that button, that action will be triggered. If you specify 91 a delegate but no target, the delegate will be used as the target. The action will 92 be called with the alert pane itself as the sender (first argument). 93 94 ## Customizing Buttons 95 96 SC.AlertPane allows you to specify up to three buttons, arranged from right to left (as 97 on Mac OS X). You can customize them by passing an array of up to three options hashes 98 on the `buttons` property. By default, the first, rightmost button is the default (i.e. 99 it is triggered when the user hits the enter key), and the second button is the "cancel" 100 button (triggered by the escape key). 101 102 If you don't specify any buttons, a single default "OK" button will appear. 103 104 You may customize the following button options: 105 106 - `title` -- The button text. Highly recommended unless you like empty buttons. 107 - `localize` -- Whether to localize the title. 108 - `toolTip` -- An extra hint to show when the user hovers the mouse over the button. 109 Make sure that the user can get along fine without this, as tooltips are hard to 110 discover and unavailable on touch devices! 111 - `isDefault` -- You may specify a different button than the first, rightmost button 112 to be the default (triggered by the enter key, and visually distinct in the default 113 Ace theme). 114 - `isCancel` -- You may specify a different button than the second, middle button 115 to be the cancel button (triggered by the escape key). 116 - `target` & `action` -- Supports the target/action pattern (see "Responding to User 117 Actions" above). 118 119 (You may also specify a layerId for the button if needed. As always, using custom 120 layerIds is dangerous and should be avoided unless you know what you're doing.) 121 122 ## Examples 123 124 Show a simple AlertPane with a warning (!) icon and an OK button: 125 126 SC.AlertPane.warn({ 127 message: "Could not load calendar", 128 description: "Your internet connection may be unavailable or our servers may be down.", 129 caption: "Try again in a few minutes." 130 }); 131 132 Show an AlertPane with a customized OK button title (title will be 'Try Again'): 133 134 SC.AlertPane.warn({ 135 message: "Could not load calendar", 136 description: "Your internet connection may be unavailable or our servers may be down.", 137 caption: "Try again in a few minutes.", 138 buttons: [ 139 { title: "Try Again" } 140 ] 141 }); 142 143 Show an AlertPane with fully customized buttons: 144 145 SC.AlertPane.show({ 146 message: "Could not load calendar", 147 description: "Your internet connection may be unavailable or our servers may be down.", 148 caption: "Try again in a few minutes.", 149 buttons: [ 150 { title: "Try Again", toolTip: "Retry the connection", isDefault: true }, 151 { title: "More Info...", toolTip: "Get more info" }, 152 { title: "Cancel", toolTip: "Cancel the action", isCancel: true } 153 ] 154 }); 155 156 Show an alert pane, using the delegate pattern to respond to how the user dismisses it. 157 158 MyApp.calendarController = SC.Object.create({ 159 alertPaneDidDismiss: function(pane, status) { 160 switch(status) { 161 case SC.BUTTON1_STATUS: 162 this.tryAgain(); 163 break; 164 case SC.BUTTON2_STATUS: 165 // do nothing 166 break; 167 case SC.BUTTON3_STATUS: 168 this.showMoreInfo(); 169 break; 170 } 171 }, 172 ... 173 }); 174 175 SC.AlertPane.warn({ 176 message: "Could not load calendar", 177 description: "Your internet connection may be unavailable or our servers may be down.", 178 caption: "Try again in a few minutes.", 179 delegate: MyApp.calendarController, 180 buttons: [ 181 { title: "Try Again" }, 182 { title: "Cancel" }, 183 { title: "More Info…" } 184 ] 185 }); 186 187 Show an alert pane using the target/action pattern on each button to respond to how the user 188 dismisses it. 189 190 SC.AlertPane.warn({ 191 message: "Could not load calendar", 192 description: "Your internet connection may be unavailable or our servers may be down.", 193 caption: "Try again in a few minutes.", 194 buttons: [ 195 { 196 title: "Try Again", 197 action: "doTryAgain", 198 target: MyApp.calendarController 199 }, 200 { 201 title: "Cancel", 202 action: "doCancel", 203 target: MyApp.calendarController 204 }, 205 { 206 title: "More Info…", 207 action: "doGiveMoreInfo", 208 target: MyApp.calendarController 209 } 210 ] 211 }); 212 213 @extends SC.PanelPane 214 @since SproutCore 1.0 215 */ 216 SC.AlertPane = SC.PanelPane.extend( 217 /** @scope SC.AlertPane.prototype */{ 218 219 /** 220 @type Array 221 @default ['sc-alert'] 222 @see SC.View#classNames 223 */ 224 classNames: ['sc-alert'], 225 226 /** 227 The WAI-ARIA role for alert pane. 228 229 @type String 230 @default 'alertdialog' 231 @constant 232 */ 233 ariaRole: 'alertdialog', 234 235 /** 236 If defined, the delegate is notified when the pane is dismissed. If you have 237 set specific button actions, they will be called on the delegate object 238 239 The method to be called on your delegate will be: 240 241 alertPaneDidDismiss: function(pane, status) {} 242 243 The status will be one of `SC.BUTTON1_STATUS`, `SC.BUTTON2_STATUS` or `SC.BUTTON3_STATUS` 244 depending on which button was clicked. 245 246 @type Object 247 @default null 248 */ 249 delegate: null, 250 251 /** 252 The icon URL or class name. If you do not set this, an alert icon will 253 be shown instead. 254 255 @type String 256 @default 'sc-icon-alert-48' 257 */ 258 icon: 'sc-icon-alert-48', 259 260 /** 261 The primary message to display. This message will appear in large bold 262 type at the top of the alert. 263 264 @type String 265 @default "" 266 */ 267 message: "", 268 269 /** 270 The ARIA label for the alert is the message, by default. 271 272 @field {String} 273 */ 274 ariaLabel: function() { 275 return this.get('message'); 276 }.property('message').cacheable(), 277 278 /** 279 An optional detailed description. Use this string to provide further 280 explanation of the condition and, optionally, ways the user can resolve 281 the problem. 282 283 @type String 284 @default "" 285 */ 286 description: "", 287 288 /** 289 An escaped and formatted version of the description property. 290 291 @field 292 @type String 293 @observes description 294 */ 295 displayDescription: function() { 296 var desc = this.get('description'); 297 if (!desc || desc.length === 0) return desc ; 298 299 desc = SC.RenderContext.escapeHTML(desc); // remove HTML 300 return '<p class="description">' + desc.split('\n').join('</p><p class="description">') + '</p>'; 301 }.property('description').cacheable(), 302 303 /** 304 An optional detailed caption. Use this string to provide further 305 fine print explanation of the condition and, optionally, ways the user can resolve 306 the problem. 307 308 @type String 309 @default "" 310 */ 311 caption: "", 312 313 /** 314 An escaped and formatted version of the caption property. 315 316 @field 317 @type String 318 @observes caption 319 */ 320 displayCaption: function() { 321 var caption = this.get('caption'); 322 if (!caption || caption.length === 0) return caption ; 323 324 caption = SC.RenderContext.escapeHTML(caption); // remove HTML 325 return '<p class="caption">' + caption.split('\n').join('</p><p class="caption">') + '</p>'; 326 }.property('caption').cacheable(), 327 328 /** 329 The button view for button 1 (OK). 330 331 @type SC.ButtonView 332 */ 333 button1: SC.outlet('contentView.childViews.1.childViews.1'), 334 335 /** 336 The button view for the button 2 (Cancel). 337 338 @type SC.ButtonView 339 */ 340 button2: SC.outlet('contentView.childViews.1.childViews.0'), 341 342 /** 343 The button view for the button 3 (Extra). 344 345 @type SC.ButtonView 346 */ 347 button3: SC.outlet('contentView.childViews.2.childViews.0'), 348 349 /** 350 The view for the button 3 (Extra) wrapper. 351 352 @type SC.View 353 */ 354 buttonThreeWrapper: SC.outlet('contentView.childViews.2'), 355 356 /** 357 @type Hash 358 @default { top : 0.3, centerX: 0, width: 500 } 359 @see SC.View#layout 360 */ 361 layout: { top : 0.3, centerX: 0, width: 500 }, 362 363 /** @private - internal view that is actually displayed */ 364 contentView: SC.View.extend({ 365 366 useStaticLayout: YES, 367 368 layout: { left: 0, right: 0, top: 0, height: "auto" }, 369 370 childViews: [ 371 SC.View.extend({ 372 classNames: ['info'], 373 useStaticLayout: YES, 374 375 /** @private */ 376 render: function(context, firstTime) { 377 var pane = this.get('pane'); 378 if(pane.get('icon') == 'blank') context.addClass('plain'); 379 context.push('<img src="'+SC.BLANK_IMAGE_URL+'" class="icon '+pane.get('icon')+'" />'); 380 context.begin('h1').addClass('header').text(pane.get('message') || '').end(); 381 context.push(pane.get('displayDescription') || ''); 382 context.push(pane.get('displayCaption') || ''); 383 context.push('<div class="separator"></div>'); 384 385 } 386 }), 387 388 SC.View.extend({ 389 layout: { bottom: 13, height: 24, right: 18, width: 466 }, 390 childViews: ['cancelButton', 'okButton'], 391 classNames: ['text-align-right'], 392 393 cancelButton: SC.ButtonView.extend({ 394 useStaticLayout: YES, 395 actionKey: SC.BUTTON2_STATUS, 396 localize: YES, 397 layout: { right: 5, height: 'auto', width: 'auto', bottom: 0 }, 398 isCancel: YES, 399 action: "dismiss", 400 isVisible: NO 401 }), 402 403 okButton: SC.ButtonView.extend({ 404 useStaticLayout: YES, 405 actionKey: SC.BUTTON1_STATUS, 406 localize: YES, 407 layout: { left: 0, height: 'auto', width: 'auto', bottom: 0 }, 408 isDefault: YES, 409 action: "dismiss", 410 isVisible: NO 411 }) 412 }), 413 414 SC.View.extend({ 415 layout: { bottom: 13, height: 24, left: 18, width: 150 }, 416 childViews: [ 417 SC.ButtonView.extend({ 418 useStaticLayout: YES, 419 actionKey: SC.BUTTON3_STATUS, 420 localize: YES, 421 layout: { left: 0, height: 'auto', width: 'auto', bottom: 0 }, 422 action: "dismiss", 423 isVisible: NO 424 })] 425 })] 426 }), 427 428 /** 429 Action triggered whenever any button is pressed. Also the hides the 430 alertpane itself. 431 432 This will trigger the following chain of events: 433 434 1. If a delegate was given, and it has alertPaneDidDismiss it will be called 435 2. Otherwise it will look for the action of the button and call: 436 a) The action function reference if one was given 437 b) The action method on the target if one was given 438 c) If both a and b are missing, call the action on the rootResponder 439 440 @param {SC.View} sender - the button view that was clicked 441 */ 442 dismiss: function(sender) { 443 var del = this.delegate, 444 rootResponder, action, target; 445 446 if (del && del.alertPaneDidDismiss) { 447 del.alertPaneDidDismiss(this, sender.get('actionKey')); 448 } 449 450 if (action = (sender && sender.get('customAction'))) { 451 if (SC.typeOf(action) === SC.T_FUNCTION) { 452 action.call(action); 453 } else { 454 rootResponder = this.getPath('pane.rootResponder'); 455 if(rootResponder) { 456 target = sender.get('customTarget'); 457 rootResponder.sendAction(action, target || del, this, this, null, this); 458 } 459 } 460 } 461 462 this.remove(); // hide alert 463 }, 464 465 /** @private 466 Executes whenever one of the icon, message, description or caption is changed. 467 This simply causes the UI to refresh. 468 */ 469 alertInfoDidChange: function() { 470 var v = this.getPath('contentView.childViews.0'); 471 if (v) v.displayDidChange(); // re-render message 472 }.observes('icon', 'message', 'displayDescription', 'displayCaption') 473 474 }); 475 476 SC.AlertPane.mixin( 477 /** @scope SC.AlertPane */{ 478 479 /** 480 Show a dialog with a given set of hash attributes: 481 482 SC.AlertPane.show({ 483 message: "Could not load calendar", 484 description: "Your internet connection may be unavailable or our servers may be down.", 485 caption: "Try again in a few minutes.", 486 delegate: MyApp.calendarController 487 }); 488 489 See more examples for how to configure buttons and individual actions in the 490 documentation for the `SC.AlertPane` class. 491 492 @param {Hash} args 493 @return {SC.AlertPane} the pane shown 494 */ 495 show: function (args) { 496 // normalize the arguments if this is a deprecated call 497 args = SC.AlertPane._argumentsCall.apply(this, arguments); 498 499 var pane = this.create(args), 500 idx, 501 buttons = args.buttons, 502 button, buttonView, layerId, title, toolTip, action, target, themeName, 503 isDefault, isCancel, hasDefault, hasCancel; 504 505 if (buttons) { 506 //@if(debug) 507 // Provide some developer support for more than three button hashes. 508 if (buttons.length > 3) { 509 SC.warn("Tried to show SC.AlertPane with %@ buttons. SC.AlertPane only supports up to three buttons.".fmt(buttons.length)); 510 } 511 //@endif 512 513 // Determine if any button hash specifies isDefault/isCancel. If so, we need 514 // to override the button views' default settings. 515 hasDefault = !!buttons.findProperty('isDefault'); 516 hasCancel = !!buttons.findProperty('isCancel'); 517 518 for (idx = 0; idx < 3; idx++) { 519 button = buttons[idx]; 520 if (!button) continue; 521 522 buttonView = pane.get('button%@'.fmt(idx + 1)); 523 524 layerId = button.layerId; 525 title = button.title; 526 localize = button.localize; 527 toolTip = button.toolTip; 528 action = button.action; 529 target = button.target; 530 themeName = args.themeName || 'capsule'; 531 532 // If any button has the isDefault/isCancel flags set, we 533 // explicitly cast the button's flag to bool, ensuring that this 534 // overrides the default. Otherwise, we use undefined so we skip 535 // setting the property, ensuring the default value is used. 536 isDefault = hasDefault ? !!button.isDefault : undefined; 537 isCancel = hasCancel ? !!button.isCancel : undefined; 538 539 buttonView.set('title', title); 540 if (localize === YES) buttonView.set('localize', YES); 541 if (toolTip) buttonView.set('toolTip', toolTip); 542 if (action) buttonView.set('customAction', action); 543 if (target) buttonView.set('customTarget', target); 544 if (layerId !== undefined) { buttonView.set('layerId', layerId); } 545 if (isDefault !== undefined) { buttonView.set('isDefault', isDefault); } 546 if (isCancel !== undefined) { buttonView.set('isCancel', isCancel); } 547 buttonView.set('isVisible', !!title); 548 buttonView.set('themeName', themeName); 549 } 550 } else { 551 // if there are no buttons defined, just add the standard OK button 552 buttonView = pane.get('button1'); 553 buttonView.set('title', "OK"); 554 buttonView.set('isVisible', YES); 555 } 556 557 var show = pane.append(); // make visible. 558 pane.adjust('height', pane.childViews[0].$().height()); 559 pane.updateLayout(); 560 return show; 561 }, 562 563 /** 564 Same as `show()` just that it uses sc-icon-alert-48 CSS classname 565 as the dialog icon 566 567 @param {Hash} args 568 @return {SC.AlertPane} the pane shown 569 */ 570 warn: function(args) { 571 // normalize the arguments if this is a deprecated call 572 args = SC.AlertPane._argumentsCall.apply(this, arguments); 573 574 args.icon = 'sc-icon-alert-48'; 575 return this.show(args); 576 }, 577 578 /** 579 Same as `show()` just that it uses sc-icon-info-48 CSS classname 580 as the dialog icon 581 582 @param {Hash} args 583 @return {SC.AlertPane} the pane shown 584 */ 585 info: function(args) { 586 // normalize the arguments if this is a deprecated call 587 args = SC.AlertPane._argumentsCall.apply(this, arguments); 588 589 args.icon = 'sc-icon-info-48'; 590 return this.show(args); 591 }, 592 593 /** 594 Same as `show()` just that it uses sc-icon-error-48 CSS classname 595 as the dialog icon 596 597 @param {Hash} args 598 @return {SC.AlertPane} the pane shown 599 */ 600 error: function(args) { 601 // normalize the arguments if this is a deprecated call 602 args = SC.AlertPane._argumentsCall.apply(this, arguments); 603 604 args.icon = 'sc-icon-error-48'; 605 return this.show(args); 606 }, 607 608 /** 609 Same as `show()` just that it uses blank CSS classname 610 as the dialog icon 611 612 @param {Hash} args 613 @return {SC.AlertPane} the pane shown 614 */ 615 plain: function(args) { 616 // normalize the arguments if this is a deprecated call 617 args = SC.AlertPane._argumentsCall.apply(this, arguments); 618 619 args.icon = 'blank'; 620 return this.show(args); 621 }, 622 623 /** @private 624 Set properties to new structure for call that use the old arguments 625 structure. 626 627 Deprecated API but is preserved for now for backwards compatibility. 628 629 @deprecated 630 */ 631 _argumentsCall: function(args) { 632 var ret = args; 633 if(SC.typeOf(args)!==SC.T_HASH) { 634 //@if(debug) 635 SC.debug('SC.AlertPane has changed the signatures for show(), info(), warn(), error() and plain(). Please update accordingly.'); 636 //@endif 637 var normalizedArgs = this._normalizeArguments(arguments); 638 639 // now convert it to the new format for show() 640 ret = { 641 message: normalizedArgs[0], 642 description: normalizedArgs[1], 643 caption: normalizedArgs[2], 644 delegate: normalizedArgs[7], 645 icon: (normalizedArgs[6] || 'sc-icon-alert-48'), 646 themeName: 'capsule' 647 }; 648 649 // set buttons if there are any (and check if it's a string, since last 650 // argument could be the delegate object) 651 if(SC.typeOf(normalizedArgs[3])===SC.T_STRING || SC.typeOf(normalizedArgs[4])===SC.T_STRING || SC.typeOf(normalizedArgs[5])===SC.T_STRING) { 652 ret.buttons = [ 653 { title: normalizedArgs[3] }, 654 { title: normalizedArgs[4] }, 655 { title: normalizedArgs[5] } 656 ]; 657 } 658 659 } 660 return ret; 661 }, 662 663 /** @private 664 internal method normalizes arguments for processing by helper methods. 665 */ 666 _normalizeArguments: function(args) { 667 args = SC.A(args); // convert to real array 668 var len = args.length, delegate = args[len-1]; 669 if (SC.typeOf(delegate) !== SC.T_STRING) { 670 args[len-1] = null; 671 } else delegate = null ; 672 args[7] = delegate ; 673 return args ; 674 } 675 676 }); 677