diff options
author | Angie Byron <webchick@24967.no-reply.drupal.org> | 2009-12-02 07:28:22 +0000 |
---|---|---|
committer | Angie Byron <webchick@24967.no-reply.drupal.org> | 2009-12-02 07:28:22 +0000 |
commit | c912008307dd49b5e3e85c78069153f2f9f70411 (patch) | |
tree | 6e219858505bd38130a7d679488d3a41e689ce1d /modules/overlay/overlay.module | |
parent | cfc96df9a7181526cb1c71cf4e39886026c668fe (diff) | |
download | brdo-c912008307dd49b5e3e85c78069153f2f9f70411.tar.gz brdo-c912008307dd49b5e3e85c78069153f2f9f70411.tar.bz2 |
#610234 by Gábor Hojtsy, ksenzee, cwgordon7, David_Rothstein, seutje, marcvangend, sun, JoshuaRogers, markus_petrux, Bojhan, Rob Loach, Everett Zufelt, drifter, markboulton, leisareichelt, et al: Added Overlay module to core, which shows administrative pages in a JS overlay, retaining context on the front-end site.
Diffstat (limited to 'modules/overlay/overlay.module')
-rw-r--r-- | modules/overlay/overlay.module | 777 |
1 files changed, 777 insertions, 0 deletions
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); +} |