diff --git a/core/includes/ajax.inc b/core/includes/ajax.inc index 45bb706..cc62040 100644 --- a/core/includes/ajax.inc +++ b/core/includes/ajax.inc @@ -474,7 +474,7 @@ function ajax_prepare_response($page_callback_result) { // #ajax['method']. The default method is 'replaceWith', which completely // replaces the old wrapper element and its content with the new HTML. $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 +531,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 +589,15 @@ 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']['modal'])) { + $element['#attached']['library'][] = array('system', 'drupal.ajax.modal'); + } $settings = $element['#ajax']; // Assign default settings. $settings += array( - 'path' => 'system/ajax', + 'path' => isset($settings['callback']) ? 'system/ajax' : NULL, 'options' => array(), ); @@ -604,7 +607,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..dae6a30 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,53 @@ 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 modal, use the needed wrapper and + // method. + if (this.modal) { + this.wrapper = '#drupal-modal'; + this.method = 'html'; + } + 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 modal. + 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 +172,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. @@ -477,6 +513,9 @@ Drupal.ajax.prototype.commands = { * Command to insert new content into the DOM. */ insert: function (ajax, response, status) { + if (ajax.modal && (response.title || !$(ajax.wrapper).length)) { + ajax.commands.modalOpen(response.title); + } // Get information from the response. If it is not there, default to // our presets. var wrapper = response.selector ? $(response.selector) : $(ajax.wrapper); diff --git a/core/misc/ajax.modal.js b/core/misc/ajax.modal.js new file mode 100644 index 0000000..4c2f119 --- /dev/null +++ b/core/misc/ajax.modal.js @@ -0,0 +1,61 @@ +(function ($, Drupal, drupalSettings) { + +"use strict"; + +var $modal; +var modalConfig = { + //autoOpen: false, + modal: true, + dialogClass: '', + close: function (e) { + Drupal.detachBehaviors(e.target, null, 'unload'); + // remove everything from the DOM like the previous modal dialog. + $(e.target).dialog('destroy').remove(); + } +}; + +$.extend(Drupal.ajax.prototype.commands, { + /** + * Open the modal dialog wrapper, set its title, and return it. + * + * Calling code can use the returned wrapper to set content. For example: + * @code + * Drupal.ajax.modalOpen('Some title').html('Some content'); + * @endcode + */ + modalOpen: function (title) { + var modalSettings = $.extend({title: title}, modalConfig, drupalSettings.modal); + // Clean up all Modal behaviors. + $modal = $('#drupal-modal'); + if ($modal.length) { + Drupal.detachBehaviors($modal[0], null, 'unload'); + $modal.dialog('destroy').remove(); + } + $modal = $('
'); + $('body').append($modal); + + // Trigger a global event to allow scripts to bind events to the dialog. + $(window).trigger('modal:beforecreate', [$modal, modalSettings]); + $modal.dialog(modalSettings); + $(window).trigger('modal:aftercreate', [$modal, modalSettings]); + + // Bind modal dismissal behavior to elements with the modal-dismiss class. + $modal.on('click', '.modal-dismiss', function (e) { + $modal.dialog('close'); + e.preventDefault(); + e.stopPropagation(); + }); + + return $modal; + }, + + /** + * Close the modal dialog. + */ + modalClose: function () { + $modal.dialog('close'); + } + +}); + +})(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..f8ae021 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']['modal'] = TRUE; + return $element; } diff --git a/core/modules/node/node.admin.inc b/core/modules/node/node.admin.inc index bca4dc2..b0dca4e 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('modal' => TRUE), 'query' => $destination, ); } diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 6401361..f71ce0a 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -1168,6 +1168,23 @@ function system_library_info() { ), ); + // Drupal's Ajax framework, modal component. + $libraries['drupal.ajax.modal'] = array( + 'title' => 'Drupal AJAX modals', + 'website' => 'http://api.drupal.org/api/group/ajax/8', + 'version' => VERSION, + 'js' => array( + 'core/misc/ajax.modal.js' => array('group' => JS_LIBRARY, 'weight' => 2), + ), + 'dependencies' => array( + array('system', 'jquery'), + array('system', 'drupal'), + array('system', 'drupalSettings'), + array('system', 'drupal.ajax'), + array('system', 'jquery.ui.dialog') + ), + ); + // Drupal's batch API. $libraries['drupal.batch'] = array( 'title' => 'Drupal batch API', @@ -3368,6 +3385,11 @@ function confirm_form($form, $question, $path, $description = NULL, $yes = NULL, '#type' => 'link', '#title' => $no ? $no : t('Cancel'), '#href' => $options['path'], + '#attributes' => array( + 'class' => array( + 'modal-dismiss' + ) + ), '#options' => $options, ); // By default, render the form using theme_confirm_form().