4 * Implement a modal form.
6 * @see modal.inc for documentation.
8 * This javascript relies on the CTools ajax responder.
12 // Make sure our objects are defined.
13 Drupal
.CTools
= Drupal
.CTools
|| {};
14 Drupal
.CTools
.Modal
= Drupal
.CTools
.Modal
|| {};
19 * @todo -- document the settings.
21 Drupal
.CTools
.Modal
.show = function(choice
) {
24 if (choice
&& typeof choice
== 'string' && Drupal
.settings
[choice
]) {
25 // This notation guarantees we are actually copying it.
26 $.extend(true, opts
, Drupal
.settings
[choice
]);
29 $.extend(true, opts
, choice
);
33 modalTheme
: 'CToolsModalDialog',
34 throbberTheme
: 'CToolsModalThrobber',
36 animationSpeed
: 'fast',
43 // How much to remove from the inner content to make space for the
56 $.extend(true, settings
, defaults
, Drupal
.settings
.CToolsModal
, opts
);
58 if (Drupal
.CTools
.Modal
.currentSettings
&& Drupal
.CTools
.Modal
.currentSettings
!= settings
) {
59 Drupal
.CTools
.Modal
.modal
.remove();
60 Drupal
.CTools
.Modal
.modal
= null;
63 Drupal
.CTools
.Modal
.currentSettings
= settings
;
65 var resize = function(e
) {
66 // When creating the modal, it actually exists only in a theoretical
67 // place that is not in the DOM. But once the modal exists, it is in the
68 // DOM so the context must be set appropriately.
69 var context
= e
? document
: Drupal
.CTools
.Modal
.modal
;
71 if (Drupal
.CTools
.Modal
.currentSettings
.modalSize
.type
== 'scale') {
72 var width
= $(window
).width() * Drupal
.CTools
.Modal
.currentSettings
.modalSize
.width
;
73 var height
= $(window
).height() * Drupal
.CTools
.Modal
.currentSettings
.modalSize
.height
;
76 var width
= Drupal
.CTools
.Modal
.currentSettings
.modalSize
.width
;
77 var height
= Drupal
.CTools
.Modal
.currentSettings
.modalSize
.height
;
80 // Use the additionol pixels for creating the width and height.
81 $('div.ctools-modal-content', context
).css({
82 'width': width
+ Drupal
.CTools
.Modal
.currentSettings
.modalSize
.addWidth
+ 'px',
83 'height': height
+ Drupal
.CTools
.Modal
.currentSettings
.modalSize
.addHeight
+ 'px'
85 $('div.ctools-modal-content .modal-content', context
).css({
86 'width': (width
- Drupal
.CTools
.Modal
.currentSettings
.modalSize
.contentRight
) + 'px',
87 'height': (height
- Drupal
.CTools
.Modal
.currentSettings
.modalSize
.contentBottom
) + 'px'
91 if (!Drupal
.CTools
.Modal
.modal
) {
92 Drupal
.CTools
.Modal
.modal
= $(Drupal
.theme(settings
.modalTheme
));
93 if (settings
.modalSize
.type
== 'scale') {
94 $(window
).bind('resize', resize
);
100 $('span.modal-title', Drupal
.CTools
.Modal
.modal
).html(Drupal
.CTools
.Modal
.currentSettings
.loadingText
);
101 Drupal
.CTools
.Modal
.modalContent(Drupal
.CTools
.Modal
.modal
, settings
.modalOptions
, settings
.animation
, settings
.animationSpeed
, settings
.modalClass
);
102 $('#modalContent .modal-content').html(Drupal
.theme(settings
.throbberTheme
)).addClass('ctools-modal-loading');
104 // Position autocomplete results based on the scroll position of the modal.
105 $('#modalContent .modal-content').delegate('input.form-autocomplete', 'keyup', function() {
106 $('#autocomplete').css('top', $(this).position().top
+ $(this).outerHeight() + $(this).offsetParent().filter('#modal-content').scrollTop());
113 Drupal
.CTools
.Modal
.dismiss = function() {
114 if (Drupal
.CTools
.Modal
.modal
) {
115 Drupal
.CTools
.Modal
.unmodalContent(Drupal
.CTools
.Modal
.modal
);
120 * Provide the HTML to create the modal dialog.
122 Drupal
.theme
.prototype.CToolsModalDialog = function () {
124 html
+= ' <div id="ctools-modal">'
125 html
+= ' <div class="ctools-modal-content">' // panels-modal-content
126 html
+= ' <div class="modal-header">';
127 html
+= ' <a class="close" href="#">';
128 html
+= Drupal
.CTools
.Modal
.currentSettings
.closeText
+ Drupal
.CTools
.Modal
.currentSettings
.closeImage
;
130 html
+= ' <span id="modal-title" class="modal-title"> </span>';
132 html
+= ' <div id="modal-content" class="modal-content">';
141 * Provide the HTML to create the throbber.
143 Drupal
.theme
.prototype.CToolsModalThrobber = function () {
145 html
+= ' <div id="modal-throbber">';
146 html
+= ' <div class="modal-throbber-wrapper">';
147 html
+= Drupal
.CTools
.Modal
.currentSettings
.throbber
;
155 * Figure out what settings string to use to display a modal.
157 Drupal
.CTools
.Modal
.getSettings = function (object
) {
158 var match
= $(object
).attr('class').match(/ctools-modal-(\S+)/);
165 * Click function for modals that can be cached.
167 Drupal
.CTools
.Modal
.clickAjaxCacheLink = function () {
168 Drupal
.CTools
.Modal
.show(Drupal
.CTools
.Modal
.getSettings(this));
169 return Drupal
.CTools
.AJAX
.clickAJAXCacheLink
.apply(this);
173 * Handler to prepare the modal for the response
175 Drupal
.CTools
.Modal
.clickAjaxLink = function () {
176 Drupal
.CTools
.Modal
.show(Drupal
.CTools
.Modal
.getSettings(this));
181 * Submit responder to do an AJAX submit on all modal forms.
183 Drupal
.CTools
.Modal
.submitAjaxForm = function(e
) {
185 var url
= $form
.attr('action');
187 setTimeout(function() { Drupal
.CTools
.AJAX
.ajaxSubmit($form
, url
); }, 1);
192 * Bind links that will open modals to the appropriate function.
194 Drupal
.behaviors
.ZZCToolsModal
= {
195 attach: function(context
) {
197 // Note that doing so in this order means that the two classes can be
198 // used together safely.
200 * @todo remimplement the warm caching feature
201 $('a.ctools-use-modal-cache', context).once('ctools-use-modal', function() {
202 $(this).click(Drupal.CTools.Modal.clickAjaxCacheLink);
203 Drupal.CTools.AJAX.warmCache.apply(this);
207 $('area.ctools-use-modal, a.ctools-use-modal', context
).once('ctools-use-modal', function() {
209 $this.click(Drupal
.CTools
.Modal
.clickAjaxLink
);
210 // Create a drupal ajax object
211 var element_settings
= {};
212 if ($this.attr('href')) {
213 element_settings
.url
= $this.attr('href');
214 element_settings
.event
= 'click';
215 element_settings
.progress
= { type
: 'throbber' };
217 var base
= $this.attr('href');
218 Drupal
.ajax
[base
] = new Drupal
.ajax(base
, this, element_settings
);
222 $('input.ctools-use-modal, button.ctools-use-modal', context
).once('ctools-use-modal', function() {
224 $this.click(Drupal
.CTools
.Modal
.clickAjaxLink
);
226 var element_settings
= {};
228 // AJAX submits specified in this manner automatically submit to the
229 // normal form action.
230 element_settings
.url
= Drupal
.CTools
.Modal
.findURL(this);
231 if (element_settings
.url
== '') {
232 element_settings
.url
= $(this).closest('form').attr('action');
234 element_settings
.event
= 'click';
235 element_settings
.setClick
= true;
237 var base
= $this.attr('id');
238 Drupal
.ajax
[base
] = new Drupal
.ajax(base
, this, element_settings
);
240 // Make sure changes to settings are reflected in the URL.
241 $('.' + $(button
).attr('id') + '-url').change(function() {
242 Drupal
.ajax
[base
].options
.url
= Drupal
.CTools
.Modal
.findURL(button
);
246 // Bind our custom event to the form submit
247 $('#modal-content form', context
).once('ctools-use-modal', function() {
249 var element_settings
= {};
251 element_settings
.url
= $this.attr('action');
252 element_settings
.event
= 'submit';
253 element_settings
.progress
= { 'type': 'throbber' }
254 var base
= $this.attr('id');
256 Drupal
.ajax
[base
] = new Drupal
.ajax(base
, this, element_settings
);
257 Drupal
.ajax
[base
].form
= $this;
259 $('input[type=submit], button', this).click(function(event
) {
260 Drupal
.ajax
[base
].element
= this;
261 this.form
.clk
= this;
262 // Stop autocomplete from submitting.
263 if (Drupal
.autocompleteSubmit
&& !Drupal
.autocompleteSubmit()) {
266 // An empty event means we were triggered via .click() and
267 // in jquery 1.4 this won't trigger a submit.
268 if (event
.bubbles
== undefined) {
269 $(this.form
).trigger('submit');
275 // Bind a click handler to allow elements with the 'ctools-close-modal'
276 // class to close the modal.
277 $('.ctools-close-modal', context
).once('ctools-close-modal')
279 Drupal
.CTools
.Modal
.dismiss();
285 // The following are implementations of AJAX responder commands.
288 * AJAX responder command to place HTML within the modal.
290 Drupal
.CTools
.Modal
.modal_display = function(ajax
, response
, status
) {
291 if ($('#modalContent').length
== 0) {
292 Drupal
.CTools
.Modal
.show(Drupal
.CTools
.Modal
.getSettings(ajax
.element
));
294 $('#modal-title').html(response
.title
);
295 // Simulate an actual page load by scrolling to the top after adding the
296 // content. This is helpful for allowing users to see error messages at the
297 // top of a form, etc.
298 $('#modal-content').html(response
.output
).scrollTop(0);
300 // Attach behaviors within a modal dialog.
301 var settings
= response
.settings
|| ajax
.settings
|| Drupal
.settings
;
302 Drupal
.attachBehaviors('#modalContent', settings
);
304 if ($('#modal-content').hasClass('ctools-modal-loading')) {
305 $('#modal-content').removeClass('ctools-modal-loading');
308 // If the modal was already shown, and we are simply replacing its
309 // content, then focus on the first focusable element in the modal.
310 // (When first showing the modal, focus will be placed on the close
311 // button by the show() function called above.)
312 $('#modal-content :focusable:first').focus();
317 * AJAX responder command to dismiss the modal.
319 Drupal
.CTools
.Modal
.modal_dismiss = function(command
) {
320 Drupal
.CTools
.Modal
.dismiss();
321 $('link.ctools-temporary-css').remove();
327 //Drupal.CTools.AJAX.commands.modal_loading = function(command) {
328 Drupal
.CTools
.Modal
.modal_loading = function(command
) {
329 Drupal
.CTools
.Modal
.modal_display({
330 output
: Drupal
.theme(Drupal
.CTools
.Modal
.currentSettings
.throbberTheme
),
331 title
: Drupal
.CTools
.Modal
.currentSettings
.loadingText
336 * Find a URL for an AJAX button.
338 * The URL for this gadget will be composed of the values of items by
339 * taking the ID of this item and adding -url and looking for that
340 * class. They need to be in the form in order since we will
341 * concat them all together using '/'.
343 Drupal
.CTools
.Modal
.findURL = function(item
) {
345 var url_class
= '.' + $(item
).attr('id') + '-url';
349 if (url
&& $this.val()) {
360 * @param content string to display in the content box
361 * @param css obj of css attributes
362 * @param animation (fadeIn, slideDown, show)
363 * @param speed (valid animation speeds slow, medium, fast or # in ms)
364 * @param modalClass class added to div#modalContent
366 Drupal
.CTools
.Modal
.modalContent = function(content
, css
, animation
, speed
, modalClass
) {
367 // If our animation isn't set, make it just show/pop
372 // If our animation isn't "fadeIn" or "slideDown" then it always is show
373 if (animation
!= 'fadeIn' && animation
!= 'slideDown') {
382 // Build our base attributes and allow them to be overriden
383 css
= jQuery
.extend({
384 position
: 'absolute',
391 // Add opacity handling for IE.
392 css
.filter
= 'alpha(opacity=' + (100 * css
.opacity
) + ')';
395 // If we already have modalContent, remove it.
396 if ($('#modalBackdrop').length
) $('#modalBackdrop').remove();
397 if ($('#modalContent').length
) $('#modalContent').remove();
399 // position code lifted from http://www.quirksmode.org/viewport/compatibility.html
400 if (self
.pageYOffset
) { // all except Explorer
401 var wt
= self
.pageYOffset
;
402 } else if (document
.documentElement
&& document
.documentElement
.scrollTop
) { // Explorer 6 Strict
403 var wt
= document
.documentElement
.scrollTop
;
404 } else if (document
.body
) { // all other Explorers
405 var wt
= document
.body
.scrollTop
;
408 // Get our dimensions
410 // Get the docHeight and (ugly hack) add 50 pixels to make sure we dont have a *visible* border below our div
411 var docHeight
= $(document
).height() + 50;
412 var docWidth
= $(document
).width();
413 var winHeight
= $(window
).height();
414 var winWidth
= $(window
).width();
415 if( docHeight
< winHeight
) docHeight
= winHeight
;
418 $('body').append('<div id="modalBackdrop" class="backdrop-' + modalClass
+ '" style="z-index: 1000; display: none;"></div><div id="modalContent" class="modal-' + modalClass
+ '" style="z-index: 1001; position: absolute;">' + $(content
).html() + '</div>');
420 // Get a list of the tabbable elements in the modal content.
421 var getTabbableElements = function () {
422 var tabbableElements
= $('#modalContent :tabbable'),
423 radioButtons
= tabbableElements
.filter('input[type="radio"]');
425 // The list of tabbable elements from jQuery is *almost* right. The
426 // exception is with groups of radio buttons. The list from jQuery will
427 // include all radio buttons, when in fact, only the selected radio button
428 // is tabbable, and if no radio buttons in a group are selected, then only
429 // the first is tabbable.
430 if (radioButtons
.length
> 0) {
431 // First, build up an index of which groups have an item selected or not.
432 var anySelected
= {};
433 radioButtons
.each(function () {
434 var name
= this.name
;
436 if (typeof anySelected
[name
] === 'undefined') {
437 anySelected
[name
] = radioButtons
.filter('input[name="' + name
+ '"]:checked').length
!== 0;
441 // Next filter out the radio buttons that aren't really tabbable.
443 tabbableElements
= tabbableElements
.filter(function () {
446 if (this.type
== 'radio') {
447 if (anySelected
[this.name
]) {
448 // Only keep the selected one.
452 // Only keep the first one.
453 if (found
[this.name
]) {
456 found
[this.name
] = true;
464 return tabbableElements
.get();
467 // Keyboard and focus event handler ensures only modal elements gain focus.
468 modalEventHandler = function( event
) {
470 if ( event
) { //Mozilla
471 target
= event
.target
;
473 event
= window
.event
;
474 target
= event
.srcElement
;
477 var parents
= $(target
).parents().get();
478 for (var i
= 0; i
< parents
.length
; ++i
) {
479 var position
= $(parents
[i
]).css('position');
480 if (position
== 'absolute' || position
== 'fixed') {
485 if ($(target
).is('#modalContent, body') || $(target
).filter('*:visible').parents('#modalContent').length
) {
486 // Allow the event only if target is a visible child node
491 getTabbableElements()[0].focus();
494 event
.preventDefault();
496 $('body').bind( 'focus', modalEventHandler
);
497 $('body').bind( 'keypress', modalEventHandler
);
499 // Keypress handler Ensures you can only TAB to elements within the modal.
500 // Based on the psuedo-code from WAI-ARIA 1.0 Authoring Practices section
501 // 3.3.1 "Trapping Focus".
502 modalTabTrapHandler = function (evt
) {
503 // We only care about the TAB key.
504 if (evt
.which
!= 9) {
508 var tabbableElements
= getTabbableElements(),
509 firstTabbableElement
= tabbableElements
[0],
510 lastTabbableElement
= tabbableElements
[tabbableElements
.length
- 1],
511 singleTabbableElement
= firstTabbableElement
== lastTabbableElement
,
514 // If this is the first element and the user wants to go backwards, then
515 // jump to the last element.
516 if (node
== firstTabbableElement
&& evt
.shiftKey
) {
517 if (!singleTabbableElement
) {
518 lastTabbableElement
.focus();
522 // If this is the last element and the user wants to go forwards, then
523 // jump to the first element.
524 else if (node
== lastTabbableElement
&& !evt
.shiftKey
) {
525 if (!singleTabbableElement
) {
526 firstTabbableElement
.focus();
530 // If this element isn't in the dialog at all, then jump to the first
531 // or last element to get the user into the game.
532 else if ($.inArray(node
, tabbableElements
) == -1) {
533 // Make sure the node isn't in another modal (ie. WYSIWYG modal).
534 var parents
= $(node
).parents().get();
535 for (var i
= 0; i
< parents
.length
; ++i
) {
536 var position
= $(parents
[i
]).css('position');
537 if (position
== 'absolute' || position
== 'fixed') {
543 lastTabbableElement
.focus();
546 firstTabbableElement
.focus();
550 $('body').bind('keydown', modalTabTrapHandler
);
552 // Create our content div, get the dimensions, and hide it
553 var modalContent
= $('#modalContent').css('top','-1000px');
554 var mdcTop
= wt
+ ( winHeight
/ 2 ) - ( modalContent
.outerHeight() / 2);
555 var mdcLeft
= ( winWidth
/ 2 ) - ( modalContent
.outerWidth() / 2);
556 $('#modalBackdrop').css(css
).css('top', 0).css('height', docHeight
+ 'px').css('width', docWidth
+ 'px').show();
557 modalContent
.css({top
: mdcTop
+ 'px', left
: mdcLeft
+ 'px'}).hide()[animation
](speed
);
559 // Bind a click for closing the modalContent
560 modalContentClose = function(){close(); return false;};
561 $('.close').bind('click', modalContentClose
);
563 // Bind a keypress on escape for closing the modalContent
564 modalEventEscapeCloseHandler = function(event
) {
565 if (event
.keyCode
== 27) {
571 $(document
).bind('keydown', modalEventEscapeCloseHandler
);
573 // Per WAI-ARIA 1.0 Authoring Practices, initial focus should be on the
574 // close button, but we should save the original focus to restore it after
575 // the dialog is closed.
576 var oldFocus
= document
.activeElement
;
579 // Close the open modal content and backdrop
582 $(window
).unbind('resize', modalContentResize
);
583 $('body').unbind( 'focus', modalEventHandler
);
584 $('body').unbind( 'keypress', modalEventHandler
);
585 $('body').unbind( 'keydown', modalTabTrapHandler
);
586 $('.close').unbind('click', modalContentClose
);
587 $('body').unbind('keypress', modalEventEscapeCloseHandler
);
588 $(document
).trigger('CToolsDetachBehaviors', $('#modalContent'));
590 // Set our animation parameters and use them
591 if ( animation
== 'fadeIn' ) animation
= 'fadeOut';
592 if ( animation
== 'slideDown' ) animation
= 'slideUp';
593 if ( animation
== 'show' ) animation
= 'hide';
596 modalContent
.hide()[animation
](speed
);
598 // Remove the content
599 $('#modalContent').remove();
600 $('#modalBackdrop').remove();
602 // Restore focus to where it was before opening the dialog
606 // Move and resize the modalBackdrop and modalContent on window resize.
607 modalContentResize = function(){
609 // Reset the backdrop height/width to get accurate document size.
610 $('#modalBackdrop').css('height', '').css('width', '');
612 // Position code lifted from:
613 // http://www.quirksmode.org/viewport/compatibility.html
614 if (self
.pageYOffset
) { // all except Explorer
615 var wt
= self
.pageYOffset
;
616 } else if (document
.documentElement
&& document
.documentElement
.scrollTop
) { // Explorer 6 Strict
617 var wt
= document
.documentElement
.scrollTop
;
618 } else if (document
.body
) { // all other Explorers
619 var wt
= document
.body
.scrollTop
;
623 var docHeight
= $(document
).height();
624 var docWidth
= $(document
).width();
625 var winHeight
= $(window
).height();
626 var winWidth
= $(window
).width();
627 if( docHeight
< winHeight
) docHeight
= winHeight
;
629 // Get where we should move content to
630 var modalContent
= $('#modalContent');
631 var mdcTop
= wt
+ ( winHeight
/ 2 ) - ( modalContent
.outerHeight() / 2);
632 var mdcLeft
= ( winWidth
/ 2 ) - ( modalContent
.outerWidth() / 2);
635 $('#modalBackdrop').css('height', docHeight
+ 'px').css('width', docWidth
+ 'px').show();
636 modalContent
.css('top', mdcTop
+ 'px').css('left', mdcLeft
+ 'px').show();
638 $(window
).bind('resize', modalContentResize
);
643 * @param content (The jQuery object to remove)
644 * @param animation (fadeOut, slideUp, show)
645 * @param speed (valid animation speeds slow, medium, fast or # in ms)
647 Drupal
.CTools
.Modal
.unmodalContent = function(content
, animation
, speed
)
649 // If our animation isn't set, make it just show/pop
650 if (!animation
) { var animation
= 'show'; } else {
651 // If our animation isn't "fade" then it always is show
652 if (( animation
!= 'fadeOut' ) && ( animation
!= 'slideUp')) animation
= 'show';
654 // Set a speed if we dont have one
655 if ( !speed
) var speed
= 'fast';
657 // Unbind the events we bound
658 $(window
).unbind('resize', modalContentResize
);
659 $('body').unbind('focus', modalEventHandler
);
660 $('body').unbind('keypress', modalEventHandler
);
661 $('body').unbind( 'keydown', modalTabTrapHandler
);
662 $('.close').unbind('click', modalContentClose
);
663 $('body').unbind('keypress', modalEventEscapeCloseHandler
);
664 $(document
).trigger('CToolsDetachBehaviors', $('#modalContent'));
666 // jQuery magic loop through the instances and run the animations or removal.
667 content
.each(function(){
668 if ( animation
== 'fade' ) {
669 $('#modalContent').fadeOut(speed
, function() {
670 $('#modalBackdrop').fadeOut(speed
, function() {
676 if ( animation
== 'slide' ) {
677 $('#modalContent').slideUp(speed
,function() {
678 $('#modalBackdrop').slideUp(speed
, function() {
684 $('#modalContent').remove();
685 $('#modalBackdrop').remove();
692 Drupal
.ajax
.prototype.commands
.modal_display
= Drupal
.CTools
.Modal
.modal_display
;
693 Drupal
.ajax
.prototype.commands
.modal_dismiss
= Drupal
.CTools
.Modal
.modal_dismiss
;