diff --git a/includes/common.inc b/includes/common.inc index e22ce998ac275bf9f065b582928389cf4381831f..9f8b6a66b81dd551db9473e609042a9b84ac02cb 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -3590,6 +3590,28 @@ function drupal_html_id($id) { return $id; } +/** + * Provides a standard HTML class name that identifies a page region. + * + * It is recommended that template preprocess functions apply this class to any + * page region that is output by the theme (Drupal core already handles this in + * the standard template preprocess implementation). Standardizing the class + * names in this way allows modules to implement certain features, such as + * drag-and-drop or dynamic AJAX loading, in a theme-independent way. + * + * @param $region + * The name of the page region (for example, 'page_top' or 'content'). + * + * @return + * An HTML class that identifies the region (for example, 'region-page-top' + * or 'region-content'). + * + * @see template_preprocess_region() + */ +function drupal_region_class($region) { + return drupal_html_class("region-$region"); +} + /** * Add a JavaScript file, setting or inline code to the page. * diff --git a/includes/menu.inc b/includes/menu.inc index ecdd15075baa0cd59229333d043df318b0254dd7..934160684a28f5be95cadf37981855d7db6e87cd 100644 --- a/includes/menu.inc +++ b/includes/menu.inc @@ -2185,6 +2185,9 @@ function menu_cache_clear($menu_name = 'navigation') { register_shutdown_function('cache_clear_all', 'links:' . $menu_name . ':', 'cache_menu', TRUE); $cache_cleared[$menu_name] = 2; } + + // Also clear the menu system static caches. + menu_reset_static_cache(); } /** @@ -2193,6 +2196,16 @@ function menu_cache_clear($menu_name = 'navigation') { */ function menu_cache_clear_all() { cache_clear_all('*', 'cache_menu', TRUE); + menu_reset_static_cache(); +} + +/** + * Resets the menu system static cache. + */ +function menu_reset_static_cache() { + drupal_static_reset('menu_tree'); + drupal_static_reset('menu_tree_all_data'); + drupal_static_reset('menu_tree_page_data'); } /** diff --git a/includes/path.inc b/includes/path.inc index 3ce40a818b30d9694046e3eaadffe5a3f08a7765..12214daadb70af2a706bb2d7d5087422b36dc250 100644 --- a/includes/path.inc +++ b/includes/path.inc @@ -482,3 +482,65 @@ function path_delete($criteria) { drupal_clear_path_cache(); } +/** + * Determine whether a path is in the administrative section of the site. + * + * By default, paths are considered to be non-administrative. If a path does not + * match any of the patterns in path_get_admin_paths(), or if it matches both + * administrative and non-administrative patterns, it is considered + * non-administrative. + * + * @param $path + * A Drupal path. + * @return + * TRUE if the path is administrative, FALSE otherwise. + * + * @see path_get_admin_paths() + * @see hook_admin_paths() + * @see hook_admin_paths_alter() + */ +function path_is_admin($path) { + $path_map = &drupal_static(__FUNCTION__); + if (!isset($path_map['admin'][$path])) { + $patterns = path_get_admin_paths(); + $path_map['admin'][$path] = drupal_match_path($path, $patterns['admin']); + $path_map['non_admin'][$path] = drupal_match_path($path, $patterns['non_admin']); + } + return $path_map['admin'][$path] && !$path_map['non_admin'][$path]; +} + +/** + * Get a list of administrative and non-administrative paths. + * + * @return array + * An associative array containing the following keys: + * 'admin': An array of administrative paths and regular expressions + * in a format suitable for drupal_match_path(). + * 'non_admin': An array of non-administrative paths and regular expressions. + * + * @see hook_admin_paths() + * @see hook_admin_paths_alter() + */ +function path_get_admin_paths() { + $patterns = &drupal_static(__FUNCTION__); + if (!isset($patterns)) { + $paths = module_invoke_all('admin_paths'); + drupal_alter('admin_paths', $paths); + // Combine all admin paths into one array, and likewise for non-admin paths, + // for easier handling. + $patterns = array(); + $patterns['admin'] = array(); + $patterns['non_admin'] = array(); + foreach ($paths as $path => $enabled) { + if ($enabled) { + $patterns['admin'][] = $path; + } + else { + $patterns['non_admin'][] = $path; + } + } + $patterns['admin'] = implode("\n", $patterns['admin']); + $patterns['non_admin'] = implode("\n", $patterns['non_admin']); + } + return $patterns; +} diff --git a/includes/theme.inc b/includes/theme.inc index 8fb6a5acb4e6af599118505086c7f6d7be5d7a2d..63af79711fc358692accd3d733367172153e0d9f 100644 --- a/includes/theme.inc +++ b/includes/theme.inc @@ -2580,6 +2580,7 @@ function template_preprocess_maintenance_page(&$variables) { * pluggable template engine. Uses the region name to generate a template file * suggestions. If none are found, the default region.tpl.php is used. * + * @see drupal_region_class() * @see region.tpl.php */ function template_preprocess_region(&$variables) { @@ -2587,7 +2588,7 @@ function template_preprocess_region(&$variables) { $variables['content'] = $variables['elements']['#children']; $variables['region'] = $variables['elements']['#region']; - $region = 'region-' . str_replace('_', '-', $variables['region']); + $region = drupal_region_class($variables['region']); $variables['classes_array'][] = $region; $variables['template_files'][] = $region; } diff --git a/misc/jquery.ba-bbq.js b/misc/jquery.ba-bbq.js new file mode 100644 index 0000000000000000000000000000000000000000..73e64df4f519cdd56fbb2b9b7cffa5384436d59f --- /dev/null +++ b/misc/jquery.ba-bbq.js @@ -0,0 +1,11 @@ +// $Id$ + +/* + * jQuery BBQ: Back Button & Query Library - v1.0.2 - 10/10/2009 + * http://benalman.com/projects/jquery-bbq-plugin/ + * + * Copyright (c) 2009 "Cowboy" Ben Alman + * Dual licensed under the MIT and GPL licenses. + * http://benalman.com/about/license/ + */ +(function($,c){var g,k=document.location,i=Array.prototype.slice,E=decodeURIComponent,a=$.param,m,d,p,n=$.bbq=$.bbq||{},o,e,z,b="hashchange",v="querystring",y="fragment",q="hash",x="elemUrlAttr",h="href",D="src",C=$.browser,l=C.msie&&C.version<8,j="on"+b in c&&!l,r=/^.*\?|#.*$/g,A=/^.*\#/,t={};function s(F){return typeof F==="string"}function w(G){var F=i.call(arguments,1);return function(){return G.apply(this,F.concat(i.call(arguments)))}}function f(G,O,F,H,K){var M,L,J,N,I;if(H!==g){J=F.match(G?/^([^#]*)\#?(.*)$/:/^([^#?]*)\??([^#]*)(#?.*)/);I=J[3]||"";if(K===2&&s(H)){L=H.replace(O,"")}else{N=d(J[2]);H=s(H)?d[G?y:v](H):H;L=K===2?H:K===1?$.extend({},H,N):$.extend({},N,H);L=a(L)}M=J[1]+(G?"#":L||!J[1]?"?":"")+L+I}else{if(F){M=F.replace(O,"")}else{M=G?k[q]?k[h].replace(O,""):"":k.search.replace(/^\??/,"")}}return M}a[v]=w(f,0,r);a[y]=m=w(f,1,A);$.deparam=d=function(I,G){var H={},F={"true":!0,"false":!1,"null":null};$.each(I.replace(/\+/g," ").split("&"),function(M,O){var L=O.split("="),P=E(L[0]),K,Q=H,N=0,R=P.split("]["),J=R.length-1;if(/\[/.test(R[0])&&/\]$/.test(R[J])){R[J]=R[J].replace(/\]$/,"");R=R.shift().split("[").concat(R);J=R.length-1}else{J=0}if(L.length===2){K=E(L[1]);if(G){K=K&&!isNaN(K)?+K:K==="undefined"?g:F[K]!==g?F[K]:K}if(J){for(;N<=J;N++){P=R[N]===""?Q.length:R[N];Q=Q[P]=N').hide().appendTo("body")[0].contentWindow;J=function(){return F.document.location[q].replace(/^#/,"")};H=function(N,L){if(N!==L){var M=F.document;M.open();M.close();M.location[q]="#"+N}};H(m())}}G.start=function(){if(K){return}var M=m();H||I();(function L(){var O=m(),N=J(M);if(O!==M){H(M=O,N);$(c).trigger(b)}else{if(N!==M){o("#"+N)}}K=setTimeout(L,n.pollDelay)})()};G.stop=function(){if(!F){K&&clearTimeout(K);K=0}};return G})()})(jQuery,this); \ No newline at end of file diff --git a/modules/dashboard/dashboard.css b/modules/dashboard/dashboard.css index d6bd6cd7026722f10fe4b4aa95d032b39dd9f14f..840d38ce3cb54e8bd76487a41bd31da1eeaa8567 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 2b2c6cc45e3533f773296026c4d9e80598e53b7b..2199053170d655e74aa1aa9d0b244e013576fbc3 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(''); + $('div.customize .canvas-content').prepend('' + Drupal.t('Done') + ''); $('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 d91139f831725342206bab81d94b755d567f7378..24ca6c4c3bbdeff8132f958e96259f09970952ca 100644 --- a/modules/node/node.module +++ b/modules/node/node.module @@ -246,6 +246,20 @@ function node_field_build_modes($obj_type) { return $modes; } +/** + * 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. * diff --git a/modules/overlay/images/close.png b/modules/overlay/images/close.png new file mode 100644 index 0000000000000000000000000000000000000000..b76db1fcf7f303cb0e2be9b5f040c009cc96f200 --- /dev/null +++ b/modules/overlay/images/close.png @@ -0,0 +1,4 @@ +PNG + + IHDRJLsBIT|d pHYs  ~tEXtCreation Time09/09/2009tEXtSoftwareAdobe FireworksONSIDATHŖq0E2)N"Wu-PJ: +Bfn oG$=6͎xDu4;9l  X_bEQ9GQ1v-UU zUU95*s: 2r39'Au]QBP4=X])ɷ$, AI=X,KR !p8({VIP=(RzʦZ[3ncJ2 [P*!D"I@R[33E轗 դwM|B{./A!׭[6E .7P=_-IENDB` \ No newline at end of file diff --git a/modules/overlay/images/loading.gif b/modules/overlay/images/loading.gif new file mode 100644 index 0000000000000000000000000000000000000000..57e45b8573880a25996c32d590a5f2bd70764dd1 --- /dev/null +++ b/modules/overlay/images/loading.gif @@ -0,0 +1 @@ +GIF89a%%Ž{{{sssccc! NETSCAPE2.0!,%%'didDKDAGFSA$ 1@P$ $ ުytIv5qFqx1~ o~+4"/1$ ;D;z)i4T+Ok7;  4XrwQq46 â¡L, x?t,`{m*y *r#}d;{ p!!,'`Dq9|A!, hAp%a_!,  ' `y!!,! `h (!,'`Cq!|!,  @ aIE!, !G'F`!, Pa1!, @a&j!, W'hF`!,  F !D)!!,  P'hp9_!,g_ELsBƉ|!,7$'@Q4 iT2Q!,  w$ErF!!,! ABd&iH!,W EHs6i|!,  `,b13!, !DWDh!`&!, 0EK!, `0b&38j!, DWD0!,7 WD%hP|@; \ No newline at end of file diff --git a/modules/overlay/overlay-child.js b/modules/overlay/overlay-child.js new file mode 100644 index 0000000000000000000000000000000000000000..1b8e54a72eb8b5a78a08596a5e904d29ffce806e --- /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 0000000000000000000000000000000000000000..d4c9566951756ce910a511e8d64bd9066db86409 --- /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 0000000000000000000000000000000000000000..d0dae0a4e6f940a42ec481c159fe64261ef776c5 --- /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 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 '