Commit 22df5962 authored by webchick's avatar webchick

Issue #1944472 by larowlan, quicksketch, effulgentsia: Add generic content...

Issue #1944472 by larowlan, quicksketch, effulgentsia: Add generic content handler for returning dialogs.
parent cbdaaaf2
...@@ -264,6 +264,16 @@ services: ...@@ -264,6 +264,16 @@ services:
tags: tags:
- { name: event_subscriber } - { name: event_subscriber }
arguments: ['@settings'] 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: route_enhancer.ajax:
class: Drupal\Core\Routing\Enhancer\AjaxEnhancer class: Drupal\Core\Routing\Enhancer\AjaxEnhancer
arguments: ['@content_negotiation'] arguments: ['@content_negotiation']
......
...@@ -519,6 +519,7 @@ function ajax_process_form($element, &$form_state) { ...@@ -519,6 +519,7 @@ function ajax_process_form($element, &$form_state) {
* - #ajax['wrapper'] * - #ajax['wrapper']
* - #ajax['parameters'] * - #ajax['parameters']
* - #ajax['effect'] * - #ajax['effect']
* - #ajax['accepts']
* *
* @return * @return
* The processed element with the necessary JavaScript attached to it. * The processed element with the necessary JavaScript attached to it.
...@@ -607,6 +608,7 @@ function ajax_pre_render_element($element) { ...@@ -607,6 +608,7 @@ function ajax_pre_render_element($element) {
$settings += array( $settings += array(
'path' => isset($settings['callback']) ? 'system/ajax' : NULL, 'path' => isset($settings['callback']) ? 'system/ajax' : NULL,
'options' => array(), 'options' => array(),
'accepts' => 'application/vnd.drupal-ajax'
); );
// @todo Legacy support. Remove in Drupal 8. // @todo Legacy support. Remove in Drupal 8.
......
...@@ -24,9 +24,9 @@ class AjaxSubscriber implements EventSubscriberInterface { ...@@ -24,9 +24,9 @@ class AjaxSubscriber implements EventSubscriberInterface {
*/ */
public function onKernelRequest(GetResponseEvent $event) { public function onKernelRequest(GetResponseEvent $event) {
$request = $event->getRequest(); $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_ajax', 'application/vnd.drupal-ajax');
$request->setFormat('drupal_dialog', 'application/vnd.drupal-dialog');
$request->setFormat('drupal_modal', 'application/vnd.drupal-modal');
} }
/** /**
......
<?php
/**
* @file
* Contains \Drupal\core\Ajax\DialogController
*/
namespace Drupal\core\Ajax;
use Drupal\Core\ControllerInterface;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Ajax\OpenDialogCommand;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\DependencyInjection\ContainerAware;
/**
* Defines a default controller for dialog requests.
*/
class DialogController extends ContainerAware {
/**
* Forwards request to a subrequest.
*
* @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 \Symfony\Component\HttpFoundation\Response
* A response object.
*/
protected function forward(Request $request, $content) {
// @todo When we have a Generator, we can replace the forward() call with
// a render() call, which would handle ESI and hInclude as well. That will
// require an _internal route. For examples, see:
// https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing/internal.xml
// https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Controller/InternalController.php
$attributes = clone $request->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;
}
}
...@@ -26,7 +26,7 @@ class ContentNegotiation { ...@@ -26,7 +26,7 @@ class ContentNegotiation {
* @param Symfony\Component\HttpFoundation\Request $request * @param Symfony\Component\HttpFoundation\Request $request
* The request object from which to extract the content type. * The request object from which to extract the content type.
* *
* @return * @return string
* The normalized type of a given request. * The normalized type of a given request.
*/ */
public function getContentType(Request $request) { public function getContentType(Request $request) {
...@@ -36,11 +36,12 @@ public function getContentType(Request $request) { ...@@ -36,11 +36,12 @@ public function getContentType(Request $request) {
return 'iframeupload'; 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; $first_found_format = FALSE;
foreach ($request->getAcceptableContentTypes() as $mime_type) { foreach ($request->getAcceptableContentTypes() as $mime_type) {
$format = $request->getFormat($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; return $format;
} }
if (!is_null($format) && !$first_found_format) { if (!is_null($format) && !$first_found_format) {
......
<?php
/**
* @file
* Contains \Drupal\Core\Routing\Enhancer\DialogEnhancer.
*/
namespace Drupal\Core\Routing\Enhancer;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface;
use Drupal\Core\ContentNegotiation;
/**
* Enhances a route to use the DialogController for matching requests.
*/
class DialogEnhancer implements RouteEnhancerInterface {
/**
* Content negotiation library.
*
* @var \Drupal\CoreContentNegotiation
*/
protected $negotiation;
/**
* Content type this enhancer targets.
*
* @var string
*/
protected $targetContentType = 'drupal_dialog';
/**
* Controller to route matching requests to.
*
* @var string
*/
protected $controller = '\Drupal\Core\Ajax\DialogController::dialog';
/**
* Constructs a new \Drupal\Core\Routing\Enhancer\AjaxEnhancer object.
*
* @param \Drupal\Core\ContentNegotiation $negotiation
* The Content Negotiation service.
*/
public function __construct(ContentNegotiation $negotiation) {
$this->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;
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Routing\Enhancer\ModalEnhancer.
*/
namespace Drupal\Core\Routing\Enhancer;
/**
* Enhances a route to use the DialogController for matching requests.
*/
class ModalEnhancer extends DialogEnhancer {
/**
* Content type this enhancer targets.
*
* @var string
*/
protected $targetContentType = 'drupal_modal';
/**
* Controller to route matching requests to.
*
* @var string
*/
protected $controller = '\Drupal\Core\Ajax\DialogController::modal';
}
...@@ -52,6 +52,8 @@ Drupal.behaviors.AJAX = { ...@@ -52,6 +52,8 @@ Drupal.behaviors.AJAX = {
element_settings.url = $(this).attr('href'); element_settings.url = $(this).attr('href');
element_settings.event = 'click'; element_settings.event = 'click';
} }
element_settings.accepts = $(this).data('accepts');
element_settings.dialog = $(this).data('dialog-options');
var base = $(this).attr('id'); var base = $(this).attr('id');
Drupal.ajax[base] = new Drupal.ajax(base, this, element_settings); Drupal.ajax[base] = new Drupal.ajax(base, this, element_settings);
}); });
...@@ -200,11 +202,15 @@ Drupal.ajax = function (base, element, element_settings) { ...@@ -200,11 +202,15 @@ Drupal.ajax = function (base, element, element_settings) {
}, },
dataType: 'json', dataType: 'json',
accepts: { accepts: {
json: 'application/vnd.drupal-ajax' json: element_settings.accepts || 'application/vnd.drupal-ajax'
}, },
type: 'POST' type: 'POST'
}; };
if (element_settings.dialog) {
ajax.options.data.dialogOptions = element_settings.dialog;
}
// Bind the ajaxSubmit function to the element event. // Bind the ajaxSubmit function to the element event.
$(ajax.element).bind(element_settings.event, function (event) { $(ajax.element).bind(element_settings.event, function (event) {
return ajax.eventResponse(this, event); return ajax.eventResponse(this, event);
......
...@@ -77,6 +77,10 @@ function config_admin_sync_form(array &$form, array &$form_state, StorageInterfa ...@@ -77,6 +77,10 @@ function config_admin_sync_form(array &$form, array &$form_state, StorageInterfa
'href' => 'admin/config/development/sync/diff/' . $config_file, 'href' => 'admin/config/development/sync/diff/' . $config_file,
'attributes' => array( 'attributes' => array(
'class' => array('use-ajax'), '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( $form[$config_change_type]['list']['#rows'][] = array(
...@@ -138,69 +142,3 @@ function config_admin_import_form_submit($form, &$form_state) { ...@@ -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'); 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;
}
...@@ -51,10 +51,7 @@ function config_menu() { ...@@ -51,10 +51,7 @@ function config_menu() {
$items['admin/config/development/sync/diff/%'] = array( $items['admin/config/development/sync/diff/%'] = array(
'title' => 'Configuration file diff', 'title' => 'Configuration file diff',
'description' => 'Diff between active and staged configuraiton.', 'description' => 'Diff between active and staged configuraiton.',
'page callback' => 'config_admin_diff_page', 'route_name' => 'config_diff',
'page arguments' => array(5),
'access arguments' => array('synchronize configuration'),
'file' => 'config.admin.inc',
); );
$items['admin/config/development/sync/import'] = array( $items['admin/config/development/sync/import'] = array(
'title' => 'Import', 'title' => 'Import',
...@@ -62,4 +59,3 @@ function config_menu() { ...@@ -62,4 +59,3 @@ function config_menu() {
); );
return $items; return $items;
} }
config_diff:
pattern: '/admin/config/development/sync/diff/{config_file}'
defaults:
_content: '\Drupal\config\Controller\ConfigController::diff'
requirements:
_permission: 'synchronize configuration'
<?php
/**
* @file
* Contains \Drupal\config\Controller\ConfigController
*/
namespace Drupal\config\Controller;
use Drupal\Core\ControllerInterface;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Returns responses for config module routes.
*/
class ConfigController implements ControllerInterface {
/**
* The target storage.
*
* @var \Drupal\Core\Config\StorageInterface;
*/
protected $targetStorage;
/**
* The source storage.
*
* @var \Drupal\Core\Config\StorageInterface;
*/
protected $sourceStorage;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->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;
}
}
...@@ -1197,6 +1197,17 @@ protected function drupalGet($path, array $options = array(), array $headers = a ...@@ -1197,6 +1197,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. * Requests a Drupal path in JSON format, and JSON decodes the response.
*/ */
protected function drupalGetJSON($path, array $options = array(), array $headers = array()) { protected function drupalGetJSON($path, array $options = array(), array $headers = array()) {
......
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
* Tests use of dialogs as wrappers for Ajax responses. * Tests use of dialogs as wrappers for Ajax responses.
*/ */
class DialogTest extends AjaxTestBase { class DialogTest extends AjaxTestBase {
/**
* Declares test info.
*/
public static function getInfo() { public static function getInfo() {
return array( return array(
'name' => 'AJAX dialogs commands', 'name' => 'AJAX dialogs commands',
...@@ -22,7 +26,7 @@ public static function getInfo() { ...@@ -22,7 +26,7 @@ public static function getInfo() {
/** /**
* Test sending non-JS and AJAX requests to open and manipulate modals. * 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. // Ensure the elements render without notices or exceptions.
$this->drupalGet('ajax-test/dialog'); $this->drupalGet('ajax-test/dialog');
...@@ -35,8 +39,8 @@ function testDialog() { ...@@ -35,8 +39,8 @@ function testDialog() {
'settings' => NULL, 'settings' => NULL,
'data' => $dialog_contents, 'data' => $dialog_contents,
'dialogOptions' => array( 'dialogOptions' => array(
'modal' => true, 'modal' => TRUE,
'title' => 'AJAX Dialog', 'title' => 'AJAX Dialog contents',
), ),
); );
$normal_expected_response = array( $normal_expected_response = array(
...@@ -45,8 +49,8 @@ function testDialog() { ...@@ -45,8 +49,8 @@ function testDialog() {
'settings' => NULL, 'settings' => NULL,
'data' => $dialog_contents, 'data' => $dialog_contents,
'dialogOptions' => array( 'dialogOptions' => array(
'modal' => false, 'modal' => FALSE,
'title' => 'AJAX Dialog', 'title' => 'AJAX Dialog contents',
), ),
); );
$close_expected_response = array( $close_expected_response = array(
...@@ -55,20 +59,29 @@ function testDialog() { ...@@ -55,20 +59,29 @@ function testDialog() {
); );
// Check that requesting a modal dialog without JS goes to a page. // 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.'); $this->assertRaw($dialog_contents, 'Non-JS modal dialog page present.');
// Emulate going to the JS version of the page and check the JSON response. // 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.'); $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.