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' => '
', '#suffix' => '
', + ); + + $links[] = array( + '#type' => 'link', + '#href' => 'dialog_example/simple', + '#title' => t('Simple Form [normal, i.e. non-js/non-ajax fallback]'), + // i know this is evil, but alas ... + '#prefix' => '
', '#suffix' => '
', + ); + + $links[] = array( + '#type' => 'link', + '#href' => 'dialog_example/complex', + '#title' => t('Complex Form [modal]'), + '#ajax' => array( + 'dialog' => array( + 'modal' => TRUE, + ), + ), + // i know this is evil, but alas ... + '#prefix' => '
', '#suffix' => '
', + ); + + $links[] = array( + '#type' => 'link', + '#href' => 'dialog_example/complex', + '#title' => t('Complex Form [modal w/ dialog-settings: 750px wide]'), + '#ajax' => array( + 'dialog' => array( + 'modal' => TRUE, + // Some settings passed to the dialog + 'settings' => array( + 'width' => '750px', + ) + ), + ), + // i know this is evil, but alas ... + '#prefix' => '
', '#suffix' => '
', + ); + + $links[] = array( + '#type' => 'link', + '#href' => 'dialog_example/complex', + '#title' => t('Complex Form [normal, i.e. non-js/non-ajax fallback]'), + // i know this is evil, but alas ... + '#prefix' => '
', '#suffix' => '
', + ); + + $links[] = array( + '#type' => 'link', + '#href' => 'dialog_example/complex/1', + '#title' => t('Complex Form [non-modal dialog-1]'), + '#ajax' => array( + 'wrapper' => '#dialog-1', + 'dialog' => array( + 'modal' => FALSE, + 'settings' => array( + 'width' => '500px', + ) + ) + ), + // i know this is evil, but alas ... + '#prefix' => '

NON-MODAL DIALOGS:

', '#suffix' => '
' + ); + + $links[] = array( + '#type' => 'link', + '#href' => 'dialog_example/complex/2', + '#title' => t('Complex Form [non-modal dialog-2]'), + '#ajax' => array( + 'wrapper' => '#dialog-1', + 'dialog' => array( + 'modal' => FALSE, + // @todo: some settings passed to the dialog + 'settings' => array( + 'width' => '500px', + ) + ) + ), + // i know this is evil, but alas ... + '#prefix' => '
', '#suffix' => '
' + ); + + return drupal_render($links); +} + +function dialog_example_form($type, $dialog_selector=NULL) { + switch ($type) { + case 'simple': + return drupal_get_form('dialog_example_simple_form'); + break; + case 'complex': + if (!ajax_is_ajax_request()) { + return drupal_get_form('dialog_example_complex_form'); + } + // For non-modal dialogs the current API suggest, creating + $dialog_selector = !empty($dialog_selector) ? ( '#dialog-' . $dialog_selector ) : FALSE; + $form_state = array(); + + $response = new AjaxResponse(); + // + $openDialogCommand = ajax_dialog_form_wrapper('dialog_example_complex_form', $form_state, array( + 'selector' => $dialog_selector, + 'modal' => ( $dialog_selector == '#drupal-modal' || !$dialog_selector ) + )); + // Depending on form state do things ... + if (!empty($form_state['executed'])) { + $response->addCommand(new CloseDialogCommand($dialog_selector ? $dialog_selector : '#drupal-modal')); + $response->addCommand(new ReplaceCommand('h1', '

' . date('Ymd His') . ' - ' . t('thanks for confirming') . '

')); + } + elseif (!empty($form_state['submitted'])) { + // Change the settings of the dialog. + $openDialogCommand->setDialogSetting('width', '700' + rand(0,200) + 'px'); + $openDialogCommand->setDialogSetting('height', '200' + rand(0,200) + 'px'); + + $response->addCommand($openDialogCommand); + $response->addCommand(new ReplaceCommand('h1', '

' . date('Ymd His') . ' - ' . t('shenanigans under the dialog') . '

')); + } else { + $response->addCommand($openDialogCommand); + } + return $response; + } +} + +function dialog_example_simple_form() { + $form = array(); + $cancel = 'dialog_example'; + $form = confirm_form($form, + t('Simple form'), + $cancel, + t('Click ok or cancel.'), + t('Ok'), + t('Cancel')); + return $form; +} + +function dialog_example_simple_form_submit($form, &$form_state) { + drupal_set_message(t('Submitted and full page reload / redirect')); + $form_state['redirect'] = 'dialog_example'; +} + + +function dialog_example_complex_form() { + $form = array(); + $cancel = 'dialog_example'; + $form = confirm_form($form, + t('Complex form'), + $cancel, + t('Click ok or cancel.'), + t('Ok'), + t('Cancel')); + $form['actions']['okayish'] = array( + '#type' => 'submit', + '#value' => t('Okayish'), + '#weight' => -10 + ); + $form['#validate'][] = 'dialog_example_complex_form_validate'; + return $form; +} + +function dialog_example_complex_form_validate(&$form, &$form_state) { + $values = $form_state['values']; + if ($values['op'] !== t('Ok')) { + form_set_error('example', t('Click "Ok" to execute this form. Note: if this message is w/in a dialog, you should see the dialog being resized randomly to illustrate changing dialog settings via AJAX.')); + } +} + +function dialog_example_complex_form_submit($form, &$form_state) { + drupal_set_message(t('You submitted the complex form')); + $form_state['redirect'] = 'dialog_example'; +}