Commit 6399141c authored by webchick's avatar webchick

Issue #1667742 by larowlan, nod_, effulgentsia, jessebeach, sun: Change...

Issue #1667742 by larowlan, nod_, effulgentsia, jessebeach, sun: Change notice: Add abstracted dialog to core (resolves accessibility bug).
parent fe9978cc
......@@ -473,8 +473,13 @@ function ajax_prepare_response($page_callback_result) {
// manipulation method is used. The method used is specified by
// #ajax['method']. The default method is 'replaceWith', which completely
// replaces the old wrapper element and its content with the new HTML.
// Since this is the primary response content returned to the client, we
// also attach the page title. It is up to client code to determine if and
// how to display that. For example, if the requesting element is configured
// to display the response in a dialog (via #ajax['dialog']), it can use
// this for the dialog title.
$html = is_string($page_callback_result) ? $page_callback_result : drupal_render($page_callback_result);
$commands[] = ajax_command_insert(NULL, $html);
$commands[] = ajax_command_insert(NULL, $html) + array('title' => drupal_get_title());
// Add the status messages inside the new content's wrapper element, so that
// on subsequent Ajax requests, it is treated as old content.
$commands[] = ajax_command_prepend(NULL, theme('status_messages'));
......@@ -531,8 +536,8 @@ function ajax_pre_render_element($element) {
// Initialize #ajax_processed, so we do not process this element again.
$element['#ajax_processed'] = FALSE;
// Nothing to do if there is neither a callback nor a path.
if (!(isset($element['#ajax']['callback']) || isset($element['#ajax']['path']))) {
// Nothing to do if there are no Ajax settings.
if (empty($element['#ajax'])) {
return $element;
}
......@@ -589,12 +594,24 @@ function ajax_pre_render_element($element) {
if (isset($element['#ajax']['event'])) {
$element['#attached']['library'][] = array('system', 'jquery.form');
$element['#attached']['library'][] = array('system', 'drupal.ajax');
if (!empty($element['#ajax']['dialog'])) {
$element['#attached']['library'][] = array('system', 'drupal.dialog');
}
$settings = $element['#ajax'];
// Assign default settings.
// Assign default settings. When 'path' is set to NULL, ajax.js submits the
// Ajax request to the same URL as the form or link destination is for
// someone with JavaScript disabled. This is generally preferred as a way to
// ensure consistent server processing for js and no-js users, and Drupal's
// content negotiation takes care of formatting the response appropriately.
// However, 'path' and 'options' may be set when wanting server processing
// to be substantially different for a JavaScript triggered submission.
// One such substantial difference is form elements that use
// #ajax['callback'] for determining which part of the form needs
// re-rendering. For that, we have a special 'system/ajax' route.
$settings += array(
'path' => 'system/ajax',
'path' => isset($settings['callback']) ? 'system/ajax' : NULL,
'options' => array(),
);
......@@ -604,7 +621,7 @@ function ajax_pre_render_element($element) {
}
// Change path to URL.
$settings['url'] = url($settings['path'], $settings['options']);
$settings['url'] = isset($settings['path']) ? url($settings['path'], $settings['options']) : NULL;
unset($settings['path'], $settings['options']);
// Add special data to $settings['submit'] so that when this element
......
......@@ -1739,8 +1739,24 @@ function theme_links($variables) {
if ($is_current_path && $is_current_language) {
$class[] = 'active';
}
// Pass in $link as $options, they share the same keys.
$item = l($link['title'], $link['href'], $link);
// @todo Reconcile Views usage of 'ajax' as a boolean with the rest of
// core's usage of it as an array.
if (isset($link['ajax']) && is_array($link['ajax'])) {
// To attach Ajax behavior, render a link element, rather than just
// call l().
$link_element = array(
'#type' => 'link',
'#title' => $link['title'],
'#href' => $link['href'],
'#ajax' => $link['ajax'],
'#options' => array_diff_key($link, drupal_map_assoc(array('title', 'href', 'ajax'))),
);
$item = drupal_render($link_element);
}
else {
// Pass in $link as $options, they share the same keys.
$item = l($link['title'], $link['href'], $link);
}
}
// Handle title-only text items.
else {
......
......@@ -101,7 +101,6 @@ Drupal.behaviors.AJAX = {
*/
Drupal.ajax = function (base, element, element_settings) {
var defaults = {
url: 'system/ajax',
event: 'mousedown',
keypress: true,
selector: '#' + base,
......@@ -119,9 +118,55 @@ Drupal.ajax = function (base, element, element_settings) {
$.extend(this, defaults, element_settings);
// @todo Remove this after refactoring the PHP code to:
// - Call this 'selector'.
// - Include the '#' for ID-based selectors.
// - Support non-ID-based selectors.
if (this.wrapper) {
this.wrapper = '#' + this.wrapper;
}
// For Ajax responses that are wanted in a dialog, use the needed method.
// If wanted in a modal dialog, also use the needed wrapper.
if (this.dialog) {
this.method = 'html';
if (this.dialog.modal) {
this.wrapper = '#drupal-modal';
}
}
this.element = element;
this.element_settings = element_settings;
// If there isn't a form, jQuery.ajax() will be used instead, allowing us to
// bind Ajax to links as well.
if (this.element.form) {
this.form = $(this.element.form);
}
// If no Ajax callback URL was given, use the link href or form action.
if (!this.url) {
if ($(element).is('a')) {
this.url = $(element).attr('href');
}
else if (element.form) {
this.url = this.form.attr('action');
// @todo If there's a file input on this form, then jQuery will submit the
// AJAX response with a hidden Iframe rather than the XHR object. If the
// response to the submission is an HTTP redirect, then the Iframe will
// follow it, but the server won't content negotiate it correctly,
// because there won't be an ajax_iframe_upload POST variable. Until we
// figure out a work around to this problem, we prevent AJAX-enabling
// elements that submit to the same URL as the form when there's a file
// input. For example, this means the Delete button on the edit form of
// an Article node doesn't open its confirmation form in a dialog.
if (this.form.find(':file').length) {
return;
}
}
}
// Replacing 'nojs' with 'ajax' in the URL allows for an easy method to let
// the server detect when it needs to degrade gracefully.
// There are four scenarios to check for:
......@@ -129,14 +174,7 @@ Drupal.ajax = function (base, element, element_settings) {
// 2. /nojs$ - The end of a URL string.
// 3. /nojs? - Followed by a query (e.g. path/nojs?destination=foobar).
// 4. /nojs# - Followed by a fragment (e.g.: path/nojs#myfragment).
this.url = element_settings.url.replace(/\/nojs(\/|$|\?|#)/g, '/ajax$1');
this.wrapper = '#' + element_settings.wrapper;
// If there isn't a form, jQuery.ajax() will be used instead, allowing us to
// bind Ajax to links as well.
if (this.element.form) {
this.form = $(this.element.form);
}
this.url = this.url.replace(/\/nojs(\/|$|\?|#)/g, '/ajax$1');
// Set the options for the ajaxSubmit function.
// The 'this' variable will not persist inside of the options object.
......@@ -520,6 +558,18 @@ Drupal.ajax.prototype.commands = {
// Add the new content to the page.
wrapper[method](new_content);
// If the requesting object wanted the response in a dialog, open that
// dialog. However, a single server response can include multiple insert
// commands (e.g., one for the primary content and another one for status
// messages), but we only want to open the dialog once, so we assume that
// only commands with a title property are dialog eligible.
// @todo Consider whether this is overloading title inappropriately, and
// if so, find another way to determine dialog eligibility.
if (ajax.dialog && ('title' in response)) {
var dialog = Drupal.dialog(wrapper, {title: response.title});
ajax.dialog.modal ? dialog.showModal() : dialog.show();
}
// Immediately hide the new content if we're using any effects.
if (effect.showEffect !== 'show') {
new_content.hide();
......
/**
* @file
*
* Dialog API inspired by HTML5 dialog element:
* http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#the-dialog-element
*/
(function ($, Drupal, drupalSettings) {
"use strict";
drupalSettings.dialog = {
autoOpen: true,
dialogClass: '',
close: function (e) {
Drupal.detachBehaviors(e.target, null, 'unload');
}
};
Drupal.behaviors.dialog = {
attach: function () {
// Provide a known 'drupal-modal' dom element for Drupal code to use for
// modal dialogs. Since there can be multiple non-modal dialogs at a time,
// it is the responsibility of calling code to create the elements it needs.
if (!$('#drupal-modal').length) {
$('<div id="drupal-modal" />').hide().appendTo('body');
}
}
};
Drupal.dialog = function (element, options) {
function openDialog (settings) {
settings = $.extend(settings, defaults);
// Trigger a global event to allow scripts to bind events to the dialog.
$(window).trigger('dialog:beforecreate', [dialog, $element, settings]);
$element.dialog(settings);
dialog.open = true;
$(window).trigger('dialog:aftercreate', [dialog, $element, settings]);
}
function closeDialog (value) {
$(window).trigger('dialog:beforeclose', [dialog, $element]);
$element.dialog('close');
dialog.returnValue = value;
dialog.open = false;
$(window).trigger('dialog:afterclose', [dialog, $element]);
}
var undef;
var $element = $(element);
var defaults = $.extend(options, drupalSettings.dialog);
var dialog = {
open: false,
returnValue: undef,
show: function () {
openDialog({modal: false});
},
showModal: function () {
openDialog({modal: true});
},
close: closeDialog
};
return dialog;
};
/**
* Binds a listener on dialog creation to handle the cancel link.
*/
$(window).on('dialog:aftercreate', function (e, dialog, $element, settings) {
$element.on('click.dialog', '.dialog-cancel', function (e) {
dialog.close('cancel');
e.preventDefault();
e.stopPropagation();
});
});
/**
* Removes all 'dialog' listeners.
*/
$(window).on('dialog:beforeclose', function (e, dialog, $element) {
$element.off('.dialog');
});
})(jQuery, Drupal, drupalSettings);
<?php
/**
* @file
* Definition of Drupal\system\Tests\Ajax\DialogTest.
*/
namespace Drupal\system\Tests\Ajax;
/**
* Tests use of dialogs as wrappers for Ajax responses.
*/
class DialogTest extends AjaxTestBase {
public static function getInfo() {
return array(
'name' => 'Dialog',
'description' => 'Performs tests on #ajax[\'dialog\'].',
'group' => 'AJAX',
);
}
/**
* Ensure elements with #ajax['dialog'] render correctly.
*/
function testDialog() {
// Ensure the elements render without notices or exceptions.
$this->drupalGet('ajax-test/dialog');
// @todo What else should we assert?
}
}
......@@ -1171,6 +1171,21 @@ function system_library_info() {
),
);
// Drupal's dialog component.
$libraries['drupal.dialog'] = array(
'title' => 'Drupal Dialog',
'version' => VERSION,
'js' => array(
'core/misc/dialog.js' => array('group' => JS_LIBRARY),
),
'dependencies' => array(
array('system', 'jquery'),
array('system', 'drupal'),
array('system', 'drupalSettings'),
array('system', 'jquery.ui.dialog')
),
);
// Drupal's states library.
$libraries['drupal.states'] = array(
'title' => 'Drupal states',
......
......@@ -26,6 +26,16 @@ function ajax_test_menu() {
'page callback' => 'ajax_test_link',
'access callback' => TRUE,
);
$items['ajax-test/dialog'] = array(
'title' => 'AJAX Dialog',
'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,
);
return $items;
}
......@@ -74,3 +84,100 @@ function ajax_test_link() {
);
return $build;
}
/**
* Menu callback: Renders a form elements and links with #ajax['dialog'].
*/
function ajax_test_dialog() {
// Add two wrapper elements for testing non-modal dialogs. Modal dialogs use
// the global drupal-modal wrapper by default.
$build['dialog_wrappers'] = array('#markup' => '<div id="ajax-test-dialog-wrapper-1"></div><div id="ajax-test-dialog-wrapper-2"></div>');
// Dialog behavior applied to a button.
$build['form'] = drupal_get_form('ajax_test_dialog_form');
// Dialog behavior applied to a #type => 'link'.
$build['link'] = array(
'#type' => 'link',
'#title' => 'Link 1 (modal)',
'#href' => 'ajax-test/dialog-contents',
'#ajax' => array(
'dialog' => array('modal' => TRUE),
),
);
// Dialog behavior applied to links rendered by theme_links().
$build['links'] = array(
'#theme' => 'links',
'#links' => array(
'link2' => array(
'title' => 'Link 2 (modal)',
'href' => 'ajax-test/dialog-contents',
'ajax' => array(
'dialog' => array('modal' => TRUE),
),
),
'link3' => array(
'title' => 'Link 3 (non-modal)',
'href' => 'ajax-test/dialog-contents',
'ajax' => array(
'dialog' => array(),
'wrapper' => 'ajax-test-dialog-wrapper-2',
),
),
),
);
return $build;
}
/**
* Form builder: Renders buttons with #ajax['dialog'].
*/
function ajax_test_dialog_form($form, &$form_state) {
$form['button1'] = array(
'#type' => 'submit',
'#value' => 'Button 1 (modal)',
'#ajax' => array(
'dialog' => array('modal' => TRUE),
),
);
$form['button2'] = array(
'#type' => 'submit',
'#value' => 'Button 2 (non-modal)',
'#ajax' => array(
'dialog' => array(),
'wrapper' => 'ajax-test-dialog-wrapper-1',
),
);
return $form;
}
/**
* Form submit handler for ajax_test_dialog_form().
*/
function ajax_test_dialog_form_submit($form, &$form_state) {
$form_state['redirect'] = 'ajax-test/dialog-contents';
}
/**
* Menu callback: Returns the contents for dialogs opened by ajax_test_dialog().
*/
function ajax_test_dialog_contents() {
// This is a regular render array; the keys do not have special meaning.
return array(
'content' => array(
'#markup' => 'Example message',
),
'cancel'=> array(
'#type' => 'link',
'#title' => 'Cancel',
'#href' => '',
'#attributes' => array(
// This is a special class to which JavaScript assigns dialog closing
// behavior.
'class' => array('dialog-cancel'),
),
),
);
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment