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