drupal/modules/overlay/overlay.module

778 lines
25 KiB
Plaintext

<?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);
}