diff --git a/core/core.services.yml b/core/core.services.yml index 5284bce..abce915 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -264,6 +264,16 @@ services: tags: - { name: event_subscriber } arguments: ['@settings'] + route_enhancer.modal: + class: Drupal\Core\Routing\Enhancer\ModalEnhancer + arguments: ['@content_negotiation'] + tags: + - { name: route_enhancer, priority: 40 } + route_enhancer.dialog: + class: Drupal\Core\Routing\Enhancer\DialogEnhancer + arguments: ['@content_negotiation'] + tags: + - { name: route_enhancer, priority: 30 } route_enhancer.ajax: class: Drupal\Core\Routing\Enhancer\AjaxEnhancer 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..595d64e 100644 --- a/core/lib/Drupal/Core/Ajax/AjaxSubscriber.php +++ b/core/lib/Drupal/Core/Ajax/AjaxSubscriber.php @@ -24,9 +24,9 @@ class AjaxSubscriber implements EventSubscriberInterface { */ public function onKernelRequest(GetResponseEvent $event) { $request = $event->getRequest(); - // @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'); } /** diff --git a/core/lib/Drupal/Core/Ajax/DialogController.php b/core/lib/Drupal/Core/Ajax/DialogController.php new file mode 100644 index 0000000..eff0d1f --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/DialogController.php @@ -0,0 +1,116 @@ +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'); + + // 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()); + } + + /** + * Displays content in a modal dialog. + * + * @param \Symfony\Component\HttpFoundation\RequestRequest $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); + } + + /** + * Displays content in a dialog. + * + * @param \Symfony\Component\HttpFoundation\RequestRequest $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()) { + $content = $subrequest->getContent(); + // @todo Remove use of drupal_get_title() when + // http://drupal.org/node/1871596 is in. + $title = drupal_get_title(); + $response = new AjaxResponse(); + // Fetch any modal options passed in from data-dialog-options. + if (!($options = $request->request->get('dialogOptions'))) { + $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..c14b6fa --- /dev/null +++ b/core/lib/Drupal/Core/Routing/Enhancer/DialogEnhancer.php @@ -0,0 +1,59 @@ +negotiation = $negotiation; + } + + /** + * {@inheritdoc} + */ + public function enhance(array $defaults, Request $request) { + if (empty($defaults['_controller']) && !empty($defaults['_content']) && $this->negotiation->getContentType($request) == $this->targetContentType) { + $defaults['_controller'] = $this->controller; + } + return $defaults; + } +} 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..652206b --- /dev/null +++ b/core/lib/Drupal/Core/Routing/Enhancer/ModalEnhancer.php @@ -0,0 +1,29 @@ + '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' => 700 + )), ), ); $form[$config_change_type]['list']['#rows'][] = array( @@ -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..8bb9d21 100644 --- a/core/modules/config/config.module +++ b/core/modules/config/config.module @@ -51,10 +51,7 @@ function config_menu() { $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', + 'route_name' => 'config_diff', ); $items['admin/config/development/sync/import'] = array( 'title' => 'Import', @@ -62,4 +59,3 @@ function config_menu() { ); 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..905c2bc --- /dev/null +++ b/core/modules/config/lib/Drupal/config/Controller/ConfigController.php @@ -0,0 +1,98 @@ +get('config.storage'), $container->get('config.storage.staging')); + } + + /** + * Constructs a ConfigController object. + * + * @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', + ); + + // @todo Remove use of drupal_set_title() when + // http://drupal.org/node/1871596 is in. + drupal_set_title('View changes of @config_file', array('@config_file' => $config_file)); + + return $output; + } +} diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php index 4b66223..144e5f5 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php @@ -1196,6 +1196,17 @@ protected function drupalGet($path, array $options = array(), array $headers = a } /** + * Retrieves 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. * Requests a Drupal path in JSON format, and JSON decodes the response. */ protected function drupalGetJSON($path, array $options = array(), array $headers = array()) { 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..64780fb 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( + 'dialogOptions[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..e7a252c 100644 --- a/core/modules/system/tests/modules/ajax_test/ajax_test.module +++ b/core/modules/system/tests/modules/ajax_test/ajax_test.module @@ -40,11 +40,6 @@ function ajax_test_menu() { 'page callback' => 'ajax_test_dialog', 'access callback' => TRUE, ); - $items['ajax-test/dialog-contents'] = array( - 'title' => 'AJAX Dialog contents', - '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', @@ -124,8 +119,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 +132,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 +169,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 +205,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); } /** - * Menu callback: Returns the contents for dialogs opened by ajax_test_dialog(). + * 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_contents($page_mode = 'nojs', $is_modal = 0) { +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; +} + +/** + * Returns example content for dialog tests. + */ +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 +257,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; } /** diff --git a/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml b/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml new file mode 100644 index 0000000..8379d64 --- /dev/null +++ b/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml @@ -0,0 +1,6 @@ +ajax_test_dialog_contents: + pattern: ajax-test/dialog-contents + defaults: + _content: '\Drupal\ajax_test\AjaxTestController::dialogContents' + requirements: + _access: 'TRUE' diff --git a/core/modules/system/tests/modules/ajax_test/lib/Drupal/ajax_test/AjaxTestController.php b/core/modules/system/tests/modules/ajax_test/lib/Drupal/ajax_test/AjaxTestController.php new file mode 100644 index 0000000..53044c5 --- /dev/null +++ b/core/modules/system/tests/modules/ajax_test/lib/Drupal/ajax_test/AjaxTestController.php @@ -0,0 +1,24 @@ +