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
534fe84bfc
commit
a1bc737216
|
@ -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 @@ class Html {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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 @@ class Html {
|
|||
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 @@ use Symfony\Component\HttpKernel\KernelEvents;
|
|||
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 @@ class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormS
|
|||
}
|
||||
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 @@ class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormS
|
|||
}
|
||||
|
||||
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 @@ class BooleanFieldTest extends WebTestBase {
|
|||
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 @@ class ManagedFile extends FormElement {
|
|||
* 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 @@ class ManagedFile extends FormElement {
|
|||
$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 @@ class ManagedFile extends FormElement {
|
|||
'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 @@ class ManagedFile extends FormElement {
|
|||
$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 @@ namespace Drupal\file\Tests;
|
|||
|
||||
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 @@ class FileFieldWidgetTest extends FileFieldTestBase {
|
|||
$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 @@ class ImageFieldDisplayTest extends ImageFieldTestBase {
|
|||
$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 @@ trait AssertContentTrait {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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 @@ trait AssertContentTrait {
|
|||
* @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 @@ trait AssertContentTrait {
|
|||
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 @@ trait AssertContentTrait {
|
|||
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 @@ trait AssertContentTrait {
|
|||
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\Component\Utility\SafeMarkup;
|
|||
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 @@ abstract class WebTestBase extends TestBase {
|
|||
$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 @@ class AjaxFormCacheTest extends AjaxTestBase {
|
|||
|
||||
$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 @@ class DialogTest extends AjaxTestBase {
|
|||
// 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 @@ class MultiFormTest extends AjaxTestBase {
|
|||
// 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 @@ class MultiFormTest extends AjaxTestBase {
|
|||
// 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\AjaxResponse;
|
|||
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 @@ class ViewAjaxController implements ContainerInjectionInterface {
|
|||
|
||||
// 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 @@ class ViewsExposedForm extends FormBase {
|
|||
|
||||
$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\Html;
|
|||
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 @@ class ViewUI implements ViewEntityInterface {
|
|||
// 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 @@ class HtmlTest extends UnitTestCase {
|
|||
* The expected result.
|
||||
* @param string $source
|
||||
* The string being transformed to an ID.
|
||||
* @param bool $reset
|
||||
* (optional) If TRUE, reset the list of seen IDs. Defaults to FALSE.
|
||||
*
|
||||
* @dataProvider providerTestHtmlGetUniqueIdWithAjaxIds
|
||||
*
|
||||
* @covers ::getUniqueId
|
||||
*/
|
||||
public function testHtmlGetUniqueIdWithAjaxIds($expected, $source, $reset = FALSE) {
|
||||
if ($reset) {
|
||||
Html::resetSeenIds();
|
||||
public function testHtmlGetUniqueIdWithAjaxIds($expected, $source) {
|
||||
Html::setIsAjax(TRUE);
|
||||
$id = Html::getUniqueId($source);
|
||||
|
||||
// Note, we truncate two hyphens at the end.
|
||||
// @see \Drupal\Component\Utility\Html::getId()
|
||||
if (strpos($source, '--') !== FALSE) {
|
||||
$random_suffix = substr($id, strlen($source) + 1);
|
||||
}
|
||||
Html::setAjaxHtmlIds('test-unique-id1 test-unique-id2--3');
|
||||
$this->assertSame($expected, Html::getUniqueId($source));
|
||||
else {
|
||||
$random_suffix = substr($id, strlen($source) + 2);
|
||||
}
|
||||
$expected = $expected . $random_suffix;
|
||||
$this->assertSame($expected, $id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -166,10 +172,11 @@ class HtmlTest extends UnitTestCase {
|
|||
*/
|
||||
public function providerTestHtmlGetUniqueIdWithAjaxIds() {
|
||||
return array(
|
||||
array('test-unique-id1--2', 'test-unique-id1', TRUE),
|
||||
array('test-unique-id1--3', 'test-unique-id1'),
|
||||
array('test-unique-id2--4', 'test-unique-id2', TRUE),
|
||||
array('test-unique-id2--5', 'test-unique-id2'),
|
||||
array('test-unique-id1--', 'test-unique-id1'),
|
||||
// Note, we truncate two hyphens at the end.
|
||||
// @see \Drupal\Component\Utility\Html::getId()
|
||||
array('test-unique-id1---', 'test-unique-id1--'),
|
||||
array('test-unique-id2--', 'test-unique-id2'),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -186,6 +193,7 @@ class HtmlTest extends UnitTestCase {
|
|||
* @covers ::getId
|
||||
*/
|
||||
public function testHtmlGetId($expected, $source) {
|
||||
Html::setIsAjax(FALSE);
|
||||
$this->assertSame($expected, Html::getId($source));
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue