diff --git a/modules/overlay/overlay-child.js b/modules/overlay/overlay-child.js index 5a81de20fc27d9996aa4ff741891a5382059f90e..e3e1cb93352fbbd791129f5a553056875eb9a75c 100644 --- a/modules/overlay/overlay-child.js +++ b/modules/overlay/overlay-child.js @@ -99,43 +99,51 @@ Drupal.overlayChild.behaviors.scrollToTop = function (context, settings) { * @see Drupal.overlay.isAdminLink() */ Drupal.overlayChild.behaviors.parseLinks = function (context, settings) { - $('a:not(.overlay-exclude)', context).once('overlay').each(function () { - // Non-admin links should close the overlay and open in the main window. - if (!parent.Drupal.overlay.isAdminLink(this.href)) { - $(this).click(function () { - // We need to store the parent variable locally because it will - // disappear as soon as we close the iframe. - var parentWindow = parent; - if (parentWindow.Drupal.overlay.close(false)) { - parentWindow.Drupal.overlay.redirect($(this).attr('href')); - } - return false; - }); + var closeAndRedirectOnClick = function (event) { + // We need to store the parent variable locally because it will + // disappear as soon as we close the iframe. + var parentWindow = parent; + if (parentWindow.Drupal.overlay.close(false)) { + parentWindow.Drupal.overlay.redirect($(this).attr('href')); + } + return false; + }; + var redirectOnClick = function (event) { + parent.Drupal.overlay.redirect($(this).attr('href')); + return false; + }; + + $('a:not(.overlay-exclude)', context).once('overlay', function () { + var href = $(this).attr('href'); + // Skip links that don't have an href attribute. + if (href == undefined) { return; } + // Non-admin links should close the overlay and open in the main window. + else if (!parent.Drupal.overlay.isAdminLink(href)) { + $(this).click(closeAndRedirectOnClick); + } + // Open external links in a new window. + else if (href.indexOf('http') > 0 || href.indexOf('https') > 0) { + $(this).attr('target', '_new'); + } + // Open admin links in the overlay. else { - var href = $(this).attr('href'); - if (href.indexOf('http') > 0 || href.indexOf('https') > 0) { - $(this).attr('target', '_new'); - } - else { - $(this).each(function(){ - this.href = parent.Drupal.overlay.fragmentizeLink(this); - }).click(function () { - parent.window.location.href = this.href; - return false; - }); - } + $(this) + .attr('href', parent.Drupal.overlay.fragmentizeLink(this)) + .click(redirectOnClick); } }); - $('form:not(.overlay-processed)', context).addClass('overlay-processed').each(function () { + + $('form', context).once('overlay', function () { // Obtain the action attribute of the form. var action = $(this).attr('action'); - if (action.indexOf('http') != 0 && action.indexOf('https') != 0) { - // Keep internal forms in the overlay. + // Keep internal forms in the overlay. + if (action == undefined || (action.indexOf('http') != 0 && action.indexOf('https') != 0)) { action += (action.indexOf('?') > -1 ? '&' : '?') + 'render=overlay'; $(this).attr('action', action); } + // Submit external forms into a new window. else { $(this).attr('target', '_new'); } diff --git a/modules/overlay/overlay-parent.css b/modules/overlay/overlay-parent.css index 70959766b5bbebd167023f4ba2e128ca1a1341ed..417315755ffb76536e25a13af866ccae04dd8ba4 100644 --- a/modules/overlay/overlay-parent.css +++ b/modules/overlay/overlay-parent.css @@ -10,11 +10,32 @@ background-image: none; } +body.overlay-autofit { + overflow-y: scroll; +} + +/** + * Overlay wrapper. + */ +#overlay-wrapper { + position: absolute; + top: 0; + left: 0; + width: 100%; + z-index: 501; + padding: 20px 0 15px 0; +} + /** * jQuery UI Dialog classes. */ .overlay { + position: static; padding-right: 26px; + margin: 0 auto; + width: 78%; + min-width: 700px; + min-height: 100px; } .overlay.ui-widget-content, .overlay .ui-widget-header { @@ -61,22 +82,22 @@ } /** - * Overlay content and shadows. + * Overlay content. */ .overlay #overlay-container { margin: 0; padding: 0; + width: 100%; overflow: visible; background: #fff url(images/loading.gif) no-repeat 50% 50%; - -webkit-box-shadow: 8px 8px 8px rgba(0,0,0,.5); - -moz-box-shadow: 8px 8px 8px rgba(0,0,0,.5); - box-shadow: 8px 8px 8px rgba(0,0,0,.5); } .overlay-loaded #overlay-container { - background: none; + background: #fff; } .overlay #overlay-element { overflow: hidden; + width: 100%; + height: 100%; } /** @@ -91,15 +112,17 @@ text-transform: uppercase; } .overlay .ui-dialog-titlebar ul li { - display: inline-block; + display: inline; list-style: none; margin: 0 0 0 -3px; padding: 0; } + .overlay .ui-dialog-titlebar ul li a, .overlay .ui-dialog-titlebar ul li a:active, .overlay .ui-dialog-titlebar ul li a:visited, .overlay .ui-dialog-titlebar ul li a:hover { + display: inline-block; background-color: #a6a7a2; -moz-border-radius: 8px 8px 0 0; -webkit-border-top-left-radius: 8px; @@ -107,16 +130,18 @@ border-radius: 8px 8px 0 0; color: #000; font-weight: bold; - padding: 5px 14px; + padding: 0 14px; text-decoration: none; font-size: 11px; + margin: 0 0 2px 0; } .overlay .ui-dialog-titlebar ul li.active a, .overlay .ui-dialog-titlebar ul li.active a.active, .overlay .ui-dialog-titlebar ul li.active a:active, .overlay .ui-dialog-titlebar ul li.active a:visited { background-color: #fff; - padding-bottom: 7px; + padding-bottom: 2px; + margin: 0; } .overlay .ui-dialog-titlebar ul li a:hover { color: #fff; diff --git a/modules/overlay/overlay-parent.js b/modules/overlay/overlay-parent.js index c79276f823cd9bed10150e4ffa01fc0960fcaea8..5be4cb7be49175bf06995e6f93404821000d8796 100644 --- a/modules/overlay/overlay-parent.js +++ b/modules/overlay/overlay-parent.js @@ -7,6 +7,8 @@ */ Drupal.behaviors.overlayParent = { attach: function (context, settings) { + var $window = $(window); + // Alter all admin links so that they will open in the overlay. $('a', context).filter(function () { return Drupal.overlay.isAdminLink(this.href); @@ -29,10 +31,10 @@ Drupal.behaviors.overlayParent = { $('#toolbar a.toggle', context).once('overlay').click(function () { setTimeout(function () { // Resize the overlay, if it's open. - if (Drupal.overlay.iframe.documentSize) { - Drupal.overlay.resize(Drupal.overlay.iframe.documentSize); + if (Drupal.overlay.isOpen) { + Drupal.overlay.outerResize(); } - }, 150); + }, 10); }); // Make sure the onhashchange handling below is only processed once. @@ -42,7 +44,7 @@ Drupal.behaviors.overlayParent = { this.processed = true; // When the hash (URL fragment) changes, open the overlay if needed. - $(window).bind('hashchange', function (e) { + $window.bind('hashchange', function (e) { // If we changed the hash to reflect an internal redirect in the overlay, // its location has already been changed, so don't do anything. if ($.data(window.location, window.location.href) === 'redirect') { @@ -56,7 +58,7 @@ Drupal.behaviors.overlayParent = { // Trigger the hashchange event once, after the page is loaded, so that // permalinks open the overlay. - $(window).trigger('hashchange'); + $window.trigger('hashchange'); } }; @@ -65,8 +67,26 @@ Drupal.behaviors.overlayParent = { */ Drupal.overlay = Drupal.overlay || { options: {}, - iframe: { $container: null, $element: null }, - isOpen: false + isOpen: false, + isOpening: false, + isClosing: false, + isLoading: false, + + onOverlayCloseArgs: null, + onOverlayCloseStatusMessages: null, + + resizeTimeoutID: null, + lastHeight: 0, + + $wrapper: null, + $dialog: null, + $dialogTitlebar: null, + $container: null, + $iframe: null, + + $iframeWindow: null, + $iframeDocument: null, + $iframeBody: null }; /** @@ -95,9 +115,10 @@ Drupal.overlay.open = function (options) { var self = this; // Just one overlay is allowed. - if (self.isOpen || $('#overlay-container').size()) { + if (self.isOpen || self.isOpening) { return false; } + self.isOpening = true; var defaultOptions = { url: options.url, @@ -108,15 +129,15 @@ Drupal.overlay.open = function (options) { onOverlayCanClose: options.onOverlayCanClose, onOverlayClose: options.onOverlayClose, customDialogOptions: options.customDialogOptions || {} - } + }; self.options = $.extend(defaultOptions, options); // Create the dialog and related DOM elements. self.create(); - // Open the dialog offscreen where we can set its size, etc. - var temp = self.iframe.$container.dialog('option', { position: ['-999em', '-999em'] }).dialog('open');; + // Open the dialog. + self.$container.dialog('open'); return true; }; @@ -129,11 +150,12 @@ Drupal.overlay.open = function (options) { */ Drupal.overlay.create = function () { var self = this; + var $window = $(window); + var $body = $('body'); - self.iframe.$element = $(Drupal.theme('overlayElement')); - self.iframe.$container = $(Drupal.theme('overlayContainer')).append(self.iframe.$element); - - $('body').append(self.iframe.$container); + var delayedOuterResize = function() { + setTimeout(self.outerResize, 1); + }; // Open callback for jQuery UI Dialog. var dialogOpen = function () { @@ -143,106 +165,129 @@ Drupal.overlay.create = function () { // Also, this is not necessary here because we need to deal with an // iframe element that contains a separate window. // We'll try to provide our own behavior from bindChild() method. - $('.overlay').unbind('keypress.ui-dialog'); - - // Adjust close button features. - $('.overlay .ui-dialog-titlebar-close:not(.overlay-processed)').addClass('overlay-processed') - .attr('href', '#') - .attr('title', Drupal.t('Close')) - .unbind('click') - .bind('click', function () { - try { self.close(); } catch(e) {} - // Allow the click event to propagate, to clear the hash state. - return true; - }); + self.$dialog.unbind('keypress.ui-dialog'); + + // Add title to close button features for accessibility. + self.$dialogTitlebar.find('.ui-dialog-titlebar-close').attr('title', Drupal.t('Close')); // Replace the title span element with an h1 element for accessibility. - $('.overlay .ui-dialog-title').replaceWith(Drupal.theme('overlayTitleHeader', $('.overlay .ui-dialog-title').html())); + var $dialogTitle = self.$dialogTitlebar.find('.ui-dialog-title'); + $dialogTitle.replaceWith(Drupal.theme('overlayTitleHeader', $dialogTitle.html())); - // Compute initial dialog size. - var dialogSize = self.sanitizeSize({width: self.options.width, height: self.options.height}); - - // Compute frame size and dialog position based on dialog size. - var frameSize = $.extend({}, dialogSize); - frameSize.height -= $('.overlay .ui-dialog-titlebar').outerHeight(true); - var dialogPosition = self.computePosition($('.overlay'), dialogSize); - - // Adjust size of the iframe element and container. - $('.overlay').width(dialogSize.width).height(dialogSize.height); - self.iframe.$container.width(frameSize.width).height(frameSize.height); - self.iframe.$element.width(frameSize.width).height(frameSize.height); - - // Update the dialog size so that UI internals are aware of the change. - self.iframe.$container.dialog('option', { width: dialogSize.width, height: dialogSize.height }); - - // Hide the dialog, position it on the viewport and then fade it in with - // the frame hidden until the child document is loaded. - self.iframe.$element.hide(); - $('.overlay').hide().css({top: dialogPosition.top, left: dialogPosition.left}); - $('.overlay').fadeIn('fast', function () { - // Load the document on hidden iframe (see bindChild method). - self.load(self.options.url); + // Wrap the dialog into a div so we can center it using CSS. + self.$dialog.wrap(Drupal.theme('overlayWrapper')); + self.$wrapper = self.$dialog.parent(); + + self.$dialog.css({ + // Remove some CSS properties added by ui.dialog itself. + position: '', left: '', top: '', height: '' }); + // Add a class to the body to indicate the overlay is open. + $body.addClass('overlay-open'); + + // Adjust overlay size when window is resized. + $window.bind('resize', delayedOuterResize); + + if (self.options.autoFit) { + $body.addClass('overlay-autofit'); + } + else { + // Add scrollbar to the iframe when autoFit is disabled. + self.$iframe.css('overflow', 'auto').attr('scrolling', 'yes'); + } + + // Compute initial dialog size. + self.outerResize(); + + // Load the document on hidden iframe (see bindChild method). + self.load(self.options.url); + if ($.isFunction(self.options.onOverlayOpen)) { self.options.onOverlayOpen(self); } self.isOpen = true; + self.isOpening = false; }; // Before close callback for jQuery UI Dialog. var dialogBeforeClose = function () { - if (self.beforeCloseEnabled) { - return true; + // Prevent double execution when close is requested more than once. + if (!self.isOpen || self.isClosing) { + return false; } - if (!self.beforeCloseIsBusy) { - self.beforeCloseIsBusy = true; - setTimeout(function () { self.close(); }, 1); + + // Allow external scripts decide if the overlay can be closed. + // The external script should call Drupal.overlay.close() again when it is ready for closing. + if ($.isFunction(self.options.onOverlayCanClose) && self.options.onOverlayCanClose(self) === false) { + return false; } - return false; + + self.isClosing = true; + + // Stop all animations. + $window.unbind('resize', delayedOuterResize); + clearTimeout(self.resizeTimeoutID); }; // Close callback for jQuery UI Dialog. var dialogClose = function () { $(document).unbind('keydown.overlay-event'); - $('.overlay .ui-dialog-titlebar-close').unbind('keydown.overlay-event'); - try { - self.iframe.$element.remove(); - self.iframe.$container.dialog('destroy').remove(); - } catch(e) {}; - delete self.iframe.documentSize; - delete self.iframe.Drupal; - delete self.iframe.$element; - delete self.iframe.$container; - if (self.beforeCloseEnabled) { - delete self.beforeCloseEnabled; - } - if (self.beforeCloseIsBusy) { - delete self.beforeCloseIsBusy; + + $body.removeClass('overlay-open').removeClass('overlay-autofit'); + + // When the iframe is still loading don't destroy it immediately but after + // the content is loaded (see self.load). + if (!self.isLoading) { + self.$iframe.unbind('load'); + self.destroy(); } + self.isOpen = false; + self.isClosing = false; + + self.lastHeight = 0; + + if ($.isFunction(self.options.onOverlayClose)) { + self.options.onOverlayClose(self.onOverlayCloseArgs, self.onOverlayCloseStatusMessages); + } + self.onOverlayCloseArgs = null; + self.onOverlayCloseStatusMessages = null; }; // Default jQuery UI Dialog options. var dialogOptions = { - modal: true, autoOpen: false, closeOnEscape: true, + dialogClass: 'overlay', + draggable: false, + modal: true, resizable: false, title: Drupal.t('Loading...'), - dialogClass: 'overlay', zIndex: 500, + + // When not set use a empty string so it is not applied and CSS can handle it. + width: self.options.width || '', + height: self.options.height, + open: dialogOpen, beforeclose: dialogBeforeClose, close: dialogClose }; + // Create the overlay container and iframe. + self.$iframe = $(Drupal.theme('overlayElement')); + self.$container = $(Drupal.theme('overlayContainer')).append(self.$iframe); + // Allow external script override default jQuery UI Dialog options. $.extend(dialogOptions, self.options.customDialogOptions); // Create the jQuery UI Dialog. - self.iframe.$container.dialog(dialogOptions); + self.$container.dialog(dialogOptions); + // Cache dialog selector. + self.$dialog = self.$container.parents('.' + dialogOptions.dialogClass); + self.$dialogTitlebar = self.$dialog.find('.ui-dialog-titlebar'); }; /** @@ -253,82 +298,84 @@ Drupal.overlay.create = function () { */ Drupal.overlay.load = function (url) { var self = this; - var iframe = self.iframe.$element.get(0); - - // Add a loaded class to the overlay once the iframe is loaded. - $(iframe).load(function () { - $('.overlay').addClass('overlay-loaded'); + var iframeElement = self.$iframe.get(0); + + self.isLoading = true; + + self.$iframeWindow = null; + self.$iframeDocument = null; + self.$iframeBody = null; + + // No need to resize when loading. + clearTimeout(self.resizeTimeoutID); + + // Change the overlay title. + self.$container.dialog('option', 'title', Drupal.t('Loading...')); + + // When a new overlay is opened and loaded, we add a loaded class to + // the dialog. The loaded class is not removed and added back again + // while switching between pages with the overlay already open, + // due to performance issues. + + //self.$dialog.removeClass('overlay-loaded'); + self.$iframe + .css('opacity', 0.2) + .load(function () { + self.isLoading = false; + + // Only continue when overlay is still open and not closing. + if (self.isOpen && !self.isClosing) { + self.$iframe.css('opacity', 1); + self.$dialog.addClass('overlay-loaded'); + } + else { + self.destroy(); + } }); - + // Get the document object of the iframe window. // @see http://xkr.us/articles/dom/iframe-document/ - var doc = (iframe.contentWindow || iframe.contentDocument); - if (doc.document) { - doc = doc.document; + var iframeDocument = (iframeElement.contentWindow || iframeElement.contentDocument); + if (iframeDocument.document) { + iframeDocument = iframeDocument.document; } + // location.replace doesn't create a history entry. location.href does. // In this case, we want location.replace, as we're creating the history // entry using URL fragments. - doc.location.replace(url); + iframeDocument.location.replace(url); }; /** - * Check if the dialog can be closed. + * Close the overlay and remove markup related to it from the document. */ -Drupal.overlay.canClose = function () { +Drupal.overlay.close = function (args, statusMessages) { var self = this; - if (!self.isOpen) { - return false; - } - // Allow external scripts decide if the overlay can be closed. - if ($.isFunction(self.options.onOverlayCanClose)) { - if (!self.options.onOverlayCanClose(self)) { - return false; - } - } - return true; + + self.onOverlayCloseArgs = args; + self.onOverlayCloseStatusMessages = statusMessages; + + return self.$container.dialog('close'); }; /** - * Close the overlay and remove markup related to it from the document. + * Destroy the overlay. */ -Drupal.overlay.close = function (args, statusMessages) { +Drupal.overlay.destroy = function () { var self = this; - // Offer the user a chance to change their mind if there is a form on the - // page, which may have unsaved work on it. - var iframeElement = self.iframe.$element.get(0); - var iframeDocument = (iframeElement.contentWindow || iframeElement.contentDocument); - if (iframeDocument.document) { - iframeDocument = iframeDocument.document; - } + self.$container.dialog('destroy').remove(); + self.$wrapper.remove(); - // Check if the dialog can be closed. - if (!self.canClose()) { - delete self.beforeCloseIsBusy; - return false; - } + self.$wrapper = null; + self.$dialog = null; + self.$dialogTitlebar = null; + self.$container = null; + self.$iframe = null; - // Hide and destroy the dialog. - function closeDialog() { - // Prevent double execution when close is requested more than once. - if (!$.isObject(self.iframe.$container)) { - return; - } - self.beforeCloseEnabled = true; - self.iframe.$container.dialog('close'); - if ($.isFunction(self.options.onOverlayClose)) { - self.options.onOverlayClose(args, statusMessages); - } - } - if (!$.isObject(self.iframe.$element) || !self.iframe.$element.size() || !self.iframe.$element.is(':visible')) { - closeDialog(); - } - else { - self.iframe.$container.animate({height: 'hide'}, { duration: 'fast', 'queue': false }); - $('.overlay').animate({opacity: 'hide'}, closeDialog); - } - return true; + self.$iframeWindow = null; + self.$iframeDocument = null; + self.$iframeBody = null; }; /** @@ -344,203 +391,159 @@ Drupal.overlay.redirect = function (link) { } location.href = link; return true; -} +}; /** * Bind the child window. * * Add tabs on the overlay, keyboard actions and display animation. */ -Drupal.overlay.bindChild = function (iFrameWindow, isClosing) { +Drupal.overlay.bindChild = function (iframeWindow, isClosing) { var self = this; - var $iFrameWindow = iFrameWindow.jQuery; - var $iFrameDocument = $iFrameWindow(iFrameWindow.document); - var autoResizing = false; - self.iframe.Drupal = iFrameWindow.Drupal; + self.$iframeWindow = iframeWindow.jQuery; + self.$iframeDocument = self.$iframeWindow(iframeWindow.document); + self.$iframeBody = self.$iframeWindow('body'); // We are done if the child window is closing. - if (isClosing) { + if (isClosing || self.isClosing || !self.isOpen) { return; } // Make sure the parent window URL matches the child window URL. - self.syncChildLocation($iFrameDocument[0].location); + self.syncChildLocation(iframeWindow.document.location); + + // Reset the scroll to the top of the window so that the overlay is visible again. + window.scrollTo(0, 0); + + var iframeTitle = self.$iframeDocument.attr('title'); + // Update the dialog title with the child window title. - $('.overlay .ui-dialog-title').html($iFrameDocument.attr('title')).focus(); + self.$container.dialog('option', 'title', iframeTitle); + self.$dialogTitlebar.find('.ui-dialog-title').focus(); // Add a title attribute to the iframe for accessibility. - self.iframe.$element.attr('title', Drupal.t('@title dialog', { '@title': $iFrameDocument.attr('title') })); + self.$iframe.attr('title', Drupal.t('@title dialog', { '@title': iframeTitle })); + // Remove any existing shortcut button markup in the title section. + self.$dialogTitlebar.find('.add-or-remove-shortcuts').remove(); // If the shortcut add/delete button exists, move it to the dialog title. - var addToShortcuts = $('.add-or-remove-shortcuts', $iFrameDocument); - if (addToShortcuts.length) { - // Remove any existing shortcut button markup in the title section. - $('.ui-dialog-titlebar .add-or-remove-shortcuts').remove(); + var $addToShortcuts = self.$iframeWindow('.add-or-remove-shortcuts'); + if ($addToShortcuts.length) { // Make the link overlay-friendly. - var $link = $('a', addToShortcuts); + var $link = $('a', $addToShortcuts); $link.attr('href', Drupal.overlay.fragmentizeLink($link.get(0))); // Move the button markup to the title section. We need to copy markup - // instead of moving the DOM element, because Webkit browsers will not - // move DOM elements between two DOM documents. - var shortcutsMarkup = '