Index: modules/help/help.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/help/help.admin.inc,v retrieving revision 1.6 diff -u -r1.6 help.admin.inc --- modules/help/help.admin.inc 14 Apr 2008 17:48:37 -0000 1.6 +++ modules/help/help.admin.inc 8 May 2008 01:23:25 -0000 @@ -23,7 +23,7 @@ $output = ''; if (module_hook($name, 'help')) { $module = drupal_parse_info_file(drupal_get_path('module', $name) . '/' . $name . '.info'); - drupal_set_title($module['name']); + drupal_set_title(t('Help: ') . $module['name']); $temp = module_invoke($name, 'help', "admin/help#$name", drupal_help_arg()); if (empty($temp)) { Index: modules/system/system.css =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.css,v retrieving revision 1.51 diff -u -r1.51 system.css --- modules/system/system.css 5 May 2008 21:10:48 -0000 1.51 +++ modules/system/system.css 8 May 2008 01:23:25 -0000 @@ -549,3 +549,54 @@ span.password-confirm span { font-weight: normal; } + +/* +** AJAX popup dialog box styles +*/ + +#popup-overlay { + position: absolute; + background: black; + z-index: 9; + top: 0; +} +#popup-loading { + z-index: 10; + opacity: 0.75; + position: relative; +} +#popup-loading div{ + position: absolute; +} +#popup { + background: white; + position: absolute; + z-index: 10; + padding: 0.5em; + width: 600px; + overflow: auto; +} +#popup-title { + font-weight: bold; + margin-bottom: 0.25em; +} +#popup-title div.title { + float: left; +} +#popup-title #popup-close { + float: right; +} +#popup-title #popup-close a { + font-weight: normal; +} +#popup-title div.clear { + clear: both; +} +/* Allow messages to be used as the title of the popup */ +#popup div.messages { + background: transparent; + border: none; + padding: 0; + margin: 0; +} + Index: modules/system/system.module =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.module,v retrieving revision 1.602 diff -u -r1.602 system.module --- modules/system/system.module 7 May 2008 19:17:50 -0000 1.602 +++ modules/system/system.module 8 May 2008 01:23:26 -0000 @@ -147,6 +147,11 @@ 'arguments' => array('image_path' => NULL), ), 'system_compact_link' => array(), + 'popup' => array( + 'template' => 'popup', + ), + 'popup_save_dialog' => array(), + )); } @@ -643,6 +648,13 @@ 'access callback' => TRUE, 'type' => MENU_CALLBACK, ); + + // Themable page save dialog. + $items['popup/save_dialog'] = array( + 'page callback' => 'theme', + 'page arguments' => array('popup_save_dialog'), + 'access callback' => TRUE, + ); return $items; } @@ -2045,6 +2057,63 @@ } /** + * Implements hook_theme_modes. + * + * Provide a rendering mode for JSON based popup dialogs. + */ +function system_theme_modes() { + return array( + 'json/popup' => 'system_json_popup_render' + ); +} + +/** + * Render the page as JSON, for consumption by AJAX calls. + * Optimized for AJAX popups. + * + * @return + * JSON object with metadata and themed page content. + */ +function system_json_popup_render() { + $args = func_get_args(); + $hook = array_shift($args); + switch ($hook) { + case 'page': // render('page', $content); + return drupal_json(array( + 'status' => 'ok', + 'title' => drupal_get_title(), // TODO: filter. + 'messages' => theme('status_messages'), + 'path' => $_GET['q'], + 'content' => $args[0], + )); + default: + array_unshift($args, $hook); + return call_user_func_array('theme_render', $args); + } +} + +/** + * Implements hook_after_process_form + */ +function system_after_process_form($request_mode, $form, $form_state) { + switch ($request_mode) { + case 'XMLHttpRequest': + // Request comes from jQuery AJAX call. + // Return status, next page URL and form state as JSON. + $url = drupal_redirect_form($form, $form_state['redirect'], FALSE); + print drupal_json(array( + 'status' => 'redirect', + 'path' => $url, + 'form_state' => $form_state, + )); + exit; + case 'standard': + // Default Drupal behavior, redirect to the next page. + drupal_redirect_form($form, $form_state['redirect']); + } +} + +/** * Format the Powered by Drupal text. * * @ingroup themeable Index: modules/system/system.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.admin.inc,v retrieving revision 1.75 diff -u -r1.75 system.admin.inc --- modules/system/system.admin.inc 7 May 2008 19:17:50 -0000 1.75 +++ modules/system/system.admin.inc 8 May 2008 01:23:25 -0000 @@ -2092,7 +2092,7 @@ $row = array(); $description = ''; if (isset($form['help'][$key])) { - $description = '
'. drupal_render($form['help'][$key]) .'
'; + $description = '
' . drupal_render($form['help'][$key]) . '
'; } $description .= drupal_render($form['description'][$key]); if (isset($form['status']['#incompatible_modules_core'][$key])) { Index: includes/form.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/form.inc,v retrieving revision 1.272 diff -u -r1.272 form.inc --- includes/form.inc 6 May 2008 12:18:45 -0000 1.272 +++ includes/form.inc 8 May 2008 01:23:25 -0000 @@ -435,7 +435,9 @@ // however, we'll skip this and let the calling function examine // the resulting $form_state bundle itself. if (!$form['#programmed'] && empty($form_state['rebuild']) && empty($form_state['storage'])) { - drupal_redirect_form($form, $form_state['redirect']); + global $request_mode; + // TODO - more comments here, and update the above comments. + module_invoke_all('after_process_form', $request_mode, $form, $form_state); } } } @@ -608,8 +610,13 @@ * @param $redirect * An optional value containing the destination path to redirect * to if none is specified by the form. + * @param $do_goto + * Boolean flag. + * If true, call to drupal_goto to do the redirect. + * If false, return the URL of the redirect, but don't go there. This allows + * for alternal flows of control, such as AJAX or webservices. */ -function drupal_redirect_form($form, $redirect = NULL) { +function drupal_redirect_form($form, $redirect = NULL, $do_goto = TRUE) { $goto = NULL; if (isset($redirect)) { $goto = $redirect; @@ -617,17 +624,31 @@ if ($goto !== FALSE && isset($form['#redirect'])) { $goto = $form['#redirect']; } - if (!isset($goto) || ($goto !== FALSE)) { - if (isset($goto)) { - if (is_array($goto)) { - call_user_func_array('drupal_goto', $goto); + if (!isset($goto) || ($goto !== FALSE)) { + // If $do_goto is set, call drupal_goto and do the redirect & exit. + if ($do_goto) { + if (isset($goto)) { + if (is_array($goto)) { + call_user_func_array('drupal_goto', $goto); + } + else { + drupal_goto($goto); + } } - else { - drupal_goto($goto); + drupal_goto($_GET['q']); + } + else { // If $do_goto is false, just calculate and return the next url. + if (isset($goto)) { + if (is_array($goto)) { + return call_user_func_array('drupal_get_goto_url', $goto); + } + else { + return drupal_get_goto_url($goto); + } } + return drupal_get_goto_url($_GET['q']); } - drupal_goto($_GET['q']); - } + } } /** Index: includes/theme.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/theme.inc,v retrieving revision 1.422 diff -u -r1.422 theme.inc --- includes/theme.inc 6 May 2008 12:18:45 -0000 1.422 +++ includes/theme.inc 8 May 2008 01:23:25 -0000 @@ -465,7 +465,7 @@ } /** - * Generate the themed output. + * Generate the themed output for the current rendering mode. * * All requests for theme hooks must go through this function. It examines * the request and routes it to the appropriate theme function. The theme @@ -549,9 +549,41 @@ * @param ... * Additional arguments to pass along to the theme function. * @return - * An HTML string that generates the themed output. + * An string that generates the themed output according to the current render + * mode. HTML is the default output. */ function theme() { + global $render_mode; + $args = func_get_args(); + + // Check if we're using a custom render mode. + if ($render_mode != 'xhtml/plain') { + static $theme_renderers; + if (!isset($theme_renderers)) { + $theme_renderers = module_invoke_all('theme_modes'); + } + $theme_renderer = isset($theme_renderers[$render_mode]) ? $theme_renderers[$render_mode] : NULL; + + // If a different theme rendering system is available, call it instead of the default. + if (function_exists($theme_renderer)) { + return call_user_func_array($theme_renderer, $args); + } + } + + // If not using a custom render mode, use the default renderer. + return call_user_func_array('theme_render', $args); +} + +/** + * The default theme mode renderer. Generates the HTML output for most requests. + * + * Invokes the theme system and generates the page layout when making a normal + * page request. This function is called by theme() when using the default + * rendering mode. + * + * @see theme() + */ +function theme_render() { $args = func_get_args(); $hook = array_shift($args); @@ -1451,7 +1483,7 @@ * Returns code that emits the 'more help'-link. */ function theme_more_help_link($url) { - return ''; + return ''; } /** @@ -1461,7 +1493,7 @@ * * @see theme_feed_icon() * @param $url - * The url of the feed. + * The URL of the feed. */ function theme_xml_icon($url) { if ($image = theme('image', 'misc/xml.png', t('XML feed'), t('XML feed'))) { @@ -1473,7 +1505,7 @@ * Return code that emits an feed icon. * * @param $url - * The url of the feed. + * The URL of the feed. * @param $title * A descriptive title of the feed. */ @@ -1487,7 +1519,7 @@ * Returns code that emits the 'more' link used on blocks. * * @param $url - * The url of the main page + * The URL of the main page * @param $title * A descriptive verb for the link, like 'Read more' */ @@ -1618,6 +1650,22 @@ } /** + * An unusual type of theme, this function returns json version of content for + * the save-on-changed ajax popup dialog. + */ +function theme_popup_save_dialog() { + print drupal_json(array( + 'title' => t('Warning: please confirm'), + 'content' => t("There are unsaved changes on this page, which you will lose if you continue."), + 'buttons' => array( + 'save' => t('Save changes'), + 'submit' => t('Continue'), + 'cancel' => t('Cancel'), + ), + )); +} + +/** * @} End of "defgroup themeable". */ Index: includes/menu.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/menu.inc,v retrieving revision 1.271 diff -u -r1.271 menu.inc --- includes/menu.inc 6 May 2008 12:18:45 -0000 1.271 +++ includes/menu.inc 8 May 2008 01:23:25 -0000 @@ -1172,6 +1172,7 @@ // Add "more help" link on admin pages if the module provides a // standalone help page. if ($arg[0] == "admin" && module_exists('help') && module_invoke($name, 'help', 'admin/help#' . $arg[2], $empty_arg) && $help) { + drupal_add_popup(); $output .= theme("more_help_link", url('admin/help/' . $arg[2])); } } Index: includes/bootstrap.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/bootstrap.inc,v retrieving revision 1.209 diff -u -r1.209 bootstrap.inc --- includes/bootstrap.inc 6 May 2008 12:18:45 -0000 1.209 +++ includes/bootstrap.inc 8 May 2008 01:23:24 -0000 @@ -271,7 +271,7 @@ * session name correctly. */ function conf_init() { - global $base_url, $base_path, $base_root; + global $base_url, $base_path, $base_root, $render_mode, $request_mode; // Export the following settings.php variables to the global namespace global $db_url, $db_prefix, $cookie_domain, $conf, $installed_profile, $update_free_access; @@ -337,6 +337,10 @@ ini_set('session.cookie_domain', $cookie_domain); } session_name('SESS' . md5($session_name)); + + // TODO - add comments to describe what these globals control. + $render_mode = isset($_SERVER['HTTP_X_DRUPAL_RENDER_MODE']) ? $_SERVER['HTTP_X_DRUPAL_RENDER_MODE'] : 'xhtml/plain'; + $request_mode = isset($_SERVER['HTTP_X_REQUESTED_WITH']) ? $_SERVER['HTTP_X_REQUESTED_WITH'] : 'standard'; } /** Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.765 diff -u -r1.765 common.inc --- includes/common.inc 6 May 2008 12:18:45 -0000 1.765 +++ includes/common.inc 8 May 2008 01:23:24 -0000 @@ -155,7 +155,7 @@ * Add a feed URL for the current page. * * @param $url - * A url for the feed. + * A URL for the feed. * @param $title * The title of the feed. */ @@ -293,8 +293,8 @@ * supported. * @see drupal_get_destination() */ -function drupal_goto($path = '', $query = NULL, $fragment = NULL, $http_response_code = 302) { - +// TODO - update comments to reflect breaking drupal_goto into two functions. +function drupal_get_goto_url($path = '', $query = NULL, $fragment = NULL) { if (isset($_REQUEST['destination'])) { extract(parse_url(urldecode($_REQUEST['destination']))); } @@ -311,7 +311,12 @@ if (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update') { module_invoke_all('exit', $url); } + return $url; +} +function drupal_goto($path = '', $query = NULL, $fragment = NULL, $http_response_code = 302) { + $url = drupal_get_goto_url($path, $query, $fragment); + // Even though session_write_close() is registered as a shutdown function, we // need all session data written to the database before redirecting. session_write_close(); @@ -477,7 +482,7 @@ 'Content-Length' => 'Content-Length: ' . strlen($data) ); - // If the server url has a user then attempt to use basic authentication + // If the server URL has a user then attempt to use basic authentication if (isset($uri['user'])) { $defaults['Authorization'] = 'Authorization: Basic ' . base64_encode($uri['user'] . (!empty($uri['pass']) ? ":" . $uri['pass'] : '')); } @@ -2215,6 +2220,24 @@ } /** + * Attach the popup behavior to the page. + */ +function drupal_add_popup() { + static $added = FALSE; + if (!$added) { + drupal_add_js('misc/popup.js', 'core'); + drupal_add_js('misc/jquery.form.js'); + $settings = array( 'popup' => array( + 'defaultTargetSelector' => variable_get('popup_content_selector', 'div.left-corner > div.clear-block:last'), + 'defaultTitleSelector' => variable_get('popup_title_selector', 'div.left-corner > h2:eq(0)'), + 'template' => theme('popup'), + )); + drupal_add_js( $settings, 'setting' ); + $added = TRUE; + } +} + +/** * Aggregate JS files, putting them in the files directory. * * @param $files @@ -2673,7 +2696,6 @@ } } - /** * Renders HTML given a structured array tree. * Index: modules/filter/filter.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/filter/filter.admin.inc,v retrieving revision 1.11 diff -u -r1.11 filter.admin.inc --- modules/filter/filter.admin.inc 14 Apr 2008 17:48:37 -0000 1.11 +++ modules/filter/filter.admin.inc 8 May 2008 01:23:25 -0000 @@ -159,7 +159,8 @@ // Composition tips (guidelines) $tips = _filter_tips($format->format, FALSE); - $extra = '

' . l(t('More information about formatting options'), 'filter/tips') . '

'; + drupal_add_popup(); + $extra = '

' . l(t('More information about formatting options'), 'filter/tips', array('attributes' => array('class' => 'popup'))) . '

'; $tiplist = theme('filter_tips', $tips, FALSE, $extra); if (!$tiplist) { $tiplist = '

' . t('No guidelines available.') . '

'; Index: modules/filter/filter.module =================================================================== RCS file: /cvs/drupal/drupal/modules/filter/filter.module,v retrieving revision 1.213 diff -u -r1.213 filter.module --- modules/filter/filter.module 6 May 2008 12:18:47 -0000 1.213 +++ modules/filter/filter.module 8 May 2008 01:23:25 -0000 @@ -474,6 +474,7 @@ $value = filter_resolve_format($value); $formats = filter_formats(); + drupal_add_popup(); $extra = theme('filter_tips_more_info'); if (count($formats) > 1) { @@ -578,7 +579,7 @@ * @ingroup themeable */ function theme_filter_tips_more_info() { - return '

' . l(t('More information about formatting options'), 'filter/tips') . '

'; + return '

' . l(t('More information about formatting options'), 'filter/tips', array('attributes' => array('class' => 'popup'))) . '

'; } /** Index: modules/node/content_types.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/node/content_types.inc,v retrieving revision 1.52 diff -u -r1.52 content_types.inc --- modules/node/content_types.inc 14 Apr 2008 17:48:38 -0000 1.52 +++ modules/node/content_types.inc 8 May 2008 01:23:25 -0000 @@ -10,6 +10,7 @@ * Displays the content type admin overview page. */ function node_overview_types() { + drupal_add_popup(); $types = node_get_types(); $names = node_get_types('names'); $header = array(t('Name'), t('Type'), t('Description'), array('data' => t('Operations'), 'colspan' => '2')); @@ -29,7 +30,7 @@ // Set the delete column. if ($type->custom) { - $row[] = array('data' => l(t('delete'), 'admin/content/node-type/' . $type_url_str . '/delete')); + $row[] = array('data' => l(t('delete'), 'admin/content/node-type/' . $type_url_str . '/delete', array('attributes' => array('class' => 'popup-form')))); } else { $row[] = array('data' => ''); Index: modules/path/path.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/path/path.admin.inc,v retrieving revision 1.8 diff -u -r1.8 path.admin.inc --- modules/path/path.admin.inc 14 Apr 2008 17:48:38 -0000 1.8 +++ modules/path/path.admin.inc 8 May 2008 01:23:25 -0000 @@ -41,7 +41,12 @@ $rows = array(); $destination = drupal_get_destination(); while ($data = db_fetch_object($result)) { - $row = array(check_plain($data->dst), check_plain($data->src), l(t('edit'), "admin/build/path/edit/$data->pid", array('query' => $destination)), l(t('delete'), "admin/build/path/delete/$data->pid", array('query' => $destination))); + $row = array( + check_plain($data->dst), + check_plain($data->src), + l(t('edit'), "admin/build/path/edit/$data->pid", array('query' => $destination)), + l(t('delete'), "admin/build/path/delete/$data->pid", array('query' => $destination, 'attributes' => array('class' => 'popup-form'))) + ); if ($multilanguage) { $row[4] = $row[3]; $row[3] = $row[2]; Index: modules/block/block.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/block/block.admin.inc,v retrieving revision 1.16 diff -u -r1.16 block.admin.inc --- modules/block/block.admin.inc 16 Apr 2008 11:35:51 -0000 1.16 +++ modules/block/block.admin.inc 8 May 2008 01:23:25 -0000 @@ -30,6 +30,7 @@ // Add CSS drupal_add_css(drupal_get_path('module', 'block') . '/block.css', 'module', 'all', FALSE); + drupal_add_popup(); // If non-default theme configuration has been selected, set the custom theme. $custom_theme = isset($theme) ? $theme : variable_get('theme_default', 'garland'); @@ -71,7 +72,7 @@ ); $form[$key]['configure'] = array('#value' => l(t('configure'), 'admin/build/block/configure/' . $block['module'] . '/' . $block['delta'])); if ($block['module'] == 'block') { - $form[$key]['delete'] = array('#value' => l(t('delete'), 'admin/build/block/delete/' . $block['delta'])); + $form[$key]['delete'] = array('#value' => l(t('delete'), 'admin/build/block/delete/' . $block['delta'], array('attributes' => array('class' => 'popup-form')))); } } Index: modules/menu/menu.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/menu/menu.admin.inc,v retrieving revision 1.30 diff -u -r1.30 menu.admin.inc --- modules/menu/menu.admin.inc 14 Apr 2008 17:48:37 -0000 1.30 +++ modules/menu/menu.admin.inc 8 May 2008 01:23:25 -0000 @@ -61,6 +61,7 @@ */ function _menu_overview_tree_form($tree) { static $form = array('#tree' => TRUE); + drupal_add_popup(); foreach ($tree as $data) { $title = ''; $item = $data['link']; @@ -97,11 +98,11 @@ $operations['edit'] = l(t('edit'), 'admin/build/menu/item/' . $item['mlid'] . '/edit'); // Only items created by the menu module can be deleted. if ($item['module'] == 'menu' || $item['updated'] == 1) { - $operations['delete'] = l(t('delete'), 'admin/build/menu/item/' . $item['mlid'] . '/delete'); + $operations['delete'] = l(t('delete'), 'admin/build/menu/item/' . $item['mlid'] . '/delete', array('attributes' => array('class' => 'popup-form'))); } // Set the reset column. elseif ($item['module'] == 'system' && $item['customized']) { - $operations['reset'] = l(t('reset'), 'admin/build/menu/item/' . $item['mlid'] . '/reset'); + $operations['reset'] = l(t('reset'), 'admin/build/menu/item/' . $item['mlid'] . '/reset', array('attributes' => array('class' => 'popup-form'))); } $form[$mlid]['operations'] = array(); Index: misc/popup.js =================================================================== RCS file: misc/popup.js diff -N misc/popup.js --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ misc/popup.js 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,461 @@ +// $Id:$ + +/** + * Popup Modal Dialog API + * + * Provide an API for building and displaying JavaScript, in-page, popup modal dialogs. + * Modality is provided by a fixed, semi-opaque div, positioned in front of the page contents. + * + */ + +/** + * Attach the popup bevior to the all the requested links on the page. + * + * @param context + * The jQuery object to apply the behaviors to. + */ +Drupal.behaviors.popup = function(context) { + var $body = $('body'); + if(!$body.hasClass('popup-processed')) { + $(document).bind('keydown', Drupal.popup.keyHandle); + $body.addClass('popup-processed'); + } + Drupal.popup.attach(context, 'a.popup', {}); + Drupal.popup.attach(context, 'a.popup-form', {updatePage: true}); + Drupal.popup.attach(context, 'a.popup-nonmodal', {nonModal: true}); +}; + +/** + * Create the popup object/namespace. + */ +Drupal.popup = function() {}; + +/** + * Attach the popup behavior to a particular link. + * + * @param selector + * jQuery selector for links to attach popup behavior to. + * @param options + * Hash of options associated with these links. + */ +Drupal.popup.attach = function(context, selector, options) { + $(selector, context).not('.popup-processed').each( function() { + var $link = $(this); + $link.click( function(e){ + var a = this; + // If the option is distructive, check if the page is already modified, and offer to save. + var pageIsDirty = $('span.tabledrag-changed').size() > 0; + var willModifyOriginal = options.updatePage; + if( pageIsDirty && willModifyOriginal ) { + // The user will lose modifications, so popup dialog offering to save current state. + $.getJSON(Drupal.settings.basePath + 'popup/save_dialog', function(json) { + // Attach behaviors to the buttons in the save-if-modified dialog. + var buttons = { + 'popup_save': {title: json.buttons.save, func: function(){Drupal.popup.savePage(a, options);}}, + 'popup_submit': {title: json.buttons.submit, func: function(){Drupal.popup.removePopup(); Drupal.popup.openPath(a, options);}}, + 'popup_cancel': {title: json.buttons.cancel, func: Drupal.popup.close} + }; + Drupal.popup.open(json.title, json.content, buttons); + }); + return false; + } + else { + return Drupal.popup.openPath(a, options); + } + }); + var title = $link.attr('title') || ''; + $link.attr('title', title + Drupal.t('[Popup]')); // Append note to link title. + $link.addClass('popup-processed'); + }); +}; + + +/** + * Generic dialog builder. + */ +Drupal.popup.open = function(title, body, buttons) { + + Drupal.popup.addOverlay(); // TODO - nonModal option. + var $popup = $(Drupal.theme('popupDialog', title, body, buttons)); + // Start with dialog off the side. Making it invisible causes flash in FF2. + $popup.css('left', '-9999px'); + $('body').append( $popup ); // Add the popup to the DOM. + + // Adding button functions + if (buttons) { + for (var id in buttons) { + if (buttons[id]) { // to make jslint happy. + var func = buttons[id].func; + $('#'+id).click( func ); + } + } + } + $('#popup-close').click( Drupal.popup.close ); + $('a.popup-close').click( Drupal.popup.close ); + + // center on the screen, adding in offsets if the window has been scrolled + var popupWidth = $popup.width(); + var windowWidth = $(window).width(); + var left = (windowWidth / 2) - (popupWidth / 2) + Drupal.popup.scrollLeft(); + + // Get popup's height on the page. + // Causes flash in FF2 if popup is not visible! + var popupHeight = $popup.height(); + var windowHeight = $(window).height(); + if (popupHeight > (0.9 * windowHeight) ) { // Must fit in 90% of window. + popupHeight = 0.9 * windowHeight; + $popup.height(popupHeight); + } + var top = (windowHeight / 2) - (popupHeight / 2) + Drupal.popup.scrollTop(); + + $popup.css('top', top).css('left', left); // Position the popup to be visible. + + this.refocus(); // TODO: capture the focus when it leaves the dialog. + Drupal.popup.removeLoading(); // Remove the loading img. + + return false; +}; + +/** + * Simple popup that functions like the browser's alert box. + */ +Drupal.popup.message = function(message, body) { + body = body || ''; + var buttons = { + 'popup_ok': { title: Drupal.t('OK'), func: Drupal.popup.close } + }; + Drupal.popup.open( message, body, buttons ); +}; + +/** + * Handle any special keys when popup is active. + */ +Drupal.popup.keyHandle = function(e) { + if (!e) { + e = window.event; + } + switch (e.keyCode) { + case 27: // esc + Drupal.popup.close(); + break; + case 191: // '?' key, show help. + if (e.shiftKey && e.ctrlKey) { + var $help = $('a.popup.more-help'); + if ($help.size()) { + $help.click(); + } + else { + Drupal.popup.message(Drupal.t("Sorry, there is no additional help for this page")); + } + } + break; + } +}; + +/***************************************************************************** + * Appearence Functions (overlay, loading graphic, remove popup) ********* + *****************************************************************************/ + +Drupal.popup.removePopup = function() { + $('#popup').remove(); +}; + +Drupal.popup.addOverlay = function() { + var $overlay = $('#popup-overlay'); + if (!$overlay.size()) { // Overlay does not already exist, so create it. + $overlay = $(Drupal.theme('popupOverlay')); + $overlay.css('opacity', '0.4'); // for ie6(?) + $body = $('body'); + // Doing absolute positioning, so make overlay's size equal the entire body. + $overlay.width($body.width()).height($body.height()); + $overlay.click(Drupal.popup.close); + $body.prepend($overlay); + } +// return $overlay; +}; + +//Drupal.popup.getOverlay = function() { +// return $('#popup-overlay'); +//}; + +Drupal.popup.removeOverlay = function() { + $('#popup-overlay').remove(); +}; + +Drupal.popup.addLoading = function() { + var $loading = $('#popup-loading'); + if (!$loading.size()) { // Overlay does not already exist, so create it. + var waitImageSize = 100; + var left = ($(window).width() / 2) - (waitImageSize / 2) + Drupal.popup.scrollLeft(); + var top = ($(window).height() / 2) - (waitImageSize / 2) + Drupal.popup.scrollTop(); + $loading = $( Drupal.theme('popupLoading', left, top) ); + $('body').prepend($loading); + } +}; + +Drupal.popup.removeLoading = function() { + $('#popup-loading').remove(); +}; + +/** + * Remove everything. + */ +Drupal.popup.close = function() { + Drupal.popup.removePopup(); + Drupal.popup.removeLoading(); + Drupal.popup.removeOverlay(); +}; + +/** + * Set the focus on the popup to the first visible form element, or the first button, or the close link. + */ +Drupal.popup.refocus = function() { + $focus = $('#popup input:visible:eq(0)'); + if (!focus) { + $focus = $('#popup-close'); // Doesn't seem to work. + } + $focus.focus(); +}; + +/**************************************************************************** + * Theme Functions ******************************************************** + ****************************************************************************/ + +Drupal.theme.prototype.popupLoading = function(left, top) { + var loading = ''; + return loading; +}; + +Drupal.theme.prototype.popupOverlay = function() { + return ''; +}; + +Drupal.theme.prototype.popupButton = function(title, id) { + return ''; +}; + +Drupal.theme.prototype.popupDialog = function(title, body, buttons) { + var template = Drupal.settings.popup.template; + var popup = template.replace('%title', title).replace('%body', body); + + var themedButtons = ''; + for ( var id in buttons) { + if (buttons[id]) { + themedButtons += Drupal.theme('popupButton', buttons[id].title, id); + } + } + popup = popup.replace('%buttons', themedButtons); + return popup; +}; + +/**************************************************************************** + * Utility scroll functions taken from: *** + * http://www.softcomplex.com/docs/get_window_size_and_scrollbar_position.html + ****************************************************************************/ + +// FROM jQuery offset +// elem = this[0]; +// doc = elem.ownerDocument; +// add(box.left + Math.max(doc.documentElement.scrollLeft, doc.body.scrollLeft), +// box.top + Math.max(doc.documentElement.scrollTop, doc.body.scrollTop)); + +Drupal.popup.scrollLeft = function() { + return Math.max(document.documentElement.scrollLeft, document.body.scrollLeft); +// return Drupal.popup.filterResults ( +// window.pageXOffset ? window.pageXOffset : 0, +// document.documentElement ? document.documentElement.scrollLeft : 0, +// document.body ? document.body.scrollLeft : 0 ); +}; + +Drupal.popup.scrollTop = function() { + return Math.max(document.documentElement.scrollTop, document.body.scrollTop); +// return Drupal.popup.filterResults ( +// window.pageYOffset ? window.pageYOffset : 0, +// document.documentElement ? document.documentElement.scrollTop : 0, +// document.body ? document.body.scrollTop : 0 ); +}; + +Drupal.popup.filterResults = function(win, docel, body) { + var result = win ? win : 0; + if (docel && (!result || (result > docel))) { + result = docel; + } + return body && (!result || (result > body)) ? body : result; +}; + + +/**************************************************************************** + * Page & Form in popup functions *** + ****************************************************************************/ + +/** + * Use Ajax to open the link in a popup window. + * + * @param a + * Link that was clicked to open the popup. + * @param options + * Hash of options controlling how the popup interacts with the underlying page. + */ +Drupal.popup.openPath = function(a, options) { + // let the user know something is happening + $('body').css("cursor", "wait"); + + // TODO - get nonmodal working. + if (!options.nonModal) { + Drupal.popup.addOverlay(); + } + Drupal.popup.addLoading(); + + // Set custom headers for all following requests. + $.ajaxSetup({ + beforeSend: function(xhr) { + xhr.setRequestHeader("X-Drupal-Render-Mode", 'json/popup'); + } + }); + + $.getJSON(a.href, function(json) { + Drupal.popup.openContent(json.title, json.messages + json.content, options); + $('body').css("cursor", "auto"); // Return the cursor to normal state. + }); + + return false; +}; + +/** + * Open content in an ajax popup. + * + * @param title + * String title of the popup. + * @param content + * HTML to show in the popup. + * @param options + * Hash of options controlling how the popup interacts with the underlying page. + */ +Drupal.popup.openContent = function(title, content, options) { + Drupal.popup.open(title, content); + // Add behaviors to content in popup. + // TODO: d-n-d: need to click to let go of selection. + Drupal.attachBehaviors($('#popup-body')); + // Adding collapse moves focus. + Drupal.popup.refocus(); + + // If the popup contains a form, capture submits. + var $form = $('form', '#popup-body'); + $form.ajaxForm({ + dataType: 'json', + beforeSubmit: Drupal.popup.beforeSubmit, + success: function(response, status) { + Drupal.popup.formSuccess(response, options); + } + }); +}; + +/** + * Do before the form in the popup is submitted. + */ +Drupal.popup.beforeSubmit = function(formData, $form, options) { + Drupal.popup.removePopup(); // Remove just the dialog, but not the overlay. + Drupal.popup.addLoading(); +}; + +/** + * The form in the popup was successfully submitted + * Update the originating page. + * Show any messages in a popup (TODO - make this a configurable option). + * + * @param response + * JSON object from server with status of form submission. + * @param options + * Hash of options controlling how the popup interacts with the underlying page. + * //noReload: bool, does the popup effect the underlying page. + * updatePage: bool, does the popup effect the underlying page. + * updateTitle: bool, does the popup change the title of the page. + * nonModal: bool, does the popup block access to the underlying page. + * targetSelector: jQuery selector, overrides defaultTargetSelector. + */ +Drupal.popup.formSuccess = function(response, options) { + if (response.status != 'redirect') { // something went wrong + Drupal.popup.message( "Error: " + response.messages + response.content); + } + else { // Got a good response back from the server. + $.getJSON(response.path, function(data) { + // Determine if we are at an end point, or just moving from one popup to another. + if (!location.href.match(data.path)) { // Not done yet, so show results in new popup. + Drupal.popup.removeLoading(); + Drupal.popup.openContent(data.title, data.messages + data.content, options); + } + else { // Done, so show messages in dialog and embed the results in the original page. + if (data.messages) { + Drupal.popup.message(data.messages); + // Also insert the message into the page above the content. + // Might not be the standard spot, but it is the easiest to find. + var $next = $(Drupal.settings.popup.defaultTargetSelector); + $next.parent().find('div.messages').remove(); // Remove the current messages. + $next.before(data.messages); + } + + // Update the entire content area (defined by 'target selector'). + if (options.updatePage) { + var target = options.targetSelector; + if (!target) { + target = Drupal.settings.popup.defaultTargetSelector; + } + // Update the original page. + var $c = $(target).html(data.content); // Inject the new content into the page. + Drupal.attachBehaviors($c); + } + + // Update the title of the page. + if (options.updateTitle) { + $(Drupal.settings.popup.defaultTitleSelector).html(data.title); + document.title = data.title; // Also update the browser page title (TODO: include site name?). + } + + // Done with changes to the original page, remove effects. + Drupal.popup.removeLoading(); + if (!data.messages) { + // If there is not a messages popup remove the overlay. + Drupal.popup.removeOverlay(); + } + } // End of updating original page. + }); // End of good response. + } +}; + +/** + * Submit the page and reload the results, before popping up the real dialog. + * + * @param a + * Link that was clicked to open the popup. + * @param options + * Hash of options controlling how the popup interacts with the underlying page. + */ +Drupal.popup.savePage = function(a, options) { + var target = Drupal.settings.popup.defaultTargetSelector; + var $form = $('form', target); + var ajaxOptions = { + dataType: 'json', + beforeSubmit: Drupal.popup.beforeSubmit, + success: function(response, status) { + // Sync up the current page contents with the submit. + $.ajax({ + url: response.path, + dataType: 'json', + beforeSend: function(xhr) { + xhr.setRequestHeader("X-Drupal-Render-Mode", 'json/popup'); + }, + success: function(json) { + // Update the original page. + var $c = $(target).html(json.content); // Inject the new content into the page. + Drupal.attachBehaviors($c); + // The form has been saved and the page reloaded, now safe to open the clicked link in a popup. + Drupal.popup.openPath(a, options); + } + }); + } + }; + $form.ajaxSubmit(ajaxOptions); // Submit the form. +}; Index: modules/system/popup.tpl.php =================================================================== RCS file: modules/system/popup.tpl.php diff -N modules/system/popup.tpl.php --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/system/popup.tpl.php 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,13 @@ + + +