Commit a1bc7372 authored by catch's avatar catch

Issue #1305882 by dawehner, nod_, Fabianx, Steven Jones, sun, nlisgo:...

Issue #1305882 by dawehner, nod_, Fabianx, Steven Jones, sun, nlisgo: drupal_html_id() considered harmful; remove ajax_html_ids to use GET (not POST) AJAX requests
parent 534fe84b
......@@ -35,11 +35,11 @@ class Html {
protected static $seenIds;
/**
* Contains the current AJAX HTML IDs.
* Stores whether the current request was sent via AJAX.
*
* @var string
* @var bool
*/
protected static $ajaxHTMLIDs;
protected static $isAjax = FALSE;
/**
* Prepares a string for use as a valid class name.
......@@ -102,13 +102,13 @@ public static function cleanCssIdentifier($identifier, array $filter = array(
}
/**
* Sets the AJAX HTML IDs.
* Sets if this request is an Ajax request.
*
* @param string $ajax_html_ids
* The AJAX HTML IDs, probably coming from the current request.
* @param bool $is_ajax
* TRUE if this request is an Ajax request, FALSE otherwise.
*/
public static function setAjaxHtmlIds($ajax_html_ids = '') {
static::$ajaxHTMLIDs = $ajax_html_ids;
public static function setIsAjax($is_ajax) {
static::$isAjax = $is_ajax;
}
/**
......@@ -142,43 +142,15 @@ public static function setAjaxHtmlIds($ajax_html_ids = '') {
public static function getUniqueId($id) {
// If this is an Ajax request, then content returned by this page request
// will be merged with content already on the base page. The HTML IDs must
// be unique for the fully merged content. Therefore, initialize $seen_ids
// to take into account IDs that are already in use on the base page.
// be unique for the fully merged content. Therefore use unique IDs.
if (static::$isAjax) {
return static::getId($id) . '--' . Crypt::randomBytesBase64(8);
}
// @todo Remove all that code once we switch over to random IDs only,
// see https://www.drupal.org/node/1090592.
if (!isset(static::$seenIdsInit)) {
// Ideally, Drupal would provide an API to persist state information about
// prior page requests in the database, and we'd be able to add this
// function's $seen_ids static variable to that state information in order
// to have it properly initialized for this page request. However, no such
// page state API exists, so instead, ajax.js adds all of the in-use HTML
// IDs to the POST data of Ajax submissions. Direct use of $_POST is
// normally not recommended as it could open up security risks, but
// because the raw POST data is cast to a number before being returned by
// this function, this usage is safe.
if (empty(static::$ajaxHTMLIDs)) {
static::$seenIdsInit = array();
}
else {
// This function ensures uniqueness by appending a counter to the base
// id requested by the calling function after the first occurrence of
// that requested id. $_POST['ajax_html_ids'] contains the ids as they
// were returned by this function, potentially with the appended
// counter, so we parse that to reconstruct the $seen_ids array.
$ajax_html_ids = explode(' ', static::$ajaxHTMLIDs);
foreach ($ajax_html_ids as $seen_id) {
// We rely on '--' being used solely for separating a base id from the
// counter, which this function ensures when returning an id.
$parts = explode('--', $seen_id, 2);
if (!empty($parts[1]) && is_numeric($parts[1])) {
list($seen_id, $i) = $parts;
}
else {
$i = 1;
}
if (!isset(static::$seenIdsInit[$seen_id]) || ($i > static::$seenIdsInit[$seen_id])) {
static::$seenIdsInit[$seen_id] = $i;
}
}
}
static::$seenIdsInit = array();
}
if (!isset(static::$seenIds)) {
static::$seenIds = static::$seenIdsInit;
......
......@@ -20,13 +20,21 @@
class AjaxSubscriber implements EventSubscriberInterface {
/**
* Sets the AJAX HTML IDs from the current request.
* Request parameter to indicate that a request is a Drupal Ajax request.
*/
const AJAX_REQUEST_PARAMETER = '_drupal_ajax';
/**
* Sets the AJAX parameter from the current request.
*
* @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
* The response event, which contains the current request.
*/
public function onRequest(GetResponseEvent $event) {
Html::setAjaxHtmlIds($event->getRequest()->request->get('ajax_html_ids', ''));
// Pass to the Html class that the current request is an Ajax request.
if ($event->getRequest()->request->get(static::AJAX_REQUEST_PARAMETER)) {
Html::setIsAjax(TRUE);
}
}
/**
......
......@@ -647,6 +647,9 @@ public function prepareForm($form_id, &$form, FormStateInterface &$form_state) {
}
if (!isset($form['#id'])) {
$form['#id'] = Html::getUniqueId($form_id);
// Provide a selector usable by JavaScript. As the ID is unique, its not
// possible to rely on it in JavaScript.
$form['#attributes']['data-drupal-selector'] = Html::getId($form_id);
}
$form += $this->elementInfo->getInfo('form');
......@@ -782,7 +785,16 @@ public function doBuildForm($form_id, &$element, FormStateInterface &$form_state
}
if (!isset($element['#id'])) {
$element['#id'] = Html::getUniqueId('edit-' . implode('-', $element['#parents']));
$unprocessed_id = 'edit-' . implode('-', $element['#parents']);
$element['#id'] = Html::getUniqueId($unprocessed_id);
// Provide a selector usable by JavaScript. As the ID is unique, its not
// possible to rely on it in JavaScript.
$element['#attributes']['data-drupal-selector'] = Html::getId($unprocessed_id);
}
else {
// Provide a selector usable by JavaScript. As the ID is unique, its not
// possible to rely on it in JavaScript.
$element['#attributes']['data-drupal-selector'] = Html::getId($element['#id']);
}
// Add the aria-describedby attribute to associate the form control with its
......
......@@ -455,6 +455,13 @@
*/
Drupal.ajax.WRAPPER_FORMAT = '_wrapper_format';
/**
* Request parameter to indicate that a request is a Drupal Ajax request.
*
* @const
*/
Drupal.Ajax.AJAX_REQUEST_PARAMETER = '_drupal_ajax';
/**
* Execute the ajax request.
*
......@@ -580,16 +587,8 @@
Drupal.detachBehaviors(this.$form.get(0), settings, 'serialize');
}
// Prevent duplicate HTML ids in the returned markup.
// @see \Drupal\Component\Utility\Html::getUniqueId()
var ids = document.querySelectorAll('[id]');
var ajaxHtmlIds = [];
var il = ids.length;
for (var i = 0; i < il; i++) {
ajaxHtmlIds.push(ids[i].id);
}
// Join IDs to minimize request size.
options.data.ajax_html_ids = ajaxHtmlIds.join(' ');
// Inform Drupal that this is an AJAX request.
options.data[Drupal.Ajax.AJAX_REQUEST_PARAMETER] = 1;
// Allow Drupal to return new JavaScript and CSS files to load without
// returning the ones already loaded.
......
......@@ -92,7 +92,7 @@
Drupal.behaviors.blockHighlightPlacement = {
attach: function (context, settings) {
if (settings.blockPlacement) {
$('#blocks').once('block-highlight').each(function () {
$(context).find('[data-drupal-selector="edit-blocks"]').once('block-highlight').each(function () {
var $container = $(this);
// Just scrolling the document.body will not work in Firefox. The html
// element is needed as well.
......
......@@ -34,9 +34,9 @@
return vals.join(', ');
}
$('#edit-visibility-node-type, #edit-visibility-language, #edit-visibility-user-role').drupalSetSummary(checkboxesSummary);
$('[data-drupal-selector="edit-visibility-node-type"], [data-drupal-selector="edit-visibility-language"], [data-drupal-selector="edit-visibility-user-role"]').drupalSetSummary(checkboxesSummary);
$('#edit-visibility-request-path').drupalSetSummary(function (context) {
$('[data-drupal-selector="edit-visibility-request-path"]').drupalSetSummary(function (context) {
var $pages = $(context).find('textarea[name="visibility[request_path][pages]"]');
if (!$pages.val()) {
return Drupal.t('Not restricted');
......
......@@ -14,7 +14,7 @@
*/
Drupal.behaviors.ckeditorDrupalImageSettingsSummary = {
attach: function () {
$('#edit-editor-settings-plugins-drupalimage').drupalSetSummary(function (context) {
$('[data-ckeditor-plugin-id="drupalimage"]').drupalSetSummary(function (context) {
var root = 'input[name="editor[settings][plugins][drupalimage][image_upload]';
var $status = $(root + '[status]"]');
var $maxFileSize = $(root + '[max_size]"]');
......
......@@ -33,7 +33,7 @@
var that = this;
$context.find('[name="editor[settings][plugins][stylescombo][styles]"]')
.on('blur.ckeditorStylesComboSettings', function () {
var styles = $.trim($('#edit-editor-settings-plugins-stylescombo-styles').val());
var styles = $.trim($(this).val());
var stylesSet = that._generateStylesSetSetting(styles);
if (!_.isEqual(previousStylesSet, stylesSet)) {
previousStylesSet = stylesSet;
......@@ -105,8 +105,8 @@
*/
Drupal.behaviors.ckeditorStylesComboSettingsSummary = {
attach: function () {
$('#edit-editor-settings-plugins-stylescombo').drupalSetSummary(function (context) {
var styles = $.trim($('#edit-editor-settings-plugins-stylescombo-styles').val());
$('[data-ckeditor-plugin-id="stylescombo"]').drupalSetSummary(function (context) {
var styles = $.trim($('[data-drupal-selector="edit-editor-settings-plugins-stylescombo-styles"]').val());
if (styles.length === 0) {
return Drupal.t('No styles configured');
}
......
......@@ -171,7 +171,7 @@ function testBooleanField() {
t('Display setting checkbox is available')
);
$this->assertFieldByXPath(
'*//input[@id="edit-fields-' . $field_name . '-settings-edit-form-settings-display-label" and @value="1"]',
'*//input[starts-with(@id, "edit-fields-' . $field_name . '-settings-edit-form-settings-display-label") and @value="1"]',
TRUE,
t('Display label changes label of the checkbox')
);
......
......@@ -12,7 +12,7 @@
*/
Drupal.behaviors.fieldUIFieldStorageAddForm = {
attach: function (context) {
var $form = $(context).find('#field-ui-field-storage-add-form').once('field_ui_add');
var $form = $(context).find('[data-drupal-selector="field-ui-field-storage-add-form"]').once('field_ui_add');
if ($form.length) {
// Add a few 'form-required' css classes here. We can not use the Form
// API '#required' property because both label elements for "add new"
......@@ -214,7 +214,7 @@
// Fire the Ajax update.
$('input[name=refresh_rows]').val(rowNames.join(' '));
$('input#edit-refresh').trigger('mousedown');
$('input[data-drupal-selector="edit-refresh"]').trigger('mousedown');
// Disabled elements do not appear in POST ajax data, so we mark the
// elements disabled only after firing the request.
......
......@@ -7,6 +7,7 @@
namespace Drupal\file\Element;
use Drupal\Component\Utility\Html;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element\FormElement;
use Drupal\Core\Url;
......@@ -130,9 +131,6 @@ public static function valueCallback(&$element, $input, FormStateInterface $form
* support for a default value.
*/
public static function processManagedFile(&$element, FormStateInterface $form_state, &$complete_form) {
// Append the '-upload' to the #id so the field label's 'for' attribute
// corresponds with the file element.
$element['#id'] .= '-upload';
// This is used sometimes so let's implode it just once.
$parents_prefix = implode('_', $element['#parents']);
......@@ -144,6 +142,9 @@ public static function processManagedFile(&$element, FormStateInterface $form_st
$element['#files'] = !empty($fids) ? File::loadMultiple($fids) : FALSE;
$element['#tree'] = TRUE;
// Generate a unique wrapper HTML ID.
$ajax_wrapper_id = Html::getUniqueId('ajax-wrapper');
$ajax_settings = [
// @todo Remove this in https://www.drupal.org/node/2500527.
'url' => Url::fromRoute('file.ajax_upload'),
......@@ -153,7 +154,7 @@ public static function processManagedFile(&$element, FormStateInterface $form_st
'form_build_id' => $complete_form['form_build_id']['#value'],
],
],
'wrapper' => $element['#id'] . '-ajax-wrapper',
'wrapper' => $ajax_wrapper_id,
'effect' => 'fade',
'progress' => [
'type' => $element['#progress_indicator'],
......@@ -261,8 +262,12 @@ public static function processManagedFile(&$element, FormStateInterface $form_st
$element['upload']['#attached']['drupalSettings']['file']['elements']['#' . $element['#id']] = $extension_list;
}
// Let #id point to the file element, so the field label's 'for' corresponds
// with it.
$element['#id'] = &$element['upload']['#id'];
// Prefix and suffix used for Ajax replacement.
$element['#prefix'] = '<div id="' . $element['#id'] . '-ajax-wrapper">';
$element['#prefix'] = '<div id="' . $ajax_wrapper_id . '">';
$element['#suffix'] = '</div>';
return $element;
......
......@@ -9,7 +9,6 @@
use Drupal\comment\Entity\Comment;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\Component\Utility\Html;
use Drupal\field\Entity\FieldConfig;
use Drupal\field_ui\Tests\FieldUiTestTrait;
use Drupal\user\RoleInterface;
......@@ -86,7 +85,8 @@ function testSingleValuedWidget() {
$this->assertNoFieldByXPath('//input[@type="submit"]', t('Remove'), 'After clicking the "Remove" button, it is no longer displayed.');
$this->assertFieldByXpath('//input[@type="submit"]', t('Upload'), 'After clicking the "Remove" button, the "Upload" button is displayed.');
// Test label has correct 'for' attribute.
$label = $this->xpath("//label[@for='edit-" . Html::cleanCssIdentifier($field_name) . "-0-upload']");
$input = $this->xpath('//input[@name="files[' . $field_name . '_0]"]');
$label = $this->xpath('//label[@for="' . (string) $input[0]['id'] . '"]');
$this->assertTrue(isset($label[0]), 'Label for upload found.');
// Save the node and ensure it does not have the file.
......
......@@ -251,15 +251,9 @@ function testImageFieldSettings() {
$node_storage->resetCache(array($nid));
$node = $node_storage->load($nid);
$file = $node->{$field_name}->entity;
$image_style = array(
'#theme' => 'image_style',
'#uri' => $file->getFileUri(),
'#width' => 40,
'#height' => 20,
'#style_name' => 'medium',
);
$default_output = drupal_render($image_style);
$this->assertRaw($default_output, "Preview image is displayed using 'medium' style.");
$url = file_create_url(ImageStyle::load('medium')->buildUrl($file->getFileUri()));
$this->assertTrue($this->cssSelect('img[width=40][height=20][class=image-style-medium][src="' . $url . '"]'));
// Add alt/title fields to the image and verify that they are displayed.
$image = array(
......
......@@ -822,14 +822,13 @@ protected function assertThemeOutput($callback, array $variables = array(), $exp
}
/**
* Asserts that a field exists in the current page by the given XPath.
* Asserts that a field exists in the current page with a given Xpath result.
*
* @param string $xpath
* XPath used to find the field.
* @param \SimpleXmlElement[] $fields
* Xml elements.
* @param string $value
* (optional) Value of the field to assert. You may pass in NULL (default)
* to skip checking the actual value, while still checking that the field
* exists.
* (optional) Value of the field to assert. You may pass in NULL (default) to skip
* checking the actual value, while still checking that the field exists.
* @param string $message
* (optional) A message to display with the assertion. Do not translate
* messages: use format_string() to embed variables in the message text, not
......@@ -843,9 +842,7 @@ protected function assertThemeOutput($callback, array $variables = array(), $exp
* @return bool
* TRUE on pass, FALSE on fail.
*/
protected function assertFieldByXPath($xpath, $value = NULL, $message = '', $group = 'Other') {
$fields = $this->xpath($xpath);
protected function assertFieldsByValue($fields, $value = NULL, $message = '', $group = 'Other') {
// If value specified then check array for match.
$found = TRUE;
if (isset($value)) {
......@@ -880,6 +877,34 @@ protected function assertFieldByXPath($xpath, $value = NULL, $message = '', $gro
return $this->assertTrue($fields && $found, $message, $group);
}
/**
* Asserts that a field exists in the current page by the given XPath.
*
* @param string $xpath
* XPath used to find the field.
* @param string $value
* (optional) Value of the field to assert. You may pass in NULL (default)
* to skip checking the actual value, while still checking that the field
* exists.
* @param string $message
* (optional) A message to display with the assertion. Do not translate
* messages: use format_string() to embed variables in the message text, not
* t(). If left blank, a default message will be displayed.
* @param string $group
* (optional) The group this message is in, which is displayed in a column
* in test output. Use 'Debug' to indicate this is debugging output. Do not
* translate this string. Defaults to 'Other'; most tests do not override
* this default.
*
* @return bool
* TRUE on pass, FALSE on fail.
*/
protected function assertFieldByXPath($xpath, $value = NULL, $message = '', $group = 'Other') {
$fields = $this->xpath($xpath);
return $this->assertFieldsByValue($fields, $value, $message, $group);
}
/**
* Get the selected value from a select field.
*
......@@ -1133,6 +1158,31 @@ protected function assertOption($id, $option, $message = '', $group = 'Browser')
return $this->assertTrue(isset($options[0]), $message ? $message : SafeMarkup::format('Option @option for field @id exists.', array('@option' => $option, '@id' => $id)), $group);
}
/**
* Asserts that a select option in the current page exists.
*
* @param string $drupal_selector
* The data drupal selector of select field to assert.
* @param string $option
* Option to assert.
* @param string $message
* (optional) A message to display with the assertion. Do not translate
* messages: use format_string() to embed variables in the message text, not
* t(). If left blank, a default message will be displayed.
* @param string $group
* (optional) The group this message is in, which is displayed in a column
* in test output. Use 'Debug' to indicate this is debugging output. Do not
* translate this string. Defaults to 'Browser'; most tests do not override
* this default.
*
* @return bool
* TRUE on pass, FALSE on fail.
*/
protected function assertOptionWithDrupalSelector($drupal_selector, $option, $message = '', $group = 'Browser') {
$options = $this->xpath('//select[@data-drupal-selector=:data_drupal_selector]//option[@value=:option]', array(':data_drupal_selector' => $drupal_selector, ':option' => $option));
return $this->assertTrue(isset($options[0]), $message ? $message : SafeMarkup::format('Option @option for field @data_drupal_selector exists.', array('@option' => $option, '@data_drupal_selector' => $drupal_selector)), $group);
}
/**
* Asserts that a select option in the current page does not exist.
*
......@@ -1186,6 +1236,33 @@ protected function assertOptionSelected($id, $option, $message = '', $group = 'B
return $this->assertTrue(isset($elements[0]) && !empty($elements[0]['selected']), $message ? $message : SafeMarkup::format('Option @option for field @id is selected.', array('@option' => $option, '@id' => $id)), $group);
}
/**
* Asserts that a select option in the current page is checked.
*
* @param string $drupal_selector
* The data drupal selector of select field to assert.
* @param string $option
* Option to assert.
* @param string $message
* (optional) A message to display with the assertion. Do not translate
* messages: use format_string() to embed variables in the message text, not
* t(). If left blank, a default message will be displayed.
* @param string $group
* (optional) The group this message is in, which is displayed in a column
* in test output. Use 'Debug' to indicate this is debugging output. Do not
* translate this string. Defaults to 'Browser'; most tests do not override
* this default.
*
* @return bool
* TRUE on pass, FALSE on fail.
*
* @todo $id is unusable. Replace with $name.
*/
protected function assertOptionSelectedWithDrupalSelector($drupal_selector, $option, $message = '', $group = 'Browser') {
$elements = $this->xpath('//select[@data-drupal-selector=:data_drupal_selector]//option[@value=:option]', array(':data_drupal_selector' => $drupal_selector, ':option' => $option));
return $this->assertTrue(isset($elements[0]) && !empty($elements[0]['selected']), $message ? $message : SafeMarkup::format('Option @option for field @data_drupal_selector is selected.', array('@option' => $option, '@data_drupal_selector' => $drupal_selector)), $group);
}
/**
* Asserts that a select option in the current page is not checked.
*
......
......@@ -19,6 +19,7 @@
use Drupal\Core\Database\Database;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\EventSubscriber\AjaxSubscriber;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Extension\MissingDependencyException;
use Drupal\Core\Render\Element;
......@@ -1800,13 +1801,7 @@ protected function drupalPostAjaxForm($path, $edit, $triggering_element, $ajax_p
$extra_post[$key] = $value;
}
}
$ajax_html_ids = array();
foreach ($this->xpath('//*[@id]') as $element) {
$ajax_html_ids[] = (string) $element['id'];
}
if (!empty($ajax_html_ids)) {
$extra_post['ajax_html_ids'] = implode(' ', $ajax_html_ids);
}
$extra_post[AjaxSubscriber::AJAX_REQUEST_PARAMETER] = 1;
$extra_post += $this->getAjaxPageStatePostData();
// Now serialize all the $extra_post values, and prepend it with an '&'.
$extra_post = '&' . $this->serializePostValues($extra_post);
......
......@@ -62,8 +62,8 @@ public function testBlockForms() {
$this->drupalGet('');
$this->drupalPostAjaxForm(NULL, ['test1' => 'option1'], 'test1');
$this->assertOptionSelected('edit-test1--2', 'option1');
$this->assertOption('edit-test1--2', 'option3');
$this->assertOptionSelectedWithDrupalSelector('edit-test1', 'option1');
$this->assertOptionWithDrupalSelector('edit-test1', 'option3');
$this->drupalPostForm($this->getUrl(), ['test1' => 'option1'], 'Submit');
$this->assertText('Submission successful.');
}
......
......@@ -125,6 +125,10 @@ public function testDialog() {
// Don't send a target.
'submit' => array()
));
// Make sure the selector ID starts with the right string.
$this->assert(strpos($ajax_result[3]['selector'], $no_target_expected_response['selector']) === 0, 'Selector starts with right string.');
unset($ajax_result[3]['selector']);
unset($no_target_expected_response['selector']);
$this->assertEqual($no_target_expected_response, $ajax_result[3], 'Normal dialog with no target JSON response matches.');
// Emulate closing the dialog via an AJAX request. There is no non-JS
......
......@@ -58,10 +58,9 @@ function testMultiForm() {
// each Ajax submission, but these variables are stable and help target the
// desired elements.
$field_name = 'field_ajax_test';
$field_xpaths = array(
'node-page-form' => '//form[@id="node-page-form"]//div[contains(@class, "field-name-field-ajax-test")]',
'node-page-form--2' => '//form[@id="node-page-form--2"]//div[contains(@class, "field-name-field-ajax-test")]',
);
$form_xpath = '//form[starts-with(@id, "node-page-form")]';
$field_xpath = '//div[contains(@class, "field-name-field-ajax-test")]';
$button_name = $field_name . '_add_more';
$button_value = t('Add another item');
$button_xpath_suffix = '//input[@name="' . $button_name . '"]';
......@@ -71,19 +70,29 @@ function testMultiForm() {
// of field items and "add more" button for the multi-valued field within
// each form.
$this->drupalGet('form-test/two-instances-of-same-form');
foreach ($field_xpaths as $field_xpath) {
$this->assert(count($this->xpath($field_xpath . $field_items_xpath_suffix)) == 1, 'Found the correct number of field items on the initial page.');
$this->assertFieldByXPath($field_xpath . $button_xpath_suffix, NULL, 'Found the "add more" button on the initial page.');
$fields = $this->xpath($form_xpath . $field_xpath);
$this->assertEqual(count($fields), 2);
foreach ($fields as $field) {
$this->assertEqual(count($field->xpath('.' . $field_items_xpath_suffix)), 1, 'Found the correct number of field items on the initial page.');
$this->assertFieldsByValue($field->xpath('.' . $button_xpath_suffix), NULL, 'Found the "add more" button on the initial page.');
}
$this->assertNoDuplicateIds(t('Initial page contains unique IDs'), 'Other');
// Submit the "add more" button of each form twice. After each corresponding
// page update, ensure the same as above.
foreach ($field_xpaths as $form_html_id => $field_xpath) {
for ($i = 0; $i < 2; $i++) {
for ($i = 0; $i < 2; $i++) {
$forms = $this->xpath($form_xpath);
foreach ($forms as $offset => $form) {
$form_html_id = (string) $form['id'];
$this->drupalPostAjaxForm(NULL, array(), array($button_name => $button_value), NULL, array(), array(), $form_html_id);
$this->assert(count($this->xpath($field_xpath . $field_items_xpath_suffix)) == $i+2, 'Found the correct number of field items after an AJAX submission.');
$this->assertFieldByXPath($field_xpath . $button_xpath_suffix, NULL, 'Found the "add more" button after an AJAX submission.');
$form = $this->xpath($form_xpath)[$offset];
$field = $form->xpath('.' . $field_xpath);
$this->assertEqual(count($field[0]->xpath('.' . $field_items_xpath_suffix)), $i+2, 'Found the correct number of field items after an AJAX submission.');
$this->assertFieldsByValue($field[0]->xpath('.' . $button_xpath_suffix), NULL, 'Found the "add more" button after an AJAX submission.');
$this->assertNoDuplicateIds(t('Updated page contains unique IDs'), 'Other');
}
}
......
......@@ -12,6 +12,7 @@
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\EventSubscriber\AjaxSubscriber;
use Drupal\Core\Path\CurrentPathStack;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\RedirectDestinationInterface;
......@@ -133,7 +134,7 @@ public function ajaxView(Request $request) {
// Remove all of this stuff from the query of the request so it doesn't
// end up in pagers and tablesort URLs.
foreach (array('view_name', 'view_display_id', 'view_args', 'view_path', 'view_dom_id', 'pager_element', 'view_base_path', 'ajax_html_ids') as $key) {
foreach (array('view_name', 'view_display_id', 'view_args', 'view_path', 'view_dom_id', 'pager_element', 'view_base_path', AjaxSubscriber::AJAX_REQUEST_PARAMETER) as $key) {
$request->query->remove($key);
$request->request->remove($key);
}
......
......@@ -118,7 +118,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
$form['#action'] = $view->hasUrl() ? $view->getUrl()->toString() : Url::fromRoute('<current>')->toString();
$form['#theme'] = $view->buildThemeFunctions('views_exposed_form');
$form['#id'] = Html::cleanCssIdentifier('views_exposed_form-' . SafeMarkup::checkPlain($view->storage->id()) . '-' . SafeMarkup::checkPlain($display['id']));
$form['#id'] = Html::cleanCssIdentifier('views_exposed_form-' . $view->storage->id() . '-' . $display['id']);
/** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginBase $exposed_form_plugin */
$exposed_form_plugin = $view->display_handler->getPlugin('exposed_form');
......
......@@ -21,7 +21,7 @@
attach: function () {
// Only show the SQL rewrite warning when the user has chosen the
// corresponding checkbox.
$('#edit-query-options-disable-sql-rewrite').on('click', function () {
$('[data-drupal-selector="edit-query-options-disable-sql-rewrite"]').on('click', function () {
$('.sql-rewrite-warning').toggleClass('js-hide');
});
}
......@@ -412,7 +412,7 @@
/**
* Add a keyup handler to the search box.
*/
this.$searchBox = this.$form.find('#edit-override-controls-options-search');
this.$searchBox = this.$form.find('[data-drupal-selector="edit-override-controls-options-search"]');
this.$searchBox.on('keyup', $.proxy(this.handleKeyup, this));
/**
......@@ -950,25 +950,27 @@
*/
Drupal.behaviors.viewsFilterConfigSelectAll = {
attach: function (context) {
// Show the select all checkbox.
$(context).find('#views-ui-handler-form div.form-item-options-value-all').once('filterConfigSelectAll')
.show()
.find('input[type=checkbox]')
.on('click', function () {
var checked = $(this).is(':checked');
var $context = $(context);
var $selectAll = $context.find('.form-item-options-value-all').once('filterConfigSelectAll');
var $selectAllCheckbox = $selectAll.find('input[type=checkbox]');
var $checkboxes = $selectAll.closest('.form-checkboxes').find('.js-form-type-checkbox:not(.form-item-options-value-all) input[type="checkbox"]');
if ($selectAll.length) {
// Show the select all checkbox.
$selectAll.show();
$selectAllCheckbox.on('click', function () {
// Update all checkbox beside the select all checkbox.
$(this).parents('.form-checkboxes').find('input[type=checkbox]').each(function () {
$(this).attr('checked', checked);
});
$checkboxes.prop('checked', $(this).is(':checked'));
});
// Uncheck the select all checkbox if any of the others are unchecked.
$('#views-ui-handler-form').find('div.js-form-type-checkbox').not($('.form-item-options-value-all'))
.find('input[type=checkbox]')
.on('click', function () {
// Uncheck the select all checkbox if any of the others are unchecked.
$checkboxes.on('click', function () {
if ($(this).is('checked') === false) {
$('#edit-options-value-all').prop('checked', false);
$selectAllCheckbox.prop('checked', false);
}
});
}
}
};
......@@ -990,7 +992,7 @@
*/
Drupal.behaviors.viewsUiCheckboxify = {
attach: function (context, settings) {
var $buttons = $('#edit-options-expose-button-button, #edit-options-group-button-button').once('views-ui-checkboxify');
var $buttons = $('[data-drupal-selector="edit-options-expose-button-button"], [data-drupal-selector="edit-options-group-button-button"]').once('views-ui-checkboxify');
var length = $buttons.length;
var i;
for (i = 0; i < length; i++) {
......@@ -1068,7 +1070,7 @@
*/
Drupal.behaviors.viewsUiOverrideSelect = {
attach: function (context) {
$(context).find('#edit-override-dropdown').once('views-ui-override-button-text').each(function () {
$(context).find('[data-drupal-selector="edit-override-dropdown"]').once('views-ui-override-button-text').each(function () {
// Closures! :(
var $context = $(context);
var $submit = $context.find('[id^=edit-submit]');
......
......@@ -11,6 +11,7 @@
use Drupal\Component\Utility\Timer;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
use Drupal\Core\EventSubscriber\AjaxSubscriber;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\views\Views;
......@@ -577,7 +578,7 @@ public function renderPreview($display_id, $args = array()) {
// have some input in the query parameters, so we merge request() and
// query() to ensure we get it all.
$exposed_input = array_merge(\Drupal::request()->request->all(), \Drupal::request()->query->all());
foreach (array('view_name', 'view_display_id', 'view_args', 'view_path', 'view_dom_id', 'pager_element', 'view_base_path', 'ajax_html_ids', 'ajax_page_state', 'form_id', 'form_build_id', 'form_token') as $key) {
foreach (array('view_name', 'view_display_id', 'view_args', 'view_path', 'view_dom_id', 'pager_element', 'view_base_path', AjaxSubscriber::AJAX_REQUEST_PARAMETER, 'ajax_page_state', 'form_id', 'form_build_id', 'form_token') as $key) {
if (isset($exposed_input[$key])) {
unset($exposed_input[$key]);
}
......
<
......@@ -143,19 +143,25 @@ public function providerTestHtmlGetUniqueId() {