first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
/**
* This file is for testing CSS file override in
* CascadingStylesheetsTestCase::testRenderOverride().
* No contents are necessary.
*/

View File

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1 @@
{}

View File

View File

View File

View File

@@ -0,0 +1,33 @@
{
"linkset": [
{
"anchor": "/aa/system/menu/main/linkset",
"item": [
{
"href": "/aa",
"title": "Home",
"hierarchy": ["0"],
"machine-name": ["main"]
},
{
"href": "/aa/node/1",
"title": "aa|A multi-lingual-node",
"hierarchy": ["1"],
"machine-name": ["main"]
},
{
"href": "/aa/node/2",
"title": "aa|Second multi-lingual-node",
"hierarchy": ["2"],
"machine-name": ["main"]
},
{
"href": "/aa/node/3",
"title": "aa|Third multi-lingual-node",
"hierarchy": ["3"],
"machine-name": ["main"]
}
]
}
]
}

View File

@@ -0,0 +1,33 @@
{
"linkset": [
{
"anchor": "/bb/system/menu/main/linkset",
"item": [
{
"href": "/bb",
"title": "Home",
"hierarchy": ["0"],
"machine-name": ["main"]
},
{
"href": "/bb/node/1",
"title": "bb|A multi-lingual-node",
"hierarchy": ["1"],
"machine-name": ["main"]
},
{
"href": "/bb/node/2",
"title": "bb|Second multi-lingual-node",
"hierarchy": ["2"],
"machine-name": ["main"]
},
{
"href": "/bb/node/3",
"title": "bb|Third multi-lingual-node",
"hierarchy": ["3"],
"machine-name": ["main"]
}
]
}
]
}

View File

@@ -0,0 +1,33 @@
{
"linkset": [
{
"anchor": "/cc/system/menu/main/linkset",
"item": [
{
"href": "/cc",
"title": "Home",
"hierarchy": ["0"],
"machine-name": ["main"]
},
{
"href": "/cc/node/1",
"title": "cc|A multi-lingual-node",
"hierarchy": ["1"],
"machine-name": ["main"]
},
{
"href": "/cc/node/2",
"title": "aa|Second multi-lingual-node",
"hierarchy": ["2"],
"machine-name": ["main"]
},
{
"href": "/cc/node/3",
"title": "aa|Third multi-lingual-node",
"hierarchy": ["3"],
"machine-name": ["main"]
}
]
}
]
}

View File

@@ -0,0 +1,33 @@
{
"linkset": [
{
"anchor": "/dd/system/menu/main/linkset",
"item": [
{
"href": "/dd",
"title": "Home",
"hierarchy": ["0"],
"machine-name": ["main"]
},
{
"href": "/dd/node/1",
"title": "A multi-lingual-node",
"hierarchy": ["1"],
"machine-name": ["main"]
},
{
"href": "/dd/node/2",
"title": "Second multi-lingual-node",
"hierarchy": ["2"],
"machine-name": ["main"]
},
{
"href": "/dd/node/3",
"title": "Third multi-lingual-node",
"hierarchy": ["3"],
"machine-name": ["main"]
}
]
}
]
}

View File

@@ -0,0 +1,33 @@
{
"linkset": [
{
"anchor": "/system/menu/main/linkset",
"item": [
{
"href": "/",
"title": "Home",
"hierarchy": ["0"],
"machine-name": ["main"]
},
{
"href": "/node/1",
"title": "A multi-lingual-node",
"hierarchy": ["1"],
"machine-name": ["main"]
},
{
"href": "/node/2",
"title": "Second multi-lingual-node",
"hierarchy": ["2"],
"machine-name": ["main"]
},
{
"href": "/node/3",
"title": "Third multi-lingual-node",
"hierarchy": ["3"],
"machine-name": ["main"]
}
]
}
]
}

View File

@@ -0,0 +1,47 @@
{
"linkset": [
{
"anchor": "/system/menu/main/linkset",
"item": [
{
"href": "/",
"title": "Home",
"hierarchy": ["0"],
"machine-name": ["main"]
},
{
"href": "/about",
"title": "About us",
"hierarchy": ["1"],
"machine-name": ["main"]
},
{
"href": "/about/custom-attributes",
"title": "Custom attributes test page",
"hierarchy": ["1", "0"],
"class": [
"foo",
"bar",
"1729",
"1",
"",
"0",
"-1",
"3.141592"
],
"data-baz": [
"42"
],
"¯\\_(ツ)_/¯": ["ok"],
"machine-name": ["main"]
},
{
"href": "/about/name",
"title": "Our name",
"hierarchy": ["1","1"],
"machine-name": ["main"]
}
]
}
]
}

View File

@@ -0,0 +1 @@
[{"title":"You can't parse this! Oh no! 🔥🙀🐶

View File

@@ -0,0 +1,80 @@
[
{
"title":"Critical Release - SA-2019-02-19",
"link":"https:\/\/www.drupal.org\/sa-2019-02-19",
"project":"drupal",
"type":"core",
"insecure":[
"7.65",
"8.5.14",
"8.6.13",
"8.7.0-alpha2",
"8.7.0-beta1",
"8.7.0-beta2",
"8.6.14",
"8.6.15",
"7.66",
"8.7.0",
"[CORE_VERSION]"
],
"is_psa":"0",
"pubDate":"Tue, 19 Feb 2019 14:11:01 +0000"
},
{
"title":"Critical Release - PSA-Really Old",
"link":"https:\/\/www.drupal.org\/psa",
"project":"drupal",
"type":"core",
"is_psa":"1",
"insecure":[
],
"pubDate":"Tue, 19 Feb 2017 14:11:01 +0000"
},
{
"title":"Generic Module1 Project - Moderately critical - Access bypass - SA-CONTRIB-2019-02-02",
"link":"https:\/\/www.drupal.org\/SA-CONTRIB-2019-02-02",
"project":"generic_module1_project",
"type":"module",
"is_psa":"0",
"insecure":[
"8.x-1.1"
],
"pubDate":"Tue, 19 Mar 2019 12:50:00 +0000"
},
{
"title":"Generic Module1 Test - Moderately critical - Access bypass - SA-CONTRIB-2019-02-02",
"link":"https:\/\/www.drupal.org\/SA-CONTRIB-2019-02-02",
"project":"generic_module1_test",
"type":"module",
"is_psa":"0",
"insecure":[
"8.x-1.1"
],
"pubDate":"Tue, 19 Mar 2019 12:50:00 +0000"
},
{
"title":"Generic Module2 project - Moderately critical - Access bypass - SA-CONTRIB-2019-02-02",
"link":"https:\/\/www.drupal.org\/SA-CONTRIB-2019-02-02",
"project":"generic_module2_project",
"type":"module",
"is_psa":"1",
"insecure":[
],
"pubDate":"Tue, 19 Mar 2019 12:50:00 +0000"
},
{
"title":"Missing Project - Moderately critical - Access bypass - SA-CONTRIB-2019-02-02",
"link":"https:\/\/www.drupal.org\/SA-CONTRIB-2019-02-02",
"project":"missing_project",
"type":"module",
"is_psa":"1",
"insecure":[
"7.x-1.7",
"8.x-1.4"
],
"pubDate":"Tue, 19 Mar 2019 12:50:00 +0000"
}
]

View File

@@ -0,0 +1,58 @@
[
{
"title":"Critical Release - SA-2019-02-19",
"link":"https:\/\/www.drupal.org\/sa-2019-02-19",
"project":"drupal",
"type":"core",
"insecure":[
"7.65",
"8.5.14",
"8.6.13",
"8.7.0-alpha2",
"8.7.0-beta1",
"8.7.0-beta2",
"8.6.14",
"8.6.15",
"7.66",
"8.7.0",
"[CORE_VERSION]"
],
"is_psa":"0",
"pubDate":"Tue, 19 Feb 2019 14:11:01 +0000"
},
{
"title":"Generic Module1 Project - Moderately critical - Access bypass - SA-CONTRIB-2019-02-02",
"link":"https:\/\/www.drupal.org\/SA-CONTRIB-2019-02-02",
"project":"generic_module1_project",
"type":"module",
"is_psa":"0",
"insecure":[
"8.x-1.1"
],
"pubDate":"Tue, 19 Mar 2019 12:50:00 +0000"
},
{
"title":"Generic Module1 Test - Moderately critical - Access bypass - SA-CONTRIB-2019-02-02",
"link":"https:\/\/www.drupal.org\/SA-CONTRIB-2019-02-02",
"project":"generic_module1_test",
"type":"module",
"is_psa":"0",
"insecure":[
"8.x-1.1"
],
"pubDate":"Tue, 19 Mar 2019 12:50:00 +0000"
},
{
"title":"Missing Project - Moderately critical - Access bypass - SA-CONTRIB-2019-02-02",
"link":"https:\/\/www.drupal.org\/SA-CONTRIB-2019-02-02",
"project":"missing_project",
"type":"module",
"is_psa":"1",
"insecure":[
"7.x-1.7",
"8.x-1.4"
],
"pubDate":"Tue, 19 Mar 2019 12:50:00 +0000"
}
]

View File

@@ -0,0 +1,47 @@
[
{
"title":"Critical Release - PSA-Really Old",
"link":"https:\/\/www.drupal.org\/psa",
"project":"drupal",
"type":"core",
"is_psa":"1",
"insecure":[
],
"pubDate":"Tue, 19 Feb 2017 14:11:01 +0000"
},
{
"title":"Generic Module1 Test - Moderately critical - Access bypass - SA-CONTRIB-2019-02-02",
"link":"https:\/\/www.drupal.org\/SA-CONTRIB-2019-02-02",
"project":"generic_module1_test",
"type":"module",
"is_psa":"0",
"insecure":[
"8.x-1.1"
],
"pubDate":"Tue, 19 Mar 2019 12:50:00 +0000"
},
{
"title":"Generic Module2 project - Moderately critical - Access bypass - SA-CONTRIB-2019-02-02",
"link":"https:\/\/www.drupal.org\/SA-CONTRIB-2019-02-02",
"project":"generic_module2_project",
"type":"module",
"is_psa":"1",
"insecure":[
],
"pubDate":"Tue, 19 Mar 2019 12:50:00 +0000"
},
{
"title":"Missing Project - Moderately critical - Access bypass - SA-CONTRIB-2019-02-02",
"link":"https:\/\/www.drupal.org\/SA-CONTRIB-2019-02-02",
"project":"missing_project",
"type":"module",
"is_psa":"1",
"insecure":[
"7.x-1.7",
"8.x-1.4"
],
"pubDate":"Tue, 19 Mar 2019 12:50:00 +0000"
}
]

View File

@@ -0,0 +1,92 @@
[
{
"title":"Critical Release - SA-2019-02-19",
"link":"https:\/\/www.drupal.org\/sa-2019-02-19",
"project":"drupal",
"type":"core",
"insecure":[
"7.65",
"8.5.14",
"8.6.13",
"8.7.0-alpha2",
"8.7.0-beta1",
"8.7.0-beta2",
"8.6.14",
"8.6.15",
"7.66",
"8.7.0",
"[CORE_VERSION]"
],
"is_psa":"0",
"pubDate":"Tue, 19 Feb 2019 14:11:01 +0000"
},
{
"title":"Critical Release - PSA-Really Old",
"link":"https:\/\/www.drupal.org\/psa",
"project":"drupal",
"type":"core",
"is_psa":"1",
"insecure":[
],
"pubDate":"Tue, 19 Feb 2017 14:11:01 +0000"
},
{
"title":"Generic Module1 Project - Moderately critical - Access bypass - SA-CONTRIB-2019-02-02",
"link":"https:\/\/www.drupal.org\/SA-CONTRIB-2019-02-02",
"project":"generic_module1_project",
"type":"module",
"is_psa":"0",
"insecure":[
"8.x-1.1",
"8.x-8.7.0"
],
"pubDate":"Tue, 19 Mar 2019 12:50:00 +0000"
},
{
"title":"Generic Module1 Test - Moderately critical - Access bypass - SA-CONTRIB-2019-02-02",
"link":"https:\/\/www.drupal.org\/SA-CONTRIB-2019-02-02",
"project":"generic_module1_test",
"type":"module",
"is_psa":"0",
"insecure":[
"8.x-1.1",
"8.x-8.7.0"
],
"pubDate":"Tue, 19 Mar 2019 12:50:00 +0000"
},
{
"title":"Generic Module2 project - Moderately critical - Access bypass - SA-CONTRIB-2019-02-02",
"link":"https:\/\/www.drupal.org\/SA-CONTRIB-2019-02-02",
"project":"generic_module2_project",
"type":"module",
"is_psa":"1",
"insecure":[
],
"pubDate":"Tue, 19 Mar 2019 12:50:00 +0000"
},
{
"title":"Missing Project - Moderately critical - Access bypass - SA-CONTRIB-2019-02-02",
"link":"https:\/\/www.drupal.org\/SA-CONTRIB-2019-02-02",
"project":"missing_project",
"type":"module",
"is_psa":"1",
"insecure":[
"7.x-1.7",
"8.x-1.4"
],
"pubDate":"Tue, 19 Mar 2019 12:50:00 +0000"
},
{
"title":"Critical Release - PSA because 2020",
"link":"https:\/\/www.drupal.org\/psa",
"project":"drupal",
"type":"core",
"is_psa":"1",
"insecure":[
],
"pubDate":"Tue, 19 Feb 2020 14:11:01 +0000"
}
]

View File

@@ -0,0 +1,69 @@
<?php
/**
* @file
* Contains database additions for testing year 2038 update.
*/
use Drupal\Core\Database\Database;
// cspell:ignore destid sourceid
$connection = Database::getConnection();
// Add a migrate map table.
$connection->schema()->createTable('migrate_map_d7_file', [
'fields' => [
'source_ids_hash' => [
'type' => 'varchar',
'not null' => TRUE,
'length' => '64',
],
'sourceid1' => [
'type' => 'int',
'not null' => TRUE,
'size' => 'normal',
],
'destid1' => [
'type' => 'int',
'not null' => FALSE,
'size' => 'normal',
'unsigned' => TRUE,
],
'source_row_status' => [
'type' => 'int',
'not null' => TRUE,
'size' => 'tiny',
'default' => '0',
'unsigned' => TRUE,
],
'rollback_action' => [
'type' => 'int',
'not null' => TRUE,
'size' => 'tiny',
'default' => '0',
'unsigned' => TRUE,
],
'last_imported' => [
'type' => 'int',
'not null' => TRUE,
'size' => 'normal',
'default' => '0',
'unsigned' => TRUE,
],
'hash' => [
'type' => 'varchar',
'not null' => FALSE,
'length' => '64',
],
],
'primary key' => [
'source_ids_hash',
],
'indexes' => [
'source' => [
'sourceid1',
],
],
'mysql_character_set' => 'utf8mb4',
]);

View File

@@ -0,0 +1,58 @@
<?php
/**
* @file
* Provides database changes for testing upgrade path of system_update_8404().
*
* @see \Drupal\Tests\system\Functional\Update\SqlContentEntityStorageRevisionDataCleanupTest
*/
use Drupal\Core\Database\Database;
use Drupal\Core\Language\LanguageInterface;
$connection = Database::getConnection();
// Manually add a record to the node_revision
$connection->insert('node_field_revision')
->fields([
'nid' => 8,
'vid' => 8,
'langcode' => 'en',
'title' => 'Deleted revision',
'uid' => 1,
'status' => 1,
'created' => 1439731773,
'changed' => 1439732036,
'promote' => 1,
'sticky' => 0,
'revision_translation_affected' => 1,
'default_langcode' => 1,
'content_translation_source' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
'content_translation_outdated' => 0,
])
->execute();
// Add 100 more rows to test the loop in system_update_10201.
for ($i = 1; $i <= 100; $i++) {
// Ensure that the new nodes use a vid that is greater than the maximum vid
// in drupal-9.4.0.filled.standard.php.gz for node id 8.
$vid = 10 + $i;
$connection->insert('node_field_revision')
->fields([
'nid' => 8,
'vid' => $vid,
'langcode' => 'en',
'title' => 'Deleted revision',
'uid' => 1,
'status' => 1,
'created' => 1439732773,
'changed' => 1439733036,
'promote' => 1,
'sticky' => 0,
'revision_translation_affected' => 1,
'default_langcode' => 1,
'content_translation_source' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
'content_translation_outdated' => 0,
])
->execute();
}

View File

@@ -0,0 +1,21 @@
<?php
/**
* @file
* Database to mimic the installation of the update_test_schema module.
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
// Set the schema version.
$connection->merge('key_value')
->condition('collection', 'system.schema')
->condition('name', 'update_test_schema')
->fields([
'collection' => 'system.schema',
'name' => 'update_test_schema',
'value' => 's:4:"8901";',
])
->execute();

View File

@@ -0,0 +1,54 @@
<?php
/**
* @file
* Partial database to mimic the installation of the update_test_schema module.
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
// Create the table.
$connection->schema()->createTable('update_test_schema_table', [
'fields' => [
'a' => [
'type' => 'int',
'not null' => TRUE,
'size' => 'normal',
],
'b' => [
'type' => 'blob',
'not null' => FALSE,
'size' => 'normal',
],
],
]);
// Set the schema version.
$connection->merge('key_value')
->condition('collection', 'system.schema')
->condition('name', 'update_test_schema')
->fields([
'collection' => 'system.schema',
'name' => 'update_test_schema',
'value' => 'i:8000;',
])
->execute();
// Update core.extension.
$extensions = $connection->select('config')
->fields('config', ['data'])
->condition('collection', '')
->condition('name', 'core.extension')
->execute()
->fetchField();
$extensions = unserialize($extensions);
$extensions['module']['update_test_schema'] = 8000;
$connection->update('config')
->fields([
'data' => serialize($extensions),
])
->condition('collection', '')
->condition('name', 'core.extension')
->execute();

View File

@@ -0,0 +1,38 @@
<?php
/**
* @file
* Database to mimic the installation of the update_test_semver_update_n module.
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
// Set the schema version.
$connection->merge('key_value')
->condition('collection', 'system.schema')
->condition('name', 'update_test_semver_update_n')
->fields([
'collection' => 'system.schema',
'name' => 'update_test_semver_update_n',
'value' => 'i:8000;',
])
->execute();
// Update core.extension.
$extensions = $connection->select('config')
->fields('config', ['data'])
->condition('collection', '')
->condition('name', 'core.extension')
->execute()
->fetchField();
$extensions = unserialize($extensions);
$extensions['module']['update_test_semver_update_n'] = 8000;
$connection->update('config')
->fields([
'data' => serialize($extensions),
])
->condition('collection', '')
->condition('name', 'core.extension')
->execute();

View File

@@ -0,0 +1,139 @@
<?php
/**
* @file
* Provides database changes for testing the TimestampFormatter upgrade path.
*
* @see \Drupal\Tests\system\Functional\Update\TimestampFormatterSettingsUpdateTest
*/
use Drupal\Core\Database\Database;
use Drupal\field\Entity\FieldStorageConfig;
$connection = Database::getConnection();
// Add a new timestamp field 'field_foo'.
$connection->insert('config')
->fields(['collection', 'name', 'data'])->values([
'collection' => '',
'name' => 'field.storage.node.field_foo',
'data' => $field_storage = 'a:16:{s:4:"uuid";s:36:"815278cf-a977-4700-aad9-d58034de0115";s:8:"langcode";s:2:"en";s:6:"status";b:1;s:12:"dependencies";a:1:{s:6:"module";a:1:{i:0;s:4:"node";}}s:2:"id";s:14:"node.field_foo";s:10:"field_name";s:9:"field_foo";s:11:"entity_type";s:4:"node";s:4:"type";s:9:"timestamp";s:8:"settings";a:0:{}s:6:"module";s:4:"core";s:6:"locked";b:0;s:11:"cardinality";i:1;s:12:"translatable";b:1;s:7:"indexes";a:0:{}s:22:"persist_with_no_fields";b:0;s:14:"custom_storage";b:0;}',
])->values([
'collection' => '',
'name' => 'field.field.node.page.field_foo',
'data' => 'a:16:{s:4:"uuid";s:36:"ea669e7e-532e-41ad-9322-13ba6a9901b0";s:8:"langcode";s:2:"en";s:6:"status";b:1;s:12:"dependencies";a:1:{s:6:"config";a:2:{i:0;s:28:"field.storage.node.field_foo";i:1;s:14:"node.type.page";}}s:2:"id";s:19:"node.page.field_foo";s:10:"field_name";s:9:"field_foo";s:11:"entity_type";s:4:"node";s:6:"bundle";s:4:"page";s:5:"label";s:3:"Foo";s:11:"description";s:0:"";s:8:"required";b:0;s:12:"translatable";b:0;s:13:"default_value";a:1:{i:0;a:1:{s:5:"value";i:1511630653;}}s:22:"default_value_callback";s:0:"";s:8:"settings";a:0:{}s:10:"field_type";s:9:"timestamp";}',
])->execute();
$connection->insert('key_value')
->fields(['collection', 'name', 'value'])
->values([
'collection' => 'config.entity.key_store.field_config',
'name' => 'uuid:ea669e7e-532e-41ad-9322-13ba6a9901b0',
'value' => 'a:1:{i:0;s:31:"field.field.node.page.field_foo";}',
])
->values([
'collection' => 'config.entity.key_store.field_storage_config',
'name' => 'uuid:815278cf-a977-4700-aad9-d58034de0115',
'value' => 'a:1:{i:0;s:28:"field.storage.node.field_foo";}',
])
->values([
'collection' => 'entity.storage_schema.sql',
'name' => 'node.field_schema_data.field_foo',
'value' => 'a:2:{s:15:"node__field_foo";a:4:{s:11:"description";s:38:"Data storage for node field field_foo.";s:6:"fields";a:7:{s:6:"bundle";a:5:{s:4:"type";s:13:"varchar_ascii";s:6:"length";i:128;s:8:"not null";b:1;s:7:"default";s:0:"";s:11:"description";s:88:"The field instance bundle to which this row belongs, used when deleting a field instance";}s:7:"deleted";a:5:{s:4:"type";s:3:"int";s:4:"size";s:4:"tiny";s:8:"not null";b:1;s:7:"default";i:0;s:11:"description";s:60:"A boolean indicating whether this data item has been deleted";}s:9:"entity_id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:38:"The entity id this data is attached to";}s:11:"revision_id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:47:"The entity revision id this data is attached to";}s:8:"langcode";a:5:{s:4:"type";s:13:"varchar_ascii";s:6:"length";i:32;s:8:"not null";b:1;s:7:"default";s:0:"";s:11:"description";s:37:"The language code for this data item.";}s:5:"delta";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:67:"The sequence number for this data item, used for multi-value fields";}s:15:"field_foo_value";a:2:{s:4:"type";s:3:"int";s:8:"not null";b:1;}}s:11:"primary key";a:4:{i:0;s:9:"entity_id";i:1;s:7:"deleted";i:2;s:5:"delta";i:3;s:8:"langcode";}s:7:"indexes";a:2:{s:6:"bundle";a:1:{i:0;s:6:"bundle";}s:11:"revision_id";a:1:{i:0;s:11:"revision_id";}}}s:24:"node_revision__field_foo";a:4:{s:11:"description";s:50:"Revision archive storage for node field field_foo.";s:6:"fields";a:7:{s:6:"bundle";a:5:{s:4:"type";s:13:"varchar_ascii";s:6:"length";i:128;s:8:"not null";b:1;s:7:"default";s:0:"";s:11:"description";s:88:"The field instance bundle to which this row belongs, used when deleting a field instance";}s:7:"deleted";a:5:{s:4:"type";s:3:"int";s:4:"size";s:4:"tiny";s:8:"not null";b:1;s:7:"default";i:0;s:11:"description";s:60:"A boolean indicating whether this data item has been deleted";}s:9:"entity_id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:38:"The entity id this data is attached to";}s:11:"revision_id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:47:"The entity revision id this data is attached to";}s:8:"langcode";a:5:{s:4:"type";s:13:"varchar_ascii";s:6:"length";i:32;s:8:"not null";b:1;s:7:"default";s:0:"";s:11:"description";s:37:"The language code for this data item.";}s:5:"delta";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:67:"The sequence number for this data item, used for multi-value fields";}s:15:"field_foo_value";a:2:{s:4:"type";s:3:"int";s:8:"not null";b:1;}}s:11:"primary key";a:5:{i:0;s:9:"entity_id";i:1;s:11:"revision_id";i:2;s:7:"deleted";i:3;s:5:"delta";i:4;s:8:"langcode";}s:7:"indexes";a:2:{s:6:"bundle";a:1:{i:0;s:6:"bundle";}s:11:"revision_id";a:1:{i:0;s:11:"revision_id";}}}}',
])
->execute();
$data = $connection->select('key_value')
->fields('key_value', ['value'])
->condition('collection', 'entity.definitions.bundle_field_map')
->condition('name', 'node')
->execute()
->fetchField();
$data = unserialize($data);
$data['field_foo']['type'] = 'timestamp';
$data['field_foo']['bundles']['page'] = 'page';
$connection->update('key_value')
->fields(['value' => serialize($data)])
->condition('collection', 'entity.definitions.bundle_field_map')
->condition('name', 'node')
->execute();
$data = $connection->select('key_value')
->fields('key_value', ['value'])
->condition('collection', 'entity.definitions.installed')
->condition('name', 'node.field_storage_definitions')
->execute()
->fetchField();
$data = unserialize($data);
$data['field_foo'] = new FieldStorageConfig(unserialize($field_storage));
$connection->update('key_value')
->fields(['value' => serialize($data)])
->condition('collection', 'entity.definitions.installed')
->condition('name', 'node.field_storage_definitions')
->execute();
// Add the new field to default entity view display.
$config = $connection->select('config')
->fields('config', ['data'])
->condition('collection', '')
->condition('name', 'core.entity_view_display.node.page.default')
->execute()
->fetchField();
$config = unserialize($config);
$config['content']['field_foo'] = [
'type' => 'timestamp',
'label' => 'hidden',
'weight' => 0,
'region' => 'content',
'settings' => [
'date_format' => 'custom',
'custom_date_format' => 'Y-m-d',
'timezone' => '',
],
'third_party_settings' => [],
];
$config['third_party_settings']['layout_builder'] = [
'enabled' => TRUE,
'allow_custom' => FALSE,
'sections' => [
[
'layout_id' => 'layout_onecol',
'layout_settings' => [
'label' => '',
],
'components' => [
'93bf4359-06a6-4263-bce9-15c90dc8f357' => [
'uuid' => '93bf4359-06a6-4263-bce9-15c90dc8f357',
'region' => 'content',
'configuration' => [
'id' => 'field_block:node:page:field_foo',
'label_display' => '0',
'context_mapping' => [
'entity' => 'layout_builder.entity',
],
'formatter' => [
'type' => 'timestamp',
'label' => 'inline',
'settings' => [
'date_format' => 'custom',
'custom_date_format' => 'Y-m-d',
'timezone' => '',
],
'third_party_settings' => [],
],
],
'weight' => 0,
'additional' => [],
],
],
'third_party_settings' => [],
],
],
];
$connection->update('config')
->fields(['data' => serialize($config)])
->condition('collection', '')
->condition('name', 'core.entity_view_display.node.page.default')
->execute();

View File

@@ -0,0 +1,30 @@
<?php
/**
* @file
* Fake an HTTP request, for use during testing.
*/
use Drupal\Core\Test\TestKernel;
use Symfony\Component\HttpFoundation\Request;
chdir('../../../..');
$autoloader = require_once 'autoload.php';
// Change to HTTP.
$_SERVER['HTTPS'] = NULL;
ini_set('session.cookie_secure', FALSE);
foreach ($_SERVER as &$value) {
// Because HTTPS is null.
$value = $value ? str_replace('core/modules/system/tests/http.php', 'index.php', $value) : "";
$value = $value ? str_replace('https://', 'http://', $value) : "";
}
$kernel = new TestKernel('testing', $autoloader, TRUE);
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);

View File

@@ -0,0 +1,31 @@
<?php
/**
* @file
* Fake an HTTPS request, for use during testing.
*
* @todo Fix this to use a new request rather than modifying server variables,
* see http.php.
*/
use Drupal\Core\Test\TestKernel;
use Symfony\Component\HttpFoundation\Request;
chdir('../../../..');
$autoloader = require_once 'autoload.php';
// Change to HTTPS.
$_SERVER['HTTPS'] = 'on';
foreach ($_SERVER as &$value) {
$value = str_replace('core/modules/system/tests/https.php', 'index.php', $value);
$value = str_replace('http://', 'https://', $value);
}
$kernel = new TestKernel('testing', $autoloader, TRUE);
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);

Binary file not shown.

View File

@@ -0,0 +1,9 @@
name: Accept header based routing test
type: module
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,5 @@
services:
accept_header_matcher:
class: Drupal\accept_header_routing_test\Routing\AcceptHeaderMatcher
tags:
- { name: route_filter }

View File

@@ -0,0 +1,47 @@
<?php
namespace Drupal\accept_header_routing_test;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* Example implementation of "accept header"-based content negotiation.
*/
class AcceptHeaderMiddleware implements HttpKernelInterface {
/**
* The app kernel.
*/
protected HttpKernelInterface $app;
/**
* Constructs a new AcceptHeaderMiddleware instance.
*
* @param \Symfony\Component\HttpKernel\HttpKernelInterface $app
* The app.
*/
public function __construct(HttpKernelInterface $app) {
$this->app = $app;
}
/**
* {@inheritdoc}
*/
public function handle(Request $request, $type = self::MAIN_REQUEST, $catch = TRUE): Response {
$mapping = [
'application/json' => 'json',
'application/xml' => 'xml',
'text/html' => 'html',
];
$accept = $request->headers->get('Accept') ?: ['text/html'];
if (isset($mapping[$accept[0]])) {
$request->setRequestFormat($mapping[$accept[0]]);
}
return $this->app->handle($request, $type, $catch);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Drupal\accept_header_routing_test;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceModifierInterface;
/**
* Service provider for the accept_header_routing_test module.
*/
class AcceptHeaderRoutingTestServiceProvider implements ServiceModifierInterface {
/**
* {@inheritdoc}
*/
public function alter(ContainerBuilder $container) {
// Remove the basic content negotiation middleware and replace it with a
// basic header based one.
$container->register('http_middleware.negotiation', 'Drupal\accept_header_routing_test\AcceptHeaderMiddleware')
->addTag('http_middleware', ['priority' => 400]);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Drupal\accept_header_routing_test\Routing;
use Drupal\Core\Routing\FilterInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
use Symfony\Component\Routing\RouteCollection;
/**
* Filters routes based on the media type specified in the HTTP Accept headers.
*/
class AcceptHeaderMatcher implements FilterInterface {
/**
* {@inheritdoc}
*/
public function filter(RouteCollection $collection, Request $request) {
// Generates a list of Symfony formats matching the acceptable MIME types.
// @todo replace by proper content negotiation library.
$acceptable_mime_types = $request->getAcceptableContentTypes();
$acceptable_formats = array_filter(array_map([$request, 'getFormat'], $acceptable_mime_types));
$primary_format = $request->getRequestFormat();
foreach ($collection as $name => $route) {
// _format could be a |-delimited list of supported formats.
$supported_formats = array_filter(explode('|', $route->getRequirement('_format') ?? ''));
if (empty($supported_formats)) {
// No format restriction on the route, so it always matches. Move it to
// the end of the collection by re-adding it.
$collection->add($name, $route);
}
elseif (in_array($primary_format, $supported_formats)) {
// Perfect match, which will get a higher priority by leaving the route
// on top of the list.
}
// The route partially matches if it doesn't care about format, if it
// explicitly allows any format, or if one of its allowed formats is
// in the request's list of acceptable formats.
elseif (in_array('*/*', $acceptable_mime_types) || array_intersect($acceptable_formats, $supported_formats)) {
// Move it to the end of the list.
$collection->add($name, $route);
}
else {
// Remove the route if it does not match at all.
$collection->remove($name);
}
}
if (count($collection)) {
return $collection;
}
// We do not throw a
// \Symfony\Component\Routing\Exception\ResourceNotFoundException here
// because we don't want to return a 404 status code, but rather a 406.
throw new NotAcceptableHttpException('No route found for the specified formats ' . implode(' ', $acceptable_mime_types));
}
}

View File

@@ -0,0 +1,10 @@
name: 'Action test'
type: module
description: 'Support module for action testing.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,34 @@
<?php
namespace Drupal\action_test\Plugin\Action;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Action\Attribute\Action;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides an operation with no type specified.
*/
#[Action(
id: 'action_test_no_type',
label: new TranslatableMarkup('An operation with no type specified'),
)]
class NoType extends ActionBase {
/**
* {@inheritdoc}
*/
public function execute($entity = NULL) {
}
/**
* {@inheritdoc}
*/
public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE) {
$result = AccessResult::allowed();
return $return_as_object ? $result : $result->isAllowed();
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Drupal\action_test\Plugin\Action;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Action\Attribute\Action;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides an operation to save user entities.
*/
#[Action(
id: 'action_test_save_entity',
label: new TranslatableMarkup('Saves entities'),
type: 'user'
)]
class SaveEntity extends ActionBase {
/**
* {@inheritdoc}
*/
public function execute($entity = NULL) {
$entity->save();
}
/**
* {@inheritdoc}
*/
public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\Core\Entity\EntityInterface $object */
return $object->access('update', $account, $return_as_object);
}
}

View File

@@ -0,0 +1,9 @@
name: 'Advisory feed test'
type: module
description: 'Support module for advisory feed testing.'
package: Testing
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,35 @@
<?php
/**
* @file
* Module for testing the display of security advisories.
*/
use Drupal\Core\Extension\Extension;
/**
* Implements hook_system_info_alter().
*/
function advisory_feed_test_system_info_alter(&$info, Extension $file) {
// Alter the 'generic_module1_test' module to use the 'generic_module1_project'
// project name. This ensures that for an extension where the 'name' and
// the 'project' properties do not match, 'project' is used for matching
// 'project' in the JSON feed.
$system_info = [
'generic_module1_test' => [
'project' => 'generic_module1_project',
'version' => '8.x-1.1',
'hidden' => FALSE,
],
'generic_module2_test' => [
'project' => 'generic_module2_project',
'version' => '8.x-1.1',
'hidden' => FALSE,
],
];
if (!empty($system_info[$file->getName()])) {
foreach ($system_info[$file->getName()] as $key => $value) {
$info[$key] = $value;
}
}
}

View File

@@ -0,0 +1,7 @@
advisory_feed_test.json_test:
path: '/advisory-feed-json/{json_name}'
defaults:
_title: 'Update test'
_controller: '\Drupal\advisory_feed_test\Controller\AdvisoryTestController::getPsaJson'
requirements:
_access: 'TRUE'

View File

@@ -0,0 +1,10 @@
services:
http_client.advisory_feed_test:
class: Drupal\advisory_feed_test\AdvisoryTestClientMiddleware
tags:
- { name: http_client_middleware }
logger.advisory_feed_test:
public: false
class: Drupal\advisory_feed_test\TestSystemLoggerChannel
decorates: logger.channel.system
arguments: ['@logger.advisory_feed_test.inner', '@state']

View File

@@ -0,0 +1,47 @@
<?php
namespace Drupal\advisory_feed_test;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Psr7\Uri;
use Psr\Http\Message\RequestInterface;
/**
* Overrides the User-Agent HTTP header for outbound HTTP requests.
*/
class AdvisoryTestClientMiddleware {
/**
* HTTP middleware that replaces URI for advisory requests.
*/
public function __invoke() {
return function ($handler) {
return function (RequestInterface $request, array $options) use ($handler): PromiseInterface {
$test_end_point = \Drupal::state()->get('advisories_test_endpoint');
if ($test_end_point && str_contains($request->getUri(), '://updates.drupal.org/psa.json')) {
// Only override $uri if it matches the advisories JSON feed to avoid
// changing any other uses of the 'http_client' service during tests with
// this module installed.
$request = $request->withUri(new Uri($test_end_point));
}
return $handler($request, $options);
};
};
}
/**
* Sets the test endpoint for the advisories JSON feed.
*
* @param string $test_endpoint
* The test endpoint.
* @param bool $delete_stored_response
* Whether to delete stored feed response.
*/
public static function setTestEndpoint(string $test_endpoint, bool $delete_stored_response = FALSE): void {
\Drupal::state()->set('advisories_test_endpoint', $test_endpoint);
if ($delete_stored_response) {
\Drupal::service('system.sa_fetcher')->deleteStoredResponse();
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Drupal\advisory_feed_test\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
/**
* Defines a controller to return JSON for security advisory tests.
*/
class AdvisoryTestController {
/**
* Reads a JSON file and returns the contents as a Response.
*
* This method will replace the string '[CORE_VERSION]' with the current core
* version to allow testing core version matches.
*
* @param string $json_name
* The name of the JSON file without the file extension.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse|\Symfony\Component\HttpFoundation\Response
* If a fixture file with the name $json_name + '.json' is found a
* JsonResponse will be returned using the contents of the file, otherwise a
* Response will be returned with a 404 status code.
*/
public function getPsaJson(string $json_name): Response {
$file = __DIR__ . "/../../../../fixtures/psa_feed/$json_name.json";
$headers = ['Content-Type' => 'application/json; charset=utf-8'];
if (!is_file($file)) {
// Return an empty response.
return new Response('', 404, $headers);
}
$contents = file_get_contents($file);
$contents = str_replace('[CORE_VERSION]', \Drupal::VERSION, $contents);
return new JsonResponse($contents, 200, $headers, TRUE);
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Drupal\advisory_feed_test;
use Drupal\Core\Logger\LoggerChannel;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\Core\State\StateInterface;
use Psr\Log\LogLevel;
/**
* Provides a decorator for the 'logger.channel.system' service for testing.
*/
final class TestSystemLoggerChannel extends LoggerChannel {
/**
* The decorated logger.channel.system service.
*
* @var \Drupal\Core\Logger\LoggerChannelInterface
*/
protected $innerLogger;
/**
* The state service.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* Constructs an AdvisoriesTestHttpClient object.
*
* @param \Drupal\Core\Logger\LoggerChannelInterface $inner_logger
* The decorated logger.channel.system service.
* @param \Drupal\Core\State\StateInterface $state
* The state service.
*/
public function __construct(LoggerChannelInterface $inner_logger, StateInterface $state) {
$this->innerLogger = $inner_logger;
$this->state = $state;
}
/**
* {@inheritdoc}
*
* @see \Drupal\Tests\system\Functional\SecurityAdvisories\SecurityAdvisoriesTestTrait::assertServiceAdvisoryLoggedErrors()
*/
public function log($level, $message, array $context = []): void {
if ($level === LogLevel::ERROR) {
$messages = $this->state->get('advisory_feed_test.error_messages', []);
$messages[] = $message;
$this->state->set('advisory_feed_test.error_messages', $messages);
}
$this->innerLogger->log($level, $message, $context);
}
}

View File

@@ -0,0 +1,10 @@
name: 'AJAX form test mock module'
type: module
description: 'Test for AJAX form calls.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,262 @@
<?php
/**
* @file
* Mock module for Ajax forms testing.
*/
use Drupal\Core\Ajax\AddCssCommand;
use Drupal\Core\Ajax\AfterCommand;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\AlertCommand;
use Drupal\Core\Ajax\AnnounceCommand;
use Drupal\Core\Ajax\AppendCommand;
use Drupal\Core\Ajax\BeforeCommand;
use Drupal\Core\Ajax\ChangedCommand;
use Drupal\Core\Ajax\CssCommand;
use Drupal\Core\Ajax\DataCommand;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Ajax\InsertCommand;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Ajax\PrependCommand;
use Drupal\Core\Ajax\RemoveCommand;
use Drupal\Core\Ajax\RestripeCommand;
use Drupal\Core\Ajax\SettingsCommand;
use Drupal\Core\Form\FormStateInterface;
/**
* Ajax form callback: Selects 'after'.
*/
function ajax_forms_test_advanced_commands_after_callback($form, FormStateInterface $form_state) {
$selector = '#after_div';
$response = new AjaxResponse();
$response->addCommand(new AfterCommand($selector, "This will be placed after"));
return $response;
}
/**
* Ajax form callback: Selects 'alert'.
*/
function ajax_forms_test_advanced_commands_alert_callback($form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$response->addCommand(new AlertCommand('Alert'));
return $response;
}
/**
* Ajax form callback: Selects 'announce' with no priority specified.
*/
function ajax_forms_test_advanced_commands_announce_callback($form, FormStateInterface $form_state) {
return (new AjaxResponse())->addCommand(new AnnounceCommand('Default announcement.'));
}
/**
* Ajax form callback: Selects 'announce' with 'polite' priority.
*/
function ajax_forms_test_advanced_commands_announce_polite_callback($form, FormStateInterface $form_state) {
return (new AjaxResponse())->addCommand(new AnnounceCommand('Polite announcement.', 'polite'));
}
/**
* Ajax form callback: Selects 'announce' with 'assertive' priority.
*/
function ajax_forms_test_advanced_commands_announce_assertive_callback($form, FormStateInterface $form_state) {
return (new AjaxResponse())->addCommand(new AnnounceCommand('Assertive announcement.', 'assertive'));
}
/**
* Ajax form callback: Selects 'announce' with two announce commands returned.
*/
function ajax_forms_test_advanced_commands_double_announce_callback($form, FormStateInterface $form_state) {
return (new AjaxResponse())->addCommand(new AnnounceCommand('Assertive announcement.', 'assertive'))
->addCommand(new AnnounceCommand('Another announcement.'));
}
/**
* Ajax form callback: Selects 'append'.
*/
function ajax_forms_test_advanced_commands_append_callback($form, FormStateInterface $form_state) {
$selector = '#append_div';
$response = new AjaxResponse();
$response->addCommand(new AppendCommand($selector, "Appended text"));
return $response;
}
/**
* Ajax form callback: Selects 'before'.
*/
function ajax_forms_test_advanced_commands_before_callback($form, FormStateInterface $form_state) {
$selector = '#before_div';
$response = new AjaxResponse();
$response->addCommand(new BeforeCommand($selector, "Before text"));
return $response;
}
/**
* Ajax form callback: Selects 'changed'.
*/
function ajax_forms_test_advanced_commands_changed_callback($form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$response->addCommand(new ChangedCommand('#changed_div'));
return $response;
}
/**
* Ajax form callback: Selects 'changed' with asterisk marking inner div.
*/
function ajax_forms_test_advanced_commands_changed_asterisk_callback($form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$response->addCommand(new ChangedCommand('#changed_div', '#changed_div_mark_this'));
return $response;
}
/**
* Ajax form callback: Selects 'css'.
*/
function ajax_forms_test_advanced_commands_css_callback($form, FormStateInterface $form_state) {
$selector = '#css_div';
$color = 'blue';
$response = new AjaxResponse();
$response->addCommand(new CssCommand($selector, ['background-color' => $color]));
return $response;
}
/**
* Ajax form callback: Selects 'data'.
*/
function ajax_forms_test_advanced_commands_data_callback($form, FormStateInterface $form_state) {
$selector = '#data_div';
$response = new AjaxResponse();
$response->addCommand(new DataCommand($selector, 'test_key', 'test_value'));
return $response;
}
/**
* Ajax form callback: Selects 'invoke'.
*/
function ajax_forms_test_advanced_commands_invoke_callback($form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$response->addCommand(new InvokeCommand('#invoke_div', 'addClass', ['error']));
return $response;
}
/**
* Ajax form callback: Selects 'html'.
*/
function ajax_forms_test_advanced_commands_html_callback($form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$response->addCommand(new HtmlCommand('#html_div', 'replacement text'));
return $response;
}
/**
* Ajax form callback: Selects 'insert'.
*/
function ajax_forms_test_advanced_commands_insert_callback($form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$response->addCommand(new InsertCommand('#insert_div', 'insert replacement text'));
return $response;
}
/**
* Ajax form callback: Selects 'prepend'.
*/
function ajax_forms_test_advanced_commands_prepend_callback($form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$response->addCommand(new PrependCommand('#prepend_div', "prepended text"));
return $response;
}
/**
* Ajax form callback: Selects 'remove'.
*/
function ajax_forms_test_advanced_commands_remove_callback($form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$response->addCommand(new RemoveCommand('#remove_text'));
return $response;
}
/**
* Ajax form callback: Selects 'restripe'.
*/
function ajax_forms_test_advanced_commands_restripe_callback($form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$response->addCommand(new RestripeCommand('#restripe_table'));
return $response;
}
/**
* Ajax form callback: Selects 'settings'.
*/
function ajax_forms_test_advanced_commands_settings_callback($form, FormStateInterface $form_state) {
$setting['ajax_forms_test']['foo'] = 42;
$response = new AjaxResponse();
$response->addCommand(new SettingsCommand($setting));
return $response;
}
/**
* Ajax callback for 'add_css'.
*/
function ajax_forms_test_advanced_commands_add_css_callback($form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$response->addCommand(new AddCssCommand([
[
'href' => 'my/file.css',
'media' => 'all',
],
[
'href' => 'https://example.com/css?family=Open+Sans',
'media' => 'all',
],
]));
return $response;
}
/**
* Ajax callback for 'add_css' that uses legacy string parameter.
*
* @todo Remove in Drupal 11.0.0 https://www.drupal.org/i/3339374
*/
function ajax_forms_test_advanced_commands_add_css_legacy_callback($form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$response->addCommand(new AddCssCommand("<link rel='stylesheet' href='my/file.css' media='all' />"));
return $response;
}
/**
* Ajax form callback: Selects the 'driver_text' element of the validation form.
*/
function ajax_forms_test_validation_form_callback($form, FormStateInterface $form_state) {
\Drupal::messenger()->addStatus("ajax_forms_test_validation_form_callback invoked");
\Drupal::messenger()->addStatus(t("Callback: driver_text=%driver_text, spare_required_field=%spare_required_field", ['%driver_text' => $form_state->getValue('driver_text'), '%spare_required_field' => $form_state->getValue('spare_required_field')]));
return ['#markup' => '<div id="message_area">ajax_forms_test_validation_form_callback at ' . date('c') . '</div>'];
}
/**
* Ajax form callback: Selects the 'driver_number' element of the validation form.
*/
function ajax_forms_test_validation_number_form_callback($form, FormStateInterface $form_state) {
\Drupal::messenger()->addStatus("ajax_forms_test_validation_number_form_callback invoked");
\Drupal::messenger()->addStatus(t("Callback: driver_number=%driver_number, spare_required_field=%spare_required_field", ['%driver_number' => $form_state->getValue('driver_number'), '%spare_required_field' => $form_state->getValue('spare_required_field')]));
return ['#markup' => '<div id="message_area_number">ajax_forms_test_validation_number_form_callback at ' . date('c') . '</div>'];
}
/**
* AJAX form callback: Selects for the ajax_forms_test_lazy_load_form() form.
*/
function ajax_forms_test_lazy_load_form_ajax($form, FormStateInterface $form_state) {
$build = [
'#markup' => 'new content',
];
if ($form_state->getValue('add_files')) {
$build['#attached']['library'][] = 'system/admin';
$build['#attached']['library'][] = 'system/drupal.system';
$build['#attached']['drupalSettings']['ajax_forms_test_lazy_load_form_submit'] = 'executed';
}
return $build;
}

View File

@@ -0,0 +1,55 @@
ajax_forms_test.get_form:
path: '/ajax_forms_test_get_form'
defaults:
_title: 'AJAX forms simple form test'
_form: '\Drupal\ajax_forms_test\Form\AjaxFormsTestSimpleForm'
requirements:
_access: 'TRUE'
ajax_forms_test.commands_form:
path: '/ajax_forms_test_ajax_commands_form'
defaults:
_title: 'AJAX forms AJAX commands test'
_form: '\Drupal\ajax_forms_test\Form\AjaxFormsTestCommandsForm'
requirements:
_access: 'TRUE'
ajax_forms_test.validation_test:
path: '/ajax_validation_test'
defaults:
_title: 'AJAX Validation Test'
_form: '\Drupal\ajax_forms_test\Form\AjaxFormsTestValidationForm'
requirements:
_access: 'TRUE'
ajax_forms_test.lazy_load_form:
path: '/ajax_forms_test_lazy_load_form'
defaults:
_title: 'AJAX forms lazy load test'
_form: '\Drupal\ajax_forms_test\Form\AjaxFormsTestLazyLoadForm'
requirements:
_access: 'TRUE'
ajax_forms_test.image_button_form:
path: '/ajax_forms_image_button_form'
defaults:
_title: 'AJAX forms image button test'
_form: '\Drupal\ajax_forms_test\Form\AjaxFormsTestImageButtonForm'
requirements:
_access: 'TRUE'
ajax_forms_test.ajax_element_form:
path: '/ajax_forms_test_ajax_element_form'
defaults:
_title: 'AJAX forms elements test'
_form: '\Drupal\ajax_forms_test\Form\AjaxFormsTestAjaxElementsForm'
requirements:
_access: 'TRUE'
ajax_forms_test.dialog_form_link:
path: '/ajax_forms_test_dialog_form_link'
defaults:
_title: 'Dialog form link test'
_controller: '\Drupal\ajax_forms_test\Controller\DialogFormLink::makeDialogFormLink'
requirements:
_access: 'TRUE'

View File

@@ -0,0 +1,74 @@
<?php
namespace Drupal\ajax_forms_test;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\DataCommand;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Form\FormStateInterface;
/**
* Simple object for testing methods as Ajax callbacks.
*/
class Callbacks {
/**
* Ajax callback triggered by select.
*/
public function selectCallback($form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$response->addCommand(new HtmlCommand('#ajax_selected_color', $form_state->getValue('select')));
$response->addCommand(new DataCommand('#ajax_selected_color', 'form_state_value_select', $form_state->getValue('select')));
return $response;
}
/**
* Ajax callback triggered by date.
*/
public function dateCallback($form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$date = $form_state->getValue('date');
$response->addCommand(new HtmlCommand('#ajax_date_value', sprintf('<div>%s</div>', $date)));
$response->addCommand(new DataCommand('#ajax_date_value', 'form_state_value_date', $form_state->getValue('date')));
return $response;
}
/**
* Ajax callback triggered by datetime.
*/
public function datetimeCallback($form, FormStateInterface $form_state) {
$datetime = $form_state->getValue('datetime')['date'] . ' ' . $form_state->getValue('datetime')['time'];
$response = new AjaxResponse();
$response->addCommand(new HtmlCommand('#ajax_datetime_value', sprintf('<div>%s</div>', $datetime)));
$response->addCommand(new DataCommand('#ajax_datetime_value', 'form_state_value_datetime', $datetime));
return $response;
}
/**
* Ajax callback triggered by checkbox.
*/
public function checkboxCallback($form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$response->addCommand(new HtmlCommand('#ajax_checkbox_value', $form_state->getValue('checkbox') ? 'checked' : 'unchecked'));
$response->addCommand(new DataCommand('#ajax_checkbox_value', 'form_state_value_select', (int) $form_state->getValue('checkbox')));
return $response;
}
/**
* Ajax callback to confirm image button was submitted.
*/
public function imageButtonCallback($form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$response->addCommand(new HtmlCommand('#ajax_image_button_result', "<div id='ajax-1-more-div'>Something witty!</div>"));
return $response;
}
/**
* Ajax callback triggered by the checkbox in a #group.
*/
public function checkboxGroupCallback($form, FormStateInterface $form_state) {
return $form['checkbox_in_group_wrapper'];
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Drupal\ajax_forms_test\Controller;
use Drupal\Core\Url;
/**
* Test class to create dialog form link.
*/
class DialogFormLink {
/**
* Builds an associative array representing a link that opens a dialog.
*
* @return array
* An associative array of link to a form to be opened.
*/
public function makeDialogFormLink() {
return [
'dialog' => [
'#type' => 'link',
'#title' => 'Open form in dialog',
'#url' => Url::fromRoute('ajax_forms_test.get_form'),
'#attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'dialog',
],
],
'off_canvas' => [
'#type' => 'link',
'#title' => 'Open form in off canvas dialog',
'#url' => Url::fromRoute('ajax_forms_test.get_form'),
'#attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'dialog',
'data-dialog-renderer' => 'off_canvas',
],
],
'#attached' => [
'library' => ['core/drupal.dialog.ajax'],
],
];
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Drupal\ajax_forms_test\Form;
use Drupal\Core\Form\FormBase;
use Drupal\ajax_forms_test\Callbacks;
use Drupal\Core\Form\FormStateInterface;
/**
* Builds a form where each Form API element triggers a simple Ajax callback.
*
* @internal
*/
class AjaxFormsTestAjaxElementsForm extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'ajax_forms_test_ajax_elements_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$callback_object = new Callbacks();
$form['date'] = [
'#type' => 'date',
'#ajax' => [
'callback' => [$callback_object, 'dateCallback'],
],
'#suffix' => '<div id="ajax_date_value">No date yet selected</div>',
];
$form['datetime'] = [
'#type' => 'datetime',
'#ajax' => [
'callback' => [$callback_object, 'datetimeCallback'],
'wrapper' => 'ajax_datetime_value',
],
];
$form['datetime_result'] = [
'#type' => 'markup',
'#markup' => '<div id="ajax_datetime_value">No datetime selected.</div>',
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {}
}

View File

@@ -0,0 +1,251 @@
<?php
namespace Drupal\ajax_forms_test\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Form constructor for the Ajax Command display form.
*
* @internal
*/
class AjaxFormsTestCommandsForm extends FormBase {
/**
* {@inheritdoc}.
*/
public function getFormId() {
return 'ajax_forms_test_ajax_commands_form';
}
/**
* {@inheritdoc}.
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form = [];
// Shows the 'after' command with a callback generating commands.
$form['after_command_example'] = [
'#value' => $this->t("AJAX 'After': Click to put something after the div"),
'#type' => 'submit',
'#ajax' => [
'callback' => 'ajax_forms_test_advanced_commands_after_callback',
],
'#suffix' => '<div id="after_div">Something can be inserted after this</div>',
];
// Shows the 'alert' command.
$form['alert_command_example'] = [
'#value' => $this->t("AJAX 'Alert': Click to alert"),
'#type' => 'submit',
'#ajax' => [
'callback' => 'ajax_forms_test_advanced_commands_alert_callback',
],
];
// Shows the 'announce' command with default priority.
$form['announce_command_example'] = [
'#value' => $this->t("AJAX 'Announce': Click to announce"),
'#type' => 'submit',
'#ajax' => [
'callback' => 'ajax_forms_test_advanced_commands_announce_callback',
],
];
// Shows the 'announce' command with 'polite' priority.
$form['announce_command_polite_example'] = [
'#value' => $this->t("AJAX 'Announce': Click to announce with 'polite' priority"),
'#type' => 'submit',
'#ajax' => [
'callback' => 'ajax_forms_test_advanced_commands_announce_polite_callback',
],
];
// Shows the 'announce' command with 'assertive' priority.
$form['announce_command_assertive_example'] = [
'#value' => $this->t("AJAX 'Announce': Click to announce with 'assertive' priority"),
'#type' => 'submit',
'#ajax' => [
'callback' => 'ajax_forms_test_advanced_commands_announce_assertive_callback',
],
];
// Shows the 'announce' command used twice in one AjaxResponse.
$form['announce_command_double_example'] = [
'#value' => $this->t("AJAX 'Announce': Click to announce twice"),
'#type' => 'submit',
'#ajax' => [
'callback' => 'ajax_forms_test_advanced_commands_double_announce_callback',
],
];
// Shows the 'append' command.
$form['append_command_example'] = [
'#value' => $this->t("AJAX 'Append': Click to append something"),
'#type' => 'submit',
'#ajax' => [
'callback' => 'ajax_forms_test_advanced_commands_append_callback',
],
'#suffix' => '<div id="append_div">Append inside this div</div>',
];
// Shows the 'before' command.
$form['before_command_example'] = [
'#value' => $this->t("AJAX 'before': Click to put something before the div"),
'#type' => 'submit',
'#ajax' => [
'callback' => 'ajax_forms_test_advanced_commands_before_callback',
],
'#suffix' => '<div id="before_div">Insert something before this.</div>',
];
// Shows the 'changed' command without asterisk.
$form['changed_command_example'] = [
'#value' => $this->t("AJAX changed: Click to mark div changed."),
'#type' => 'submit',
'#ajax' => [
'callback' => 'ajax_forms_test_advanced_commands_changed_callback',
],
'#suffix' => '<div id="changed_div"> <div id="changed_div_mark_this">This div can be marked as changed or not.</div></div>',
];
// Shows the 'changed' command adding the asterisk.
$form['changed_command_asterisk_example'] = [
'#value' => $this->t("AJAX changed: Click to mark div changed with asterisk."),
'#type' => 'submit',
'#ajax' => [
'callback' => 'ajax_forms_test_advanced_commands_changed_asterisk_callback',
],
];
// Shows the Ajax 'css' command.
$form['css_command_example'] = [
'#value' => $this->t("Set the '#box' div to be blue."),
'#type' => 'submit',
'#ajax' => [
'callback' => 'ajax_forms_test_advanced_commands_css_callback',
],
'#suffix' => '<div id="css_div" style="height: 50px; width: 50px; border: 1px solid black"> box</div>',
];
// Shows the Ajax 'data' command. But there is no use of this information,
// as this would require a javascript client to use the data.
$form['data_command_example'] = [
'#value' => $this->t("AJAX data command: Issue command."),
'#type' => 'submit',
'#ajax' => [
'callback' => 'ajax_forms_test_advanced_commands_data_callback',
],
'#suffix' => '<div id="data_div">Data attached to this div.</div>',
];
// Shows the Ajax 'invoke' command.
$form['invoke_command_example'] = [
'#value' => $this->t("AJAX invoke command: Invoke addClass() method."),
'#type' => 'submit',
'#ajax' => [
'callback' => 'ajax_forms_test_advanced_commands_invoke_callback',
],
'#suffix' => '<div id="invoke_div">Original contents</div>',
];
// Shows the Ajax 'html' command.
$form['html_command_example'] = [
'#value' => $this->t("AJAX html: Replace the HTML in a selector."),
'#type' => 'submit',
'#ajax' => [
'callback' => 'ajax_forms_test_advanced_commands_html_callback',
],
'#suffix' => '<div id="html_div">Original contents</div>',
];
// Shows the Ajax 'insert' command.
$form['insert_command_example'] = [
'#value' => $this->t("AJAX insert: Let client insert based on #ajax['method']."),
'#type' => 'submit',
'#ajax' => [
'callback' => 'ajax_forms_test_advanced_commands_insert_callback',
'method' => 'prepend',
],
'#suffix' => '<div id="insert_div">Original contents</div>',
];
// Shows the Ajax 'prepend' command.
$form['prepend_command_example'] = [
'#value' => $this->t("AJAX 'prepend': Click to prepend something"),
'#type' => 'submit',
'#ajax' => [
'callback' => 'ajax_forms_test_advanced_commands_prepend_callback',
],
'#suffix' => '<div id="prepend_div">Something will be prepended to this div. </div>',
];
// Shows the Ajax 'remove' command.
$form['remove_command_example'] = [
'#value' => $this->t("AJAX 'remove': Click to remove text"),
'#type' => 'submit',
'#ajax' => [
'callback' => 'ajax_forms_test_advanced_commands_remove_callback',
],
'#suffix' => '<div id="remove_div"><div id="remove_text">text to be removed</div></div>',
];
// Shows the Ajax 'restripe' command.
$form['restripe_command_example'] = [
'#type' => 'submit',
'#value' => $this->t("AJAX 'restripe' command"),
'#ajax' => [
'callback' => 'ajax_forms_test_advanced_commands_restripe_callback',
],
'#suffix' => '<div id="restripe_div">
<table id="restripe_table" style="border: 1px solid black" >
<tr id="table-first"><td>first row</td></tr>
<tr ><td>second row</td></tr>
</table>
</div>',
];
// Demonstrates the Ajax 'settings' command. The 'settings' command has
// nothing visual to "show", but it can be tested.
$form['settings_command_example'] = [
'#type' => 'submit',
'#value' => $this->t("AJAX 'settings' command"),
'#ajax' => [
'callback' => 'ajax_forms_test_advanced_commands_settings_callback',
],
];
// Shows the Ajax 'add_css' command.
$form['add_css_command_example'] = [
'#type' => 'submit',
'#value' => $this->t("AJAX 'add_css' command"),
'#ajax' => [
'callback' => 'ajax_forms_test_advanced_commands_add_css_callback',
],
];
// Shows the Ajax 'add_css' command with legacy string parameter.
// @todo Remove in Drupal 11.0.0 https://www.drupal.org/i/3339374
$form['add_css_legacy_command_example'] = [
'#type' => 'submit',
'#value' => $this->t("AJAX 'add_css' legacy command"),
'#ajax' => [
'callback' => 'ajax_forms_test_advanced_commands_add_css_legacy_callback',
],
];
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Submit'),
];
return $form;
}
/**
* {@inheritdoc}.
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Drupal\ajax_forms_test\Form;
use Drupal\ajax_forms_test\Callbacks;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Form builder: Builds a form that has image button with an ajax callback.
*
* @internal
*/
class AjaxFormsTestImageButtonForm extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'ajax_forms_test_image_button_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$object = new Callbacks();
$form['image_button'] = [
'#type' => 'image_button',
'#name' => 'image_button',
'#src' => 'core/misc/icons/787878/cog.svg',
'#attributes' => ['alt' => $this->t('Edit')],
'#op' => 'edit',
'#ajax' => [
'callback' => [$object, 'imageButtonCallback'],
],
'#suffix' => '<div id="ajax_image_button_result">Image button not pressed yet.</div>',
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// No submit code needed.
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Drupal\ajax_forms_test\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Form builder: Builds a form that triggers a simple AJAX callback.
*
* @internal
*/
class AjaxFormsTestLazyLoadForm extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'ajax_forms_test_lazy_load_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
// We attach a JavaScript setting, so that one of the generated AJAX
// commands will be a settings command. We can then check the settings
// command to ensure that the 'currentPath' setting is not part
// of the Ajax response.
$form['#attached']['drupalSettings']['test'] = 'currentPathUpdate';
$form['add_files'] = [
'#title' => $this->t('Add files'),
'#type' => 'checkbox',
'#default_value' => FALSE,
];
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Submit'),
'#ajax' => [
'wrapper' => 'ajax-forms-test-lazy-load-ajax-wrapper',
'callback' => 'ajax_forms_test_lazy_load_form_ajax',
],
'#prefix' => '<div id="ajax-forms-test-lazy-load-ajax-wrapper"></div>',
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$form_state->setRebuild();
}
}

View File

@@ -0,0 +1,157 @@
<?php
namespace Drupal\ajax_forms_test\Form;
use Drupal\Core\Form\FormBase;
use Drupal\ajax_forms_test\Callbacks;
use Drupal\Core\Form\FormStateInterface;
/**
* Form builder: Builds a form that triggers a simple AJAX callback.
*
* @internal
*/
class AjaxFormsTestSimpleForm extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'ajax_forms_test_simple_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$object = new Callbacks();
$form = [];
$form['select'] = [
'#title' => $this->t('Color'),
'#type' => 'select',
'#options' => [
'red' => 'red',
'green' => 'green',
'blue' => 'blue',
],
'#ajax' => [
'callback' => [$object, 'selectCallback'],
],
'#suffix' => '<div id="ajax_selected_color">No color yet selected</div>',
];
$form['checkbox'] = [
'#type' => 'checkbox',
'#title' => $this->t('Test checkbox'),
'#ajax' => [
'callback' => [$object, 'checkboxCallback'],
],
'#suffix' => '<div id="ajax_checkbox_value">No action yet</div>',
];
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('submit'),
];
// This is for testing invalid callbacks that should return a 500 error in
// \Drupal\Core\Form\FormAjaxResponseBuilderInterface::buildResponse().
$invalid_callbacks = [
'null' => NULL,
'empty' => '',
'nonexistent' => 'some_function_that_does_not_exist',
];
foreach ($invalid_callbacks as $key => $value) {
$form['select_' . $key . '_callback'] = [
'#type' => 'select',
'#title' => $this->t('Test %key callbacks', ['%key' => $key]),
'#options' => ['red' => 'red', 'green' => 'green'],
'#ajax' => ['callback' => $value],
];
}
$form['test_group'] = [
'#type' => 'details',
'#title' => $this->t('Test group'),
'#open' => TRUE,
];
// Test ajax element in a #group.
$form['checkbox_in_group_wrapper'] = [
'#type' => 'container',
'#attributes' => ['id' => 'checkbox-wrapper'],
'#group' => 'test_group',
'checkbox_in_group' => [
'#type' => 'checkbox',
'#title' => $this->t('AJAX checkbox in a group'),
'#ajax' => [
'callback' => [$object, 'checkboxGroupCallback'],
'wrapper' => 'checkbox-wrapper',
],
],
'nested_group' => [
'#type' => 'details',
'#title' => $this->t('Nested group'),
'#open' => TRUE,
],
'checkbox_in_nested' => [
'#type' => 'checkbox',
'#group' => 'nested_group',
'#title' => $this->t('AJAX checkbox in a nested group'),
'#ajax' => [
'callback' => [$object, 'checkboxGroupCallback'],
'wrapper' => 'checkbox-wrapper',
],
],
];
$form['another_checkbox_in_nested'] = [
'#type' => 'checkbox',
'#group' => 'nested_group',
'#title' => $this->t('Another AJAX checkbox in a nested group'),
];
$form['textfield_focus_tests'] = [
'#type' => 'details',
'#title' => $this->t('Test group 2'),
'#open' => TRUE,
];
$form['textfield_focus_tests']['textfield'] = [
'#type' => 'textfield',
'#title' => 'Textfield 1',
'#ajax' => [
'callback' => [static::class, 'textfieldCallback'],
],
];
$form['textfield_focus_tests']['textfield_2'] = [
'#type' => 'textfield',
'#title' => 'Textfield 2',
'#ajax' => [
'callback' => [static::class, 'textfieldCallback'],
'event' => 'change',
'refocus-blur' => FALSE,
],
];
$form['textfield_focus_tests']['textfield_3'] = [
'#type' => 'textfield',
'#title' => 'Textfield 3',
'#ajax' => [
'callback' => [static::class, 'textfieldCallback'],
'event' => 'change',
],
];
return $form;
}
public static function textfieldCallback($form) {
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Drupal\ajax_forms_test\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Form builder: Builds a form that triggers a simple AJAX callback.
*
* @internal
*/
class AjaxFormsTestValidationForm extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'ajax_forms_test_validation_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['driver_text'] = [
'#title' => $this->t('AJAX-enabled textfield.'),
'#description' => $this->t("When this one AJAX-triggers and the spare required field is empty, you should not get an error."),
'#type' => 'textfield',
'#default_value' => $form_state->getValue('driver_text', ''),
'#ajax' => [
'callback' => 'ajax_forms_test_validation_form_callback',
'wrapper' => 'message_area',
'method' => 'replaceWith',
],
'#suffix' => '<div id="message_area"></div>',
];
$form['driver_number'] = [
'#title' => $this->t('AJAX-enabled number field.'),
'#description' => $this->t("When this one AJAX-triggers and the spare required field is empty, you should not get an error."),
'#type' => 'number',
'#default_value' => $form_state->getValue('driver_number', ''),
'#ajax' => [
'callback' => 'ajax_forms_test_validation_number_form_callback',
'wrapper' => 'message_area_number',
'method' => 'replaceWith',
],
'#suffix' => '<div id="message_area_number"></div>',
];
$form['spare_required_field'] = [
'#title' => $this->t("Spare Required Field"),
'#type' => 'textfield',
'#required' => TRUE,
];
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Submit'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->messenger()->addStatus($this->t("Validation form submitted"));
}
}

View File

@@ -0,0 +1,148 @@
<?php
namespace Drupal\ajax_forms_test\Plugin\Block;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides an AJAX form block.
*/
#[Block(
id: "ajax_forms_test_block",
admin_label: new TranslatableMarkup("AJAX test form"),
category: new TranslatableMarkup("Forms")
)]
class AjaxFormBlock extends BlockBase implements FormInterface, ContainerFactoryPluginInterface {
/**
* The form builder.
*
* @var \Drupal\Core\Form\FormBuilderInterface
*/
protected $formBuilder;
/**
* The messenger.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* Constructs a new AjaxFormBlock.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
* The form builder.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, FormBuilderInterface $form_builder, MessengerInterface $messenger) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->formBuilder = $form_builder;
$this->messenger = $messenger;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('form_builder'),
$container->get('messenger')
);
}
/**
* {@inheritdoc}
*/
public function build() {
return $this->formBuilder->getForm($this);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'ajax_forms_test_block';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['test1'] = [
'#type' => 'select',
'#title' => $this->t('Test 1'),
'#required' => TRUE,
'#options' => [
'option1' => $this->t('Option 1'),
'option2' => $this->t('Option 2'),
],
'#ajax' => [
'callback' => '::updateOptions',
'wrapper' => 'edit-test1-wrapper',
],
'#prefix' => '<div id="edit-test1-wrapper">',
'#suffix' => '</div>',
];
$form['actions'] = [
'#type' => 'actions',
];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Submit'),
];
return $form;
}
/**
* Updates the options of a select list.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return array
* The updated form element.
*/
public function updateOptions(array $form, FormStateInterface $form_state) {
$form['test1']['#options']['option1'] = $this->t('Option 1!!!');
$form['test1']['#options'] += [
'option3' => $this->t('Option 3'),
'option4' => $this->t('Option 4'),
];
return $form['test1'];
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->messenger->addStatus('Submission successful.');
}
}

View File

@@ -0,0 +1,12 @@
name: 'AJAX Test'
type: module
description: 'Support module for AJAX framework tests.'
package: Testing
# version: VERSION
dependencies:
- drupal:contact
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,50 @@
ajax_insert:
js:
js/insert-ajax.js: {}
dependencies:
- core/drupal.ajax
order:
drupalSettings:
ajax: test
dependencies:
- ajax_test/order-css-command
- ajax_test/order-footer-js-command
- ajax_test/order-header-js-command
order-css-command:
css:
theme:
# Two CSS files (order should remain the same).
a.css: {}
b.css: {}
order-footer-js-command:
js:
footer.js: {}
order-header-js-command:
header: true
js:
header.js: {}
focus.first:
js:
js/focus-ajax.js: {}
dependencies:
- core/drupal
- core/once
command_promise:
version: VERSION
js:
js/command_promise-ajax.js: {}
dependencies:
- core/jquery
- core/drupal
- core/drupal.ajax
global_events:
js:
js/global_events.js: {}
dependencies:
- core/drupal.ajax

View File

@@ -0,0 +1,147 @@
ajax_test.dialog_contents:
path: '/ajax-test/dialog-contents'
defaults:
_title: 'AJAX Dialog contents routing'
_controller: '\Drupal\ajax_test\Controller\AjaxTestController::dialogContents'
requirements:
_access: 'TRUE'
ajax_test.ajax_render_types:
path: '/ajax-test/dialog-contents-types/{type}'
defaults:
_title: 'AJAX Dialog contents routing'
_controller: '\Drupal\ajax_test\Controller\AjaxTestController::renderTypes'
requirements:
_access: 'TRUE'
ajax_test.dialog_form:
path: '/ajax-test/dialog-form'
defaults:
_title: 'Ajax Form contents'
_form: '\Drupal\ajax_test\Form\AjaxTestForm'
requirements:
_access: 'TRUE'
ajax_test.dialog:
path: '/ajax-test/dialog'
defaults:
_controller: '\Drupal\ajax_test\Controller\AjaxTestController::dialog'
requirements:
_access: 'TRUE'
ajax_test.insert_links_block_wrapper:
path: '/ajax-test/insert-block-wrapper'
defaults:
_controller: '\Drupal\ajax_test\Controller\AjaxTestController::insertLinksBlockWrapper'
requirements:
_access: 'TRUE'
ajax_test.insert_links_inline_wrapper:
path: '/ajax-test/insert-inline-wrapper'
defaults:
_controller: '\Drupal\ajax_test\Controller\AjaxTestController::insertLinksInlineWrapper'
requirements:
_access: 'TRUE'
ajax_test.dialog_close:
path: '/ajax-test/dialog-close'
defaults:
_controller: '\Drupal\ajax_test\Controller\AjaxTestController::dialogClose'
requirements:
_access: 'TRUE'
ajax_test.render:
path: '/ajax-test/render'
defaults:
_controller: '\Drupal\ajax_test\Controller\AjaxTestController::render'
requirements:
_access: 'TRUE'
ajax_test.admin.theme:
path: '/admin/ajax-test/theme'
defaults:
_controller: '\Drupal\ajax_test\Controller\AjaxTestController::theme'
requirements:
_access: 'TRUE'
ajax_test.order:
path: '/ajax-test/order'
defaults:
_controller: '\Drupal\ajax_test\Controller\AjaxTestController::order'
requirements:
_access: 'TRUE'
ajax_test.render_error:
path: '/ajax-test/render-error'
defaults:
_controller: '\Drupal\ajax_test\Controller\AjaxTestController::renderError'
requirements:
_access: 'TRUE'
ajax_test.message_form:
path: '/ajax-test/message'
defaults:
_title: 'Ajax Message Form'
_form: '\Drupal\ajax_test\Form\AjaxTestMessageCommandForm'
requirements:
_access: 'TRUE'
ajax_test.focus_first_form:
path: '/ajax-test/focus-first'
defaults:
_title: 'Ajax Focus First Form'
_form: '\Drupal\ajax_test\Form\AjaxTestFocusFirstForm'
requirements:
_access: 'TRUE'
ajax_test.promise:
path: '/ajax-test/promise-form'
defaults:
_title: 'Ajax Form Command Promise'
_form: '\Drupal\ajax_test\Form\AjaxTestFormPromise'
requirements:
_access: 'TRUE'
ajax_test.global_events:
path: '/ajax-test/global-events'
defaults:
_controller: '\Drupal\ajax_test\Controller\AjaxTestController::globalEvents'
requirements:
_access: 'TRUE'
ajax_test.global_events_clear_log:
path: '/ajax-test/global-events/clear-log'
defaults:
_controller: '\Drupal\ajax_test\Controller\AjaxTestController::globalEventsClearLog'
requirements:
_access: 'TRUE'
ajax_test.exception_link:
path: '/ajax-test/exception-link'
defaults:
_controller: '\Drupal\ajax_test\Controller\AjaxTestController::exceptionLink'
requirements:
_access: 'TRUE'
ajax_test.throw_exception:
path: '/ajax-test/throw-exception'
defaults:
_controller: '\Drupal\ajax_test\Controller\AjaxTestController::throwException'
requirements:
_access: 'TRUE'
ajax_test.http_methods:
path: '/ajax-test/http-methods'
defaults:
_controller: Drupal\ajax_test\Controller\AjaxTestController::httpMethods
requirements:
_access: 'TRUE'
options:
no_cache: TRUE
ajax_test.http_methods.dialog:
path: '/ajax-test/http-methods-dialog'
defaults:
_controller: Drupal\ajax_test\Controller\AjaxTestController::httpMethodsDialog
requirements:
_access: 'TRUE'

View File

@@ -0,0 +1,31 @@
/**
* @file
* Testing behavior for the add_js command.
*/
(($, Drupal) => {
/**
* Test Ajax execution Order.
*
* @param {Drupal.Ajax} [ajax]
* {@link Drupal.Ajax} object created by {@link Drupal.Ajax}.
* @param {object} response
* The response from the Ajax request.
* @param {string} response.selector
* A jQuery selector string.
*
* @return {Promise}
* The promise that will resolve once this command has finished executing.
*/
Drupal.AjaxCommands.prototype.ajaxCommandReturnPromise = function (
ajax,
response,
) {
return new Promise((resolve, reject) => {
setTimeout(() => {
this.insert(ajax, response);
resolve();
}, Math.random() * 500);
});
};
})(jQuery, Drupal);

View File

@@ -0,0 +1,23 @@
/**
* @file
* For testing FocusFirstCommand.
*/
((Drupal) => {
Drupal.behaviors.focusFirstTest = {
attach() {
// Add data-has-focus attribute to focused elements so tests have a
// selector to wait for before moving to the next test step.
once('focusin', document.body).forEach((element) => {
element.addEventListener('focusin', (e) => {
document
.querySelectorAll('[data-has-focus]')
.forEach((wasFocused) => {
wasFocused.removeAttribute('data-has-focus');
});
e.target.setAttribute('data-has-focus', true);
});
});
},
};
})(Drupal, once);

View File

@@ -0,0 +1,14 @@
/**
* @file
* For testing that jQuery's ajaxSuccess, ajaxComplete, and ajaxStop events
* are triggered only after commands in a Drupal Ajax response are executed.
*/
(($, Drupal) => {
['ajaxSuccess', 'ajaxComplete', 'ajaxStop'].forEach((eventName) => {
$(document)[eventName](() => {
$('#test_global_events_log').append(eventName);
$('#test_global_events_log2').append(eventName);
});
});
})(jQuery, Drupal);

View File

@@ -0,0 +1,41 @@
/**
* @file
* Drupal behavior to attach click event handlers to ajax-insert and
* ajax-insert-inline links for testing ajax requests.
*/
(function ($, window, Drupal) {
Drupal.behaviors.insertTest = {
attach(context) {
$(once('ajax-insert', '.ajax-insert')).on('click', (event) => {
event.preventDefault();
const ajaxSettings = {
url: event.currentTarget.getAttribute('href'),
wrapper: 'ajax-target',
base: false,
element: false,
method: event.currentTarget.getAttribute('data-method'),
effect: event.currentTarget.getAttribute('data-effect'),
};
const myAjaxObject = Drupal.ajax(ajaxSettings);
myAjaxObject.execute();
});
$(once('ajax-insert', '.ajax-insert-inline')).on('click', (event) => {
event.preventDefault();
const ajaxSettings = {
url: event.currentTarget.getAttribute('href'),
wrapper: 'ajax-target-inline',
base: false,
element: false,
method: event.currentTarget.getAttribute('data-method'),
effect: event.currentTarget.getAttribute('data-effect'),
};
const myAjaxObject = Drupal.ajax(ajaxSettings);
myAjaxObject.execute();
});
$(context).addClass('processed');
},
};
})(jQuery, window, Drupal);

View File

@@ -0,0 +1,26 @@
<?php
namespace Drupal\ajax_test\Ajax;
use Drupal\Core\Ajax\AppendCommand;
/**
* Test Ajax command.
*/
class AjaxTestCommandReturnPromise extends AppendCommand {
/**
* Implements Drupal\Core\Ajax\CommandInterface:render().
*/
public function render() {
return [
'command' => 'ajaxCommandReturnPromise',
'method' => 'append',
'selector' => $this->selector,
'data' => $this->getRenderedContent(),
'settings' => $this->settings,
];
}
}

View File

@@ -0,0 +1,460 @@
<?php
namespace Drupal\ajax_test\Controller;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\AlertCommand;
use Drupal\Core\Ajax\CloseDialogCommand;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Url;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides content for dialog tests.
*/
class AjaxTestController {
/**
* Example content for dialog testing.
*
* @return array
* Renderable array of AJAX dialog contents.
*/
public static function dialogContents() {
// This is a regular render array; the keys do not have special meaning.
$content = [
'#title' => '<em>AJAX Dialog & contents</em>',
'content' => [
'#markup' => 'Example message',
],
'cancel' => [
'#type' => 'link',
'#title' => 'Cancel',
'#url' => Url::fromRoute('<front>'),
'#attributes' => [
// This is a special class to which JavaScript assigns dialog closing
// behavior.
'class' => ['dialog-cancel'],
],
],
];
return $content;
}
/**
* Example content for testing the wrapper of the response.
*
* @param string $type
* Type of response.
*
* @return array
* Renderable array of AJAX response contents.
*/
public function renderTypes($type) {
return [
'#title' => '<em>AJAX Dialog & contents</em>',
'content' => [
'#type' => 'inline_template',
'#template' => $this->getRenderTypes()[$type]['render'],
],
];
}
/**
* Returns a render array of links that directly Drupal.ajax().
*
* @return array
* Renderable array of AJAX response contents.
*/
public function insertLinksBlockWrapper() {
$methods = [
'html',
'replaceWith',
];
$build['links'] = [
'ajax_target' => [
'#markup' => '<div class="ajax-target-wrapper"><div id="ajax-target">Target</div></div>',
],
'links' => [
'#theme' => 'links',
'#attached' => ['library' => ['ajax_test/ajax_insert']],
],
];
foreach ($methods as $method) {
foreach ($this->getRenderTypes() as $type => $item) {
$class = 'ajax-insert';
$build['links']['links']['#links']["$method-$type"] = [
'title' => "Link $method $type",
'url' => Url::fromRoute('ajax_test.ajax_render_types', ['type' => $type]),
'attributes' => [
'class' => [$class],
'data-method' => $method,
'data-effect' => $item['effect'],
],
];
}
}
return $build;
}
/**
* Returns a render array of links that directly Drupal.ajax().
*
* @return array
* Renderable array of AJAX response contents.
*/
public function insertLinksInlineWrapper() {
$methods = [
'html',
'replaceWith',
];
$build['links'] = [
'ajax_target' => [
'#markup' => '<div class="ajax-target-wrapper"><span id="ajax-target-inline">Target inline</span></div>',
],
'links' => [
'#theme' => 'links',
'#attached' => ['library' => ['ajax_test/ajax_insert']],
],
];
foreach ($methods as $method) {
foreach ($this->getRenderTypes() as $type => $item) {
$class = 'ajax-insert-inline';
$build['links']['links']['#links']["$method-$type"] = [
'title' => "Link $method $type",
'url' => Url::fromRoute('ajax_test.ajax_render_types', ['type' => $type]),
'attributes' => [
'class' => [$class],
'data-method' => $method,
'data-effect' => $item['effect'],
],
];
}
}
return $build;
}
/**
* Returns a render array that will be rendered by AjaxRenderer.
*
* Verifies that the response incorporates JavaScript settings generated
* during the page request by adding a dummy setting.
*/
public function render() {
return [
'#attached' => [
'library' => [
'core/drupalSettings',
],
'drupalSettings' => [
'ajax' => 'test',
],
],
];
}
/**
* Returns the used theme.
*/
public function theme() {
return [
'#markup' => 'Current theme: ' . \Drupal::theme()->getActiveTheme()->getName(),
];
}
/**
* Returns an AjaxResponse; settings command set last.
*
* Helps verifying AjaxResponse reorders commands to ensure correct execution.
*/
public function order() {
$response = new AjaxResponse();
// HTML insertion command.
$response->addCommand(new HtmlCommand('body', 'Hello, world!'));
$build['#attached']['library'][] = 'ajax_test/order';
$response->setAttachments($build['#attached']);
return $response;
}
/**
* Returns an AjaxResponse with alert command.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request object.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The JSON response object.
*/
public function renderError(Request $request) {
$message = '';
$query = $request->query;
if ($query->has('message')) {
$message = $query->get('message');
}
$response = new AjaxResponse();
$response->addCommand(new AlertCommand($message));
return $response;
}
/**
* Returns a render array of form elements and links for dialog.
*/
public function dialog() {
// Add two wrapper elements for testing non-modal dialogs. Modal dialogs use
// the global drupal-modal wrapper by default.
$build['dialog_wrappers'] = ['#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::formBuilder()->getForm('Drupal\ajax_test\Form\AjaxTestDialogForm');
// Dialog behavior applied to a #type => 'link'.
$build['link'] = [
'#type' => 'link',
'#title' => 'Link 1 (modal)',
'#url' => Url::fromRoute('ajax_test.dialog_contents'),
'#attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
],
];
// Dialog behavior applied to links rendered by links.html.twig.
$build['links'] = [
'#theme' => 'links',
'#links' => [
'link2' => [
'title' => 'Link 2 (modal)',
'url' => Url::fromRoute('ajax_test.dialog_contents'),
'attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => json_encode([
'width' => 400,
]),
],
],
'link3' => [
'title' => 'Link 3 (non-modal)',
'url' => Url::fromRoute('ajax_test.dialog_contents'),
'attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'dialog',
'data-dialog-options' => json_encode([
'target' => 'ajax-test-dialog-wrapper-1',
'width' => 800,
]),
],
],
'link4' => [
'title' => 'Link 4 (close non-modal if open)',
'url' => Url::fromRoute('ajax_test.dialog_close'),
'attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
],
],
'link5' => [
'title' => 'Link 5 (form)',
'url' => Url::fromRoute('ajax_test.dialog_form'),
'attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
],
],
'link6' => [
'title' => 'Link 6 (entity form)',
'url' => Url::fromRoute('contact.form_add'),
'attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => json_encode([
'width' => 800,
'height' => 500,
]),
],
],
'link7' => [
'title' => 'Link 7 (non-modal, no target)',
'url' => Url::fromRoute('ajax_test.dialog_contents'),
'attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'dialog',
'data-dialog-options' => json_encode([
'width' => 800,
]),
],
],
'link8' => [
'title' => 'Link 8 (ajax)',
'url' => Url::fromRoute('ajax_test.admin.theme'),
'attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => json_encode([
'width' => 400,
]),
],
],
],
];
return $build;
}
/**
* Returns an AjaxResponse with command to close dialog.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The JSON response object.
*/
public function dialogClose() {
$response = new AjaxResponse();
$response->addCommand(new CloseDialogCommand('#ajax-test-dialog-wrapper-1'));
return $response;
}
/**
* Render types.
*
* @return array
* Render types.
*/
protected function getRenderTypes() {
$render_single_root = [
'pre-wrapped-div' => '<div class="pre-wrapped">pre-wrapped<script> var test;</script></div>',
'pre-wrapped-span' => '<span class="pre-wrapped">pre-wrapped<script> var test;</script></span>',
'pre-wrapped-whitespace' => ' <div class="pre-wrapped-whitespace">pre-wrapped-whitespace</div>' . "\r\n",
'not-wrapped' => 'not-wrapped',
'comment-string-not-wrapped' => '<!-- COMMENT -->comment-string-not-wrapped',
'comment-not-wrapped' => '<!-- COMMENT --><div class="comment-not-wrapped">comment-not-wrapped</div>',
'svg' => '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><rect x="0" y="0" height="10" width="10" fill="green"></rect></svg>',
'empty' => '',
];
$render_multiple_root = [
'mixed' => ' foo <!-- COMMENT --> foo bar<div class="a class"><p>some string</p></div> additional not wrapped strings, <!-- ANOTHER COMMENT --> <p>final string</p>',
'top-level-only' => '<div>element #1</div><div>element #2</div>',
'top-level-only-pre-whitespace' => ' <div>element #1</div><div>element #2</div> ',
'top-level-only-middle-whitespace-span' => '<span>element #1</span> <span>element #2</span>',
'top-level-only-middle-whitespace-div' => '<div>element #1</div> <div>element #2</div>',
];
$render_info = [];
foreach ($render_single_root as $key => $render) {
$render_info[$key] = ['render' => $render, 'effect' => 'fade'];
}
foreach ($render_multiple_root as $key => $render) {
$render_info[$key] = ['render' => $render, 'effect' => 'none'];
$render_info["$key--effect"] = ['render' => $render, 'effect' => 'fade'];
}
return $render_info;
}
/**
* Returns a page from which to test Ajax global events.
*
* @return array
* The render array.
*/
public function globalEvents() {
return [
'#attached' => [
'library' => [
'ajax_test/global_events',
],
],
'#markup' => implode('', [
'<div id="test_global_events_log"></div>',
'<a id="test_global_events_drupal_ajax_link" class="use-ajax" href="' . Url::fromRoute('ajax_test.global_events_clear_log')->toString() . '">Drupal Ajax</a>',
'<div id="test_global_events_log2"></div>',
]),
];
}
/**
* Returns an AjaxResponse with command to clear the 'test_global_events_log'.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The JSON response object.
*/
public function globalEventsClearLog() {
$response = new AjaxResponse();
$response->addCommand(new HtmlCommand('#test_global_events_log', ''));
return $response;
}
/**
* Callback to provide an exception via Ajax.
*
* @throws \Exception
* The expected exception.
*/
public function throwException() {
throw new \Exception('This is an exception.');
}
/**
* Provides an Ajax link for the exception.
*
* @return array
* The Ajax link.
*/
public function exceptionLink() {
return [
'#type' => 'link',
'#url' => Url::fromRoute('ajax_test.throw_exception'),
'#title' => 'Ajax Exception',
'#attributes' => [
'class' => ['use-ajax'],
],
'#attached' => [
'library' => [
'core/drupal.ajax',
],
],
];
}
/**
* Provides an Ajax link used with different HTTP methods.
*
* @return array
* The AJAX link.
*/
public function httpMethods(): array {
return [
'#type' => 'link',
'#title' => 'Link',
'#url' => Url::fromRoute('ajax_test.http_methods.dialog'),
'#attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode(['width' => 800]),
// Use this state var to change the HTTP method in tests.
// @see \Drupal\FunctionalJavascriptTests\Ajax\DialogTest::testHttpMethod()
'data-ajax-http-method' => \Drupal::state()->get('ajax_test.http_method', 'POST'),
],
'#attached' => [
'library' => [
'core/drupal.dialog.ajax',
],
],
];
}
/**
* Provides a modal dialog to test links with different HTTP methods.
*
* @return array
* The render array.
*/
public function httpMethodsDialog(): array {
return ['#markup' => 'Modal dialog contents'];
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace Drupal\ajax_test\Form;
use Drupal\ajax_test\Controller\AjaxTestController;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\OpenDialogCommand;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Ajax\OpenModalDialogWithUrl;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Dummy form for testing DialogRenderer with _form routes.
*
* @internal
*/
class AjaxTestDialogForm extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'ajax_test_dialog_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['button1'] = [
'#type' => 'submit',
'#name' => 'button1',
'#value' => 'Button 1 (modal)',
'#ajax' => [
'callback' => '::modal',
],
];
$form['button2'] = [
'#type' => 'submit',
'#name' => 'button2',
'#value' => 'Button 2 (non-modal)',
'#ajax' => [
'callback' => '::nonModal',
],
];
$form['button3'] = [
'#type' => 'submit',
'#name' => 'button3',
'#value' => 'Button 3 (modal from url)',
'#ajax' => [
'callback' => '::modalFromUrl',
],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$form_state->setRedirect('ajax_test.dialog_contents');
}
/**
* AJAX callback handler for AjaxTestDialogForm.
*/
public function modal(&$form, FormStateInterface $form_state) {
return $this->dialog(TRUE);
}
/**
* AJAX callback handler for Url modal, AjaxTestDialogForm.
*/
public function modalFromUrl(&$form, FormStateInterface $form_state) {
return $this->dialog(TRUE, TRUE);
}
/**
* AJAX callback handler for AjaxTestDialogForm.
*/
public function nonModal(&$form, FormStateInterface $form_state) {
return $this->dialog(FALSE);
}
/**
* Util to render dialog in ajax callback.
*
* @param bool $is_modal
* (optional) TRUE if modal, FALSE if plain dialog. Defaults to FALSE.
* @param bool $is_url
* (optional) True if modal is from a URL, Defaults to FALSE.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* An ajax response object.
*/
protected function dialog(bool $is_modal = FALSE, bool $is_url = FALSE) {
$content = AjaxTestController::dialogContents();
$response = new AjaxResponse();
$title = $this->t('AJAX Dialog & contents');
// Attach the library necessary for using the Open(Modal)DialogCommand and
// set the attachments for this Ajax response.
$content['#attached']['library'][] = 'core/drupal.dialog.ajax';
if ($is_modal) {
if ($is_url) {
$response->addCommand(new OpenModalDialogWithUrl(Url::fromRoute('ajax_test.dialog_form')->toString(), []));
}
else {
$response->addCommand(new OpenModalDialogCommand($title, $content));
}
}
else {
$selector = '#ajax-test-dialog-wrapper-1';
$response->addCommand(new OpenDialogCommand($selector, $title, $content));
}
return $response;
}
}

View File

@@ -0,0 +1,228 @@
<?php
namespace Drupal\ajax_test\Form;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\FocusFirstCommand;
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Form for testing AJAX FocusFirstCommand.
*
* @internal
*/
class AjaxTestFocusFirstForm implements FormInterface {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'ajax_test_focus_first_command_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['first_input'] = [
'#type' => 'textfield',
];
$form['second_input'] = [
'#type' => 'textfield',
];
$form['a_container'] = [
'#type' => 'container',
'#attributes' => [
'id' => 'a-container',
],
];
$form['a_container']['first_container_input'] = [
'#type' => 'textfield',
];
$form['a_container']['second_container_input'] = [
'#type' => 'textfield',
];
$form['focusable_container_without_tabbable_children'] = [
'#type' => 'container',
'#attributes' => [
'tabindex' => '-1',
'id' => 'focusable-container-without-tabbable-children',
],
'#markup' => 'No tabbable children here',
];
$form['multiple_of_same_selector_1'] = [
'#type' => 'container',
'#attributes' => [
'id' => 'multiple-of-same-selector-1',
'class' => ['multiple-of-same-selector'],
],
];
$form['multiple_of_same_selector_1']['inside_same_selector_container_1'] = [
'#type' => 'textfield',
];
$form['multiple_of_same_selector_2'] = [
'#type' => 'container',
'#attributes' => [
'id' => 'multiple-of-same-selector-2',
'class' => ['multiple-of-same-selector'],
],
];
$form['multiple_of_same_selector_2']['inside_same_selector_container_2'] = [
'#type' => 'textfield',
];
$form['nothing_tabbable'] = [
'#type' => 'container',
'#attributes' => [
'id' => 'nothing-tabbable',
],
'#markup' => 'nothing tabbable',
];
$form['nothing_tabbable']['nested'] = [
'#type' => 'container',
'#markup' => 'There are divs in here, but nothing tabbable',
];
$form['focus_first_in_container'] = [
'#type' => 'submit',
'#value' => 'Focus the first item in a container',
'#name' => 'focusFirstContainer',
'#ajax' => [
'callback' => '::focusFirstInContainer',
],
];
$form['focus_first_in_form'] = [
'#type' => 'submit',
'#value' => 'Focus the first item in the form',
'#name' => 'focusFirstForm',
'#ajax' => [
'callback' => '::focusFirstInForm',
],
];
$form['uses_selector_with_multiple_matches'] = [
'#type' => 'submit',
'#value' => 'Uses selector with multiple matches',
'#name' => 'SelectorMultipleMatches',
'#ajax' => [
'callback' => '::focusFirstSelectorMultipleMatch',
],
];
$form['focusable_container_no_tabbable_children'] = [
'#type' => 'submit',
'#value' => 'Focusable container, no tabbable children',
'#name' => 'focusableContainerNotTabbableChildren',
'#ajax' => [
'callback' => '::focusableContainerNotTabbableChildren',
],
];
$form['selector_has_nothing_tabbable'] = [
'#type' => 'submit',
'#value' => 'Try to focus container with nothing tabbable',
'#name' => 'SelectorNothingTabbable',
'#ajax' => [
'callback' => '::selectorHasNothingTabbable',
],
];
$form['selector_does_not_exist'] = [
'#type' => 'submit',
'#value' => 'Call FocusFirst on selector that does not exist.',
'#name' => 'SelectorNotExist',
'#ajax' => [
'callback' => '::selectorDoesNotExist',
],
];
$form['#attached']['library'][] = 'ajax_test/focus.first';
return $form;
}
/**
* Callback for testing FocusFirstCommand on a container.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The AJAX response.
*/
public function selectorDoesNotExist() {
$response = new AjaxResponse();
return $response->addCommand(new FocusFirstCommand('#selector-does-not-exist'));
}
/**
* Callback for testing FocusFirstCommand on a container.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The AJAX response.
*/
public function selectorHasNothingTabbable() {
$response = new AjaxResponse();
return $response->addCommand(new FocusFirstCommand('#nothing-tabbable'));
}
/**
* Callback for testing FocusFirstCommand on a container.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The AJAX response.
*/
public function focusableContainerNotTabbableChildren() {
$response = new AjaxResponse();
return $response->addCommand(new FocusFirstCommand('#focusable-container-without-tabbable-children'));
}
/**
* Callback for testing FocusFirstCommand on a container.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The AJAX response.
*/
public function focusFirstSelectorMultipleMatch() {
$response = new AjaxResponse();
return $response->addCommand(new FocusFirstCommand('.multiple-of-same-selector'));
}
/**
* Callback for testing FocusFirstCommand on a container.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The AJAX response.
*/
public function focusFirstInContainer() {
$response = new AjaxResponse();
return $response->addCommand(new FocusFirstCommand('#a-container'));
}
/**
* Callback for testing FocusFirstCommand on a form.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The AJAX response.
*/
public function focusFirstInForm() {
$response = new AjaxResponse();
return $response->addCommand(new FocusFirstCommand('#ajax-test-focus-first-command-form'));
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace Drupal\ajax_test\Form;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\MessageCommand;
use Drupal\Core\Url;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Dummy form for testing DialogRenderer with _form routes.
*
* @internal
*/
class AjaxTestForm extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'ajax_test_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['#action'] = Url::fromRoute('ajax_test.dialog')->toString();
$form['description'] = [
'#markup' => '<p>' . $this->t("Ajax Form contents description.") . '</p>',
];
$form['actions'] = [
'#type' => 'actions',
];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Do it'),
];
$form['actions']['preview'] = [
'#title' => 'Preview',
'#type' => 'link',
'#url' => Url::fromRoute('ajax_test.dialog_form'),
'#attributes' => [
'class' => ['use-ajax', 'button'],
'data-dialog-type' => 'modal',
],
];
$form['actions']['hello_world'] = [
'#type' => 'submit',
'#value' => $this->t('Hello world'),
// No regular submit-handler. This form only works via JavaScript.
'#submit' => [],
'#ajax' => [
'callback' => '::helloWorld',
'event' => 'click',
],
];
return $form;
}
/**
* An AJAX callback that prints "Hello World!" as a message.
*
* @param array $form
* The form array to remove elements from.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* An AJAX response.
*/
public function helloWorld(array &$form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$response->addCommand(new MessageCommand('Hello world!'));
return $response;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {}
}

Some files were not shown because too many files have changed in this diff Show More