diff --git a/core/core.services.yml b/core/core.services.yml index 9fb9ebd..250e387 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -261,8 +261,18 @@ services: class: Drupal\Core\Routing\Enhancer\AjaxEnhancer arguments: ['@content_negotiation'] tags: + - { name: route_enhancer, priority: 40 } + - { name: legacy_route_enhancer, priority: 40 } + route_enhancer.dialog: + class: Drupal\Core\Routing\Enhancer\DialogEnhancer + arguments: ['@content_negotiation'] + tags: + - { name: route_enhancer, priority: 30 } + route_enhancer.modal: + class: Drupal\Core\Routing\Enhancer\ModalEnhancer + arguments: ['@content_negotiation'] + tags: - { name: route_enhancer, priority: 20 } - - { name: legacy_route_enhancer, priority: 20 } route_enhancer.form: class: Drupal\Core\Routing\Enhancer\FormEnhancer arguments: ['@content_negotiation'] diff --git a/core/includes/ajax.inc b/core/includes/ajax.inc index ce56aa7..42843fa 100644 --- a/core/includes/ajax.inc +++ b/core/includes/ajax.inc @@ -519,6 +519,7 @@ function ajax_process_form($element, &$form_state) { * - #ajax['wrapper'] * - #ajax['parameters'] * - #ajax['effect'] + * - #ajax['accepts'] * * @return * The processed element with the necessary JavaScript attached to it. @@ -607,6 +608,7 @@ function ajax_pre_render_element($element) { $settings += array( 'path' => isset($settings['callback']) ? 'system/ajax' : NULL, 'options' => array(), + 'accepts' => 'application/vnd.drupal-ajax' ); // @todo Legacy support. Remove in Drupal 8. diff --git a/core/lib/Drupal/Core/Ajax/AjaxSubscriber.php b/core/lib/Drupal/Core/Ajax/AjaxSubscriber.php index 5ef67c0..414511f 100644 --- a/core/lib/Drupal/Core/Ajax/AjaxSubscriber.php +++ b/core/lib/Drupal/Core/Ajax/AjaxSubscriber.php @@ -27,6 +27,8 @@ public function onKernelRequest(GetResponseEvent $event) { // @todo Refactor 'drupal_ajax' to just 'ajax' once all Ajax is converted to // Drupal 8's API. $request->setFormat('drupal_ajax', 'application/vnd.drupal-ajax'); + $request->setFormat('drupal_dialog', 'application/vnd.drupal-dialog'); + $request->setFormat('drupal_modal', 'application/vnd.drupal-modal'); } /** @@ -40,4 +42,4 @@ static function getSubscribedEvents(){ return $events; } -} \ No newline at end of file +} diff --git a/core/lib/Drupal/Core/Ajax/DialogController.php b/core/lib/Drupal/Core/Ajax/DialogController.php new file mode 100644 index 0000000..1ff011e --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/DialogController.php @@ -0,0 +1,135 @@ +attributes; + // We need to clean up the derived information and such so that the + // subrequest can be processed properly without leaking data through. + $attributes->remove('system_path'); + $attributes->remove('_legacy'); + + // Remove the accept header so the subrequest does not end up back in this + // controller. + $request->headers->remove('accept'); + + return $this->container->get('http_kernel')->forward($content, $attributes->all(), $request->query->all()); + } + + /** + * Wrapper to display content in a modal dialog. + * + * @param Request $request + * The request object. + * @param callable $_content + * The body content callable that contains the body region of this page. + * + * @return \Drupal\Core\Ajax\AjaxResponse + * AjaxResponse to return the content wrapper in a modal dialog. + */ + public function modal(Request $request, $_content) { + return $this->dialog($request, $_content, TRUE); + } + + /** + * Wrapper to display content in a dialog. + * + * @param Request $request + * The request object. + * @param callable $_content + * The body content callable that contains the body region of this page. + * @param bool $modal + * (optional) TRUE to render a modal dialog. Defaults to FALSE. + * + * @return \Drupal\Core\Ajax\AjaxResponse + * AjaxResponse to return the content wrapper in a dialog. + */ + public function dialog(Request $request, $_content, $modal = FALSE) { + $subrequest = $this->forward($request, $_content); + if ($subrequest->isOk()) { + $output = $subrequest->getContent(); + // A page callback could return a render array or a string. + if (is_array($output)) { + // Subrequest has set dialog title in the output. + if (isset($output['title'])) { + $title = $output['title']['#value']; + unset($output['title']); + } + else { + // Legacy fallback. + // @todo remove when http://drupal.org/node/1830588 lands. + $title = drupal_get_title(); + } + $content = drupal_render($output); + } + else { + // Legacy fallback. + // @todo remove when http://drupal.org/node/1830588 lands and all routes + // converted to return render arrays. + $title = drupal_get_title(); + $content = $output; + } + $response = new AjaxResponse(); + // Fetch any modal options passed in from data-dialog-options. + if (!($options = $request->request->get('dialog_options'))) { + $options = array(); + } + // Set modal flag and re-use the modal id. + if ($modal) { + $options['modal'] = TRUE; + $target = '#drupal-modal'; + } + else { + // Generate the target wrapper for the dialog. + if (isset($options['target'])) { + // If the target was nominated in the incoming options, use that. + $target = $options['target']; + // Ensure the target includes the #. + if (substr($target, 0, 1) != '#') { + $target = '#' . $target; + } + // This shouldn't be passed on to jQuery.ui.dialog. + unset($options['target']); + } + else { + // Generate a target based on the controller. + $target = '#drupal-dialog-' . drupal_html_id(drupal_clean_css_identifier(drupal_strtolower($_content))); + } + } + $response->addCommand(new OpenDialogCommand($target, $title, $content, $options)); + return $response; + } + // An error occurred in the subrequest, return that. + return $subrequest; + } +} diff --git a/core/lib/Drupal/Core/ContentNegotiation.php b/core/lib/Drupal/Core/ContentNegotiation.php index a2c17b5..d2ffa45 100644 --- a/core/lib/Drupal/Core/ContentNegotiation.php +++ b/core/lib/Drupal/Core/ContentNegotiation.php @@ -26,7 +26,7 @@ class ContentNegotiation { * @param Symfony\Component\HttpFoundation\Request $request * The request object from which to extract the content type. * - * @return + * @return string * The normalized type of a given request. */ public function getContentType(Request $request) { @@ -36,11 +36,12 @@ public function getContentType(Request $request) { return 'iframeupload'; } - // Check all formats, it HTML is found return it. + // Check all formats, if priority format is found return it. $first_found_format = FALSE; foreach ($request->getAcceptableContentTypes() as $mime_type) { $format = $request->getFormat($mime_type); - if ($format === 'html' || $format === 'drupal_ajax') { + $priority = array('html', 'drupal_ajax', 'drupal_modal', 'drupal_dialog'); + if (in_array($format, $priority, TRUE)) { return $format; } if (!is_null($format) && !$first_found_format) { diff --git a/core/lib/Drupal/Core/Routing/Enhancer/DialogEnhancer.php b/core/lib/Drupal/Core/Routing/Enhancer/DialogEnhancer.php new file mode 100644 index 0000000..ded14e0 --- /dev/null +++ b/core/lib/Drupal/Core/Routing/Enhancer/DialogEnhancer.php @@ -0,0 +1,62 @@ +negotiation = $negotiation; + } + + /** + * {@inheritdoc} + */ + public function enhance(array $defaults, Request $request) { + if ($this->negotiation->getContentType($request) == $this->targetContentType) { + if (empty($defaults['_content']) && !empty($defaults['controller'])) { + // Pass the controller on as content. + $defaults['_content'] = $defaults['_controller']; + } + $defaults['_controller'] = $this->controller; + } + } +} diff --git a/core/lib/Drupal/Core/Routing/Enhancer/ModalEnhancer.php b/core/lib/Drupal/Core/Routing/Enhancer/ModalEnhancer.php new file mode 100644 index 0000000..c492e8c --- /dev/null +++ b/core/lib/Drupal/Core/Routing/Enhancer/ModalEnhancer.php @@ -0,0 +1,32 @@ + 'admin/config/development/sync/diff/' . $config_file, 'attributes' => array( 'class' => array('use-ajax'), + 'data-accepts' => 'application/vnd.drupal-modal', + 'data-dialog-options' => json_encode(array( + 'width' => 500 + )), ), ); $form[$config_change_type]['list']['#rows'][] = array( @@ -121,7 +125,7 @@ function config_admin_import_form_submit($form, &$form_state) { if (!lock()->lockMayBeAvailable(CONFIG_IMPORT_LOCK)) { drupal_set_message(t('Another request may be synchronizing configuration already.')); } - else if (config_import()) { + elseif (config_import()) { // Once a sync completes, we empty the staging directory. This prevents // changes from being accidentally overwritten by stray files getting // imported later. @@ -138,69 +142,3 @@ function config_admin_import_form_submit($form, &$form_state) { drupal_set_message(t('The import failed due to an error. Any errors have been logged.'), 'error'); } } - -/** - * Page callback: Shows diff of specificed configuration file. - * - * @param string $config_file - * The name of the configuration file. - * - * @return string - * Table showing a two-way diff between the active and staged configuration. - */ -function config_admin_diff_page($config_file) { - // Retrieve a list of differences between last known state and active store. - $source_storage = drupal_container()->get('config.storage.staging'); - $target_storage = drupal_container()->get('config.storage'); - - // Add the CSS for the inline diff. - $output['#attached']['css'][] = drupal_get_path('module', 'system') . '/system.diff.css'; - - $diff = config_diff($target_storage, $source_storage, $config_file); - $formatter = new DrupalDiffFormatter(); - $formatter->show_header = FALSE; - - $variables = array( - 'header' => array( - array('data' => t('Old'), 'colspan' => '2'), - array('data' => t('New'), 'colspan' => '2'), - ), - 'rows' => $formatter->format($diff), - ); - - $output['diff'] = array( - '#markup' => theme('table', $variables), - ); - - $output['back'] = array( - '#type' => 'link', - '#title' => "Back to 'Synchronize configuration' page.", - '#href' => 'admin/config/development/sync', - ); - - $title = t('View changes of @config_file', array('@config_file' => $config_file)); - - // Return AJAX requests as a dialog. - // @todo: Set up separate content callbacks for the non-JS and dialog versions - // of this page using the router system. See http://drupal.org/node/1944472. - if (Drupal::request()->isXmlHttpRequest()) { - // Add class to the close link. - $output['back']['#attributes']['class'][] = 'dialog-cancel'; - - $dialog_content = drupal_render($output); - $response = new AjaxResponse(); - $response->addCommand(new OpenModalDialogCommand($title, $dialog_content, array('width' => '700'))); - return $response; - } - // Otherwise show the page title as an element. - else { - $output['title'] = array( - '#theme' => 'html_tag', - '#tag' => 'h3', - '#value' => $title, - '#weight' => -10, - ); - } - - return $output; -} diff --git a/core/modules/config/config.module b/core/modules/config/config.module index e3b6027..651e387 100644 --- a/core/modules/config/config.module +++ b/core/modules/config/config.module @@ -48,18 +48,9 @@ function config_menu() { 'access arguments' => array('synchronize configuration'), 'file' => 'config.admin.inc', ); - $items['admin/config/development/sync/diff/%'] = array( - 'title' => 'Configuration file diff', - 'description' => 'Diff between active and staged configuraiton.', - 'page callback' => 'config_admin_diff_page', - 'page arguments' => array(5), - 'access arguments' => array('synchronize configuration'), - 'file' => 'config.admin.inc', - ); $items['admin/config/development/sync/import'] = array( 'title' => 'Import', 'type' => MENU_DEFAULT_LOCAL_TASK, ); return $items; } - diff --git a/core/modules/config/config.routing.yml b/core/modules/config/config.routing.yml new file mode 100644 index 0000000..505539d --- /dev/null +++ b/core/modules/config/config.routing.yml @@ -0,0 +1,6 @@ +config_diff: + pattern: '/admin/config/development/sync/diff/{config_file}' + defaults: + _content: '\Drupal\config\Controller\ConfigController::diff' + requirements: + _permission: 'synchronize configuration' diff --git a/core/modules/config/lib/Drupal/config/Controller/ConfigController.php b/core/modules/config/lib/Drupal/config/Controller/ConfigController.php new file mode 100644 index 0000000..471d656 --- /dev/null +++ b/core/modules/config/lib/Drupal/config/Controller/ConfigController.php @@ -0,0 +1,102 @@ +get('config.storage'), $container->get('config.storage.staging')); + } + + /** + * Constructor. + * + * @param \Drupal\Core\Config\StorageInterface $target_storage + * The target storage. + * @param \Drupal\Core\Config\StorageInterface $source_storage + * The source storage + */ + public function __construct(StorageInterface $target_storage, StorageInterface $source_storage) { + $this->targetStorage = $target_storage; + $this->sourceStorage = $source_storage; + } + + /** + * Shows diff of specificed configuration file. + * + * @param string $config_file + * The name of the configuration file. + * + * @return string + * Table showing a two-way diff between the active and staged configuration. + */ + public function diff($config_file) { + // Add the CSS for the inline diff. + $output['#attached']['css'][] = drupal_get_path('module', 'system') . '/system.diff.css'; + + $diff = config_diff($this->targetStorage, $this->sourceStorage, $config_file); + $formatter = new \DrupalDiffFormatter(); + $formatter->show_header = FALSE; + + $variables = array( + 'header' => array( + array('data' => t('Old'), 'colspan' => '2'), + array('data' => t('New'), 'colspan' => '2'), + ), + 'rows' => $formatter->format($diff), + ); + + $output['diff'] = array( + '#markup' => theme('table', $variables), + ); + + $output['back'] = array( + '#type' => 'link', + '#attributes' => array( + 'class' => array( + 'dialog-cancel', + ), + ), + '#title' => "Back to 'Synchronize configuration' page.", + '#href' => 'admin/config/development/sync', + ); + + $title = t('View changes of @config_file', array('@config_file' => $config_file)); + $output['title'] = array( + '#theme' => 'html_tag', + '#tag' => 'h3', + '#value' => $title, + '#weight' => -10, + ); + + return $output; + } +} diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php index a5c34b3..d79bf81 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php @@ -1197,6 +1197,16 @@ protected function drupalGet($path, array $options = array(), array $headers = a /** * Retrieve a Drupal path or an absolute path and JSON decode the result. + * + * @param string $path + * Path to request AJAX from. + * @param array $options + * Array of options to pass to url(). + * @param array $headers + * Array of headers. Eg array('Accept: application/vnd.drupal-ajax'). + * + * @return array + * Decoded json. */ protected function drupalGetAJAX($path, array $options = array(), array $headers = array()) { $headers[] = 'X-Requested-With: XMLHttpRequest'; diff --git a/core/modules/system/lib/Drupal/system/Tests/Ajax/DialogTest.php b/core/modules/system/lib/Drupal/system/Tests/Ajax/DialogTest.php index 32aea71..7b4e093 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Ajax/DialogTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Ajax/DialogTest.php @@ -11,6 +11,10 @@ * Tests use of dialogs as wrappers for Ajax responses. */ class DialogTest extends AjaxTestBase { + + /** + * Declares test info. + */ public static function getInfo() { return array( 'name' => 'AJAX dialogs commands', @@ -22,7 +26,7 @@ public static function getInfo() { /** * Test sending non-JS and AJAX requests to open and manipulate modals. */ - function testDialog() { + public function testDialog() { // Ensure the elements render without notices or exceptions. $this->drupalGet('ajax-test/dialog'); @@ -35,8 +39,8 @@ function testDialog() { 'settings' => NULL, 'data' => $dialog_contents, 'dialogOptions' => array( - 'modal' => true, - 'title' => 'AJAX Dialog', + 'modal' => TRUE, + 'title' => 'AJAX Dialog contents', ), ); $normal_expected_response = array( @@ -45,8 +49,8 @@ function testDialog() { 'settings' => NULL, 'data' => $dialog_contents, 'dialogOptions' => array( - 'modal' => false, - 'title' => 'AJAX Dialog', + 'modal' => FALSE, + 'title' => 'AJAX Dialog contents', ), ); $close_expected_response = array( @@ -55,20 +59,29 @@ function testDialog() { ); // Check that requesting a modal dialog without JS goes to a page. - $this->drupalGet('ajax-test/dialog-contents/nojs/1'); + $this->drupalGet('ajax-test/dialog-contents'); $this->assertRaw($dialog_contents, 'Non-JS modal dialog page present.'); // Emulate going to the JS version of the page and check the JSON response. - $ajax_result = $this->drupalGetAJAX('ajax-test/dialog-contents/ajax/1'); + $ajax_result = $this->drupalGetAJAX('ajax-test/dialog-contents', array(), array('Accept: application/vnd.drupal-modal')); $this->assertEqual($modal_expected_response, $ajax_result[1], 'Modal dialog JSON response matches.'); // Check that requesting a "normal" dialog without JS goes to a page. - $this->drupalGet('ajax-test/dialog-contents/nojs'); + $this->drupalGet('ajax-test/dialog-contents'); $this->assertRaw($dialog_contents, 'Non-JS normal dialog page present.'); // Emulate going to the JS version of the page and check the JSON response. - $ajax_result = $this->drupalGetAJAX('ajax-test/dialog-contents/ajax'); - $this->assertEqual($normal_expected_response, $ajax_result[1], 'Normal dialog JSON response matches.'); + // This needs to use WebTestBase::drupalPostAJAX() so that the correct + // dialog options are sent. + $ajax_result = $this->drupalPostAJAX('ajax-test/dialog', array( + // We have to mock a form element to make drupalPost submit from a link. + 'textfield' => 'test', + ), array(), 'ajax-test/dialog-contents', array(), array('Accept: application/vnd.drupal-dialog'), NULL, array( + 'submit' => array( + 'dialog_options[target]' => 'ajax-test-dialog-wrapper-1', + ) + )); + $this->assertEqual($normal_expected_response, $ajax_result[3], 'Normal dialog JSON response matches.'); // Emulate closing the dialog via an AJAX request. There is no non-JS // version of this test. 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 2ed5a16..5421f16 100644 --- a/core/modules/system/tests/modules/ajax_test/ajax_test.module +++ b/core/modules/system/tests/modules/ajax_test/ajax_test.module @@ -124,8 +124,11 @@ function ajax_test_dialog() { $build['link'] = array( '#type' => 'link', '#title' => 'Link 1 (modal)', - '#href' => 'ajax-test/dialog-contents/nojs/1', - '#attributes' => array('class' => array('use-ajax')), + '#href' => 'ajax-test/dialog-contents', + '#attributes' => array( + 'class' => array('use-ajax'), + 'data-accepts' => 'application/vnd.drupal-modal', + ), ); // Dialog behavior applied to links rendered by theme_links(). @@ -134,18 +137,33 @@ function ajax_test_dialog() { '#links' => array( 'link2' => array( 'title' => 'Link 2 (modal)', - 'href' => 'ajax-test/dialog-contents/nojs/1', - 'attributes' => array('class' => array('use-ajax')), + 'href' => 'ajax-test/dialog-contents', + 'attributes' => array( + 'class' => array('use-ajax'), + 'data-accepts' => 'application/vnd.drupal-modal', + 'data-dialog-options' => json_encode(array( + 'width' => 400, + )) + ), ), 'link3' => array( 'title' => 'Link 3 (non-modal)', - 'href' => 'ajax-test/dialog-contents/nojs', - 'attributes' => array('class' => array('use-ajax')), + 'href' => 'ajax-test/dialog-contents', + 'attributes' => array( + 'class' => array('use-ajax'), + 'data-accepts' => 'application/vnd.drupal-dialog', + 'data-dialog-options' => json_encode(array( + 'target' => 'ajax-test-dialog-wrapper-1', + 'width' => 800, + )) + ), ), 'link4' => array( 'title' => 'Link 4 (close non-modal if open)', 'href' => 'ajax-test/dialog-close', - 'attributes' => array('class' => array('use-ajax')), + 'attributes' => array( + 'class' => array('use-ajax'), + ), ), ), ); @@ -156,6 +174,12 @@ function ajax_test_dialog() { * Form builder: Renders buttons with #ajax['dialog']. */ function ajax_test_dialog_form($form, &$form_state) { + // In order to use WebTestBase::drupalPostAJAX() to POST from a link, we need + // to have a dummy field we can set in WebTestBase::drupalPost() else it won't + // submit anything. + $form['textfield'] = array( + '#type' => 'hidden' + ); $form['button1'] = array( '#type' => 'submit', '#name' => 'button1', @@ -186,26 +210,47 @@ function ajax_test_dialog_form_submit($form, &$form_state) { * 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); + return _ajax_test_dialog(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); + return _ajax_test_dialog(FALSE); +} + +/** + * Util to render dialog in ajax callback. + * + * @param bool $is_modal + * (optional) TRUE if modal, FALSE if plain dialog. Defaults to FALSE. + */ +function _ajax_test_dialog($is_modal = FALSE) { + $content = ajax_test_dialog_contents(); + $response = new AjaxResponse(); + $title = t('AJAX Dialog contents'); + $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; } /** * Menu callback: Returns the contents for dialogs opened by ajax_test_dialog(). */ -function ajax_test_dialog_contents($page_mode = 'nojs', $is_modal = 0) { +function ajax_test_dialog_contents() { // This is a regular render array; the keys do not have special meaning. $content = array( 'content' => array( '#markup' => 'Example message', ), - 'cancel'=> array( + 'cancel' => array( '#type' => 'link', '#title' => 'Cancel', '#href' => '', @@ -217,22 +262,7 @@ function ajax_test_dialog_contents($page_mode = 'nojs', $is_modal = 0) { ), ); - 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; - } + return $content; } /**