diff options
Diffstat (limited to 'modules/overlay/overlay-parent.js')
-rw-r--r-- | modules/overlay/overlay-parent.js | 885 |
1 files changed, 885 insertions, 0 deletions
diff --git a/modules/overlay/overlay-parent.js b/modules/overlay/overlay-parent.js new file mode 100644 index 000000000..d0dae0a4e --- /dev/null +++ b/modules/overlay/overlay-parent.js @@ -0,0 +1,885 @@ +// $Id$ + +(function ($) { + +/** + * Open the overlay, or load content into it, when an admin link is clicked. + */ +Drupal.behaviors.overlayParent = { + attach: function (context, settings) { + // Alter all admin links so that they will open in the overlay. + $('a', context).filter(function () { + return Drupal.overlay.isAdminLink(this.href); + }) + .once('overlay') + .each(function () { + // Move the link destination to a URL fragment. + this.href = Drupal.overlay.fragmentizeLink(this); + }); + + // Simulate the native click event for all links that appear outside the + // overlay. jQuery UI Dialog prevents all clicks outside a modal dialog. + $('.overlay-displace-top a', context) + .add('.overlay-displace-bottom a', context) + .click(function () { + window.location.href = this.href; + }); + + + // Resize the overlay when the toolbar drawer is toggled. + $('#toolbar a.toggle', context).once('overlay').click(function () { + setTimeout(function () { + Drupal.overlay.resize(Drupal.overlay.iframe.documentSize); + }, 150); + + }); + + // Make sure the onhashchange handling below is only processed once. + if (this.processed) { + return; + } + this.processed = true; + + // When the hash (URL fragment) changes, open the overlay if needed. + $(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') { + $.data(window.location, window.location.href, null); + } + // Otherwise, change the contents of the overlay to reflect the new hash. + else { + Drupal.overlay.trigger(); + } + }); + + // Trigger the hashchange event once, after the page is loaded, so that + // permalinks open the overlay. + $(window).trigger('hashchange'); + } +}; + +/** + * Overlay object for parent windows. + */ +Drupal.overlay = Drupal.overlay || { + options: {}, + iframe: { $container: null, $element: null }, + isOpen: false +}; + +/** + * Open an overlay. + * + * Ensure that only one overlay is opened ever. Use Drupal.overlay.load() if + * the overlay is already open but a new page needs to be opened. + * + * @param options + * Properties of the overlay to open: + * - url: the URL of the page to open in the overlay. + * - width: width of the overlay in pixels. + * - height: height of the overlay in pixels. + * - autoFit: boolean indicating whether the overlay should be resized to + * fit the contents of the document loaded. + * - onOverlayOpen: callback to invoke when the overlay is opened. + * - onOverlayCanClose: callback to allow external scripts decide if the + * overlay can be closed. + * - onOverlayClose: callback to invoke when the overlay is closed. + * - customDialogOptions: an object with custom jQuery UI Dialog options. + * + * @return + * If the overlay was opened true, otherwise false. + */ +Drupal.overlay.open = function (options) { + var self = this; + + // Just one overlay is allowed. + if (self.isOpen || $('#overlay-container').size()) { + return false; + } + + var defaultOptions = { + url: options.url, + width: options.width, + height: options.height, + autoFit: (options.autoFit == undefined || options.autoFit), + onOverlayOpen: options.onOverlayOpen, + 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');; + + return true; +}; + +/** + * Create the underlying markup and behaviors for the overlay. + * + * Reuses jQuery UI's dialog component to construct the overlay markup and + * behaviors, sanitizing the options previously set in self.options. + */ +Drupal.overlay.create = function () { + var self = this; + + self.iframe.$element = $(Drupal.theme('overlayElement')); + self.iframe.$container = $(Drupal.theme('overlayContainer')).append(self.iframe.$element); + + $('body').append(self.iframe.$container); + + // Open callback for jQuery UI Dialog. + var dialogOpen = function () { + // Unbind the keypress handler installed by ui.dialog itself. + // IE does not fire keypress events for some non-alphanumeric keys + // such as the tab character. http://www.quirksmode.org/js/keys.html + // 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; + }); + + // Replace the title span element with an h1 element for accessibility. + $('.overlay .ui-dialog-title').replaceWith(Drupal.theme('overlayTitleHeader', $('.overlay .ui-dialog-title').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); + }); + + if ($.isFunction(self.options.onOverlayOpen)) { + self.options.onOverlayOpen(self); + } + + self.isOpen = true; + }; + + // Before close callback for jQuery UI Dialog. + var dialogBeforeClose = function () { + if (self.beforeCloseEnabled) { + return true; + } + if (!self.beforeCloseIsBusy) { + self.beforeCloseIsBusy = true; + setTimeout(function () { self.close(); }, 1); + } + return false; + }; + + // 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; + } + self.isOpen = false; + }; + + // Default jQuery UI Dialog options. + var dialogOptions = { + modal: true, + autoOpen: false, + closeOnEscape: true, + resizable: false, + title: Drupal.t('Loading...'), + dialogClass: 'overlay', + zIndex: 500, + open: dialogOpen, + beforeclose: dialogBeforeClose, + close: dialogClose + }; + + // Allow external script override default jQuery UI Dialog options. + $.extend(dialogOptions, self.options.customDialogOptions); + + // Create the jQuery UI Dialog. + self.iframe.$container.dialog(dialogOptions); +}; + +/** + * Load the given URL into the overlay iframe. + * + * Use this method to change the URL being loaded in the overlay if it is + * already open. + */ +Drupal.overlay.load = function (url) { + var self = this; + var iframe = self.iframe.$element.get(0); + // 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; + } + // 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); +}; + +/** + * Check if the dialog can be closed. + */ +Drupal.overlay.canClose = function () { + 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; +}; + +/** + * Close the overlay and remove markup related to it from the document. + */ +Drupal.overlay.close = function (args, statusMessages) { + 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; + } + + // Check if the dialog can be closed. + if (!self.canClose()) { + delete self.beforeCloseIsBusy; + return false; + } + + // 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; +}; + +/** + * Redirect the overlay parent window to the given URL. + * + * @param link + * Can be an absolute URL or a relative link to the domain root. + */ +Drupal.overlay.redirect = function (link) { + if (link.indexOf('http') != 0 && link.indexOf('https') != 0) { + var absolute = location.href.match(/https?:\/\/[^\/]*/)[0]; + link = absolute + 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) { + var self = this; + var $iFrameWindow = iFrameWindow.jQuery; + var $iFrameDocument = $iFrameWindow(iFrameWindow.document); + var autoResizing = false; + self.iframe.Drupal = iFrameWindow.Drupal; + + // We are done if the child window is closing. + if (isClosing) { + return; + } + + // Make sure the parent window URL matches the child window URL. + self.syncChildLocation($iFrameDocument[0].location); + // Update the dialog title with the child window title. + $('.overlay .ui-dialog-title').html($iFrameDocument.attr('title')).focus(); + // Add a title attribute to the iframe for accessibility. + self.iframe.$element.attr('title', Drupal.t('@title dialog', { '@title': $iFrameDocument.attr('title') })); + + // 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(); + // Make the link overlay-friendly. + var $link = $('a', addToShortcuts); + $link.attr('href', Drupal.overlay.fragmentizeLink($link.get(0))); + // Move the button markup to the title section. + $('.overlay .ui-dialog-title').after(addToShortcuts); + } + + // Remove any existing tabs. + $('.overlay .ui-dialog-titlebar ul').remove(); + + // Setting tabIndex makes the div focusable. + $iFrameDocument.attr('tabindex', -1); + + $('.ui-dialog-titlebar-close-bg').animate({opacity: 0.9999}, 'fast'); + + // Perform animation to show the iframe element. + self.iframe.$element.fadeIn('fast', function () { + // @todo: Watch for experience in the way we compute the size of the + // iframed document. There are many ways to do it, and none of them + // seem to be perfect. Note though, that the size of the iframe itself + // may affect the size of the child document, especially on fluid layouts. + self.iframe.documentSize = { width: $iFrameDocument.width(), height: $iFrameWindow('body').height() + 25 }; + + // Adjust overlay to fit the iframe content? + if (self.options.autoFit) { + self.resize(self.iframe.documentSize); + } + + // Try to enhance keyboard based navigation of the overlay. + // Logic inspired by the open() method in ui.dialog.js, and + // http://wiki.codetalks.org/wiki/index.php/Docs/Keyboard_navigable_JS_widgets + + // Get a reference to the close button. + var $closeButton = $('.overlay .ui-dialog-titlebar-close'); + + // Search tabbable elements on the iframed document to speed up related + // keyboard events. + // @todo: Do we need to provide a method to update these references when + // AJAX requests update the DOM on the child document? + var $iFrameTabbables = $iFrameWindow(':tabbable:not(form)'); + var $firstTabbable = $iFrameTabbables.filter(':first'); + var $lastTabbable = $iFrameTabbables.filter(':last'); + + // Unbind keyboard event handlers that may have been enabled previously. + $(document).unbind('keydown.overlay-event'); + $closeButton.unbind('keydown.overlay-event'); + + // When the focus leaves the close button, then we want to jump to the + // first/last inner tabbable element of the child window. + $closeButton.bind('keydown.overlay-event', function (event) { + if (event.keyCode && event.keyCode == $.ui.keyCode.TAB) { + var $target = (event.shiftKey ? $lastTabbable : $firstTabbable); + if (!$target.size()) { + $target = $iFrameDocument; + } + setTimeout(function () { $target.focus(); }, 10); + return false; + } + }); + + // When the focus leaves the child window, then drive the focus to the + // close button of the dialog. + $iFrameDocument.bind('keydown.overlay-event', function (event) { + if (event.keyCode) { + if (event.keyCode == $.ui.keyCode.TAB) { + if (event.shiftKey && event.target == $firstTabbable.get(0)) { + setTimeout(function () { $closeButton.focus(); }, 10); + return false; + } + else if (!event.shiftKey && event.target == $lastTabbable.get(0)) { + setTimeout(function () { $closeButton.focus(); }, 10); + return false; + } + } + else if (event.keyCode == $.ui.keyCode.ESCAPE) { + setTimeout(function () { self.close(); }, 10); + return false; + } + } + }); + + var autoResize = function () { + if (typeof self.iframe.$element == 'undefined') { + autoResizing = false; + $(window).unbind('resize', windowResize); + return; + } + var iframeElement = self.iframe.$element.get(0); + var iframeDocument = (iframeElement.contentWindow || iframeElement.contentDocument); + if (iframeDocument.document) { + iframeDocument = iframeDocument.document; + } + // Use outerHeight() because otherwise the calculation will be off + // because of padding and/or border added by the theme. + var height = $(iframeDocument).find('body').outerHeight() + 25; + self.iframe.$element.css('height', height); + self.iframe.$container.css('height', height); + self.iframe.$container.parent().css('height', height + 45); + // Don't allow the shadow background to shrink so it's not enough to hide + // the whole page. Take the existing document height (with overlay) and + // the body height itself for our base calculation. + var docHeight = Math.min($(document).find('body').outerHeight(), $(document).height()); + $('.ui-widget-overlay').height(Math.max(docHeight, $(window).height(), height + 145)); + setTimeout(autoResize, 150); + }; + + var windowResize = function () { + var width = $(window).width() + var change = lastWidth - width; + var currentWidth = self.iframe.$element.width(); + var newWidth = lastFrameWidth - change; + lastWidth = width; + lastFrameWidth = newWidth; + + if (newWidth >= 300) { + self.iframe.$element.css('width', newWidth); + self.iframe.$container.css('width', newWidth); + self.iframe.$container.parent().css('width', newWidth); + widthBelowMin = false; + } + else { + widthBelowMin = true; + } + } + + if (!autoResizing) { + autoResizing = true; + autoResize(); + var lastFrameWidth = self.iframe.$element.width(); + var lastWidth = $(window).width(); + $(window).resize(windowResize); + } + + // When the focus is captured by the parent document, then try + // to drive the focus back to the first tabbable element, or the + // close button of the dialog (default). + $(document).bind('keydown.overlay-event', function (event) { + if (event.keyCode && event.keyCode == $.ui.keyCode.TAB) { + setTimeout(function () { + if (!$iFrameWindow(':tabbable:not(form):first').focus().size()) { + $closeButton.focus(); + } + }, 10); + return false; + } + }); + + // If there are tabs in the page, move them to the titlebar. + var tabs = $iFrameDocument.find('ul.primary').get(0); + + // This breaks in anything less than IE 7. Prevent it from running. + if (typeof tabs != 'undefined' && (!$.browser.msie || parseInt($.browser.version) >= 7)) { + $('.ui-dialog-titlebar').append($(tabs).remove().get(0)); + if ($(tabs).is('.primary')) { + $(tabs).find('a').removeClass('overlay-processed'); + Drupal.attachBehaviors($(tabs)); + } + // Remove any classes from the list element to avoid theme styles + // clashing with our styling. + $(tabs).removeAttr('class'); + } + }); +}; + +/** + * Unbind the child window. + * + * Remove keyboard event handlers, reset title and hide the iframe. + */ +Drupal.overlay.unbindChild = function (iFrameWindow) { + var self = this; + + // Prevent memory leaks by explicitly unbinding keyboard event handler + // on the child document. + iFrameWindow.jQuery(iFrameWindow.document).unbind('keydown.overlay-event'); + + // Change the overlay title. + $('.overlay .ui-dialog-title').html(Drupal.t('Please wait...')); + + // Hide the iframe element. + self.iframe.$element.fadeOut('fast'); +}; + +/** + * Check if the given link is in the administrative section of the site. + * + * @param url + * The url to be tested. + * @return boolean + * TRUE if the URL represents an administrative link, FALSE otherwise. + */ +Drupal.overlay.isAdminLink = function (url) { + var self = this; + // Create a native Link object, so we can use its object methods. + var link = $(url.link(url)).get(0); + var path = link.pathname.replace(new RegExp(Drupal.settings.basePath), ''); + if (path == '') { + // If the path appears empty, it might mean the path is represented in the + // query string (clean URLs are not used). + var match = new RegExp("(\\?|&)q=(.+)(&|$)").exec(link.search); + if (match && match.length == 4) { + path = match[2]; + } + } + + // Turn the list of administrative paths into a regular expression. + if (!self.adminPathRegExp) { + var adminPaths = '^(' + Drupal.settings.overlay.paths.admin.replace(/\s+/g, ')$|^(') + ')$'; + var nonAdminPaths = '^(' + Drupal.settings.overlay.paths.non_admin.replace(/\s+/g, ')$|^(') + ')$'; + adminPaths = adminPaths.replace(/\*/g, '.*'); + nonAdminPaths = nonAdminPaths.replace(/\*/g, '.*'); + self.adminPathRegExp = new RegExp(adminPaths); + self.nonAdminPathRegExp = new RegExp(nonAdminPaths); + } + + return self.adminPathRegExp.exec(path) && !self.nonAdminPathRegExp.exec(path); +} + +/** + * Sanitize dialog size. + * + * Do not let the overlay go over the 0.78x of the width of the screen and set + * minimal height. The height is not limited due to how we rely on the parent + * window to provide scrolling instead of scrolling in scrolling with the + * overlay. + * + * @param size + * Contains 'width' and 'height' items as numbers. + * @return + * The same structure with sanitized number values. + */ +Drupal.overlay.sanitizeSize = function (size) { + var width, height; + var $window = $(window); + + // Use 300px as the minimum width but at most expand to 78% of the window. + // Ensures that users see that there is an actual website in the background. + var minWidth = 300, maxWidth = parseInt($window.width() * .78); + if (typeof size.width != 'number') { + width = maxWidth; + } + // Set to at least minWidth but at most maxWidth. + else if (size.width < minWidth || size.width > maxWidth) { + width = Math.min(maxWidth, Math.max(minWidth, size.width)); + } + else { + width = size.width; + } + + // Use 100px as the minimum height. Expand to 92% of the window if height + // was invalid, to ensure that we have a reasonable chance to show content. + var minHeight = 100, maxHeight = parseInt($window.height() * .92); + if (typeof size.height != 'number') { + height = maxHeight; + } + else if (size.height < minHeight) { + // Do not consider maxHeight as the actual maximum height, since we rely on + // the parent window scroll bar to scroll the window. Only set up to be at + // least the minimal height. + height = Math.max(minHeight, size.height); + } + else { + height = size.height; + } + return { width: width, height: height }; +}; + +/** + * Compute position to center horizontally and on viewport top vertically. + */ +Drupal.overlay.computePosition = function ($element, elementSize) { + var $window = $(window); + // Consider any region that should be visible above the overlay (such as + // an admin toolbar). + var $toolbar = $('.overlay-displace-top'); + var toolbarHeight = 0; + $toolbar.each(function () { + toolbarHeight += $toolbar.height(); + }); + var position = { + left: Math.max(0, parseInt(($window.width() - elementSize.width) / 2)), + top: toolbarHeight + 20 + }; + + // Reset the scroll to the top of the window so that the overlay is visible again. + window.scrollTo(0, 0); + return position; +}; + +/** + * Resize overlay to the given size. + * + * @param size + * Contains 'width' and 'height' items as numbers. + */ +Drupal.overlay.resize = function (size) { + var self = this; + + // Compute frame and dialog size based on requested document size. + var titleBarHeight = $('.overlay .ui-dialog-titlebar').outerHeight(true); + var frameSize = self.sanitizeSize(size); + var dialogSize = $.extend({}, frameSize); + dialogSize.height += titleBarHeight + 15; + + // Compute position on viewport. + var dialogPosition = self.computePosition($('.overlay'), dialogSize); + + var animationOptions = $.extend(dialogSize, dialogPosition); + + // Perform the resize animation. + $('.overlay').animate(animationOptions, 'fast', function () { + // Proceed only if the dialog still exists. + if ($.isObject(self.iframe.$element) && $.isObject(self.iframe.$container)) { + // Resize 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 }); + + // Keep the dim background grow or shrink with the dialog. + $('.ui-widget-overlay').height($(document).height()); + + // Animate body opacity, so we fade in the page as it loads in. + $(self.iframe.$element.get(0)).contents().find('body.overlay').animate({opacity: 0.9999}, 'slow'); + } + }); +}; + +/** + * Add overlay rendering GET parameter to the given href. + */ +Drupal.overlay.addOverlayParam = function (href) { + return $.param.querystring(href, {'render': 'overlay'}); + // Do not process links with an empty href, or that only have the fragment or + // which are external links. + if (href.length > 0 && href.charAt(0) != '#' && href.indexOf('http') != 0 && href.indexOf('https') != 0) { + var fragmentIndex = href.indexOf('#'); + var fragment = ''; + if (fragmentIndex != -1) { + fragment = href.substr(fragmentIndex); + href = href.substr(0, fragmentIndex); + } + href += (href.indexOf('?') > -1 ? '&' : '?') + 'render=overlay' + fragment; + } + return href; +}; + +/** + * Open, reload, or close the overlay, based on the current URL fragment. + */ +Drupal.overlay.trigger = function () { + // Get the overlay URL from the current URL fragment. + var state = $.bbq.getState('overlay'); + if (state) { + // Append render variable, so the server side can choose the right + // rendering and add child modal frame code to the page if needed. + var linkURL = Drupal.overlay.addOverlayParam(Drupal.settings.basePath + state); + + // If the modal frame is already open, replace the loaded document with + // this new one. + if (Drupal.overlay.isOpen) { + Drupal.overlay.load(linkURL); + } + else { + // There is not an overlay opened yet; we should open a new one. + var overlayOptions = { + url: linkURL, + onOverlayClose: function () { + // Clear the overlay URL fragment. + $.bbq.pushState(); + // Remove active class from all header buttons. + $('a.overlay-processed').each(function () { + $(this).removeClass('active'); + }); + }, + draggable: false + }; + Drupal.overlay.open(overlayOptions); + } + } + else { + // If there is no overlay URL in the fragment, close the overlay. + try { + Drupal.overlay.close(); + } + catch(e) { + // The close attempt may have failed because the overlay isn't open. + // If so, no special handling is needed here. + } + } +}; + +/** + * Make a regular admin link into a URL that will trigger the overlay to open. + * + * @param link + * A Javascript Link object (i.e. an <a> element). + * @return + * A URL that will trigger the overlay (in the form + * /node/1#overlay=admin/config). + */ +Drupal.overlay.fragmentizeLink = function (link) { + // Don't operate on links that are already overlay-ready. + var params = $.deparam.fragment(link.href); + if (params.overlay) { + return link.href; + } + + // Determine the link's original destination, and make it relative to the + // Drupal site. + var fullpath = link.pathname; + var re = new RegExp('^' + Drupal.settings.basePath); + var path = fullpath.replace(re, ''); + // Preserve existing query and fragment parameters in the URL. + var fragment = link.hash; + var querystring = link.search; + // If the query includes ?render=overlay, leave it out. + if (querystring.indexOf('render=overlay') !== -1) { + querystring = querystring.replace(/render=overlay/, ''); + if (querystring === '?') { + querystring = ''; + } + } + + var destination = path + querystring + fragment; + + // Assemble the overlay-ready link. + var base = window.location.href; + return $.param.fragment(base, {'overlay':destination}); +} + +/** + * Make sure the internal overlay URL is reflected in the parent URL fragment. + * + * Normally the parent URL fragment determines the overlay location. However, if + * the overlay redirects internally, the parent doesn't get informed, and the + * parent URL fragment will be out of date. This is a sanity check to make + * sure we're in the right place. + * + * @param childLocation + * The child window's location object. + */ +Drupal.overlay.syncChildLocation = function (childLocation) { + var expected = $.bbq.getState('overlay'); + // This is just a sanity check, so we're comparing paths, not query strings. + expected = Drupal.settings.basePath + expected.replace(/\?.+/, ''); + var actual = childLocation.pathname; + if (expected !== actual) { + // There may have been a redirect inside the child overlay window that the + // parent wasn't aware of. Update the parent URL fragment appropriately. + var newLocation = Drupal.overlay.fragmentizeLink(childLocation); + // Set a 'redirect' flag on the new location so the hashchange event handler + // knows not to change the overlay's content. + $.data(window.location, newLocation, 'redirect'); + window.location.href = newLocation; + } +}; + +/** + * Refresh any regions of the page that are displayed outside the overlay. + * + * @param data + * An array of objects with information on the page regions to be refreshed. + * For each object, the key is a CSS class identifying the region to be + * refreshed, and the value represents the section of the Drupal $page array + * corresponding to this region. + */ +Drupal.overlay.refreshRegions = function (data) { + $.each(data, function () { + var region_info = this; + $.each(region_info, function (regionClass) { + var regionName = region_info[regionClass]; + var regionSelector = '.' + regionClass; + $.get(Drupal.settings.basePath + Drupal.settings.overlay.ajaxCallback + '/' + regionName, function (newElement) { + $(regionSelector).replaceWith($(newElement)); + Drupal.attachBehaviors($(regionSelector), Drupal.settings); + }); + }); + }); +}; + +/** + * Theme function to create the overlay iframe element. + */ +Drupal.theme.prototype.overlayElement = function () { + // Note: We use scrolling="yes" for IE as a workaround to yet another IE bug + // where the horizontal scrollbar is always rendered no matter how wide the + // iframe element is defined. + return '<iframe id="overlay-element" frameborder="0" name="overlay-element"'+ ($.browser.msie ? ' scrolling="yes"' : '') +'/>'; +}; + +/** + * Theme function to create a container for the overlay iframe element. + */ +Drupal.theme.prototype.overlayContainer = function () { + return '<div id="overlay-container"/>'; +} + +/** + * Theme function for the overlay title markup. + */ +Drupal.theme.prototype.overlayTitleHeader = function (text) { + return '<h1 id="ui-dialog-title-overlay-container" class="ui-dialog-title" tabindex="-1" unselectable="on">' + text + '</h1>'; +}; + +/** + * Theme function for the shortcuts button next to the overlay title. + */ +Drupal.theme.prototype.overlayShortcutsButton = function (text) { + return '<div class="add-or-remove-shortcuts">' + text + '</div>'; +} + +})(jQuery); |