diff --git a/core/includes/ajax.inc b/core/includes/ajax.inc index 1edd871..c67e981 100644 --- a/core/includes/ajax.inc +++ b/core/includes/ajax.inc @@ -473,8 +473,13 @@ function ajax_prepare_response($page_callback_result) { // manipulation method is used. The method used is specified by // #ajax['method']. The default method is 'replaceWith', which completely // replaces the old wrapper element and its content with the new HTML. + // Since this is the primary response content returned to the client, we + // also attach the page title. It is up to client code to determine if and + // how to display that. For example, if the requesting element is configured + // to display the response in a dialog (via #ajax['dialog']), it can use + // this for the dialog title. $html = is_string($page_callback_result) ? $page_callback_result : drupal_render($page_callback_result); - $commands[] = ajax_command_insert(NULL, $html); + $commands[] = ajax_command_insert(NULL, $html) + array('title' => drupal_get_title()); // Add the status messages inside the new content's wrapper element, so that // on subsequent Ajax requests, it is treated as old content. $commands[] = ajax_command_prepend(NULL, theme('status_messages')); @@ -531,8 +536,8 @@ function ajax_pre_render_element($element) { // Initialize #ajax_processed, so we do not process this element again. $element['#ajax_processed'] = FALSE; - // Nothing to do if there is neither a callback nor a path. - if (!(isset($element['#ajax']['callback']) || isset($element['#ajax']['path']))) { + // Nothing to do if there are no Ajax settings. + if (empty($element['#ajax'])) { return $element; } @@ -589,12 +594,24 @@ function ajax_pre_render_element($element) { if (isset($element['#ajax']['event'])) { $element['#attached']['library'][] = array('system', 'jquery.form'); $element['#attached']['library'][] = array('system', 'drupal.ajax'); + if (!empty($element['#ajax']['dialog'])) { + $element['#attached']['library'][] = array('system', 'drupal.dialog'); + } $settings = $element['#ajax']; - // Assign default settings. + // Assign default settings. When 'path' is set to NULL, ajax.js submits the + // Ajax request to the same URL as the form or link destination is for + // someone with JavaScript disabled. This is generally preferred as a way to + // ensure consistent server processing for js and no-js users, and Drupal's + // content negotiation takes care of formatting the response appropriately. + // However, 'path' and 'options' may be set when wanting server processing + // to be substantially different for a JavaScript triggered submission. + // One such substantial difference is form elements that use + // #ajax['callback'] for determining which part of the form needs + // re-rendering. For that, we have a special 'system/ajax' route. $settings += array( - 'path' => 'system/ajax', + 'path' => isset($settings['callback']) ? 'system/ajax' : NULL, 'options' => array(), ); @@ -604,7 +621,7 @@ function ajax_pre_render_element($element) { } // Change path to URL. - $settings['url'] = url($settings['path'], $settings['options']); + $settings['url'] = isset($settings['path']) ? url($settings['path'], $settings['options']) : NULL; unset($settings['path'], $settings['options']); // Add special data to $settings['submit'] so that when this element diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 3ce399b..c10a3ac 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -1739,8 +1739,24 @@ function theme_links($variables) { if ($is_current_path && $is_current_language) { $class[] = 'active'; } - // Pass in $link as $options, they share the same keys. - $item = l($link['title'], $link['href'], $link); + // @todo Reconcile Views usage of 'ajax' as a boolean with the rest of + // core's usage of it as an array. + if (isset($link['ajax']) && is_array($link['ajax'])) { + // To attach Ajax behavior, render a link element, rather than just + // call l(). + $link_element = array( + '#type' => 'link', + '#title' => $link['title'], + '#href' => $link['href'], + '#ajax' => $link['ajax'], + '#options' => array_diff_key($link, drupal_map_assoc(array('title', 'href', 'ajax'))), + ); + $item = drupal_render($link_element); + } + else { + // Pass in $link as $options, they share the same keys. + $item = l($link['title'], $link['href'], $link); + } } // Handle title-only text items. else { diff --git a/core/misc/ajax.js b/core/misc/ajax.js index 58db308..c793e2b 100644 --- a/core/misc/ajax.js +++ b/core/misc/ajax.js @@ -101,7 +101,6 @@ Drupal.behaviors.AJAX = { */ Drupal.ajax = function (base, element, element_settings) { var defaults = { - url: 'system/ajax', event: 'mousedown', keypress: true, selector: '#' + base, @@ -119,9 +118,55 @@ Drupal.ajax = function (base, element, element_settings) { $.extend(this, defaults, element_settings); + // @todo Remove this after refactoring the PHP code to: + // - Call this 'selector'. + // - Include the '#' for ID-based selectors. + // - Support non-ID-based selectors. + if (this.wrapper) { + this.wrapper = '#' + this.wrapper; + } + + // For Ajax responses that are wanted in a dialog, use the needed method. + // If wanted in a modal dialog, also use the needed wrapper. + if (this.dialog) { + this.method = 'html'; + if (this.dialog.modal) { + this.wrapper = '#drupal-modal'; + } + } + this.element = element; this.element_settings = element_settings; + // If there isn't a form, jQuery.ajax() will be used instead, allowing us to + // bind Ajax to links as well. + if (this.element.form) { + this.form = $(this.element.form); + } + + // If no Ajax callback URL was given, use the link href or form action. + if (!this.url) { + if ($(element).is('a')) { + this.url = $(element).attr('href'); + } + else if (element.form) { + this.url = this.form.attr('action'); + + // @todo If there's a file input on this form, then jQuery will submit the + // AJAX response with a hidden Iframe rather than the XHR object. If the + // response to the submission is an HTTP redirect, then the Iframe will + // follow it, but the server won't content negotiate it correctly, + // because there won't be an ajax_iframe_upload POST variable. Until we + // figure out a work around to this problem, we prevent AJAX-enabling + // elements that submit to the same URL as the form when there's a file + // input. For example, this means the Delete button on the edit form of + // an Article node doesn't open its confirmation form in a dialog. + if (this.form.find(':file').length) { + return; + } + } + } + // Replacing 'nojs' with 'ajax' in the URL allows for an easy method to let // the server detect when it needs to degrade gracefully. // There are four scenarios to check for: @@ -129,14 +174,7 @@ Drupal.ajax = function (base, element, element_settings) { // 2. /nojs$ - The end of a URL string. // 3. /nojs? - Followed by a query (e.g. path/nojs?destination=foobar). // 4. /nojs# - Followed by a fragment (e.g.: path/nojs#myfragment). - this.url = element_settings.url.replace(/\/nojs(\/|$|\?|#)/g, '/ajax$1'); - this.wrapper = '#' + element_settings.wrapper; - - // If there isn't a form, jQuery.ajax() will be used instead, allowing us to - // bind Ajax to links as well. - if (this.element.form) { - this.form = $(this.element.form); - } + this.url = this.url.replace(/\/nojs(\/|$|\?|#)/g, '/ajax$1'); // Set the options for the ajaxSubmit function. // The 'this' variable will not persist inside of the options object. @@ -520,6 +558,17 @@ Drupal.ajax.prototype.commands = { // Add the new content to the page. wrapper[method](new_content); + // If the requesting object wanted the response in a dialog, open that + // dialog. However, a single server response can include multiple insert + // commands (e.g., one for the primary content and another one for status + // messages), but we only want to open the dialog once, so we assume that + // only commands with a title property are dialog eligible. + // @todo Consider whether this is overloading title inappropriately, and + // if so, find another way to determine dialog eligibility. + if (ajax.dialog && ('title' in response)) { + Drupal.dialog(wrapper, {title: response.title}).show(ajax.dialog); + } + // Immediately hide the new content if we're using any effects. if (effect.showEffect !== 'show') { new_content.hide(); diff --git a/core/misc/dialog.js b/core/misc/dialog.js new file mode 100644 index 0000000..27023e0 --- /dev/null +++ b/core/misc/dialog.js @@ -0,0 +1,82 @@ +/** + * @file + * + * Dialog API inspired by HTML5 dialog element: + * http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#the-dialog-element + */ +(function ($, Drupal, drupalSettings) { + +"use strict"; + +drupalSettings.dialog = { + autoOpen: true, + dialogClass: '', + close: function (e) { + Drupal.detachBehaviors(e.target, null, 'unload'); + } +}; + +Drupal.behaviors.dialog = { + attach: function () { + // Provide a known 'drupal-modal' dom element for Drupal code to use for + // modal dialogs. Since there can be multiple non-modal dialogs at a time, + // it is the responsibility of calling code to create the elements it needs. + if (!$('#drupal-modal').length) { + $('
').hide().appendTo('body'); + } + } +}; + +Drupal.dialog = function (element, options) { + + function openDialog (settings) { + settings = $.extend(settings, defaults); + // Trigger a global event to allow scripts to bind events to the dialog. + $(window).trigger('dialog:beforecreate', [dialog, $element, settings]); + $element.dialog(settings); + dialog.open = true; + $(window).trigger('dialog:aftercreate', [dialog, $element, settings]); + } + + function closeDialog (value) { + $(window).trigger('dialog:beforeclose', [dialog, $element]); + $element.dialog('close'); + dialog.returnValue = value; + dialog.open = false; + $(window).trigger('dialog:afterclose', [dialog, $element]); + } + + var undef; + var $element = $(element); + var defaults = $.extend(options, drupalSettings.dialog); + var dialog = { + open: false, + returnValue: undef, + show: function (settings) { + openDialog(settings); + }, + close: closeDialog + }; + + return dialog; +}; + +/** + * Binds a listener on dialog creation to handle the cancel link. + */ +$(window).on('dialog:aftercreate', function (e, dialog, $element, settings) { + $element.on('click.dialog', '.dialog-cancel', function (e) { + dialog.close('cancel'); + e.preventDefault(); + e.stopPropagation(); + }); +}); + +/** + * Removes all 'dialog' listeners. + */ +$(window).on('dialog:beforeclose', function (e, dialog, $element) { + $element.off('.dialog'); +}); + +})(jQuery, Drupal, drupalSettings); diff --git a/core/modules/node/lib/Drupal/node/NodeFormController.php b/core/modules/node/lib/Drupal/node/NodeFormController.php index 215ad81..1e576ee 100644 --- a/core/modules/node/lib/Drupal/node/NodeFormController.php +++ b/core/modules/node/lib/Drupal/node/NodeFormController.php @@ -267,6 +267,10 @@ protected function actions(array $form, array &$form_state) { $element['submit']['#access'] = $preview_mode != DRUPAL_REQUIRED || (!form_get_errors() && isset($form_state['node_preview'])); $element['delete']['#access'] = node_access('delete', $node); + // @todo Move this to EntityFormController::actions() so it applies to all + // entity types by default? + $element['delete']['#ajax']['dialog'] = array('modal' => TRUE); + return $element; } diff --git a/core/modules/node/node.admin.inc b/core/modules/node/node.admin.inc index e171b75..eb503c4 100644 --- a/core/modules/node/node.admin.inc +++ b/core/modules/node/node.admin.inc @@ -557,6 +557,7 @@ function node_admin_nodes() { $operations['delete'] = array( 'title' => t('delete'), 'href' => 'node/' . $node->nid . '/delete', + 'ajax' => array('dialog' => array('modal' =>TRUE)), 'query' => $destination, ); } diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 950fee6..f47d578 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -1171,6 +1171,21 @@ function system_library_info() { ), ); + // Drupal's dialog component. + $libraries['drupal.dialog'] = array( + 'title' => 'Drupal Dialog', + 'version' => VERSION, + 'js' => array( + 'core/misc/dialog.js' => array('group' => JS_LIBRARY), + ), + 'dependencies' => array( + array('system', 'jquery'), + array('system', 'drupal'), + array('system', 'drupalSettings'), + array('system', 'jquery.ui.dialog') + ), + ); + // Drupal's states library. $libraries['drupal.states'] = array( 'title' => 'Drupal states', @@ -3350,6 +3365,9 @@ function confirm_form($form, $question, $path, $description = NULL, $yes = NULL, '#type' => 'link', '#title' => $no ? $no : t('Cancel'), '#href' => $options['path'], + '#attributes' => array( + 'class' => array('dialog-cancel'), + ), '#options' => $options, ); // By default, render the form using theme_confirm_form().