diff --git a/core/includes/ajax.inc b/core/includes/ajax.inc index 212dff6..298f0ec 100644 --- a/core/includes/ajax.inc +++ b/core/includes/ajax.inc @@ -5,6 +5,9 @@ * Functions for use with Drupal's Ajax framework. */ +use Drupal\Core\Ajax\OpenDialogCommand; +use Drupal\Core\Ajax\OpenModalDialogCommand; + /** * @defgroup ajax Ajax framework * @{ @@ -473,13 +476,8 @@ 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) + array('title' => drupal_get_title()); + $commands[] = ajax_command_insert(NULL, $html); // 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')); @@ -594,9 +592,6 @@ 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']; diff --git a/core/lib/Drupal/Core/Ajax/CloseDialogCommand.php b/core/lib/Drupal/Core/Ajax/CloseDialogCommand.php new file mode 100644 index 0000000..1788ceb --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/CloseDialogCommand.php @@ -0,0 +1,41 @@ +selector = $selector ? $selector : '#drupal-modal'; + } + + /** + * Implements \Drupal\Core\Ajax\CommandInterface::render(). + */ + public function render() { + return array( + 'command' => 'closeDialog', + 'selector' => $this->selector, + ); + } +} diff --git a/core/lib/Drupal/Core/Ajax/CloseModalDialogCommand.php b/core/lib/Drupal/Core/Ajax/CloseModalDialogCommand.php new file mode 100644 index 0000000..85c4057 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/CloseModalDialogCommand.php @@ -0,0 +1,22 @@ +selector = '#drupal-modal'; + } +} diff --git a/core/lib/Drupal/Core/Ajax/OpenDialogCommand.php b/core/lib/Drupal/Core/Ajax/OpenDialogCommand.php new file mode 100644 index 0000000..0c293ab --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/OpenDialogCommand.php @@ -0,0 +1,105 @@ + $title); + $this->selector = $selector; + $this->html = $html; + $this->dialogOptions = $dialog_settings; + $this->settings = $settings; + } + + /** + * Returns the dialog settings. + * + * @return array + */ + public function getdialogOptions() { + return $this->dialogOptions; + } + + /** + * Sets the dialog settings array. + * + * @param array $dialog_settings + * An array of keys passed to the Drupal.dialog javascript object. + */ + public function setdialogOptions($dialog_settings) { + $this->dialogOptions = $dialog_settings; + } + + /** + * Sets a single dialog setting value. + * + * @param string $key + * Key of the dialog setting. + * @param mixed $value + * The value of the setting. + */ + public function setDialogOption($key, $value) { + $this->dialogOptions[$key] = $value; + } + + /** + * Sets the dialog title (an alias of setDialogOptions). + * + * @param mixed $title + * The new title of the dialog. + */ + public function setDialogTitle($title) { + $this->setDialogOptions('title', $title); + } + + /** + * Implements \Drupal\Core\Ajax\CommandInterface:render(). + */ + public function render() { + // Add the library for handling the dialog in the response. + drupal_add_library('system', 'drupal.dialog.ajax'); + + // For consistency ensure the modal option is set to TRUE or FALSE. + $this->dialogOptions['modal'] = isset($this->dialogOptions['modal']) && $this->dialogOptions['modal']; + return array( + 'command' => 'openDialog', + 'selector' => $this->selector, + 'settings' => $this->settings, + 'data' => $this->html, + 'dialogOptions' => $this->dialogOptions + ); + } +} diff --git a/core/lib/Drupal/Core/Ajax/OpenModalDialogCommand.php b/core/lib/Drupal/Core/Ajax/OpenModalDialogCommand.php new file mode 100644 index 0000000..bfb67c5 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/OpenModalDialogCommand.php @@ -0,0 +1,33 @@ +selector = $selector ? $selector : '#drupal-modal'; + $this->option_name = $option_name; + $this->option_value = $option_value; + } + + /** + * Implements \Drupal\Core\Ajax\CommandInterface::render(). + */ + public function render() { + return array( + 'command' => 'setDialogOption', + 'selector' => $this->selector, + 'optionName' => $this->option_name, + 'optionValue' => $this->option_value, + ); + } + +} diff --git a/core/lib/Drupal/Core/Ajax/SetDialogTitleCommand.php b/core/lib/Drupal/Core/Ajax/SetDialogTitleCommand.php new file mode 100644 index 0000000..d7aaf88 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/SetDialogTitleCommand.php @@ -0,0 +1,30 @@ +selector = $selector ? $selector : '#drupal-modal'; + $this->option_name = 'title'; + $this->option_value = $title; + } +} diff --git a/core/lib/Drupal/Core/Ajax/SetWindowLocationCommand.php b/core/lib/Drupal/Core/Ajax/SetWindowLocationCommand.php new file mode 100644 index 0000000..8dca714 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/SetWindowLocationCommand.php @@ -0,0 +1,46 @@ +url = $url; + } + + /** + * Implements \Drupal\Core\Ajax\CommandInterface:render(). + */ + public function render() { + return array( + 'command' => 'setWindowLocation', + 'url' => $this->url, + ); + } + +} diff --git a/core/misc/ajax.js b/core/misc/ajax.js index 53351ad..88b47d6 100644 --- a/core/misc/ajax.js +++ b/core/misc/ajax.js @@ -126,15 +126,6 @@ Drupal.ajax = function (base, element, element_settings) { 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; @@ -558,19 +549,6 @@ 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)) { - var dialogOptions = $.extend({title: response.title}, ajax.dialog); - var dialog = Drupal.dialog(wrapper, dialogOptions); - ajax.dialog.modal ? dialog.showModal() : dialog.show(); - } - // Immediately hide the new content if we're using any effects. if (effect.showEffect !== 'show') { new_content.hide(); @@ -626,6 +604,13 @@ Drupal.ajax.prototype.commands = { }, /** + * Command to set the window.location. + */ + setWindowLocation: function (ajax, response, status) { + window.location = response.url; + }, + + /** * Command to provide the jQuery css() function. */ css: function (ajax, response, status) { diff --git a/core/misc/dialog.ajax.js b/core/misc/dialog.ajax.js new file mode 100644 index 0000000..ceeed9f --- /dev/null +++ b/core/misc/dialog.ajax.js @@ -0,0 +1,63 @@ +/** + * @file + * Extends the Drupal AJAX functionality to integrate the dialog API. + */ + +(function ($, Drupal) { + "use strict"; + + /** + * Command to open a dialog. + */ + Drupal.ajax.prototype.commands.openDialog = function (ajax, response, status) { + if (!response.selector) { + return false; + } + var $dialog = $(response.selector); + if (!$dialog.length) { + // Create the element if needed. + $dialog = $('
').appendTo('body'); + } + // Set up the wrapper, if there isn't one. + if (!ajax.wrapper) { + ajax.wrapper = $dialog.attr('id'); + } + + // Open the dialog itself. + response.dialogOptions = response.dialogOptions || {}; + var dialog = new Drupal.dialog($dialog, response.dialogOptions); + dialog.open(); + + // Use the ajax.js insert command to populate the dialog contents. + response.command = 'insert'; + response.method = 'html'; + ajax.commands.insert(ajax, response, status); + }; + + /** + * Command to close a dialog. + * + * If no selector is given, it defaults to trying to close the modal. + */ + Drupal.ajax.prototype.commands.closeDialog = function (ajax, response, status) { + var $dialog = $(response.selector) || $('#drupal-modal'); + if ($dialog.length) { + var dialog = new Drupal.dialog($dialog); + dialog.close(); + } + }; + + /** + * Command to set a dialog property. + * + * If no selector is given, it defaults to setting the modal properties. + */ + Drupal.ajax.prototype.commands.setDialogOption = function (ajax, response, status) { + var $dialog = $(response.selector) || $('#drupal-modal'); + if ($dialog.length) { + var dialog = new Drupal.dialog($dialog); + dialog.element.dialog('option', response.optionName, response.optionValue); + } + }; + +})(jQuery, Drupal); diff --git a/core/misc/dialog.js b/core/misc/dialog.js index a0849bc..1ddc93c 100644 --- a/core/misc/dialog.js +++ b/core/misc/dialog.js @@ -10,10 +10,7 @@ drupalSettings.dialog = { autoOpen: true, - dialogClass: '', - close: function (e) { - Drupal.detachBehaviors(e.target, null, 'unload'); - } + dialogClass: '' }; Drupal.behaviors.dialog = { @@ -28,46 +25,38 @@ Drupal.behaviors.dialog = { }; Drupal.dialog = function (element, options) { + // Set a default for options, making it optional. + options = options ? options : {}; - function openDialog (settings) { - settings = $.extend({}, drupalSettings.dialog, options, settings); - // 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]); - } + this.element = $(element); + this.isOpen = false; + this.returnValue = undefined; + this.options = $.extend({}, drupalSettings.dialog, options); +}; - var undef; - var $element = $(element); - var dialog = { - open: false, - returnValue: undef, - show: function () { - openDialog({modal: false}); - }, - showModal: function () { - openDialog({modal: true}); - }, - close: closeDialog - }; +Drupal.dialog.prototype.open = function (options) { + options = $.extend({}, drupalSettings.dialog, this.options, options); + // Trigger a global event to allow scripts to bind events to the dialog. + $(window).trigger('dialog:beforecreate', [this, options]); + this.element.dialog(options); + this.isOpen = true; + $(window).trigger('dialog:aftercreate', [this, options]); +} - return dialog; -}; +Drupal.dialog.prototype.close = function (value) { + $(window).trigger('dialog:beforeclose', [this]); + Drupal.detachBehaviors(this.element[0], null, 'unload'); + this.element.dialog('close'); + this.returnValue = value; + this.isOpen = false; + $(window).trigger('dialog:afterclose', [this]); +} /** * 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) { +$(window).on('dialog:aftercreate', function (e, dialog, settings) { + dialog.element.on('click.dialog', '.dialog-cancel', function (e) { dialog.close('cancel'); e.preventDefault(); e.stopPropagation(); @@ -77,8 +66,8 @@ $(window).on('dialog:aftercreate', function (e, dialog, $element, settings) { /** * Removes all 'dialog' listeners. */ -$(window).on('dialog:beforeclose', function (e, dialog, $element) { - $element.off('.dialog'); +$(window).on('dialog:beforeclose', function (e, dialog) { + dialog.element.off('.dialog'); }); })(jQuery, Drupal, drupalSettings); diff --git a/core/modules/system/lib/Drupal/system/Tests/Ajax/AjaxCommandsUnitTest.php b/core/modules/system/lib/Drupal/system/Tests/Ajax/AjaxCommandsUnitTest.php index 2e6c421..4a723c4 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Ajax/AjaxCommandsUnitTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Ajax/AjaxCommandsUnitTest.php @@ -7,7 +7,7 @@ namespace Drupal\system\Tests\Ajax; -use Drupal\simpletest\UnitTestBase; +use Drupal\simpletest\DrupalUnitTestBase; use Drupal\Core\Ajax\AddCssCommand; use Drupal\Core\Ajax\AfterCommand; use Drupal\Core\Ajax\AlertCommand; @@ -24,11 +24,21 @@ use Drupal\Core\Ajax\ReplaceCommand; use Drupal\Core\Ajax\RestripeCommand; use Drupal\Core\Ajax\SettingsCommand; +use Drupal\Core\Ajax\OpenDialogCommand; +use Drupal\Core\Ajax\OpenModalDialogCommand; +use Drupal\Core\Ajax\CloseDialogCommand; +use Drupal\Core\Ajax\CloseModalDialogCommand; /** * Tests for all AJAX Commands. */ -class AjaxCommandsUnitTest extends UnitTestBase { +class AjaxCommandsUnitTest extends DrupalUnitTestBase { + + /** + * The modules to enable. + * @var array + */ + public static $modules = array('system'); public static function getInfo() { return array( @@ -305,5 +315,78 @@ function testSettingsCommand() { $this->assertEqual($command->render(), $expected, 'SettingsCommand::render() returns a proper array.'); } -} + /** + * Tests that OpenDialogCommand objects can be constructed and rendered. + */ + function testOpenDialogCommand() { + $command = new OpenDialogCommand('#some-dialog', 'Title', '

Text!

', array( + 'url' => FALSE, + 'width' => 500 + )); + + $expected = array( + 'command' => 'openDialog', + 'selector' => '#some-dialog', + 'settings' => NULL, + 'data' => '

Text!

', + 'dialog' => array( + 'url' => FALSE, + 'width' => 500, + 'title' => 'Title', + 'modal' => FALSE + ) + ); + $this->assertEqual($command->render(), $expected, 'OpenDialogCommand::render() returns a proper array.'); + } + + /** + * Tests that OpenModalDialogCommand objects can be constructed and rendered. + */ + function testOpenModalDialogCommand() { + $command = new OpenModalDialogCommand('Title', '

Text!

', array( + 'url' => 'example', + 'width' => 500 + )); + + $expected = array( + 'command' => 'openDialog', + 'selector' => '#drupal-modal', + 'settings' => NULL, + 'data' => '

Text!

', + 'dialog' => array( + 'url' => 'example', + 'width' => 500, + 'title' => 'Title', + 'modal' => TRUE, + ) + ); + $this->assertEqual($command->render(), $expected, 'OpenModalDialogCommand::render() returns a proper array.'); + } + + /** + * Tests that CloseModalDialogCommand objects can be constructed and rendered. + */ + function testCloseModalDialogCommand() { + $command = new CloseModalDialogCommand(); + $expected = array( + 'command' => 'closeDialog', + 'selector' => '#drupal-modal', + ); + + $this->assertEqual($command->render(), $expected, 'CloseModalDialogCommand::render() returns a proper array.'); + } + /** + * Tests that CloseDialogCommand objects can be constructed and rendered. + */ + function testCloseDialogCommand() { + $command = new CloseDialogCommand('#some-dialog'); + $expected = array( + 'command' => 'closeDialog', + 'selector' => '#some-dialog', + ); + + $this->assertEqual($command->render(), $expected, 'CloseDialogCommand::render() with a selector returns a proper array.'); + } + +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Ajax/FrameworkTest.php b/core/modules/system/lib/Drupal/system/Tests/Ajax/FrameworkTest.php index 8c408ed..219bb44 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Ajax/FrameworkTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Ajax/FrameworkTest.php @@ -35,12 +35,6 @@ function testAJAXRender() { $commands = $this->drupalGetAJAX('ajax-test/render'); $expected = new SettingsCommand(array('ajax' => 'test'), TRUE); $this->assertCommand($commands, $expected->render(), 'ajax_render() loads settings added with drupal_add_js().'); - - // Verify that JavaScript settings are loaded for #type 'link'. - $this->drupalGet('ajax-test/link'); - $settings = $this->drupalGetSettings(); - $this->assertEqual($settings['ajax']['ajax-link']['url'], url('filter/tips')); - $this->assertEqual($settings['ajax']['ajax-link']['wrapper'], 'block-system-main'); } /** diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 3f01968..1e187f2 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -1305,6 +1305,19 @@ function system_library_info() { ), ); + // Drupal's integration between AJAX and dialogs. + $libraries['drupal.dialog.ajax'] = array( + 'title' => 'Drupal Dialog AJAX', + 'version' => VERSION, + 'js' => array( + 'core/misc/dialog.ajax.js' => array('group' => JS_LIBRARY, 'weight' => 3), + ), + 'dependencies' => array( + array('system', 'drupal.ajax'), + array('system', 'drupal.dialog'), + ), + ); + // Drupal's states library. $libraries['drupal.states'] = array( 'title' => 'Drupal states', diff --git a/core/modules/system/tests/modules/ajax_test/ajax_test.module b/core/modules/system/tests/modules/ajax_test/ajax_test.module index 889d023..3cf50a4 100644 --- a/core/modules/system/tests/modules/ajax_test/ajax_test.module +++ b/core/modules/system/tests/modules/ajax_test/ajax_test.module @@ -6,6 +6,9 @@ */ use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\OpenDialogCommand; +use Drupal\Core\Ajax\OpenModalDialogCommand; +use Drupal\Core\Ajax\CloseDialogCommand; use Drupal\Core\Ajax\HtmlCommand; /** @@ -31,11 +34,6 @@ function ajax_test_menu() { 'access callback' => TRUE, 'type' => MENU_CALLBACK, ); - $items['ajax-test/link'] = array( - 'title' => 'AJAX Link', - 'page callback' => 'ajax_test_link', - 'access callback' => TRUE, - ); $items['ajax-test/dialog'] = array( 'title' => 'AJAX Dialog', 'page callback' => 'ajax_test_dialog', @@ -46,6 +44,12 @@ function ajax_test_menu() { 'page callback' => 'ajax_test_dialog_contents', 'access callback' => TRUE, ); + $items['ajax-test/dialog-close'] = array( + 'title' => 'AJAX Dialog close', + 'page callback' => 'ajax_test_dialog_close', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); return $items; } @@ -102,21 +106,6 @@ function ajax_test_error() { } /** - * Menu callback: Renders a #type link with #ajax. - */ -function ajax_test_link() { - $build['link'] = array( - '#type' => 'link', - '#title' => 'Show help', - '#href' => 'filter/tips', - '#ajax' => array( - 'wrapper' => 'block-system-main', - ), - ); - return $build; -} - -/** * Menu callback: Renders a form elements and links with #ajax['dialog']. */ function ajax_test_dialog() { @@ -131,10 +120,8 @@ function ajax_test_dialog() { $build['link'] = array( '#type' => 'link', '#title' => 'Link 1 (modal)', - '#href' => 'ajax-test/dialog-contents', - '#ajax' => array( - 'dialog' => array('modal' => TRUE), - ), + '#href' => 'ajax-test/dialog-contents/nojs/1', + '#attributes' => array('class' => array('use-ajax')), ); // Dialog behavior applied to links rendered by theme_links(). @@ -143,18 +130,18 @@ function ajax_test_dialog() { '#links' => array( 'link2' => array( 'title' => 'Link 2 (modal)', - 'href' => 'ajax-test/dialog-contents', - 'ajax' => array( - 'dialog' => array('modal' => TRUE), - ), + 'href' => 'ajax-test/dialog-contents/nojs/1', + 'attributes' => array('class' => array('use-ajax')), ), 'link3' => array( 'title' => 'Link 3 (non-modal)', - 'href' => 'ajax-test/dialog-contents', - 'ajax' => array( - 'dialog' => array(), - 'wrapper' => 'ajax-test-dialog-wrapper-2', - ), + 'href' => 'ajax-test/dialog-contents/nojs', + 'attributes' => array('class' => array('use-ajax')), + ), + 'link4' => array( + 'title' => 'Link 4 (close non-modal if open)', + 'href' => 'ajax-test/dialog-close', + 'attributes' => array('class' => array('use-ajax')), ), ), ); @@ -169,33 +156,46 @@ function ajax_test_dialog_form($form, &$form_state) { '#type' => 'submit', '#value' => 'Button 1 (modal)', '#ajax' => array( - 'dialog' => array('modal' => TRUE), + 'callback' => 'ajax_test_dialog_form_callback_modal', ), ); $form['button2'] = array( '#type' => 'submit', '#value' => 'Button 2 (non-modal)', '#ajax' => array( - 'dialog' => array(), - 'wrapper' => 'ajax-test-dialog-wrapper-1', + 'callback' => 'ajax_test_dialog_form_callback_nonmodal', ), ); return $form; } /** - * Form submit handler for ajax_test_dialog_form(). + * Non-AJAX behavior of the dialog buttons. */ function ajax_test_dialog_form_submit($form, &$form_state) { $form_state['redirect'] = 'ajax-test/dialog-contents'; } /** + * AJAX callback handler for ajax_test_dialog_form(). + */ +function ajax_test_dialog_form_callback_modal($form, &$form_state) { + return ajax_test_dialog_contents('ajax', TRUE); +} + +/** + * AJAX callback handler for ajax_test_dialog_form(). + */ +function ajax_test_dialog_form_callback_nonmodal($form, &$form_state) { + return ajax_test_dialog_contents('ajax', FALSE); +} + +/** * Menu callback: Returns the contents for dialogs opened by ajax_test_dialog(). */ -function ajax_test_dialog_contents() { +function ajax_test_dialog_contents($page_mode = 'nojs', $is_modal = 0) { // This is a regular render array; the keys do not have special meaning. - return array( + $content = array( 'content' => array( '#markup' => 'Example message', ), @@ -210,5 +210,30 @@ function ajax_test_dialog_contents() { ), ), ); + + if ($page_mode === 'ajax') { + $response = new AjaxResponse(); + $title = t('AJAX Dialog'); + $html = drupal_render($content); + if ($is_modal) { + $response->addCommand(new OpenModalDialogCommand($title, $html)); + } + else { + $selector = '#ajax-test-dialog-wrapper-1'; + $response->addCommand(new OpenDialogCommand($selector, $title, $html)); + } + return $response; + } + else { + return $content; + } } +/** + * Menu callback: Close the ajax dialog. + */ +function ajax_test_dialog_close() { + $response = new AjaxResponse(); + $response->addCommand(new CloseDialogCommand('#ajax-test-dialog-wrapper-1')); + return $response; +}