diff --git a/core/includes/ajax.inc b/core/includes/ajax.inc index d8eb913..ea7c331 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 * @{ @@ -1158,3 +1161,99 @@ function ajax_command_add_css($styles) { 'data' => $styles, ); } + +/** + * Build and wrap a form as OpenDialogCommand. + * + * @todo cleanup function signature; merging the dialog_selector, + * dialog_settings. + * + * @param string $form_id + * The unique string identifying the desired form. + * @param array $form_state + * An associative array containing the current state of the form. + * @param array $dialog_settings + * An associative array of settings to be passed to the dialog implementation: + * - url: required - if a form is to be rebound + * - modal: true|false - if true the dialog will be a modal. + * + * These keys will be passed to Drupal's dialog implementation. + * + * @return \Drupal\Core\Ajax\OpenDialogCommand + */ +function ajax_dialog_form_wrapper($form_id, array &$form_state, array $dialog_settings = array() ) { + // This won't override settings already in. + $form_state += array( + 'rerender' => FALSE, + 'ajax' => TRUE, + 'no_redirect' => TRUE, + ); + + $form = drupal_build_form($form_id, $form_state); + return ajax_dialog_form_render($form, $form_state, $dialog_settings); +} + +/** + * Render a form output as a dialog command. + * + * @param array $form + * An associative array containing the structure of the form. + * @param array $form_state + * An associative array containing the current state of the form. + * @param array $dialog_settings + * An associative array of settings to be passed to the dialog implementation: + * - url: required - if a form is to be rebound + * - modal: true|false - if true the dialog will be a modal. + * + * These keys will be passed to Drupal's dialog implementation. + * + * @return \Drupal\Core\Ajax\OpenDialogCommand + */ +function ajax_dialog_form_render($form, $form_state, array $dialog_settings = array()) { + $output = drupal_render($form); + + $title = empty($form_state['title']) ? drupal_get_title() : $form_state['title']; + $url = empty($form_state['url']) ? url(current_path(), array( + 'absolute' => TRUE, + 'query' => drupal_container()->get('request')->query->all() + ) + ) : $form_state['url']; + + // If there are messages for the form, render them. + // @todo: perhaps put this into the AjaxRender class, as it seems to be + // something fundamental like css/js (@dawehner). + if ($messages = theme('status_messages')) { + $output = $messages . $output; + } + + // Pass the form URL to the dialog settings allowing the form to be re-bound. + $dialog_settings['url'] = $url; + + // Determine when to show a modal: either the modal key it set to TRUE or the + // selector is #drupal-modal. + if (!isset($dialog_settings['modal'])) { + $dialog_settings['modal'] = !isset($dialog_settings['selector']) || isset($dialog_settings['selector']) == '#drupal-modal'; + } + + if ($dialog_settings['modal']) { + return new OpenModalDialogCommand($title, $output, $dialog_settings); + } + else { + return new OpenDialogCommand($dialog_settings['selector'], $title, $output, $dialog_settings); + } + +} + +/** + * Helper function that checks whether this is an AJAX request. + * + * @return bool + * Return TRUE if the current request is asking for ajax content, else FALSE. + */ +function ajax_is_ajax_request() { + $container = drupal_container(); + if ($container->has('content_negotiation') && $container->isScopeActive('request')) { + return $container->get('content_negotiation')->getContentType($container->get('request')) == 'ajax'; + } + return FALSE; +} diff --git a/core/lib/Drupal/Core/Ajax/CloseDialogCommand.php b/core/lib/Drupal/Core/Ajax/CloseDialogCommand.php new file mode 100644 index 0000000..278c094 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/CloseDialogCommand.php @@ -0,0 +1,42 @@ +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..c10af73 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/OpenDialogCommand.php @@ -0,0 +1,106 @@ +selector = $selector; + $this->title = $title; + $this->html = $html; + $this->dialogSettings = $dialog_settings; + $this->settings = $settings; + } + + /** + * Returns the dialog settings. + * + * @return array + */ + public function getDialogSettings() { + return $this->dialogSettings; + } + + /** + * Sets the dialog settings array. + * + * @param array $dialog_settings + * An array of keys passed to the Drupal.dialog javascript object. + */ + public function setDialogSettings($dialog_settings) { + $this->dialogSettings = $dialog_settings; + } + + /** + * Sets a single dialog setting value. + * + * @param string $key + * Key of the dialog setting + * @param mixed $value + * Value of the + * + */ + public function setDialogSetting($key, $value) { + $this->dialogSettings[$key] = $value; + } + + /** + * Implements \Drupal\Core\Ajax\CommandInterface:render(). + */ + public function render() { + // For consistency ensure the modal option is set to TRUE or FALSE. + $this->dialogSettings['modal'] = isset($this->dialogSettings['modal']) && $this->dialogSettings['modal']; + return array( + 'command' => $this->dialogSettings['modal'] ? 'openModalDialog' : 'openDialog', + 'selector' => $this->selector, + 'title' => $this->title, + 'settings' => $this->settings, + 'data' => $this->html, + 'dialog' => $this->dialogSettings + ); + } + +} diff --git a/core/lib/Drupal/Core/Ajax/OpenModalDialogCommand.php b/core/lib/Drupal/Core/Ajax/OpenModalDialogCommand.php new file mode 100644 index 0000000..4d10969 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/OpenModalDialogCommand.php @@ -0,0 +1,38 @@ +').appendTo('body'); + } + // Set up the wrapper, if there isn't one. + if (!ajax.wrapper) { + ajax.wrapper = $dialog.attr('id'); + } + + response.dialog = response.dialog || {}; + // Piggyback core's insert command: see effulgentsia's/nod's comments here: + // http://drupal.org/node/1667742#comment-6738226) + response.command = 'insert'; + ajax.commands.insert(ajax, response, status); + }; + + /** + * Command to open a modal dialog. + */ + Drupal.ajax.prototype.commands.openModalDialog = function (ajax, response, status) { + response.selector = '#drupal-modal'; + response.dialog = response.dialog || {}; + response.dialog.modal = true; + ajax.commands.openDialog(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 = Drupal.dialog($dialog, {}); + dialog.close(); + } + }; + +})(jQuery, Drupal); diff --git a/core/misc/dialog.js b/core/misc/dialog.js index c0bccf1..70e7a5e 100644 --- a/core/misc/dialog.js +++ b/core/misc/dialog.js @@ -48,7 +48,8 @@ Drupal.dialog = function (element, options) { var undef; var $element = $(element); - var defaults = $.extend(options, drupalSettings.dialog); + // Extend the default settings in drupalSettings with the local options. + var defaults = $.extend({}, drupalSettings.dialog, options); var dialog = { open: false, returnValue: undef, 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..5e5a88e 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Ajax/AjaxCommandsUnitTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Ajax/AjaxCommandsUnitTest.php @@ -24,6 +24,10 @@ 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. @@ -305,5 +309,80 @@ 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', + 'title' => 'Title', + 'settings' => NULL, + 'data' => 'Text!
', + 'dialog' => array( + 'url' => FALSE, + 'width' => 500, + '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' => 'openModalDialog', + 'selector' => '#drupal-modal', + 'title' => 'Title', + 'settings' => NULL, + 'data' => 'Text!
', + 'dialog' => array( + 'url' => 'example', + 'width' => 500, + '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/system.module b/core/modules/system/system.module index 17015c6..d91aac9 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -1299,6 +1299,8 @@ function system_library_info() { 'version' => VERSION, 'js' => array( 'core/misc/dialog.js' => array('group' => JS_LIBRARY), + // @todo: does it make sense to have dialog w/o ajax? + 'core/misc/dialog.ajax.js' => array('group' => JS_LIBRARY, 'weight' => 3), ), 'dependencies' => array( array('system', 'jquery'), @@ -3393,6 +3395,13 @@ function confirm_form($form, $question, $path, $description = NULL, $yes = NULL, '#type' => 'submit', '#value' => $yes ? $yes : t('Confirm'), ); + + if (!isset($options['attributes']) || !isset($options['attributes']['class'])) { + $options['attributes']['class'] = array(); + } + // Add the class for improved UX. OTOH, this is not proper. + $options['attributes']['class'][] = 'dialog-cancel'; + $form['actions']['cancel'] = array( '#type' => 'link', '#title' => $no ? $no : t('Cancel'), 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 efb1966..536b9ef 100644 --- a/core/modules/system/tests/modules/ajax_test/ajax_test.module +++ b/core/modules/system/tests/modules/ajax_test/ajax_test.module @@ -1,5 +1,9 @@ 'ajax_test_dialog_contents', 'access callback' => TRUE, ); + $items['ajax-test/dialog-commands'] = array( + 'title' => 'AJAX Dialog commands', + 'page callback' => 'ajax_test_dialog_commands', + 'access callback' => TRUE, + ); + $items['ajax-test/dialog-commands-close'] = array( + 'title' => 'AJAX Dialog commands: close', + 'page callback' => 'ajax_test_dialog_commands_close', + 'access callback' => TRUE, + ); return $items; } @@ -95,6 +109,7 @@ function ajax_test_dialog() { // Dialog behavior applied to a button. $build['form'] = drupal_get_form('ajax_test_dialog_form'); + $build['form']['#attached']['library'][] = array('system', 'drupal.dialog'); // Dialog behavior applied to a #type => 'link'. $build['link'] = array( @@ -125,6 +140,13 @@ function ajax_test_dialog() { 'wrapper' => 'ajax-test-dialog-wrapper-2', ), ), + 'link4' => array( + 'title' => 'Link 4 (modal with ajax commands)', + 'href' => 'ajax-test/dialog-commands', + 'ajax' => array( + 'dialog' => array('modal' => TRUE), + ), + ), ), ); return $build; @@ -181,3 +203,39 @@ function ajax_test_dialog_contents() { ); } +/** + * Menu callback: Returns ajax commands to open a dialog. + */ +function ajax_test_dialog_commands() { + $response = new AjaxResponse(); + + $content = array( + 'content' => array( + '#markup' => 'Example message', + ), + 'cancel'=> array( + '#type' => 'link', + '#title' => 'Cancel', + '#href' => 'ajax-test/dialog-commands-close', + '#ajax' => array(), + ), + ); + + $content = drupal_render($content); + // @todo: This currently only tests the modal. let's + $response->addCommand(new OpenModalDialogCommand(t('Title'), $content)); + + return $response; +} + +/** + * Menu callback: Close the ajax dialog. + */ +function ajax_test_dialog_commands_close() { + $response = new AjaxResponse(); + + $response->addCommand(new CloseDialogCommand()); + + return $response; +} + diff --git a/modules/dialog_example/dialog_example.info b/modules/dialog_example/dialog_example.info new file mode 100644 index 0000000..75c1679 --- /dev/null +++ b/modules/dialog_example/dialog_example.info @@ -0,0 +1,4 @@ +name = dialog_example +description = Example module to test/prototype a dialog API that can handle complex use-cases. +package = Core +core = 8.x diff --git a/modules/dialog_example/dialog_example.module b/modules/dialog_example/dialog_example.module new file mode 100644 index 0000000..437d22e --- /dev/null +++ b/modules/dialog_example/dialog_example.module @@ -0,0 +1,217 @@ + 'Example dialogs', + 'page callback' => 'dialog_example_page', + 'access callback' => TRUE, + 'weight' => -1, + ); + $items['dialog_example/simple'] = array( + 'title' => 'Simple form callback', + 'page callback' => 'dialog_example_form', + 'page arguments' => array('simple'), + 'access callback' => TRUE, + ); + $items['dialog_example/complex'] = array( + 'title' => 'Complex form callback', + 'page callback' => 'dialog_example_form', + 'page arguments' => array('complex'), + 'access callback' => TRUE, + ); + $items['dialog_example/complex/%'] = array( + 'title' => 'Complex form callback', + 'page callback' => 'dialog_example_form', + 'page arguments' => array('complex', 2), + 'access callback' => TRUE, + ); + return $items; +} + +function dialog_example_page() { + $links = array(); + $links[] = array( + '#type' => 'link', + '#href' => 'dialog_example/simple', + '#title' => t('Simple Form [modal]'), + '#ajax' => array( + 'dialog' => array( + 'modal' => TRUE, + ), + ), + // i know this is evil, but alas ... + '#prefix' => '