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:
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']
......
......@@ -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.
......
......@@ -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');
}
/**
......
<?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 {
* @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) {
......
<?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 = {
element_settings.url = $(this).attr('href');
element_settings.event = 'click';
}
element_settings.accepts = $(this).data('accepts');
element_settings.dialog = $(this).data('dialog-options');
var base = $(this).attr('id');
Drupal.ajax[base] = new Drupal.ajax(base, this, element_settings);
});
......@@ -200,11 +202,15 @@ Drupal.ajax = function (base, element, element_settings) {
},
dataType: 'json',
accepts: {
json: 'application/vnd.drupal-ajax'
json: element_settings.accepts || 'application/vnd.drupal-ajax'
},
type: 'POST'
};
if (element_settings.dialog) {
ajax.options.data.dialogOptions = element_settings.dialog;
}
// Bind the ajaxSubmit function to the element event.
$(ajax.element).bind(element_settings.event, function (event) {
return ajax.eventResponse(this, event);
......
......@@ -77,6 +77,10 @@ function config_admin_sync_form(array &$form, array &$form_state, StorageInterfa
'href' => '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;
}
......@@ -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;
}
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
}
/**
* 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()) {
......
......@@ -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.
......
......@@ -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));
}