diff options
Diffstat (limited to 'modules')
-rw-r--r-- | modules/dashboard/dashboard.css | 4 | ||||
-rw-r--r-- | modules/dashboard/dashboard.js | 2 | ||||
-rw-r--r-- | modules/node/node.module | 14 | ||||
-rw-r--r-- | modules/overlay/images/close.png | bin | 0 -> 505 bytes | |||
-rw-r--r-- | modules/overlay/images/loading.gif | bin | 0 -> 1154 bytes | |||
-rw-r--r-- | modules/overlay/overlay-child.js | 153 | ||||
-rw-r--r-- | modules/overlay/overlay-parent.css | 130 | ||||
-rw-r--r-- | modules/overlay/overlay-parent.js | 885 | ||||
-rw-r--r-- | modules/overlay/overlay.api.php | 46 | ||||
-rw-r--r-- | modules/overlay/overlay.info | 8 | ||||
-rw-r--r-- | modules/overlay/overlay.install | 19 | ||||
-rw-r--r-- | modules/overlay/overlay.module | 777 | ||||
-rw-r--r-- | modules/shortcut/shortcut.css | 1 | ||||
-rw-r--r-- | modules/shortcut/shortcut.module | 14 | ||||
-rw-r--r-- | modules/system/system.api.php | 46 | ||||
-rw-r--r-- | modules/system/system.module | 31 | ||||
-rw-r--r-- | modules/toolbar/toolbar.css | 3 | ||||
-rw-r--r-- | modules/toolbar/toolbar.js | 10 | ||||
-rw-r--r-- | modules/toolbar/toolbar.module | 13 |
19 files changed, 2147 insertions, 9 deletions
diff --git a/modules/dashboard/dashboard.css b/modules/dashboard/dashboard.css index d6bd6cd70..840d38ce3 100644 --- a/modules/dashboard/dashboard.css +++ b/modules/dashboard/dashboard.css @@ -64,9 +64,11 @@ border: 0; } -#dashboard .canvas-content input { +#dashboard .canvas-content a.button { float: right; margin: 0 0 0 10px; + color: #5a5a5a; + text-decoration: none; } #dashboard .region { diff --git a/modules/dashboard/dashboard.js b/modules/dashboard/dashboard.js index 2b2c6cc45..219905317 100644 --- a/modules/dashboard/dashboard.js +++ b/modules/dashboard/dashboard.js @@ -65,7 +65,7 @@ Drupal.behaviors.dashboard = { * Helper for enterCustomizeMode; sets up drag-and-drop and close button. */ setupDrawer: function () { - $('div.customize .canvas-content').prepend('<input type="button" class="form-submit" value="' + Drupal.t('Done') + '"></input>'); + $('div.customize .canvas-content').prepend('<a class="button" href="">' + Drupal.t('Done') + '</a>'); $('div.customize .canvas-content input').click(Drupal.behaviors.dashboard.exitCustomizeMode); // Initialize drag-and-drop. diff --git a/modules/node/node.module b/modules/node/node.module index d91139f83..24ca6c4c3 100644 --- a/modules/node/node.module +++ b/modules/node/node.module @@ -247,6 +247,20 @@ function node_field_build_modes($obj_type) { } /** + * Implement hook_admin_paths(). + */ +function node_admin_paths() { + $paths = array( + 'node/*/add' => TRUE, + 'node/*/edit' => TRUE, + 'node/*/delete' => TRUE, + 'node/add' => TRUE, + 'node/add/*' => TRUE, + ); + return $paths; +} + +/** * Gather a listing of links to nodes. * * @param $result diff --git a/modules/overlay/images/close.png b/modules/overlay/images/close.png Binary files differnew file mode 100644 index 000000000..b76db1fcf --- /dev/null +++ b/modules/overlay/images/close.png diff --git a/modules/overlay/images/loading.gif b/modules/overlay/images/loading.gif Binary files differnew file mode 100644 index 000000000..57e45b857 --- /dev/null +++ b/modules/overlay/images/loading.gif diff --git a/modules/overlay/overlay-child.js b/modules/overlay/overlay-child.js new file mode 100644 index 000000000..1b8e54a72 --- /dev/null +++ b/modules/overlay/overlay-child.js @@ -0,0 +1,153 @@ +// $Id$ + +(function ($) { + +/** + * Overlay object for child windows. + */ +Drupal.overlayChild = Drupal.overlayChild || { processed: false, behaviors: {} }; + +/** + * Attach the child dialog behavior to new content. + */ +Drupal.behaviors.overlayChild = { + attach: function (context, settings) { + var self = Drupal.overlayChild; + var settings = settings.overlayChild || {}; + + // Make sure this behavior is not processed more than once. + if (self.processed) { + return; + } + self.processed = true; + + // If we cannot reach the parent window, then we have nothing else to do + // here. + if (!$.isObject(parent.Drupal) || !$.isObject(parent.Drupal.overlay)) { + return; + } + + // If a form has been submitted successfully, then the server side script + // may have decided to tell us the parent window to close the popup dialog. + if (settings.closeOverlay) { + parent.Drupal.overlay.bindChild(window, true); + // Close the child window from a separate thread because the current + // one is busy processing Drupal behaviors. + setTimeout(function () { + // We need to store the parent variable locally because it will + // disappear as soon as we close the iframe. + var p = parent; + p.Drupal.overlay.close(settings.args, settings.statusMessages); + if (typeof settings.redirect == 'string') { + p.Drupal.overlay.redirect(settings.redirect); + } + }, 1); + return; + } + + // If one of the regions displaying outside the overlay needs to be + // reloaded, let the parent window know. + if (settings.refreshRegions) { + parent.Drupal.overlay.refreshRegions(settings.refreshRegions); + } + + // Ok, now we can tell the parent window we're ready. + parent.Drupal.overlay.bindChild(window); + + // If a form is being displayed, it has a hidden field for the parent + // window's location. Pass it that information. Letting the server side + // know the parent window's location lets us avoid unnecessary redirects + // when the overlay window is being closed automatically. + var re = new RegExp('^' + parent.Drupal.settings.basePath); + var path = parent.window.location.pathname.replace(re, ''); + $('#edit-overlay-parent-url').val(path); + + // Install onBeforeUnload callback, if module is present. + if ($.isObject(Drupal.onBeforeUnload) && !Drupal.onBeforeUnload.callbackExists('overlayChild')) { + Drupal.onBeforeUnload.addCallback('overlayChild', function () { + // Tell the parent window we're unloading. + parent.Drupal.overlay.unbindChild(window); + }); + } + + // Attach child related behaviors to the iframe document. + self.attachBehaviors(context, settings); + } +}; + +/** + * Attach child related behaviors to the iframe document. + */ +Drupal.overlayChild.attachBehaviors = function (context, settings) { + $.each(this.behaviors, function () { + this(context, settings); + }); +}; + +/** + * Scroll to the top of the page. + * + * This makes the overlay visible to users even if it is not as tall as the + * previously shown overlay was. + */ +Drupal.overlayChild.behaviors.scrollToTop = function (context, settings) { + window.scrollTo(0, 0); +}; + +/** + * Modify links and forms depending on their relation to the overlay. + * + * By default, forms and links are assumed to keep the flow in the overlay. + * Thus their action and href attributes respectively get a ?render=overlay + * suffix. Non-administrative links should however close the overlay and + * redirect the parent page to the given link. This would include links in a + * content listing, where administration options are mixed with links to the + * actual content to be shown on the site out of the overlay. + * + * @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; + }); + return; + } + 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; + }); + } + } + }); + $('form:not(.overlay-processed)', context).addClass('overlay-processed').each(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. + action += (action.indexOf('?') > -1 ? '&' : '?') + 'render=overlay'; + $(this).attr('action', action); + } + else { + $(this).attr('target', '_new'); + } + }); +}; + +})(jQuery); diff --git a/modules/overlay/overlay-parent.css b/modules/overlay/overlay-parent.css new file mode 100644 index 000000000..d4c956695 --- /dev/null +++ b/modules/overlay/overlay-parent.css @@ -0,0 +1,130 @@ +/* $Id$ */ + +/** + * ui-dialog overlay. + */ +.ui-widget-overlay { + background-color: #000; + opacity: 0.7; + filter: alpha(opacity=80); + background-image: none; +} + +/** + * jQuery UI Dialog classes. + */ +.overlay { + padding-right: 26px; +} + +.overlay.ui-widget-content, .overlay .ui-widget-header { + background: none; + border: none; +} + +.overlay .ui-dialog-titlebar { + white-space: nowrap; + padding: 0 20px; +} + +.overlay .ui-dialog-title { + font-family: Verdana,sans-serif; + margin: 0; + padding: 0.3em 0; + color: #fff; + font-size: 20px; +} +.overlay .ui-dialog-title:active, +.overlay .ui-dialog-title:focus { + outline: 0; +} +.overlay .ui-dialog-titlebar-close, +.overlay .ui-dialog-titlebar-close:hover { + display: block; + right: -25px; + top: 100%; + margin: 0; + border: none; + padding: 0; + width: 26px; + height: 36px; + background: transparent url(images/close.png) no-repeat; + -moz-border-radius-topleft: 0; + -webkit-border-top-left-radius: 0; +} +.overlay .ui-dialog-titlebar-close span { + display: none; +} +.overlay .ui-dialog-content { + color: #292929; + background-color: #f8f8f8; +} + +/** + * Overlay content and shadows. + */ +.overlay #overlay-container { + margin: 0; + padding: 0; + 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 #overlay-element { + overflow: hidden; +} + +/** + * Tabs on the overlay. + */ +.overlay .ui-dialog-titlebar ul { + position: absolute; + right: 20px; + bottom: 0; + margin: 0; + line-height: 27px; + text-transform: uppercase; +} +.overlay .ui-dialog-titlebar ul li { + display: inline-block; + 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 { + background-color: #a6a7a2; + -moz-border-radius: 8px 8px 0 0; + -webkit-border-top-left-radius: 8px; + -webkit-border-top-right-radius: 8px; + border-radius: 8px 8px 0 0; + color: #000; + font-weight: bold; + padding: 5px 14px; + text-decoration: none; + font-size: 11px; +} +.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; +} +.overlay .ui-dialog-titlebar ul li a:hover { + color: #fff; +} +.overlay .ui-dialog-titlebar ul li.active a:hover { + color: #000; +} + +/** + * Add to shortcuts link + */ +.overlay div.add-or-remove-shortcuts { + padding-top: 0.9em; +} 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); diff --git a/modules/overlay/overlay.api.php b/modules/overlay/overlay.api.php new file mode 100644 index 000000000..499c157c3 --- /dev/null +++ b/modules/overlay/overlay.api.php @@ -0,0 +1,46 @@ +<?php +// $Id$ + +/** + * @file + * Hooks provided by Overlay module. + */ + +/** + * @addtogroup hooks + * @{ + */ + +/** + * Allow modules to act when an overlay parent window is initialized. + * + * The parent window is initialized when a page is displayed in which the + * overlay might be required to be displayed, so modules can act here if they + * need to take action to accomodate the possibility of the overlay appearing + * within a Drupal page. + */ +function hook_overlay_parent_initialize() { + // Add our custom JavaScript. + drupal_add_js(drupal_get_path('module', 'hook') . '/hook-overlay.js'); +} + +/** + * Allow modules to act when an overlay child window is initialized. + * + * The child window is initialized when a page is displayed from within the + * overlay, so modules can act here if they need to take action to work from + * within the confines of the overlay. + */ +function hook_overlay_child_initialize() { + // Use a different theme for content administration pages. + if (arg(0) == 'admin' && arg(1) == 'content') { + if ($theme = variable_get('content_administration_pages_theme', FALSE)) { + global $custom_theme; + $custom_theme = $theme; + } + } +} + +/** + * @} End of "addtogroup hooks". + */ diff --git a/modules/overlay/overlay.info b/modules/overlay/overlay.info index e69de29bb..7afc0597e 100644 --- a/modules/overlay/overlay.info +++ b/modules/overlay/overlay.info @@ -0,0 +1,8 @@ +; $Id$ +name = Overlay +description = Displays the Drupal administration interface in an overlay. +package = Core +version = VERSION +core = 7.x +files[] = overlay.module +files[] = overlay.install diff --git a/modules/overlay/overlay.install b/modules/overlay/overlay.install new file mode 100644 index 000000000..a864b5eb1 --- /dev/null +++ b/modules/overlay/overlay.install @@ -0,0 +1,19 @@ +<?php +// $Id$ + +/** + * @file + * Install, update and uninstall functions for the overlay module. + */ + +/** + * Implements hook_enable(). + * + * If the module is being enabled through the admin UI, and not from an + * install profile, reopen the modules page in an overlay. + */ +function overlay_enable() { + if (strpos(current_path(), 'admin/config/modules') === 0) { + drupal_goto('<front>', array('fragment' => 'overlay=admin/config/modules')); + } +} diff --git a/modules/overlay/overlay.module b/modules/overlay/overlay.module new file mode 100644 index 000000000..101599009 --- /dev/null +++ b/modules/overlay/overlay.module @@ -0,0 +1,777 @@ +<?php +// $Id$ + +/** + * @file + * Displays the Drupal administration interface in an overlay. + */ + +/** + * Implements hook_menu(). + */ +function overlay_menu() { + $items['overlay-ajax/%'] = array( + 'title' => '', + 'page callback' => 'overlay_ajax_render_region', + 'page arguments' => array(1), + 'access arguments' => array('access overlay'), + 'type' => MENU_CALLBACK, + ); + return $items; +} + +/** + * Implements hook_permission(). + */ +function overlay_permission() { + return array( + 'access overlay' => array( + 'title' => t('Access the administrative overlay'), + 'description' => t('View administrative pages in the overlay.'), + ), + ); +} + +/** + * Implements hook_init(). + * + * Determine whether the current page request is destined to appear in the + * parent window or in the overlay window, and format the page accordingly. + * + * @see overlay_set_mode() + */ +function overlay_init() { + // @todo: custom_theme does not exist anymore. + global $custom_theme; + // Only act if the user has access to administration pages. Other modules can + // also enable the overlay directly for other uses of the JavaScript. + if (user_access('access overlay')) { + if (isset($_GET['render']) && $_GET['render'] == 'overlay') { + // If this page shouldn't be rendered here, redirect to the parent. + if (!path_is_admin($_GET['q'])) { + overlay_close_dialog(); + } + // If system module did not switch the theme yet (i.e. this is not an + // admin page, per se), we should switch the theme here. + $admin_theme = variable_get('admin_theme', 0); + if ($custom_theme != $admin_theme) { + $custom_theme = $admin_theme; + drupal_add_css(drupal_get_path('module', 'system') . '/admin.css'); + } + // Indicate that we are viewing an overlay child page. + overlay_set_mode('child'); + } + else { + // Otherwise add overlay parent code and our behavior. + overlay_set_mode('parent'); + } + } +} + +/** + * Implements hook_exit(). + * + * When viewing an overlay child page, check if we need to trigger a refresh of + * the supplemental regions of the overlay on the next page request. + */ +function overlay_exit() { + // Check that we are in an overlay child page. Note that this should never + // return TRUE on a cached page view, since the child mode is not set until + // overlay_init() is called. + if (overlay_get_mode() == 'child') { + // Load any markup that was stored earlier in the page request, via calls + // to overlay_store_rendered_content(). If none was stored, this is not a + // page request where we expect any changes to the overlay supplemental + // regions to have occurred, so we do not need to proceed any further. + $original_markup = overlay_get_rendered_content(); + if (!empty($original_markup)) { + // Compare the original markup to the current markup that we get from + // rendering each overlay supplemental region now. If they don't match, + // something must have changed, so we request a refresh of that region + // within the parent window on the next page request. + foreach (overlay_supplemental_regions() as $region) { + if (!isset($original_markup[$region]) || $original_markup[$region] != overlay_render_region($region)) { + overlay_request_refresh($region); + } + } + } + } +} + +/** + * Implements hook_element_info_alter(). + */ +function overlay_element_info_alter(&$types) { + foreach (array('submit', 'button', 'image_button', 'form') as $type) { + $types[$type]['#after_build'][] = 'overlay_form_after_build'; + } +} + +/** + * Implements hook_library(). + */ +function overlay_library() { + $module_path = drupal_get_path('module', 'overlay'); + + // Overlay parent. + $libraries['parent'] = array( + 'title' => 'Overlay: Parent', + 'website' => 'http://drupal.org/node/517688', + 'version' => '1.0', + 'js' => array( + $module_path . '/overlay-parent.js' => array(), + ), + 'css' => array( + $module_path . '/overlay-parent.css' => array(), + ), + 'dependencies' => array( + array('system', 'ui.dialog'), + array('system', 'jquery-bbq'), + ), + ); + // Overlay child. + $libraries['child'] = array( + 'title' => 'Overlay: Child', + 'website' => 'http://drupal.org/node/517688', + 'version' => '1.0', + 'js' => array( + $module_path . '/overlay-child.js' => array(), + ), + 'dependencies' => array( + array('system', 'ui'), + ), + ); + + return $libraries; +} + +/** + * Implements hook_form_alter(). + * + * For forms displayed in the overlay, add a hidden form field that lets us pass + * the parent window's URL into the form. + */ +function overlay_form_alter(&$form, &$form_state, $form_id) { + if (overlay_get_mode() == 'child') { + $form['overlay_parent_url'] = array( + '#type' => 'hidden', + ); + } +} + +/** + * Implements hook_drupal_goto_alter(). + * + * If the current page request is inside the overlay, add ?render=overlay to + * the new path, so that it appears correctly inside the overlay. + * + * @see overlay_get_mode() + */ +function overlay_drupal_goto_alter(&$path, &$options, &$http_response_code) { + if (overlay_get_mode() == 'child') { + if (isset($options['query'])) { + $options['query'] += array('render' => 'overlay'); + } + else { + $options['query'] = array('render' => 'overlay'); + } + } +} + +/** + * Implements hook_batch_alter(). + * + * If the current page request is inside the overlay, add ?render=overlay to + * the success callback URL, so that it appears correctly within the overlay. + * + * @see overlay_get_mode() + */ +function overlay_batch_alter(&$batch) { + if (overlay_get_mode() == 'child') { + if (isset($batch['url_options']['query'])) { + $batch['url_options']['query']['render'] = 'overlay'; + } + else { + $batch['url_options']['query'] = array('render' => 'overlay'); + } + } +} + +/** + * Implements hook_page_alter(). + */ +function overlay_page_alter(&$page) { + // If we are limiting rendering to a subset of page regions, deny access to + // all other regions so that they will not be processed. + if ($regions_to_render = overlay_get_regions_to_render()) { + $skipped_regions = array_diff(element_children($page), $regions_to_render); + foreach ($skipped_regions as $skipped_region) { + $page[$skipped_region]['#access'] = FALSE; + } + } +} + +/** + * Implements hook_block_info_alter(). + */ +function overlay_block_info_alter(&$blocks) { + // If we are limiting rendering to a subset of page regions, hide all blocks + // which appear in regions not on that list. Note that overlay_page_alter() + // does a more comprehensive job of preventing unwanted regions from being + // displayed (regardless of whether they contain blocks or not), but the + // reason for duplicating effort here is performance; we do not even want + // these blocks to be built if they are not going to be displayed. + if ($regions_to_render = overlay_get_regions_to_render()) { + foreach ($blocks as $bid => $block) { + if (!in_array($block->region, $regions_to_render)) { + unset($blocks[$bid]); + } + } + } +} + +/** + * Implements hook_system_info_alter(). + * + * Add default regions for the overlay. + */ +function overlay_system_info_alter(&$info, $file, $type) { + if ($type == 'theme') { + $info['overlay_regions'][] = 'content'; + $info['overlay_regions'][] = 'help'; + } +} + +/** + * Preprocess template variables for html.tpl.php. + * + * If the current page request is inside the overlay, add appropriate classes + * to the <body> element, and simplify the page title. + * + * @see overlay_get_mode() + */ +function overlay_preprocess_html(&$variables) { + if (overlay_get_mode() == 'child') { + // Add overlay class, so themes can react to being displayed in the overlay. + $variables['classes_array'][] = 'overlay'; + // Do not include site name or slogan in the overlay title. + $variables['head_title'] = drupal_get_title(); + } +} + +/** + * Preprocess template variables for page.tpl.php. + * + * Display breadcrumbs correctly inside the overlay. + * + * @see overlay_get_mode() + */ +function overlay_preprocess_page(&$variables) { + if (overlay_get_mode() == 'child') { + // Remove 'Home' from the breadcrumbs. + $overlay_breadcrumb = drupal_get_breadcrumb(); + array_shift($overlay_breadcrumb); + $variables['breadcrumb'] = theme('breadcrumb', array('breadcrumb' => $overlay_breadcrumb)); + } +} + +/** + * Preprocess template variables for toolbar.tpl.php. + * + * Adding the 'overlay-displace-top' class to the toolbar pushes the overlay + * down, so it appears below the toolbar. + */ +function overlay_preprocess_toolbar(&$variables) { + $variables['classes_array'][] = "overlay-displace-top"; +} + +/** + * Form after_build callback. + * + * After all hook_form_alter() implementations have been processed, we look at + * the list of submit handlers and add our own at the end. The added handler + * determines whether or not the user is redirected done at the end of form + * processing, so that it's possible to close the overlay after submitting + * a form. + * + * @see _form_builder_handle_input_element() + * @see _form_builder_ie_cleanup() + * @see form_execute_handlers() + * @see form_builder() + * @see overlay_form_submit() + * + * @ingroup forms + */ +function overlay_form_after_build($form, &$form_state) { + if (isset($_GET['render']) && $_GET['render'] == 'overlay') { + // Form API may have already captured submit handlers from the submitted + // button before after_build callback is invoked. This may have been done + // by _form_builder_handle_input_element(). If so, the list of submit + // handlers is stored in the $form_state array, which is something we can + // also alter from here, luckily. Rememeber: our goal here is to set + // $form_state['redirect'] to FALSE if the API function + // overlay_request_dialog_close() has been invoked. That's because we want + // to tell the parent window to close the overlay. + if (!empty($form_state['submit_handlers']) && !in_array('overlay_form_submit', $form_state['submit_handlers'])) { + $form_state['submit_handlers'][] = 'overlay_form_submit'; + } + // If this element has submit handlers, then append our own. + if (isset($form['#submit'])) { + $form['#submit'][] = 'overlay_form_submit'; + } + } + return $form; +} + +/** + * Generic form submit handler. + * + * When we are requested to close an overlay, we don't want Form API to + * perform any redirection once the submitted form has been processed. + * + * When $form_state['redirect'] is set to FALSE, then Form API will simply + * re-render the form with the values still in its fields. And this is all + * we need to output the JavaScript that will tell the parent window to close + * the child dialog. + * + * @see overlay_get_mode() + * @ingroup forms + */ +function overlay_form_submit($form, &$form_state) { + $settings = &drupal_static(__FUNCTION__); + + // Check if we have a request to close the overlay. + $args = overlay_request_dialog_close(); + + // Close the overlay if the overlay module has been disabled + if (!module_exists('overlay')) { + $args = overlay_request_dialog_close(TRUE); + } + + // If there is a form redirect to a non-admin page, close the overlay. + if (isset($form_state['redirect'])) { + // A destination set in the URL trumps $form_state['redirect']. + if (isset($_GET['destination'])) { + $url = $_GET['destination']; + $url_settings = array(); + } + elseif (is_array($form_state['redirect'])) { + $url = $form_state['redirect'][0]; + $url_settings = $form_state['redirect'][1]; + } + else { + $url = $form_state['redirect']; + $url_settings = array(); + } + if (!path_is_admin($url)) { + $args = overlay_request_dialog_close(TRUE); + } + } + + // If the overlay is to be closed, pass that information through JavaScript. + if ($args !== FALSE) { + if (!isset($settings)) { + $settings = array( + 'overlayChild' => array( + 'closeOverlay' => TRUE, + 'statusMessages' => theme('status_messages'), + 'args' => $args, + ), + ); + // Tell the child window to perform the redirection when requested to. + if (!empty($form_state['redirect'])) { + $settings['overlayChild']['redirect'] = url($url, $settings); + } + // If the redirect destination is the same as the parent window, just + // close the overlay without redirecting the parent. + if (url($form['overlay_parent_url']['#value']) == $settings['overlayChild']['redirect']) { + unset($settings['overlayChild']['redirect']); + } + drupal_add_js($settings, array('type' => 'setting')); + } + // Tell FAPI to redraw the form without redirection after all submit + // callbacks have been processed. + $form_state['redirect'] = FALSE; + } +} + +/** + * Get the current overlay mode. + * + * @see overlay_set_mode() + */ +function overlay_get_mode() { + return overlay_set_mode(NULL); +} + +/** + * Set overlay mode and add proper JavaScript and styles to the page. + * + * @param $mode + * To set the mode, pass in either 'parent' or 'child'. 'parent' is used in + * the context of a parent window (a regular browser window), and JavaScript + * is added so that administrative links in the parent window will open in + * an overlay. 'child' is used in the context of the child overlay window (the + * page actually appearing within the overlay iframe) and JavaScript and CSS + * are added so that Drupal behaves nicely from within the overlay. + * + * This parameter is optional, and if omitted, the current mode will be + * returned with no action taken. + * + * @return + * The current mode, if any has been set, or NULL if no mode has been set. + * + * @ingroup overlay_api + */ +function overlay_set_mode($mode = NULL) { + global $base_path; + $overlay_mode = &drupal_static(__FUNCTION__); + + // Make sure external resources are not included more than once. Also return + // the current mode, if no mode was specified. + if (isset($overlay_mode) || !isset($mode)) { + return $overlay_mode; + } + $overlay_mode = $mode; + + switch ($overlay_mode) { + case 'parent': + drupal_add_library('overlay', 'parent'); + drupal_add_library('overlay', 'jquery-bbq'); + + // Allow modules to act upon overlay events. + module_invoke_all('overlay_parent_initialize'); + break; + + case 'child': + drupal_add_library('overlay', 'child'); + + // Allow modules to act upon overlay events. + module_invoke_all('overlay_child_initialize'); + break; + } + return $overlay_mode; +} + +/** + * Implements hook_overlay_parent_initialize(). + */ +function overlay_overlay_parent_initialize() { + // Let the client side know which paths are administrative. + $paths = path_get_admin_paths(); + foreach ($paths as &$type) { + $type = str_replace('<front>', variable_get('site_frontpage', 'node'), $type); + } + drupal_add_js(array('overlay' => array('paths' => $paths)), 'setting'); + // Pass along the AJAX callback for rerendering sections of the parent window. + drupal_add_js(array('overlay' => array('ajaxCallback' => 'overlay-ajax')), 'setting'); +} + +/** + * Implements hook_overlay_child_initialize(). + */ +function overlay_overlay_child_initialize() { + // Check if the parent window needs to refresh any page regions on this page + // request. + overlay_trigger_regions_to_refresh(); + // If this is a POST request, or a GET request with a token parameter, we + // have an indication that something in the supplemental regions of the + // overlay might change during the current page request. We therefore store + // the initial rendered content of those regions here, so that we can compare + // it to the same content rendered in overlay_exit(), at the end of the page + // request. This allows us to check if anything actually did change, and, if + // so, trigger an AJAX refresh of the parent window. + if (!empty($_POST) || isset($_GET['token'])) { + foreach (overlay_supplemental_regions() as $region) { + overlay_store_rendered_content($region, overlay_render_region($region)); + } + } + // Indicate that when the main page rendering occurs later in the page + // request, only the regions that appear within the overlay should be + // rendered. + overlay_set_regions_to_render(overlay_regions()); +} + +/** + * Callback to request that the overlay close on the next page load. + * + * @param $value + * By default, the dialog will not close. Set to TRUE or a value evaluating to + * TRUE to request the dialog to close. Use FALSE to disable closing the + * dialog (if it was previously enabled). The value passed will be forwarded + * to the onOverlayClose callback of the overlay. + * + * @return + * The current overlay close dialog mode, a value evaluating to TRUE if the + * overlay should close or FALSE if it should not (default). + */ +function overlay_request_dialog_close($value = NULL) { + $close = &drupal_static(__FUNCTION__, FALSE); + if (isset($value)) { + $close = $value; + } + return $close; +} + +/** + * Close the overlay and redirect the parent window to a new path. + * + * @param $redirect + * The path that should open in the parent window after the overlay closes. + */ +function overlay_close_dialog($redirect = NULL) { + if (empty($redirect)) { + $path = $_GET['q']; + } + $settings = array( + 'overlayChild' => array( + 'closeOverlay' => TRUE, + 'statusMessages' => theme('status_messages'), + 'args' => $args, + 'redirect' => url($redirect), + ), + ); + drupal_add_js($settings, array('type' => 'setting')); + return $settings; +} + +/** + * Returns a list of page regions that appear in the overlay. + * + * Overlay regions correspond to the entire contents of the overlay child + * window and are refreshed each time a new page request is made within the + * overlay. + * + * @return + * An array of region names that correspond to those which appear in the + * overlay, within the theme that is being used to display the current page. + * + * @see overlay_supplemental_regions() + */ +function overlay_regions() { + return _overlay_region_list('overlay_regions'); +} + +/** + * Returns a list of supplemental page regions for the overlay. + * + * Supplemental overlay regions are those which are technically part of the + * parent window, but appear to the user as being related to the overlay + * (usually because they are displayed next to, rather than underneath, the + * main overlay regions) and therefore need to be dynamically refreshed if any + * administrative actions taken within the overlay change their contents. + * + * An example of a typical overlay supplemental region would be the 'page_top' + * region, in the case where a toolbar is being displayed there. + * + * @return + * An array of region names that correspond to supplemental overlay regions, + * within the theme that is being used to display the current page. + * + * @see overlay_regions() + */ +function overlay_supplemental_regions() { + return _overlay_region_list('overlay_supplemental_regions'); +} + +/** + * Helper function for returning a list of page regions related to the overlay. + * + * @param $type + * The type of regions to return. This can either be 'overlay_regions' or + * 'overlay_supplemental_regions'. + * + * @return + * An array of region names of the given type, within the theme that is being + * used to display the current page. + * + * @see overlay_regions() + * @see overlay_supplemental_regions() + */ +function _overlay_region_list($type) { + // Obtain the current theme. We need to first make sure the theme system is + // initialized, since this function can be called early in the page request. + drupal_theme_initialize(); + $themes = list_themes(); + $theme = $themes[$GLOBALS['theme']]; + // Return the list of regions stored within the theme's info array, or an + // empty array if no regions of the appropriate type are defined. + return !empty($theme->info[$type]) ? $theme->info[$type] : array(); +} + +/** + * Returns a list of page regions that rendering should be limited to. + * + * @return + * An array containing the names of the regions that will be rendered when + * drupal_render_page() is called. If empty, then no limits will be imposed, + * and all regions of the page will be rendered. + * + * @see overlay_page_alter() + * @see overlay_block_info_alter() + * @see overlay_set_regions_to_render() + */ +function overlay_get_regions_to_render() { + return overlay_set_regions_to_render(); +} + +/** + * Sets the regions of the page that rendering will be limited to. + * + * @param $regions + * (Optional) An array containing the names of the regions that should be + * rendered when drupal_render_page() is called. Pass in an empty array to + * remove all limits and cause drupal_render_page() to render all page + * regions (the default behavior). If this parameter is omitted, no change + * will be made to the current list of regions to render. + * + * @return + * The current list of regions to render, or an empty array if the regions + * are not being limited. + * + * @see overlay_page_alter() + * @see overlay_block_info_alter() + * @see overlay_get_regions_to_render() + */ +function overlay_set_regions_to_render($regions = NULL) { + $regions_to_render = &drupal_static(__FUNCTION__, array()); + if (isset($regions)) { + $regions_to_render = $regions; + } + return $regions_to_render; +} + +/** + * Renders an individual page region. + * + * This function is primarily intended to be used for checking the content of + * supplemental overlay regions (e.g., a region containing a toolbar). Passing + * in a region that is intended to display the main page content is not + * supported; the region will be rendered by this function, but the main page + * content will not appear in it. + * + * @param $region + * The name of the page region that should be rendered. + * + * @return + * The rendered HTML of the provided region. + */ +function overlay_render_region($region) { + // Indicate the region that we will be rendering, so that other regions will + // be hidden by overlay_page_alter() and overlay_block_info_alter(). + overlay_set_regions_to_render(array($region)); + // Do what is necessary to force drupal_render_page() to only display HTML + // from the requested region. Specifically, declare that the main page + // content does not need to automatically be added to the page, and pass in + // a page array that has all theme functions removed (so that overall HTML + // for the page will not be added either). + $system_main_content_added = &drupal_static('system_main_content_added'); + $system_main_content_added = TRUE; + $page = array( + '#type' => 'page', + '#theme' => NULL, + '#theme_wrappers' => array(), + ); + $markup = drupal_render_page($page); + // Indicate that the main page content has not, in fact, been displayed, so + // that future calls to drupal_render_page() will be able to render it + // correctly. + $system_main_content_added = FALSE; + // Restore the original behavior of rendering all regions for the next time + // drupal_render_page() is called. + overlay_set_regions_to_render(array()); + return $markup; +} + +/** + * Returns any rendered content that was stored earlier in the page request. + * + * @return + * An array of all rendered HTML that was stored earlier in the page request, + * keyed by the identifier with which it was stored. If no content was + * stored, an empty array is returned. + * + * @see overlay_store_rendered_content() + */ +function overlay_get_rendered_content() { + return overlay_store_rendered_content(); +} + +/** + * Stores strings representing rendered HTML content. + * + * This function is used to keep a static cache of rendered content that can be + * referred to later in the page request. + * + * @param $id + * (Optional) An identifier for the content which is being stored, which will + * be used as an array key in the returned array. If omitted, no change will + * be made to the current stored data. + * @param $content + * (Optional) A string representing the rendered data to store. This only has + * an effect if $id is also provided. + * + * @return + * An array representing all data that is currently being stored, or an empty + * array if there is none. + * + * @see overlay_get_rendered_content() + */ +function overlay_store_rendered_content($id = NULL, $content = NULL) { + $rendered_content = &drupal_static(__FUNCTION__, array()); + if (isset($id)) { + $rendered_content[$id] = $content; + } + return $rendered_content; +} + +/** + * Request that the parent window refresh a particular page region. + * + * @param $region + * The name of the page region to refresh. The parent window will trigger a + * refresh of this region on the next page load. + * + * @see overlay_trigger_regions_to_refresh() + * @see Drupal.overlay.refreshRegions() + */ +function overlay_request_refresh($region) { + $class = drupal_region_class($region); + $_SESSION['overlay_regions_to_refresh'][] = array($class => $region); +} + +/** + * Check if the parent window needs to refresh any regions on this page load. + * + * If the previous page load requested that any page regions be refreshed, pass + * that request via JavaScript to the child window, so it can in turn pass the + * request to the parent window. + * + * @see overlay_request_refresh() + * @see Drupal.overlay.refreshRegions() + */ +function overlay_trigger_regions_to_refresh() { + if (!empty($_SESSION['overlay_regions_to_refresh'])) { + $settings = array( + 'overlayChild' => array( + 'refreshRegions' => $_SESSION['overlay_regions_to_refresh'], + ), + ); + drupal_add_js($settings, array('type' => 'setting')); + unset($_SESSION['overlay_regions_to_refresh']); + } +} + +/** + * Prints the markup obtained by rendering a single region of the page. + * + * This function is intended to be called via AJAX. + * + * @param $region + * The name of the page region to render. + * + * @see Drupal.overlay.refreshRegions() + */ +function overlay_ajax_render_region($region) { + print overlay_render_region($region); +} diff --git a/modules/shortcut/shortcut.css b/modules/shortcut/shortcut.css index 3fc45148e..b07f6a025 100644 --- a/modules/shortcut/shortcut.css +++ b/modules/shortcut/shortcut.css @@ -7,7 +7,6 @@ div#toolbar div.toolbar-shortcuts ul { padding: 5px 0; height: 40px; line-height: 30px; - overflow: hidden; float: left; margin-left:5px; } diff --git a/modules/shortcut/shortcut.module b/modules/shortcut/shortcut.module index 92bd58c77..225cb8e32 100644 --- a/modules/shortcut/shortcut.module +++ b/modules/shortcut/shortcut.module @@ -330,6 +330,7 @@ function shortcut_set_assign_user($shortcut_set, $account) { ->key(array('uid' => $account->uid)) ->fields(array('set_name' => $shortcut_set->set_name)) ->execute(); + drupal_static_reset('shortcut_current_displayed_set'); } /** @@ -600,3 +601,16 @@ function shortcut_preprocess_page(&$variables) { $variables['add_or_remove_shortcut'] = drupal_render($variables['page']['add_or_remove_shortcut']); } } + +/** + * Implements hook_system_info_alter(). + * + * If the overlay module is enabled, indicate that the link for adding or + * removing shortcuts is one of the page "regions" that should display in the + * overlay. + */ +function shortcut_system_info_alter(&$info, $file, $type) { + if (module_exists('overlay') && $type == 'theme') { + $info['overlay_regions'][] = 'add_or_remove_shortcut'; + } +} diff --git a/modules/system/system.api.php b/modules/system/system.api.php index 24643966a..f85c014a1 100644 --- a/modules/system/system.api.php +++ b/modules/system/system.api.php @@ -166,6 +166,52 @@ function hook_entity_load($entities, $type) { } /** + * Define administrative paths. + * + * Modules may specify whether or not the paths they define in hook_menu() are + * to be considered administrative. Other modules may use this information to + * display those pages differently (e.g. in a modal overlay, or in a different + * theme). + * + * To change the administrative status of menu items defined in another module's + * hook_menu(), modules should implement hook_admin_paths_alter(). + * + * @return + * An associative array. For each item, the key is the path in question, in + * a format acceptable to drupal_match_path(). The value for each item should + * be TRUE (for paths considered administrative) or FALSE (for non- + * administrative paths). + * + * @see hook_menu() + * @see drupal_match_path() + * @see hook_admin_paths_alter() + */ +function hook_admin_paths() { + $paths = array( + 'mymodule/*/add' => TRUE, + 'mymodule/*/edit' => TRUE, + ); + return $paths; +} + +/** + * Redefine administrative paths defined by other modules. + * + * @param $paths + * An associative array of administrative paths, as defined by implementations + * of hook_admin_paths(). + * + * @see hook_admin_paths() + */ +function hook_admin_paths_alter(&$paths) { + // Treat all user pages as administrative. + $paths['user'] = TRUE; + $paths['user/*'] = TRUE; + // Treat the forum topic node form as a non-administrative page. + $paths['node/add/forum'] = FALSE; +} + +/** * Perform periodic actions. * * This hook will only be called if cron.php is run (e.g. by crontab). diff --git a/modules/system/system.module b/modules/system/system.module index 33219edcf..1d4d90ff3 100644 --- a/modules/system/system.module +++ b/modules/system/system.module @@ -1082,6 +1082,16 @@ function system_library() { ), ); + // jQuery BBQ plugin. + $libraries['jquery-bbq'] = array( + 'title' => 'jQuery BBQ', + 'website' => 'http://benalman.com/projects/jquery-bbq-plugin/', + 'version' => '1.0.2', + 'js' => array( + 'misc/jquery.ba-bbq.js', + ), + ); + // Contextual links. $libraries['contextual-links'] = array( 'title' => 'Contextual links', @@ -3069,13 +3079,16 @@ function system_page_build(&$page) { '#markup' => theme('system_run_cron_image', array('image_path' => 'system/run-cron-image')), ); } +} - // Find all block regions so they can be rendered. +/** + * Implements hook_page_alter(). + */ +function system_page_alter(&$page) { + // Find all non-empty page regions, and add a theme wrapper function that + // allows them to be consistently themed. $regions = system_region_list($GLOBALS['theme']); - - // Load all region content assigned via blocks. foreach (array_keys($regions) as $region) { - // Don't render empty regions. if (!empty($page[$region])) { $page[$region]['#theme_wrappers'][] = 'region'; $page[$region]['#region'] = $region; @@ -3661,3 +3674,13 @@ function system_build_contextual_links($element) { return $build; } +/** + * Implement hook_admin_paths(). + */ +function system_admin_paths() { + $paths = array( + 'admin' => TRUE, + 'admin/*' => TRUE, + ); + return $paths; +} diff --git a/modules/toolbar/toolbar.css b/modules/toolbar/toolbar.css index 2e1894f70..9f9838dcb 100644 --- a/modules/toolbar/toolbar.css +++ b/modules/toolbar/toolbar.css @@ -36,7 +36,7 @@ div#toolbar { left: 0; right: 0; top: 0; - z-index: 100; + z-index: 600; } div#toolbar .collapsed { @@ -71,7 +71,6 @@ div#toolbar div.toolbar-menu { height: 25px; line-height: 20px; padding: 5px 10px 0; - overflow: hidden; position: relative; } diff --git a/modules/toolbar/toolbar.js b/modules/toolbar/toolbar.js index 9c07f78bd..18b366102 100644 --- a/modules/toolbar/toolbar.js +++ b/modules/toolbar/toolbar.js @@ -15,6 +15,16 @@ Drupal.behaviors.admin = { Drupal.admin.toolbar.toggle(); return false; }); + + // Set the most recently clicked item as active. + $('#toolbar a').once().click(function() { + $('#toolbar a').each(function() { + $(this).removeClass('active'); + }); + if ($(this).parents('div.toolbar-shortcuts').length) { + $(this).addClass('active'); + } + }); } }; diff --git a/modules/toolbar/toolbar.module b/modules/toolbar/toolbar.module index ddb1a452d..f668cb135 100644 --- a/modules/toolbar/toolbar.module +++ b/modules/toolbar/toolbar.module @@ -134,6 +134,19 @@ function toolbar_preprocess_html(&$vars) { } /** + * Implements hook_system_info_alter(). + * + * If the overlay module is enabled, indicate that the 'page_top' region (in + * which the toolbar will be displayed) is one of the overlay supplemental + * regions that should be refreshed whenever its content is updated. + */ +function toolbar_system_info_alter(&$info, $file, $type) { + if (module_exists('overlay') && $type == 'theme') { + $info['overlay_supplemental_regions'][] = 'page_top'; + } +} + +/** * Build the admin menu as a structured array ready for drupal_render(). */ function toolbar_build() { |