Update to Drupal 8.0.0-beta15. For more information, see: https://www.drupal.org/node/2563023

This commit is contained in:
Pantheon Automation 2015-09-04 13:20:09 -07:00 committed by Greg Anderson
parent 2720a9ec4b
commit f3791f1da3
1898 changed files with 54300 additions and 11481 deletions

View file

@ -11,14 +11,12 @@ use Drupal\Component\Utility\SafeMarkup;
* Implements hook_views_form_substitutions().
*/
function action_views_form_substitutions() {
// Views SafeMarkup::checkPlain()s the column label, so we need to match that.
$select_all_placeholder = SafeMarkup::checkPlain('<!--action-bulk-form-select-all-->');
$select_all = array(
'#type' => 'checkbox',
'#default_value' => FALSE,
'#attributes' => array('class' => array('action-table-select-all')),
);
return array(
$select_all_placeholder => drupal_render($select_all),
'<!--action-bulk-form-select-all-->' => drupal_render($select_all),
);
}

View file

@ -80,7 +80,7 @@ class ActionListBuilder extends ConfigEntityListBuilder {
*/
public function buildRow(EntityInterface $entity) {
$row['type'] = $entity->getType();
$row['label'] = $this->getLabel($entity);
$row['label'] = $entity->label();
if ($this->hasConfigurableActions) {
$row += parent::buildRow($entity);
}

View file

@ -13,7 +13,7 @@ use Drupal\migrate_drupal\Tests\d6\MigrateDrupal6TestBase;
/**
* Upgrade variables to action.settings.yml.
*
* @group action
* @group migrate_drupal_6
*/
class MigrateActionConfigsTest extends MigrateDrupal6TestBase {
@ -31,7 +31,6 @@ class MigrateActionConfigsTest extends MigrateDrupal6TestBase {
*/
protected function setUp() {
parent::setUp();
$this->loadDumps(['Variable.php']);
$this->executeMigration('d6_action_settings');
}

View file

@ -24,8 +24,6 @@ class ActionTest extends MigrateSqlSourceTestCase {
protected $migrationConfiguration = array(
// The ID of the entity, can be any string.
'id' => 'test',
// Leave it empty for now.
'idlist' => array(),
'source' => array(
'plugin' => 'd6_action',
),

View file

@ -6,7 +6,6 @@
*/
use Drupal\aggregator\Entity\Feed;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Routing\RouteMatchInterface;
/**
@ -157,16 +156,15 @@ function aggregator_cron() {
}
/**
* Renders the HTML content safely, as allowed.
* Gets the list of allowed tags.
*
* @param string $value
* The content to be filtered.
* @return array
* The list of allowed tags.
*
* @return string
* The filtered content.
* @internal
*/
function aggregator_filter_xss($value) {
return SafeMarkup::xssFilter($value, preg_split('/\s+|<|>/', \Drupal::config('aggregator.settings')->get('items.allowed_html'), -1, PREG_SPLIT_NO_EMPTY));
function _aggregator_allowed_tags() {
return preg_split('/\s+|<|>/', \Drupal::config('aggregator.settings')->get('items.allowed_html'), -1, PREG_SPLIT_NO_EMPTY);
}
/**

View file

@ -5,7 +5,7 @@
* Preprocessors and theme functions of Aggregator module.
*/
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Render\Element;
/**
@ -25,8 +25,8 @@ function template_preprocess_aggregator_item(&$variables) {
$variables['content'][$key] = $variables['elements'][$key];
}
$variables['url'] = check_url($item->getLink());
$variables['title'] = SafeMarkup::checkPlain($item->label());
$variables['url'] = UrlHelper::stripDangerousProtocols($item->getLink());
$variables['title'] = $item->label();
}
/**
@ -46,5 +46,5 @@ function template_preprocess_aggregator_feed(&$variables) {
$variables['content'][$key] = $variables['elements'][$key];
}
$variables['full'] = $variables['elements']['#view_mode'] == 'full';
$variables['title'] = SafeMarkup::checkPlain($feed->label());
$variables['title'] = $feed->label();
}

View file

@ -7,7 +7,7 @@
namespace Drupal\aggregator\Controller;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Datetime\DateFormatter;
use Drupal\aggregator\FeedInterface;
@ -127,8 +127,18 @@ class AggregatorController extends ControllerBase {
$row[] = $this->formatPlural($entity_manager->getStorage('aggregator_item')->getItemCount($feed), '1 item', '@count items');
$last_checked = $feed->getLastCheckedTime();
$refresh_rate = $feed->getRefreshRate();
$row[] = ($last_checked ? $this->t('@time ago', array('@time' => $this->dateFormatter->formatTimeDiffSince($last_checked))) : $this->t('never'));
$row[] = ($last_checked && $refresh_rate ? $this->t('@time left', array('@time' => $this->dateFormatter->formatTimeDiffUntil($last_checked + $refresh_rate))) : $this->t('never'));
$row[] = ($last_checked ? $this->t('@time ago', array('@time' => $this->dateFormatter->formatInterval(REQUEST_TIME - $last_checked))) : $this->t('never'));
if (!$last_checked && $refresh_rate) {
$next_update = $this->t('imminently');
}
elseif ($last_checked && $refresh_rate) {
$next_update = $next = $this->t('%time left', array('%time' => $this->dateFormatter->formatInterval($last_checked + $refresh_rate - REQUEST_TIME)));
}
else {
$next_update = $this->t('never');
}
$row[] = $next_update;
$links['edit'] = [
'title' => $this->t('Edit'),
'url' => Url::fromRoute('entity.aggregator_feed.edit_form', ['aggregator_feed' => $feed->id()]),
@ -183,11 +193,11 @@ class AggregatorController extends ControllerBase {
* @param \Drupal\aggregator\FeedInterface $aggregator_feed
* The aggregator feed.
*
* @return string
* The feed label.
* @return array
* The feed label as a render array.
*/
public function feedTitle(FeedInterface $aggregator_feed) {
return SafeMarkup::xssFilter($aggregator_feed->label());
return ['#markup' => $aggregator_feed->label(), '#allowed_tags' => Xss::getHtmlTagList()];
}
}

View file

@ -79,7 +79,8 @@ class FeedViewBuilder extends EntityViewBuilder {
if ($display->getComponent('description')) {
$build[$id]['description'] = array(
'#markup' => aggregator_filter_xss($entity->getDescription()),
'#markup' => $entity->getDescription(),
'#allowed_tags' => _aggregator_allowed_tags(),
'#prefix' => '<div class="feed-description">',
'#suffix' => '</div>',
);

View file

@ -26,7 +26,8 @@ class ItemViewBuilder extends EntityViewBuilder {
if ($display->getComponent('description')) {
$build[$id]['description'] = array(
'#markup' => aggregator_filter_xss($entity->getDescription()),
'#markup' => $entity->getDescription(),
'#allowed_tags' => _aggregator_allowed_tags(),
'#prefix' => '<div class="item-description">',
'#suffix' => '</div>',
);

View file

@ -36,7 +36,8 @@ class AggregatorXSSFormatter extends FormatterBase {
foreach ($items as $delta => $item) {
$elements[$delta] = [
'#type' => 'markup',
'#markup' => aggregator_filter_xss($item->value),
'#markup' => $item->value,
'#allowed_tags' => _aggregator_allowed_tags(),
];
}
return $elements;

View file

@ -9,7 +9,6 @@ namespace Drupal\aggregator\Plugin\views\argument;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\views\Plugin\views\argument\NumericArgument;
use Drupal\Component\Utility\SafeMarkup;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@ -60,7 +59,7 @@ class Fid extends NumericArgument {
$feeds = $this->entityManager->getStorage('aggregator_feed')->loadMultiple($this->value);
foreach ($feeds as $feed) {
$titles[] = SafeMarkup::checkPlain($feed->label());
$titles[] = $feed->label();
}
return $titles;
}

View file

@ -9,7 +9,6 @@ namespace Drupal\aggregator\Plugin\views\argument;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\views\Plugin\views\argument\NumericArgument;
use Drupal\Component\Utility\SafeMarkup;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@ -60,7 +59,7 @@ class Iid extends NumericArgument {
$items = $this->entityManager->getStorage('aggregator_item')->loadMultiple($this->value);
foreach ($items as $feed) {
$titles[] = SafeMarkup::checkPlain($feed->label());
$titles[] = $feed->label();
}
return $titles;
}

View file

@ -8,6 +8,7 @@
namespace Drupal\aggregator\Tests;
use Drupal\aggregator\Entity\Feed;
use Drupal\Component\Utility\Html;
use Drupal\simpletest\WebTestBase;
use Drupal\aggregator\FeedInterface;
@ -28,7 +29,7 @@ abstract class AggregatorTestBase extends WebTestBase {
*
* @var array
*/
public static $modules = array('node', 'aggregator', 'aggregator_test', 'views');
public static $modules = ['block', 'node', 'aggregator', 'aggregator_test', 'views'];
/**
* {@inheritdoc}
@ -43,6 +44,7 @@ abstract class AggregatorTestBase extends WebTestBase {
$this->adminUser = $this->drupalCreateUser(array('access administration pages', 'administer news feeds', 'access news feeds', 'create article content'));
$this->drupalLogin($this->adminUser);
$this->drupalPlaceBlock('local_tasks_block');
}
/**
@ -243,7 +245,7 @@ abstract class AggregatorTestBase extends WebTestBase {
public function getValidOpml(array $feeds) {
// Properly escape URLs so that XML parsers don't choke on them.
foreach ($feeds as &$feed) {
$feed['url[0][value]'] = htmlspecialchars($feed['url[0][value]']);
$feed['url[0][value]'] = Html::escape($feed['url[0][value]']);
}
/**
* Does not have an XML declaration, must pass the parser.

View file

@ -0,0 +1,67 @@
<?php
/**
* @file
* Contains \Drupal\aggregator\Tests\FeedAdminDisplayTest.
*/
namespace Drupal\aggregator\Tests;
/**
* Tests the display of a feed on the feed aggregator list page.
*
* @group aggregator
*/
class FeedAdminDisplayTest extends AggregatorTestBase {
/**
* Tests the "Next update" and "Last update" fields.
*/
public function testFeedUpdateFields() {
// Create scheduled feed.
$scheduled_feed = $this->createFeed(NULL, array('refresh' => '900'));
$this->drupalGet('admin/config/services/aggregator');
$this->assertResponse(200, 'Aggregator feed overview page exists.');
// The scheduled feed shows that it has not been updated yet and is
// scheduled.
$this->assertText('never', 'The scheduled feed has not been updated yet. Last update shows "never".');
$this->assertText('imminently', 'The scheduled feed has not been updated yet. Next update shows "imminently".');
$this->assertNoText('ago', 'The scheduled feed has not been updated yet. Last update does not show "x x ago".');
$this->assertNoText('left', 'The scheduled feed has not been updated yet. Next update does not show "x x left".');
$this->updateFeedItems($scheduled_feed);
$this->drupalGet('admin/config/services/aggregator');
// After the update, an interval should be displayed on both last updated
// and next update.
$this->assertNoText('never', 'The scheduled feed has been updated. Last updated changed.');
$this->assertNoText('imminently', 'The scheduled feed has been updated. Next update changed.');
$this->assertText('ago', 'The scheduled feed been updated. Last update shows "x x ago".');
$this->assertText('left', 'The scheduled feed has been updated. Next update shows "x x left".');
// Delete scheduled feed.
$this->deleteFeed($scheduled_feed);
// Create non-scheduled feed.
$non_scheduled_feed = $this->createFeed(NULL, array('refresh' => '0'));
$this->drupalGet('admin/config/services/aggregator');
// The non scheduled feed shows that it has not been updated yet.
$this->assertText('never', 'The non scheduled feed has not been updated yet. Last update shows "never".');
$this->assertNoText('imminently', 'The non scheduled feed does not show "imminently" as next update.');
$this->assertNoText('ago', 'The non scheduled feed has not been updated. It does not show "x x ago" as last update.');
$this->assertNoText('left', 'The feed is not scheduled. It does not show a timeframe "x x left" for next update.');
$this->updateFeedItems($non_scheduled_feed);
$this->drupalGet('admin/config/services/aggregator');
// After the feed update, we still need to see "never" as next update label.
// Last update will show an interval.
$this->assertNoText('imminently', 'The updated non scheduled feed does not show "imminently" as next update.');
$this->assertText('never', 'The updated non scheduled feed still shows "never" as next update.');
$this->assertText('ago', 'The non scheduled feed has been updated. It shows "x x ago" as last update.');
$this->assertNoText('left', 'The feed is not scheduled. It does not show a timeframe "x x left" for next update.');
}
}

View file

@ -47,9 +47,10 @@ class FeedProcessorPluginTest extends AggregatorTestBase {
*/
public function testDelete() {
$feed = $this->createFeed();
$description = $feed->description->value ?: '';
$this->updateAndDelete($feed, NULL);
// Make sure the feed title is changed.
$entities = entity_load_multiple_by_properties('aggregator_feed', array('description' => $feed->description->value));
$entities = entity_load_multiple_by_properties('aggregator_feed', array('description' => $description));
$this->assertTrue(empty($entities));
}

View file

@ -13,7 +13,7 @@ use Drupal\migrate_drupal\Tests\d6\MigrateDrupal6TestBase;
/**
* Upgrade variables to aggregator.settings.yml.
*
* @group aggregator
* @group migrate_drupal_6
*/
class MigrateAggregatorConfigsTest extends MigrateDrupal6TestBase {
@ -31,7 +31,6 @@ class MigrateAggregatorConfigsTest extends MigrateDrupal6TestBase {
*/
protected function setUp() {
parent::setUp();
$this->loadDumps(['Variable.php']);
$this->executeMigration('d6_aggregator_settings');
}

View file

@ -13,7 +13,7 @@ use Drupal\migrate_drupal\Tests\d6\MigrateDrupal6TestBase;
/**
* Upgrade variables to aggregator_feed entities.
*
* @group aggregator
* @group migrate_drupal_6
*/
class MigrateAggregatorFeedTest extends MigrateDrupal6TestBase {
@ -25,7 +25,6 @@ class MigrateAggregatorFeedTest extends MigrateDrupal6TestBase {
protected function setUp() {
parent::setUp();
$this->installEntitySchema('aggregator_feed');
$this->loadDumps(['AggregatorFeed.php']);
$this->executeMigration('d6_aggregator_feed');
}

View file

@ -13,7 +13,7 @@ use Drupal\migrate_drupal\Tests\d6\MigrateDrupal6TestBase;
/**
* Upgrade aggregator items.
*
* @group aggregator
* @group migrate_drupal_6
*/
class MigrateAggregatorItemTest extends MigrateDrupal6TestBase {
@ -45,7 +45,6 @@ class MigrateAggregatorItemTest extends MigrateDrupal6TestBase {
));
$entity->enforceIsNew();
$entity->save();
$this->loadDumps(['AggregatorItem.php']);
$this->executeMigration('d6_aggregator_item');
}

View file

@ -24,7 +24,6 @@ class MigrateAggregatorSettingsTest extends MigrateDrupal7TestBase {
protected function setUp() {
parent::setUp();
$this->installConfig(static::$modules);
$this->loadDumps(['Variable.php']);
$this->executeMigration('d7_aggregator_settings');
}

View file

@ -7,18 +7,19 @@
namespace Drupal\aggregator\Tests\Views;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Url;
use Drupal\views\Views;
use Drupal\views\Tests\ViewTestData;
use Drupal\views\Tests\ViewUnitTestBase;
use Drupal\views\Tests\ViewKernelTestBase;
/**
* Tests basic integration of views data from the aggregator module.
*
* @group aggregator
*/
class IntegrationTest extends ViewUnitTestBase {
class IntegrationTest extends ViewKernelTestBase {
/**
* Modules to install.
@ -121,13 +122,13 @@ class IntegrationTest extends ViewUnitTestBase {
});
$this->assertEqual($output, $expected_link, 'Ensure the right link is generated');
$expected_author = aggregator_filter_xss($items[$iid]->getAuthor());
$expected_author = Xss::filter($items[$iid]->getAuthor(), _aggregator_allowed_tags());
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view, $row) {
return $view->field['author']->advancedRender($row);
});
$this->assertEqual($output, $expected_author, 'Ensure the author got filtered');
$expected_description = aggregator_filter_xss($items[$iid]->getDescription());
$expected_description = Xss::filter($items[$iid]->getDescription(), _aggregator_allowed_tags());
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view, $row) {
return $view->field['description']->advancedRender($row);
});

View file

@ -79,7 +79,12 @@ display:
table: aggregator_item
field: timestamp
id: timestamp
plugin_id: date
type: timestamp
settings:
date_format: medium
custom_date_format: ''
timezone: ''
plugin_id: field
entity_type: aggregator_item
entity_field: timestamp
author:

View file

@ -20,7 +20,6 @@ class AggregatorFeedTest extends MigrateSqlSourceTestCase {
protected $migrationConfiguration = array(
'id' => 'test',
'idlist' => array(),
'source' => array(
'plugin' => 'd6_aggregator_feed',
),

View file

@ -22,8 +22,6 @@ class AggregatorItemTest extends MigrateSqlSourceTestCase {
protected $migrationConfiguration = array(
// The ID of the entity, can be any string.
'id' => 'test',
// Leave it empty for now.
'idlist' => array(),
'source' => array(
'plugin' => 'd6_aggregator_item',
),

View file

@ -0,0 +1,10 @@
id: d7_blocked_ips
label: Drupal 7 blocked IPs
migration_tags:
- Drupal 7
source:
plugin: d7_blocked_ips
process:
ip: ip
destination:
plugin: blocked_ip

View file

@ -0,0 +1,88 @@
<?php
/**
* @file
* Contains \Drupal\ban\Plugin\migrate\destination\BlockedIP.
*/
namespace Drupal\ban\Plugin\migrate\destination;
use Drupal\ban\BanIpManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\Plugin\migrate\destination\DestinationBase;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Destination for blocked IP addresses.
*
* @MigrateDestination(
* id = "blocked_ip"
* )
*/
class BlockedIP extends DestinationBase implements ContainerFactoryPluginInterface {
/**
* The IP ban manager.
*
* @var \Drupal\ban\BanIpManagerInterface
*/
protected $banManager;
/**
* Constructs a BlockedIP object.
*
* @param array $configuration
* Plugin configuration.
* @param string $plugin_id
* The plugin ID.
* @param mixed $plugin_definition
* The plugin definiiton.
* @param \Drupal\migrate\Entity\MigrationInterface $migration
* The current migration.
* @param \Drupal\ban\BanIpManagerInterface $ban_manager
* The IP manager service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, BanIpManagerInterface $ban_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);
$this->banManager = $ban_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('ban.ip_manager')
);
}
/**
* {@inheritdoc}
*/
public function getIds() {
return ['ip' => ['type' => 'string']];
}
/**
* {@inheritdoc}
*/
public function fields(MigrationInterface $migration = NULL) {
return [
'ip' => $this->t('The blocked IP address.'),
];
}
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = array()) {
$this->banManager->banIp($row->getDestinationProperty('ip'));
}
}

View file

@ -0,0 +1,45 @@
<?php
/**
* @file
* Contains \Drupal\ban\Plugin\migrate\source\d7\BlockedIps.
*/
namespace Drupal\ban\Plugin\migrate\source\d7;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 7 blocked IPs from database.
*
* @MigrateSource(
* id = "d7_blocked_ips",
* source_provider = "system"
* )
*/
class BlockedIps extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
return $this->select('blocked_ips', 'bi')->fields('bi', ['ip']);
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'ip' => $this->t('The blocked IP address.'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
return ['ip' => ['type' => 'string']];
}
}

View file

@ -0,0 +1,45 @@
<?php
/**
* @file
* Contains \Drupal\ban\Tests\Migrate\d7\MigrateBlockedIPsTest.
*/
namespace Drupal\ban\Tests\Migrate\d7;
use Drupal\config\Tests\SchemaCheckTestTrait;
use Drupal\migrate_drupal\Tests\d7\MigrateDrupal7TestBase;
/**
* Migrate blocked IPs.
*
* @group ban
*/
class MigrateBlockedIPsTest extends MigrateDrupal7TestBase {
use SchemaCheckTestTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['ban'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installSchema('ban', ['ban_ip']);
$this->executeMigration('d7_blocked_ips');
}
/**
* Tests migration of blocked IPs.
*/
public function testBlockedIPs() {
$this->assertTrue(\Drupal::service('ban.ip_manager')->isBanned('111.111.111.111'));
}
}

View file

@ -0,0 +1,48 @@
<?php
/**
* @file
* Contains \Drupal\Tests\ban\Unit\Plugin\migrate\source\d7\BlockedIps.
*/
namespace Drupal\Tests\ban\Unit\Plugin\migrate\source\d7;
use Drupal\Tests\migrate\Unit\MigrateSqlSourceTestCase;
/**
* Tests D7 blocked_ip source plugin.
*
* @coversDefaultClass \Drupal\ban\Plugin\migrate\source\d7\BlockedIps
* @group ban
*/
class BlockedIpsTest extends MigrateSqlSourceTestCase {
const PLUGIN_CLASS = 'Drupal\ban\Plugin\migrate\source\d7\BlockedIps';
protected $migrationConfiguration = [
'id' => 'test',
'source' => [
'plugin' => 'd7_blocked_ips',
],
];
protected $expectedResults = [
[
'ip' => '127.0.0.1',
],
];
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->databaseContents['blocked_ips'] = [
[
'iid' => 1,
'ip' => '127.0.0.1',
]
];
parent::setUp();
}
}

View file

@ -126,6 +126,61 @@ function hook_block_view_BASE_BLOCK_ID_alter(array &$build, \Drupal\Core\Block\B
$build['#title'] = t('New title of the block');
}
/**
* Alter the result of \Drupal\Core\Block\BlockBase::build().
*
* Unlike hook_block_view_alter(), this hook is called very early, before the
* block is being assembled. Therefore, it is early enough to alter the
* cacheability metadata (change #cache), or to explicitly placeholder the block
* (set #create_placeholder).
*
* In addition to hook_block_build_alter(), which is called for all blocks,
* there is hook_block_build_BASE_BLOCK_ID_alter(), which can be used to target
* a specific block or set of similar blocks.
*
* @param array &$build
* A renderable array of data, only containing #cache.
* @param \Drupal\Core\Block\BlockPluginInterface $block
* The block plugin instance.
*
* @see hook_block_build_BASE_BLOCK_ID_alter()
* @see entity_crud
*
* @ingroup block_api
*/
function hook_block_build_alter(array &$build, \Drupal\Core\Block\BlockPluginInterface $block) {
// Add the 'user' cache context to some blocks.
if ($some_condition) {
$build['#contexts'][] = 'user';
}
}
/**
* Provide a block plugin specific block_build alteration.
*
* In this hook name, BASE_BLOCK_ID refers to the block implementation's plugin
* id, regardless of whether the plugin supports derivatives. For example, for
* the \Drupal\system\Plugin\Block\SystemPoweredByBlock block, this would be
* 'system_powered_by_block' as per that class's annotation. And for the
* \Drupal\system\Plugin\Block\SystemMenuBlock block, it would be
* 'system_menu_block' as per that class's annotation, regardless of which menu
* the derived block is for.
*
* @param array $build
* A renderable array of data, only containing #cache.
* @param \Drupal\Core\Block\BlockPluginInterface $block
* The block plugin instance.
*
* @see hook_block_build_alter()
* @see entity_crud
*
* @ingroup block_api
*/
function hook_block_build_BASE_BLOCK_ID_alter(array &$build, \Drupal\Core\Block\BlockPluginInterface $block) {
// Explicitly enable placeholdering of the specific block.
$build['#create_placeholder'] = TRUE;
}
/**
* Control access to a block instance.
*
@ -136,7 +191,7 @@ function hook_block_view_BASE_BLOCK_ID_alter(array &$build, \Drupal\Core\Block\B
* The block instance.
* @param string $operation
* The operation to be performed, e.g., 'view', 'create', 'delete', 'update'.
* @param \Drupal\user\Entity\User $account
* @param \Drupal\Core\Session\AccountInterface $account
* The user object to perform the access check operation on.
* @param string $langcode
* The language code to perform the access check operation on.
@ -151,7 +206,7 @@ function hook_block_view_BASE_BLOCK_ID_alter(array &$build, \Drupal\Core\Block\B
* @see \Drupal\block\BlockAccessControlHandler::checkAccess()
* @ingroup block_api
*/
function hook_block_access(\Drupal\block\Entity\Block $block, $operation, \Drupal\user\Entity\User $account, $langcode) {
function hook_block_access(\Drupal\block\Entity\Block $block, $operation, \Drupal\Core\Session\AccountInterface $account, $langcode) {
// Example code that would prevent displaying the 'Powered by Drupal' block in
// a region different than the footer.
if ($operation == 'view' && $block->getPluginId() == 'system_powered_by_block') {

View file

@ -245,7 +245,7 @@ function block_user_role_delete($role) {
$visibility = $block->getVisibility();
if (isset($visibility['user_role']['roles'][$role->id()])) {
unset($visibility['user_role']['roles'][$role->id()]);
$block->getPlugin()->setVisibilityConfig('user_role', $visibility['user_role']);
$block->setVisibilityConfig('user_role', $visibility['user_role']);
$block->save();
}
}

View file

@ -10,11 +10,18 @@
/**
* Filters the block list by a text input search string.
*
* Text search input: input.block-filter-text
* Target element: input.block-filter-text[data-element]
* Source text: .block-filter-text-source
* The text input will have the selector `input.block-filter-text`.
*
* The target element to do searching in will be in the selector
* `input.block-filter-text[data-element]`
*
* The text source where the text should be found will have the selector
* `.block-filter-text-source`
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the behavior for the block filtering.
*/
Drupal.behaviors.blockFilterByText = {
attach: function (context, settings) {
@ -22,14 +29,22 @@
var $table = $($input.attr('data-element'));
var $filter_rows;
/**
* Filters the block list.
*
* @param {jQuery.Event} e
* The jQuery event for the keyup event that triggered the filter.
*/
function filterBlockList(e) {
var query = $(e.target).val().toLowerCase();
/**
* Shows or hides the block entry based on the query.
*
* @param {number} index The index of the block.
* @param {HTMLElement} label The label of the block.
* @param {number} index
* The index in the loop, as provided by `jQuery.each`
* @param {HTMLElement} label
* The label of the block.
*/
function toggleBlockEntry(index, label) {
var $label = $(label);
@ -60,6 +75,9 @@
* Highlights the block that was just placed into the block listing.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the behavior for the block placement highlighting.
*/
Drupal.behaviors.blockHighlightPlacement = {
attach: function (context, settings) {

View file

@ -11,6 +11,9 @@
* Provide the summary information for the block settings vertical tabs.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the behavior for the block settings summaries.
*/
Drupal.behaviors.blockSettingsSummary = {
attach: function () {
@ -21,6 +24,15 @@
return;
}
/**
* Create a summary for checkboxes in the provided context.
*
* @param {HTMLDocument|HTMLElement} context
* A context where one would find checkboxes to summarize.
*
* @return {string}
* A string with the summary.
*/
function checkboxesSummary(context) {
var vals = [];
var $checkboxes = $(context).find('input[type="checkbox"]:checked + label');
@ -49,12 +61,15 @@
};
/**
* Move a block in the blocks table from one region to another via select list.
* Move a block in the blocks table between regions via select list.
*
* This behavior is dependent on the tableDrag behavior, since it uses the
* objects initialized in that behavior to update the row.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the tableDrag behaviour for blocks in block administration.
*/
Drupal.behaviors.blockDrag = {
attach: function (context, settings) {
@ -71,7 +86,8 @@
checkEmptyRegions(table, this);
};
// Add a handler so when a row is dropped, update fields dropped into new regions.
// Add a handler so when a row is dropped, update fields dropped into
// new regions.
tableDrag.onDrop = function () {
var dragObject = this;
var $rowElement = $(dragObject.rowObject.element);
@ -82,48 +98,82 @@
var regionField = $rowElement.find('select.block-region-select');
// Check whether the newly picked region is available for this block.
if (regionField.find('option[value=' + regionName + ']').length === 0) {
// If not, alert the user and keep the block in its old region setting.
// If not, alert the user and keep the block in its old region
// setting.
window.alert(Drupal.t('The block cannot be placed in this region.'));
// Simulate that there was a selected element change, so the row is put
// back to from where the user tried to drag it.
// Simulate that there was a selected element change, so the row is
// put back to from where the user tried to drag it.
regionField.trigger('change');
}
else if ($rowElement.prev('tr').is('.region-message')) {
// Update region and weight fields if the region has been changed.
if (!regionField.is('.block-region-' + regionName)) {
var weightField = $rowElement.find('select.block-weight');
var oldRegionName = weightField[0].className.replace(/([^ ]+[ ]+)*block-weight-([^ ]+)([ ]+[^ ]+)*/, '$2');
if (!regionField.is('.block-region-' + regionName)) {
regionField.removeClass('block-region-' + oldRegionName).addClass('block-region-' + regionName);
weightField.removeClass('block-weight-' + oldRegionName).addClass('block-weight-' + regionName);
regionField.val(regionName);
}
regionField.removeClass('block-region-' + oldRegionName).addClass('block-region-' + regionName);
weightField.removeClass('block-weight-' + oldRegionName).addClass('block-weight-' + regionName);
regionField.val(regionName);
}
updateBlockWeights(table, regionName);
};
// Add the behavior to each region select list.
$(context).find('select.block-region-select').once('block-region-select').each(function () {
$(this).on('change', function (event) {
$(context).find('select.block-region-select').once('block-region-select')
.on('change', function (event) {
// Make our new row and select field.
var row = $(this).closest('tr');
var select = $(this);
tableDrag.rowObject = new tableDrag.row(row);
// Find the correct region and insert the row as the last in the region.
// Find the correct region and insert the row as the last in the
// region.
table.find('.region-' + select[0].value + '-message').nextUntil('.region-message').eq(-1).before(row);
updateBlockWeights(table, select[0].value);
// Modify empty regions with added or removed fields.
checkEmptyRegions(table, row);
// Remove focus from selectbox.
select.trigger('blur');
});
});
/**
* Update block weights in the given region.
*
* @param {jQuery} $table
* Table with draggable items.
* @param {string} region
* Machine name of region containing blocks to update.
*/
var updateBlockWeights = function ($table, region) {
// Calculate minimum weight.
var weight = -Math.round($table.find('.draggable').length / 2);
// Update the block weights.
$table.find('.region-' + region + '-message').nextUntil('.region-title')
.find('select.block-weight').val(function () {
// Increment the weight before assigning it to prevent using the
// absolute minimum available weight. This way we always have an
// unused upper and lower bound, which makes manually setting the
// weights easier for users who prefer to do it that way.
return ++weight;
});
};
/**
* Checks empty regions and toggles classes based on this.
*
* @param {jQuery} table
* The jQuery object representing the table to inspect.
* @param {jQuery} rowObject
* The jQuery object representing the table row.
*/
var checkEmptyRegions = function (table, rowObject) {
table.find('tr.region-message').each(function () {
var $this = $(this);
// If the dragged row is in this region, but above the message row, swap it down one space.
// If the dragged row is in this region, but above the message row,
// swap it down one space.
if ($this.prev('tr').get(0) === rowObject.element) {
// Prevent a recursion problem when using the keyboard to move rows up.
// Prevent a recursion problem when using the keyboard to move rows
// up.
if ((rowObject.method !== 'keyboard' || rowObject.direction === 'down')) {
rowObject.swap('after', this);
}

View file

@ -89,6 +89,6 @@ destination:
plugin: entity:block
migration_dependencies:
required:
- d6_menu
- menu
- d6_custom_block
- d6_user_role

View file

@ -9,7 +9,6 @@ namespace Drupal\block;
use Drupal\Component\Utility\Html;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
@ -261,7 +260,7 @@ class BlockListBuilder extends ConfigEntityListBuilder implements FormInterface
$form[$entity_id]['#attributes']['class'][] = 'js-block-placed';
}
$form[$entity_id]['info'] = array(
'#markup' => SafeMarkup::checkPlain($info['label']),
'#plain_text' => $info['label'],
'#wrapper_attributes' => array(
'class' => array('block'),
),

View file

@ -7,19 +7,59 @@
namespace Drupal\block;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Block\MainContentBlockPluginInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityViewBuilder;
use Drupal\Core\Entity\EntityViewBuilderInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Render\Element;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a Block view builder.
*/
class BlockViewBuilder extends EntityViewBuilder {
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Constructs a new BlockViewBuilder.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager service.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
*/
public function __construct(EntityTypeInterface $entity_type, EntityManagerInterface $entity_manager, LanguageManagerInterface $language_manager, ModuleHandlerInterface $module_handler) {
parent::__construct($entity_type, $entity_manager, $language_manager);
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity.manager'),
$container->get('language_manager'),
$container->get('module_handler')
);
}
/**
* {@inheritdoc}
*/
@ -40,13 +80,9 @@ class BlockViewBuilder extends EntityViewBuilder {
public function viewMultiple(array $entities = array(), $view_mode = 'full', $langcode = NULL) {
/** @var \Drupal\block\BlockInterface[] $entities */
$build = array();
foreach ($entities as $entity) {
foreach ($entities as $entity) {
$entity_id = $entity->id();
$plugin = $entity->getPlugin();
$plugin_id = $plugin->getPluginId();
$base_id = $plugin->getBaseId();
$derivative_id = $plugin->getDerivativeId();
$configuration = $plugin->getConfiguration();
$cache_tags = Cache::mergeTags($this->getCacheTags(), $entity->getCacheTags());
$cache_tags = Cache::mergeTags($cache_tags, $plugin->getCacheTags());
@ -54,20 +90,6 @@ class BlockViewBuilder extends EntityViewBuilder {
// Create the render array for the block as a whole.
// @see template_preprocess_block().
$build[$entity_id] = array(
'#theme' => 'block',
'#attributes' => array(),
// All blocks get a "Configure block" contextual link.
'#contextual_links' => array(
'block' => array(
'route_parameters' => array('block' => $entity->id()),
),
),
'#weight' => $entity->getWeight(),
'#configuration' => $configuration,
'#plugin_id' => $plugin_id,
'#base_plugin_id' => $base_id,
'#derivative_plugin_id' => $derivative_id,
'#id' => $entity->id(),
'#cache' => [
'keys' => ['entity_view', 'block', $entity->id()],
'contexts' => Cache::mergeContexts(
@ -77,22 +99,94 @@ class BlockViewBuilder extends EntityViewBuilder {
'tags' => $cache_tags,
'max-age' => $plugin->getCacheMaxAge(),
],
'#pre_render' => [
[$this, 'buildBlock'],
],
// Add the entity so that it can be used in the #pre_render method.
'#block' => $entity,
);
$build[$entity_id]['#configuration']['label'] = SafeMarkup::checkPlain($configuration['label']);
// Don't run in ::buildBlock() to ensure cache keys can be altered. If an
// alter hook wants to modify the block contents, it can append another
// #pre_render hook.
$this->moduleHandler()->alter(array('block_view', "block_view_$base_id"), $build[$entity_id], $plugin);
// Allow altering of cacheability metadata or setting #create_placeholder.
$this->moduleHandler->alter(['block_build', "block_build_" . $plugin->getBaseId()], $build[$entity_id], $plugin);
if ($plugin instanceof MainContentBlockPluginInterface) {
// Immediately build a #pre_render-able block, since this block cannot
// be built lazily.
$build[$entity_id] += static::buildPreRenderableBlock($entity, $this->moduleHandler());
}
else {
// Assign a #lazy_builder callback, which will generate a #pre_render-
// able block lazily (when necessary).
$build[$entity_id] += [
'#lazy_builder' => [static::class . '::lazyBuilder', [$entity_id, $view_mode, $langcode]],
];
}
}
return $build;
}
/**
* Builds a #pre_render-able block render array.
*
* @param \Drupal\block\BlockInterface $entity
* A block config entity.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
*
* @return array
* A render array with a #pre_render callback to render the block.
*/
protected static function buildPreRenderableBlock($entity, ModuleHandlerInterface $module_handler) {
$plugin = $entity->getPlugin();
$plugin_id = $plugin->getPluginId();
$base_id = $plugin->getBaseId();
$derivative_id = $plugin->getDerivativeId();
$configuration = $plugin->getConfiguration();
// Create the render array for the block as a whole.
// @see template_preprocess_block().
$build = [
'#theme' => 'block',
'#attributes' => [],
// All blocks get a "Configure block" contextual link.
'#contextual_links' => [
'block' => [
'route_parameters' => ['block' => $entity->id()],
],
],
'#weight' => $entity->getWeight(),
'#configuration' => $configuration,
'#plugin_id' => $plugin_id,
'#base_plugin_id' => $base_id,
'#derivative_plugin_id' => $derivative_id,
'#id' => $entity->id(),
'#pre_render' => [
static::class . '::preRender',
],
// Add the entity so that it can be used in the #pre_render method.
'#block' => $entity,
];
// If an alter hook wants to modify the block contents, it can append
// another #pre_render hook.
$module_handler->alter(['block_view', "block_view_$base_id"], $build, $plugin);
return $build;
}
/**
* #lazy_builder callback; builds a #pre_render-able block.
*
* @param $entity_id
* A block config entity ID.
* @param $view_mode
* The view mode the block is being viewed in.
* @param $langcode
* The langcode the block is being viewed in.
*
* @return array
* A render array with a #pre_render callback to render the block.
*/
public static function lazyBuilder($entity_id, $view_mode, $langcode) {
return static::buildPreRenderableBlock(entity_load('block', $entity_id), \Drupal::service('module_handler'));
}
/**
* #pre_render callback for building a block.
*
@ -102,7 +196,7 @@ class BlockViewBuilder extends EntityViewBuilder {
* - if there is content, moves the contextual links from the block content to
* the block itself.
*/
public function buildBlock($build) {
public static function preRender($build) {
$content = $build['#block']->getPlugin()->build();
// Remove the block entity from the render array, to ensure that blocks
// can be rendered without the block config entity.

View file

@ -8,7 +8,6 @@
namespace Drupal\block\Controller;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Block\BlockManagerInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
@ -109,7 +108,7 @@ class BlockLibraryController extends ControllerBase {
'#prefix' => '<div class="block-filter-text-source">',
'#suffix' => '</div>',
];
$row['category']['data'] = SafeMarkup::checkPlain($plugin_definition['category']);
$row['category']['data'] = $plugin_definition['category'];
$links['add'] = [
'title' => $this->t('Place block'),
'url' => Url::fromRoute('block.admin_add', ['plugin_id' => $plugin_id, 'theme' => $theme]),

View file

@ -8,7 +8,6 @@
namespace Drupal\block\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
@ -18,13 +17,6 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
*/
class ThemeLocalTask extends DeriverBase implements ContainerDeriverInterface {
/**
* Stores the theme settings config object.
*
* @var \Drupal\Core\Config\Config
*/
protected $config;
/**
* The theme handler.
*
@ -35,13 +27,10 @@ class ThemeLocalTask extends DeriverBase implements ContainerDeriverInterface {
/**
* Constructs a new ThemeLocalTask.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler.
*/
public function __construct(ConfigFactoryInterface $config_factory, ThemeHandlerInterface $theme_handler) {
$this->config = $config_factory->get('system.theme');
public function __construct(ThemeHandlerInterface $theme_handler) {
$this->themeHandler = $theme_handler;
}
@ -50,7 +39,6 @@ class ThemeLocalTask extends DeriverBase implements ContainerDeriverInterface {
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('config.factory'),
$container->get('theme_handler')
);
}
@ -59,7 +47,7 @@ class ThemeLocalTask extends DeriverBase implements ContainerDeriverInterface {
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$default_theme = $this->config->get('default');
$default_theme = $this->themeHandler->getDefault();
foreach ($this->themeHandler->listInfo() as $theme_name => $theme) {
if ($theme->status) {

View file

@ -24,7 +24,7 @@ class BlockAdminThemeTest extends WebTestBase {
public static $modules = array('block');
/**
* Check for the accessibility of the admin theme on the block admin page.
* Check for the accessibility of the admin theme on the block admin page.
*/
function testAdminTheme() {
// Create administrative user.

View file

@ -0,0 +1,76 @@
<?php
/**
* @file
* Contains \Drupal\block\Tests\BlockFormInBlockTest.
*/
namespace Drupal\block\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests form in block caching.
*
* @group block
*/
class BlockFormInBlockTest extends WebTestBase {
/**
* Modules to install.
*
* @var array
*/
public static $modules = ['block', 'block_test', 'test_page_test'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Enable our test block.
$this->drupalPlaceBlock('test_form_in_block');
}
/**
* Test to see if form in block's redirect isn't cached.
*/
function testCachePerPage() {
$form_values = ['email' => 'test@example.com'];
// Go to "test-page" and test if the block is enabled.
$this->drupalGet('test-page');
$this->assertResponse(200);
$this->assertText('Your .com email address.', 'form found');
// Make sure that we're currently still on /test-page after submitting the
// form.
$this->drupalPostForm(NULL, $form_values, t('Submit'));
$this->assertUrl('test-page');
$this->assertText(t('Your email address is @email', ['@email' => 'test@example.com']));
// Go to a different page and see if the block is enabled there as well.
$this->drupalGet('test-render-title');
$this->assertResponse(200);
$this->assertText('Your .com email address.', 'form found');
// Make sure that submitting the form didn't redirect us to the first page
// we submitted the form from after submitting the form from
// /test-render-title.
$this->drupalPostForm(NULL, $form_values, t('Submit'));
$this->assertUrl('test-render-title');
$this->assertText(t('Your email address is @email', ['@email' => 'test@example.com']));
}
/**
* Test the actual placeholders
*/
public function testPlaceholders() {
$this->drupalGet('test-multiple-forms');
$placeholder = 'form_action_' . hash('crc32b', 'Drupal\Core\Form\FormBuilder::prepareForm');
$this->assertText('Form action: ' . $placeholder, 'placeholder found.');
}
}

View file

@ -42,6 +42,7 @@ class BlockHiddenRegionTest extends WebTestBase {
$this->drupalLogin($this->adminUser);
$this->drupalPlaceBlock('search_form_block');
$this->drupalPlaceBlock('local_tasks_block');
}
/**

View file

@ -8,7 +8,6 @@
namespace Drupal\block\Tests;
use Drupal\Core\Cache\Cache;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Form\FormState;
use Drupal\simpletest\KernelTestBase;
use Drupal\block\BlockInterface;
@ -73,7 +72,7 @@ class BlockInterfaceTest extends KernelTestBase {
'admin_label' => array(
'#type' => 'item',
'#title' => t('Block description'),
'#markup' => SafeMarkup::checkPlain($definition['admin_label']),
'#plain_text' => $definition['admin_label'],
),
'label' => array(
'#type' => 'textfield',

View file

@ -8,8 +8,8 @@
namespace Drupal\block\Tests;
use Drupal\Component\Utility\Html;
use Drupal\simpletest\WebTestBase;
use Drupal\block\Entity\Block;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
/**
@ -222,6 +222,7 @@ class BlockTest extends BlockTestBase {
function testThemeName() {
// Enable the help block.
$this->drupalPlaceBlock('help_block', array('region' => 'help'));
$this->drupalPlaceBlock('local_tasks_block');
// Explicitly set the default and admin themes.
$theme = 'block_test_specialchars_theme';
\Drupal::service('theme_handler')->install(array($theme));
@ -409,6 +410,22 @@ class BlockTest extends BlockTestBase {
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
}
/**
* Tests that a link exists to block layout from the appearance form.
*/
public function testThemeAdminLink() {
$this->drupalPlaceBlock('help_block', ['region' => 'help']);
$theme_admin = $this->drupalCreateUser([
'administer blocks',
'administer themes',
'access administration pages',
]);
$this->drupalLogin($theme_admin);
$this->drupalGet('admin/appearance');
$this->assertText('You can place blocks for each theme on the block layout page');
$this->assertLinkByHref('admin/structure/block');
}
/**
* Tests that uninstalling a theme removes its block configuration.
*/
@ -443,4 +460,41 @@ class BlockTest extends BlockTestBase {
$this->assertText('Hello test world');
}
/**
* Tests block_user_role_delete.
*/
public function testBlockUserRoleDelete() {
$role1 = Role::create(['id' => 'test_role1', 'name' => $this->randomString()]);
$role1->save();
$role2 = Role::create(['id' => 'test_role2', 'name' => $this->randomString()]);
$role2->save();
$block = Block::create([
'id' => $this->randomMachineName(),
'plugin' => 'system_powered_by_block',
]);
$block->setVisibilityConfig('user_role', [
'roles' => [
$role1->id() => $role1->id(),
$role2->id() => $role2->id(),
],
]);
$block->save();
$this->assertEqual($block->getVisibility()['user_role']['roles'], [
$role1->id() => $role1->id(),
$role2->id() => $role2->id()
]);
$role1->delete();
$block = Block::load($block->id());
$this->assertEqual($block->getVisibility()['user_role']['roles'], [
$role2->id() => $role2->id()
]);
}
}

View file

@ -186,61 +186,27 @@ class BlockViewBuilderTest extends KernelTestBase {
/**
* Tests block view altering.
*
* @see hook_block_view_alter()
* @see hook_block_view_BASE_BLOCK_ID_alter()
*/
public function testBlockViewBuilderAlter() {
public function testBlockViewBuilderViewAlter() {
// Establish baseline.
$build = $this->getBlockRenderArray();
$this->assertIdentical((string) $this->renderer->renderRoot($build), 'Llamas &gt; unicorns!');
$this->setRawContent((string) $this->renderer->renderRoot($build));
$this->assertIdentical(trim((string) $this->cssSelect('div')[0]), 'Llamas > unicorns!');
// Enable the block view alter hook that adds a suffix, for basic testing.
// Enable the block view alter hook that adds a foo=bar attribute.
\Drupal::state()->set('block_test_view_alter_suffix', TRUE);
Cache::invalidateTags($this->block->getCacheTagsToInvalidate());
$build = $this->getBlockRenderArray();
$this->assertTrue(isset($build['#suffix']) && $build['#suffix'] === '<br>Goodbye!', 'A block with content is altered.');
$this->assertIdentical((string) $this->renderer->renderRoot($build), 'Llamas &gt; unicorns!<br>Goodbye!');
$this->setRawContent((string) $this->renderer->renderRoot($build));
$this->assertIdentical(trim((string) $this->cssSelect('[foo=bar]')[0]), 'Llamas > unicorns!');
\Drupal::state()->set('block_test_view_alter_suffix', FALSE);
// Force a request via GET so we can test the render cache.
$request = \Drupal::request();
$request_method = $request->server->get('REQUEST_METHOD');
$request->setMethod('GET');
\Drupal::state()->set('block_test.content', NULL);
Cache::invalidateTags($this->block->getCacheTagsToInvalidate());
$default_keys = array('entity_view', 'block', 'test_block');
$default_tags = array('block_view', 'config:block.block.test_block');
// Advanced: cached block, but an alter hook adds an additional cache key.
$alter_add_key = $this->randomMachineName();
\Drupal::state()->set('block_test_view_alter_cache_key', $alter_add_key);
$cid = 'entity_view:block:test_block:' . $alter_add_key . ':' . implode(':', \Drupal::service('cache_contexts_manager')->convertTokensToKeys(['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions'])->getKeys());
$expected_keys = array_merge($default_keys, array($alter_add_key));
$build = $this->getBlockRenderArray();
$this->assertIdentical($expected_keys, $build['#cache']['keys'], 'An altered cacheable block has the expected cache keys.');
$this->assertIdentical((string) $this->renderer->renderRoot($build), '');
$cache_entry = $this->container->get('cache.render')->get($cid);
$this->assertTrue($cache_entry, 'The block render element has been cached with the expected cache ID.');
$expected_tags = array_merge($default_tags, ['rendered']);
sort($expected_tags);
$this->assertIdentical($cache_entry->tags, $expected_tags, 'The block render element has been cached with the expected cache tags.');
$this->container->get('cache.render')->delete($cid);
// Advanced: cached block, but an alter hook adds an additional cache tag.
$alter_add_tag = $this->randomMachineName();
\Drupal::state()->set('block_test_view_alter_cache_tag', $alter_add_tag);
$expected_tags = Cache::mergeTags($default_tags, array($alter_add_tag));
$build = $this->getBlockRenderArray();
sort($build['#cache']['tags']);
$this->assertIdentical($expected_tags, $build['#cache']['tags'], 'An altered cacheable block has the expected cache tags.');
$this->assertIdentical((string) $this->renderer->renderRoot($build), '');
$cache_entry = $this->container->get('cache.render')->get($cid);
$this->assertTrue($cache_entry, 'The block render element has been cached with the expected cache ID.');
$expected_tags = array_merge($default_tags, [$alter_add_tag, 'rendered']);
sort($expected_tags);
$this->assertIdentical($cache_entry->tags, $expected_tags, 'The block render element has been cached with the expected cache tags.');
$this->container->get('cache.render')->delete($cid);
// Advanced: cached block, but an alter hook adds a #pre_render callback to
// alter the eventual content.
\Drupal::state()->set('block_test_view_alter_append_pre_render_prefix', TRUE);
@ -248,11 +214,114 @@ class BlockViewBuilderTest extends KernelTestBase {
$this->assertFalse(isset($build['#prefix']), 'The appended #pre_render callback has not yet run before rendering.');
$this->assertIdentical((string) $this->renderer->renderRoot($build), 'Hiya!<br>');
$this->assertTrue(isset($build['#prefix']) && $build['#prefix'] === 'Hiya!<br>', 'A cached block without content is altered.');
}
/**
* Tests block build altering.
*
* @see hook_block_build_alter()
* @see hook_block_build_BASE_BLOCK_ID_alter()
*/
public function testBlockViewBuilderBuildAlter() {
// Force a request via GET so we can test the render cache.
$request = \Drupal::request();
$request_method = $request->server->get('REQUEST_METHOD');
$request->setMethod('GET');
$default_keys = ['entity_view', 'block', 'test_block'];
$default_contexts = [];
$default_tags = ['block_view', 'config:block.block.test_block'];
$default_max_age = Cache::PERMANENT;
// hook_block_build_alter() adds an additional cache key.
$alter_add_key = $this->randomMachineName();
\Drupal::state()->set('block_test_block_alter_cache_key', $alter_add_key);
$this->assertBlockRenderedWithExpectedCacheability(array_merge($default_keys, [$alter_add_key]), $default_contexts, $default_tags, $default_max_age);
\Drupal::state()->set('block_test_block_alter_cache_key', NULL);
// hook_block_build_alter() adds an additional cache context.
$alter_add_context = 'url.query_args:' . $this->randomMachineName();
\Drupal::state()->set('block_test_block_alter_cache_context', $alter_add_context);
$this->assertBlockRenderedWithExpectedCacheability($default_keys, Cache::mergeContexts($default_contexts, [$alter_add_context]), $default_tags, $default_max_age);
\Drupal::state()->set('block_test_block_alter_cache_context', NULL);
// hook_block_build_alter() adds an additional cache tag.
$alter_add_tag = $this->randomMachineName();
\Drupal::state()->set('block_test_block_alter_cache_tag', $alter_add_tag);
$this->assertBlockRenderedWithExpectedCacheability($default_keys, $default_contexts, Cache::mergeTags($default_tags, [$alter_add_tag]), $default_max_age);
\Drupal::state()->set('block_test_block_alter_cache_tag', NULL);
// hook_block_build_alter() alters the max-age.
$alter_max_age = 300;
\Drupal::state()->set('block_test_block_alter_cache_max_age', $alter_max_age);
$this->assertBlockRenderedWithExpectedCacheability($default_keys, $default_contexts, $default_tags, $alter_max_age);
\Drupal::state()->set('block_test_block_alter_cache_max_age', NULL);
// hook_block_build_alter() alters cache keys, contexts, tags and max-age.
\Drupal::state()->set('block_test_block_alter_cache_key', $alter_add_key);
\Drupal::state()->set('block_test_block_alter_cache_context', $alter_add_context);
\Drupal::state()->set('block_test_block_alter_cache_tag', $alter_add_tag);
\Drupal::state()->set('block_test_block_alter_cache_max_age', $alter_max_age);
$this->assertBlockRenderedWithExpectedCacheability(array_merge($default_keys, [$alter_add_key]), Cache::mergeContexts($default_contexts, [$alter_add_context]), Cache::mergeTags($default_tags, [$alter_add_tag]), $alter_max_age);
\Drupal::state()->set('block_test_block_alter_cache_key', NULL);
\Drupal::state()->set('block_test_block_alter_cache_context', NULL);
\Drupal::state()->set('block_test_block_alter_cache_tag', NULL);
\Drupal::state()->set('block_test_block_alter_cache_max_age', NULL);
// hook_block_build_alter() sets #create_placeholder.
foreach ([TRUE, FALSE] as $value) {
\Drupal::state()->set('block_test_block_alter_create_placeholder', $value);
$build = $this->getBlockRenderArray();
$this->assertTrue(isset($build['#create_placeholder']));
$this->assertIdentical($value, $build['#create_placeholder']);
}
\Drupal::state()->set('block_test_block_alter_create_placeholder', NULL);
// Restore the previous request method.
$request->setMethod($request_method);
}
/**
* Asserts that a block is built/rendered/cached with expected cacheability.
*
* @param string[] $expected_keys
* The expected cache keys.
* @param string[] $expected_contexts
* The expected cache contexts.
* @param string[] $expected_tags
* The expected cache tags.
* @param int $expected_max_age
* The expected max-age.
*/
protected function assertBlockRenderedWithExpectedCacheability(array $expected_keys, array $expected_contexts, array $expected_tags, $expected_max_age) {
$required_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions'];
// Check that the expected cacheability metadata is present in:
// - the built render array;
$this->pass('Built render array');
$build = $this->getBlockRenderArray();
$this->assertIdentical($expected_keys, $build['#cache']['keys']);
$this->assertIdentical($expected_contexts, $build['#cache']['contexts']);
$this->assertIdentical($expected_tags, $build['#cache']['tags']);
$this->assertIdentical($expected_max_age, $build['#cache']['max-age']);
$this->assertFalse(isset($build['#create_placeholder']));
// - the rendered render array;
$this->pass('Rendered render array');
$this->renderer->renderRoot($build);
// - the render cache item.
$this->pass('Render cache item');
$final_cache_contexts = Cache::mergeContexts($expected_contexts, $required_cache_contexts);
$cid = implode(':', $expected_keys) . ':' . implode(':', \Drupal::service('cache_contexts_manager')->convertTokensToKeys($final_cache_contexts)->getKeys());
$cache_item = $this->container->get('cache.render')->get($cid);
$this->assertTrue($cache_item, 'The block render element has been cached with the expected cache ID.');
$this->assertIdentical(Cache::mergeTags($expected_tags, ['rendered']), $cache_item->tags);
$this->assertIdentical($final_cache_contexts, $cache_item->data['#cache']['contexts']);
$this->assertIdentical($expected_tags, $cache_item->data['#cache']['tags']);
$this->assertIdentical($expected_max_age, $cache_item->data['#cache']['max-age']);
$this->container->get('cache.render')->delete($cid);
}
/**
* Get a fully built render array for a block.
*
@ -260,12 +329,7 @@ class BlockViewBuilderTest extends KernelTestBase {
* The render array.
*/
protected function getBlockRenderArray() {
$build = $this->container->get('entity.manager')->getViewBuilder('block')->view($this->block, 'block');
// Mock the build array to not require the theme registry.
unset($build['#theme']);
return $build;
return $this->container->get('entity.manager')->getViewBuilder('block')->view($this->block, 'block');
}
}

View file

@ -13,7 +13,7 @@ use Drupal\migrate_drupal\Tests\d6\MigrateDrupal6TestBase;
/**
* Upgrade block settings to block.block.*.yml.
*
* @group block
* @group migrate_drupal_6
*/
class MigrateBlockTest extends MigrateDrupal6TestBase {
@ -53,7 +53,7 @@ class MigrateBlockTest extends MigrateDrupal6TestBase {
array(array(1), array(1)),
array(array(2), array(2)),
),
'd6_menu' => array(
'menu' => array(
array(array('menu1'), array('menu')),
),
'd6_user_role' => array(
@ -71,7 +71,6 @@ class MigrateBlockTest extends MigrateDrupal6TestBase {
// Install one of D8's test themes.
\Drupal::service('theme_handler')->install(array('test_theme'));
$this->loadDumps(['Blocks.php', 'BlocksRoles.php', 'AggregatorFeed.php']);
$this->executeMigration('d6_block');
}

View file

@ -23,6 +23,15 @@ class NonDefaultBlockAdminTest extends WebTestBase {
*/
public static $modules = array('block');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalPlaceBlock('local_tasks_block');
}
/**
* Test non-default theme admin.
*/

View file

@ -0,0 +1,25 @@
<?php
/**
* @file
* Contains \Drupal\block\Tests\Update\BlockContextMappingUpdateFilledTest.
*/
namespace Drupal\block\Tests\Update;
/**
* Runs BlockContextMappingUpdateTest with a dump filled with content.
*
* @group Update
*/
class BlockContextMappingUpdateFilledTest extends BlockContextMappingUpdateTest {
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles() {
parent::setDatabaseDumpFiles();
$this->databaseDumpFiles[0] = __DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.filled.standard.php.gz';
}
}

View file

@ -28,12 +28,13 @@ class BlockContextMappingUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected function setUp() {
protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz',
__DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.block-context-manager-2354889.php',
__DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.language-enabled.php',
__DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.block-test-enabled.php',
];
parent::setUp();
}
/**
@ -95,7 +96,7 @@ class BlockContextMappingUpdateTest extends UpdatePathTestBase {
$disabled_block = Block::load('thirdtestfor2354889');
$this->assertFalse($disabled_block->status(), 'Block with invalid context is disabled');
$this->assertEqual(['thirdtestfor2354889' => ['missing_context_ids' => ['baloney.spam' => ['node_type']], 'status' => TRUE]], \Drupal::keyValue('update_backup')->get('block_update_8001'));
$this->assertEqual(['thirdtestfor2354889' => ['missing_context_ids' => ['baloney_spam' => ['node_type']], 'status' => TRUE]], \Drupal::keyValue('update_backup')->get('block_update_8001'));
$disabled_block_visibility = $disabled_block->get('visibility');
$this->assertTrue(!isset($disabled_block_visibility['node_type']), 'The problematic visibility condition has been removed.');

View file

@ -8,6 +8,8 @@
namespace Drupal\block\Tests\Views;
use Drupal\Component\Serialization\Json;
use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
use Drupal\views\Entity\View;
use Drupal\views\Views;
use Drupal\views\Tests\ViewTestBase;
use Drupal\views\Tests\ViewTestData;
@ -21,6 +23,8 @@ use Drupal\Core\Template\Attribute;
*/
class DisplayBlockTest extends ViewTestBase {
use AssertPageCacheContextsAndTagsTrait;
/**
* Modules to install.
*
@ -259,6 +263,87 @@ class DisplayBlockTest extends ViewTestBase {
$this->drupalGet('');
$result = $this->xpath('//div[contains(@class, "region-sidebar-first")]/div[contains(@class, "block-views")]/h2');
$this->assertTrue(empty($result), 'The title is not visible.');
$this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:system.site', 'config:views.view.test_view_block' ,'rendered']));
}
/**
* Tests the various testcases of empty block rendering.
*/
public function testBlockEmptyRendering() {
// Remove all views_test_data entries.
\Drupal::database()->truncate('views_test_data')->execute();
/** @var \Drupal\views\ViewEntityInterface $view */
$view = View::load('test_view_block');
$view->invalidateCaches();
$block = $this->drupalPlaceBlock('views_block:test_view_block-block_1', array('label' => 'test_view_block-block_1:1', 'views_label' => 'Custom title'));
$this->drupalGet('');
$this->assertEqual(1, count($this->xpath('//div[contains(@class, "block-views-blocktest-view-block-block-1")]')));
$display = &$view->getDisplay('block_1');
$display['display_options']['block_hide_empty'] = TRUE;
$view->save();
$this->drupalGet('');
$this->assertEqual(0, count($this->xpath('//div[contains(@class, "block-views-blocktest-view-block-block-1")]')));
// Ensure that the view cachability metadata is propagated even, for an
// empty block.
$this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:system.site', 'config:views.view.test_view_block' ,'rendered']));
$this->assertCacheContexts(['url.path', 'url.query_args', 'user.roles:authenticated']);
// Add a header displayed on empty result.
$display = &$view->getDisplay('block_1');
$display['display_options']['defaults']['header'] = FALSE;
$display['display_options']['header']['example'] = [
'field' => 'area_text_custom',
'id' => 'area_text_custom',
'table' => 'views',
'plugin_id' => 'text_custom',
'content' => 'test header',
'empty' => TRUE,
];
$view->save();
$this->drupalGet('');
$this->assertEqual(1, count($this->xpath('//div[contains(@class, "block-views-blocktest-view-block-block-1")]')));
$this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:system.site', 'config:views.view.test_view_block' ,'rendered']));
$this->assertCacheContexts(['url.path', 'url.query_args', 'user.roles:authenticated']);
// Hide the header on empty results.
$display = &$view->getDisplay('block_1');
$display['display_options']['defaults']['header'] = FALSE;
$display['display_options']['header']['example'] = [
'field' => 'area_text_custom',
'id' => 'area_text_custom',
'table' => 'views',
'plugin_id' => 'text_custom',
'content' => 'test header',
'empty' => FALSE,
];
$view->save();
$this->drupalGet('');
$this->assertEqual(0, count($this->xpath('//div[contains(@class, "block-views-blocktest-view-block-block-1")]')));
$this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:system.site', 'config:views.view.test_view_block' ,'rendered']));
$this->assertCacheContexts(['url.path', 'url.query_args', 'user.roles:authenticated']);
// Add an empty text.
$display = &$view->getDisplay('block_1');
$display['display_options']['defaults']['empty'] = FALSE;
$display['display_options']['empty']['example'] = [
'field' => 'area_text_custom',
'id' => 'area_text_custom',
'table' => 'views',
'plugin_id' => 'text_custom',
'content' => 'test empty',
];
$view->save();
$this->drupalGet('');
$this->assertEqual(1, count($this->xpath('//div[contains(@class, "block-views-blocktest-view-block-block-1")]')));
$this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:system.site', 'config:views.view.test_view_block' ,'rendered']));
$this->assertCacheContexts(['url.path', 'url.query_args', 'user.roles:authenticated']);
}
/**

View file

@ -12,12 +12,6 @@
* - module: The module that provided this block plugin.
* - cache: The cache settings.
* - Block plugin specific settings will also be stored here.
* - block - The full block entity.
* - label_hidden: The hidden block title value if the block was
* configured to hide the title ('label' is empty in this case).
* - module: The module that generated the block.
* - delta: An ID for the block, unique within each module.
* - region: The block region embedding the current block.
* - content: The content of this block.
* - attributes: array of HTML attributes populated by modules, intended to
* be added to the main container tag of this template.

View file

@ -22,19 +22,37 @@ function block_test_block_alter(&$block_info) {
*/
function block_test_block_view_test_cache_alter(array &$build, BlockPluginInterface $block) {
if (\Drupal::state()->get('block_test_view_alter_suffix') !== NULL) {
$build['#suffix'] = '<br>Goodbye!';
}
if (\Drupal::state()->get('block_test_view_alter_cache_key') !== NULL) {
$build['#cache']['keys'][] = \Drupal::state()->get('block_test_view_alter_cache_key');
}
if (\Drupal::state()->get('block_test_view_alter_cache_tag') !== NULL) {
$build['#cache']['tags'][] = \Drupal::state()->get('block_test_view_alter_cache_tag');
$build['#attributes']['foo'] = 'bar';
}
if (\Drupal::state()->get('block_test_view_alter_append_pre_render_prefix') !== NULL) {
$build['#pre_render'][] = 'block_test_pre_render_alter_content';
}
}
/**
* Implements hook_block_build_BASE_BLOCK_ID_alter().
*/
function block_test_block_build_test_cache_alter(array &$build, BlockPluginInterface $block) {
// Test altering cache keys, contexts, tags and max-age.
if (\Drupal::state()->get('block_test_block_alter_cache_key') !== NULL) {
$build['#cache']['keys'][] = \Drupal::state()->get('block_test_block_alter_cache_key');
}
if (\Drupal::state()->get('block_test_block_alter_cache_context') !== NULL) {
$build['#cache']['contexts'][] = \Drupal::state()->get('block_test_block_alter_cache_context');
}
if (\Drupal::state()->get('block_test_block_alter_cache_tag') !== NULL) {
$build['#cache']['tags'] = Cache::mergeTags($build['#cache']['tags'], [\Drupal::state()->get('block_test_block_alter_cache_tag')]);
}
if (\Drupal::state()->get('block_test_block_alter_cache_max_age') !== NULL) {
$build['#cache']['max-age'] = \Drupal::state()->get('block_test_block_alter_cache_max_age');
}
// Test setting #create_placeholder.
if (\Drupal::state()->get('block_test_block_alter_create_placeholder') !== NULL) {
$build['#create_placeholder'] = \Drupal::state()->get('block_test_block_alter_create_placeholder');
}
}
/**
* #pre_render callback for a block to alter its content.
*/

View file

@ -0,0 +1,7 @@
block_test.test_multipleforms:
path: '/test-multiple-forms'
defaults:
_controller: '\Drupal\block_test\Controller\TestMultipleFormController::testMultipleForms'
_title: 'Multiple forms'
requirements:
_access: 'TRUE'

View file

@ -5,3 +5,6 @@ block.settings.test_block_instantiation:
display_message:
type: string
label: 'Message text'
condition.plugin.baloney_spam:
type: condition.plugin

View file

@ -0,0 +1,43 @@
<?php
/**
* @file
* Contains \Drupal\block_test\Controller\TestMultipleFormController.
*/
namespace Drupal\block_test\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Form\FormState;
/**
* Controller for block_test module
*/
class TestMultipleFormController extends ControllerBase {
public function testMultipleForms() {
$form_state = new FormState();
$build = [
'form1' => $this->formBuilder()->buildForm('\Drupal\block_test\Form\TestForm', $form_state),
'form2' => $this->formBuilder()->buildForm('\Drupal\block_test\Form\FavoriteAnimalTestForm', $form_state),
];
// Output all attached placeholders trough drupal_set_message(), so we can
// see if there's only one in the tests.
$post_render_callable = function ($elements) {
$matches = [];
preg_match_all('<form\s(.*?)action="(.*?)"(.*)>', $elements, $matches);
$action_values = $matches[2];
foreach ($action_values as $action_value) {
drupal_set_message('Form action: ' . $action_value);
}
return $elements;
};
$build['#post_render'] = [$post_render_callable];
return $build;
}
}

View file

@ -0,0 +1,46 @@
<?php
/**
* @file
* Contains \Drupal\block_test\Form\FavoriteAnimalTestForm.
*/
namespace Drupal\block_test\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
class FavoriteAnimalTestForm extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'block_test_form_favorite_animal_test';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['favorite_animal'] = [
'#type' => 'textfield',
'#title' => $this->t('Your favorite animal.')
];
$form['submit_animal'] = [
'#type' => 'submit',
'#value' => $this->t('Submit your chosen animal'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
drupal_set_message($this->t('Your favorite animal is: @favorite_animal', ['@favorite_animal' => $form['favorite_animal']['#value']]));
}
}

View file

@ -0,0 +1,55 @@
<?php
/**
* @file
* Contains \Drupal\block_test\Form\TestForm.
*/
namespace Drupal\block_test\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
class TestForm extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'block_test_form_test';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['email'] = [
'#type' => 'email',
'#title' => $this->t('Your .com email address.')
];
$form['show'] = [
'#type' => 'submit',
'#value' => $this->t('Submit'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
if (strpos($form_state->getValue('email'), '.com') === FALSE) {
$form_state->setErrorByName('email', $this->t('This is not a .com email address.'));
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
drupal_set_message($this->t('Your email address is @email', ['@email' => $form['email']['#value']]));
}
}

View file

@ -0,0 +1,29 @@
<?php
/**
* @file
* Contains \Drupal\block_test\Plugin\Block\TestFormBlock.
*/
namespace Drupal\block_test\Plugin\Block;
use Drupal\Core\Block\BlockBase;
/**
* Provides a block to test caching.
*
* @Block(
* id = "test_form_in_block",
* admin_label = @Translation("Test form block caching")
* )
*/
class TestFormBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
return \Drupal::formBuilder()->getForm('Drupal\block_test\Form\TestForm');
}
}

View file

@ -10,10 +10,10 @@ namespace Drupal\block_test\Plugin\Condition;
use Drupal\Core\Condition\ConditionPluginBase;
/**
* Provides a 'baloney.spam' condition.
* Provides a 'baloney_spam' condition.
*
* @Condition(
* id = "baloney.spam",
* id = "baloney_spam",
* label = @Translation("Baloney spam"),
* )
*

View file

@ -25,7 +25,6 @@ class BlockTest extends MigrateSqlSourceTestCase {
protected $migrationConfiguration = array(
// The ID of the entity, can be any string.
'id' => 'test',
'idlist' => array(),
'source' => array(
'plugin' => 'd6_block',
),

View file

@ -0,0 +1,30 @@
<?php
/**
* @file
* Install, update and uninstall functions for the block_content module.
*/
use Drupal\Core\Field\BaseFieldDefinition;
/**
* Add 'revision_translation_affected' field to 'block_content' entities.
*/
function block_content_update_8001() {
// Install the definition that this field had in
// \Drupal\block_content\Entity\BlockContent::baseFieldDefinitions()
// at the time that this update function was written. If/when code is
// deployed that changes that definition, the corresponding module must
// implement an update function that invokes
// \Drupal::entityDefinitionUpdateManager()->updateFieldStorageDefinition()
// with the new definition.
$storage_definition = BaseFieldDefinition::create('boolean')
->setLabel(t('Revision translation affected'))
->setDescription(t('Indicates if the last edit of a translation belongs to current revision.'))
->setReadOnly(TRUE)
->setRevisionable(TRUE)
->setTranslatable(TRUE);
\Drupal::entityDefinitionUpdateManager()
->installFieldStorageDefinition('revision_translation_affected', 'block_content', 'block_content', $storage_definition);
}

View file

@ -8,7 +8,7 @@ id: block_content
label: 'Custom block library'
module: views
description: 'Find and manage custom blocks.'
tag: ''
tag: default
base_table: block_content_field_data
base_field: id
core: 8.x
@ -290,12 +290,14 @@ display:
hide_empty: false
empty_zero: false
hide_alter_empty: true
date_format: short
custom_date_format: ''
timezone: ''
entity_type: block_content
entity_field: changed
plugin_id: date
type: timestamp
settings:
date_format: short
custom_date_format: ''
timezone: ''
plugin_id: field
operations:
id: operations
table: block_content

View file

@ -8,7 +8,15 @@
"use strict";
/**
* Sets summaries about revision and translation of block content.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches summary behaviour block content form tabs.
*
* Specifically, it updates summaries to the revision information and the
* translation options.
*/
Drupal.behaviors.blockContentDetailsSummaries = {
attach: function (context) {
@ -17,10 +25,11 @@
var $revisionContext = $(context);
var revisionCheckbox = $revisionContext.find('.form-item-revision input');
// Return 'New revision' if the 'Create new revision' checkbox is checked,
// or if the checkbox doesn't exist, but the revision log does. For users
// without the "Administer content" permission the checkbox won't appear,
// but the revision log will if the content type is set to auto-revision.
// Return 'New revision' if the 'Create new revision' checkbox is
// checked, or if the checkbox doesn't exist, but the revision log does.
// For users without the "Administer content" permission the checkbox
// won't appear, but the revision log will if the content type is set
// to auto-revision.
if (revisionCheckbox.is(':checked') || (!revisionCheckbox.length && $revisionContext.find('.form-item-revision-log textarea').length)) {
return Drupal.t('New revision');
}

View file

@ -220,20 +220,4 @@ class BlockContentForm extends ContentEntityForm {
}
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$entity = parent::validateForm($form, $form_state);
if ($entity->isNew()) {
$exists = $this->blockContentStorage->loadByProperties(array('info' => $form_state->getValue(['info', 0, 'value'])));
if (!empty($exists)) {
$form_state->setErrorByName('info', $this->t('A block with description %name already exists.', array(
'%name' => $form_state->getValue(array('info', 0, 'value')),
)));
}
}
return $entity;
}
}

View file

@ -29,7 +29,7 @@ class BlockContentListBuilder extends EntityListBuilder {
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
$row['label'] = $this->getLabel($entity);
$row['label'] = $entity->label();
return $row + parent::buildRow($entity);
}

View file

@ -10,8 +10,6 @@ namespace Drupal\block_content;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\language\Entity\ContentLanguageSettings;
/**

View file

@ -189,7 +189,9 @@ class BlockContent extends ContentEntityBase implements BlockContentInterface {
'type' => 'string_textfield',
'weight' => -5,
))
->setDisplayConfigurable('form', TRUE);
->setDisplayConfigurable('form', TRUE)
->addConstraint('BlockContentInfo', []);
$fields['type'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('Block type'))

View file

@ -0,0 +1,31 @@
<?php
/**
* @file
* Contains \Drupal\block_content\Plugin\Validation\Constraint\BlockContentInfoConstraint.
*/
namespace Drupal\block_content\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* Supports validating custom block names.
*
* @Constraint(
* id = "BlockContentInfo",
* label = @Translation("Custom block name", context = "Validation")
* )
*/
class BlockContentInfoConstraint extends Constraint {
public $message = 'A block with description %value already exists.';
/**
* {@inheritdoc}
*/
public function validatedBy() {
return '\Drupal\Core\Validation\Plugin\Validation\Constraint\UniqueFieldValueValidator';
}
}

View file

@ -60,6 +60,7 @@ abstract class BlockContentTestBase extends WebTestBase {
}
$this->adminUser = $this->drupalCreateUser($this->permissions);
$this->drupalPlaceBlock('local_actions_block');
}
/**

View file

@ -30,6 +30,18 @@ class BlockContentTranslationUITest extends ContentTranslationUITestBase {
'block_content'
);
/**
* {@inheritdoc}
*/
protected $defaultCacheContexts = [
'languages:language_interface',
'theme',
'url.path',
'url.query_args',
'user.permissions',
'user.roles:authenticated',
];
/**
* Overrides \Drupal\simpletest\WebTestBase::setUp().
*/

View file

@ -0,0 +1,45 @@
<?php
/**
* @file
* Contains \Drupal\block_content\Tests\BlockContentValidationTest.
*/
namespace Drupal\block_content\Tests;
/**
* Tests block content validation constraints.
*
* @group block_content
*/
class BlockContentValidationTest extends BlockContentTestBase {
/**
* Tests the block content validation constraints.
*/
public function testValidation() {
// Add a block.
$description = $this->randomMachineName();
$block = $this->createBlockContent($description, 'basic');
// Validate the block.
$violations = $block->validate();
// Make sure we have no violations.
$this->assertEqual(count($violations), 0);
// Save the block.
$block->save();
// Add another block with the same description.
$block = $this->createBlockContent($description, 'basic');
// Validate this block.
$violations = $block->validate();
// Make sure we have 1 violation.
$this->assertEqual(count($violations), 1);
// Make sure the violation is on the info property
$this->assertEqual($violations[0]->getPropertyPath(), 'info');
// Make sure the message is correct.
$this->assertEqual($violations[0]->getMessage(), format_string('A block with description %value already exists.', [
'%value' => $block->label(),
]));
}
}

View file

@ -13,7 +13,7 @@ use Drupal\migrate_drupal\Tests\d6\MigrateDrupal6TestBase;
/**
* Upgrade custom blocks.
*
* @group block_content
* @group migrate_drupal_6
*/
class MigrateBlockContentTest extends MigrateDrupal6TestBase {
@ -35,7 +35,6 @@ class MigrateBlockContentTest extends MigrateDrupal6TestBase {
array(array(2), array('full_html'))
)
));
$this->loadDumps(['Boxes.php']);
$this->executeMigration('d6_custom_block');
}

View file

@ -24,8 +24,6 @@ class BoxTest extends MigrateSqlSourceTestCase {
protected $migrationConfiguration = array(
// The ID of the entity, can be any string.
'id' => 'test',
// Leave it empty for now.
'idlist' => array(),
'source' => array(
'plugin' => 'd6_boxes',
),

View file

@ -8,7 +8,12 @@
"use strict";
/**
* Adds summaries to the book outline form.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches summary behavior to book outline forms.
*/
Drupal.behaviors.bookDetailsSummaries = {
attach: function (context) {

View file

@ -7,7 +7,6 @@
use Drupal\book\BookManager;
use Drupal\book\BookManagerInterface;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
@ -384,7 +383,7 @@ function template_preprocess_book_navigation(&$variables) {
// Provide extra variables for themers. Not needed by default.
$variables['book_id'] = $book_link['bid'];
$variables['book_title'] = SafeMarkup::checkPlain($book_link['link_title']);
$variables['book_title'] = $book_link['link_title'];
$variables['book_url'] = \Drupal::url('entity.node.canonical', array('node' => $book_link['bid']));
$variables['current_depth'] = $book_link['depth'];
$variables['tree'] = '';
@ -404,7 +403,7 @@ function template_preprocess_book_navigation(&$variables) {
'href' => $prev_href,
);
$variables['prev_url'] = $prev_href;
$variables['prev_title'] = SafeMarkup::checkPlain($prev['title']);
$variables['prev_title'] = $prev['title'];
}
/** @var \Drupal\book\BookManagerInterface $book_manager */
@ -416,7 +415,7 @@ function template_preprocess_book_navigation(&$variables) {
'href' => $parent_href,
);
$variables['parent_url'] = $parent_href;
$variables['parent_title'] = SafeMarkup::checkPlain($parent['title']);
$variables['parent_title'] = $parent['title'];
}
if ($next = $book_outline->nextLink($book_link)) {
@ -426,7 +425,7 @@ function template_preprocess_book_navigation(&$variables) {
'href' => $next_href,
);
$variables['next_url'] = $next_href;
$variables['next_title'] = SafeMarkup::checkPlain($next['title']);
$variables['next_title'] = $next['title'];
}
}
@ -464,7 +463,6 @@ function template_preprocess_book_export_html(&$variables) {
global $base_url;
$language_interface = \Drupal::languageManager()->getCurrentLanguage();
$variables['title'] = SafeMarkup::checkPlain($variables['title']);
$variables['base_url'] = $base_url;
$variables['language'] = $language_interface;
$variables['language_rtl'] = ($language_interface->getDirection() == LanguageInterface::DIRECTION_RTL);
@ -490,7 +488,7 @@ function template_preprocess_book_export_html(&$variables) {
*/
function template_preprocess_book_node_export_html(&$variables) {
$variables['depth'] = $variables['node']->book['depth'];
$variables['title'] = SafeMarkup::checkPlain($variables['node']->label());
$variables['title'] = $variables['node']->label();
}
/**

View file

@ -26,6 +26,8 @@ services:
cache_context.route.book_navigation:
class: Drupal\book\Cache\BookNavigationCacheContext
arguments: ['@request_stack']
calls:
- [setContainer, ['@service_container']]
tags:
- { name: cache.context}

View file

@ -20,4 +20,4 @@ destination:
plugin: book
migration_dependencies:
required:
- d6_node
- d6_node:*

View file

@ -8,6 +8,7 @@
namespace Drupal\book;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Link;
@ -72,6 +73,8 @@ class BookBreadcrumbBuilder implements BreadcrumbBuilderInterface {
*/
public function build(RouteMatchInterface $route_match) {
$book_nids = array();
$breadcrumb = new Breadcrumb();
$links = array(Link::createFromRoute($this->t('Home'), '<front>'));
$book = $route_match->getParameter('node')->book;
$depth = 1;
@ -92,7 +95,9 @@ class BookBreadcrumbBuilder implements BreadcrumbBuilderInterface {
$depth++;
}
}
return $links;
$breadcrumb->setLinks($links);
$breadcrumb->setCacheContexts(['route.book_navigation']);
return $breadcrumb;
}
}

View file

@ -24,7 +24,7 @@ class BookTest extends WebTestBase {
*
* @var array
*/
public static $modules = array('book', 'block', 'node_access_test');
public static $modules = array('book', 'block', 'node_access_test', 'book_test');
/**
* A book node.
@ -109,6 +109,45 @@ class BookTest extends WebTestBase {
return $nodes;
}
/**
* Tests the book navigation cache context.
*
* @see \Drupal\book\Cache\BookNavigationCacheContext
*/
public function testBookNavigationCacheContext() {
// Create a page node.
$this->drupalCreateContentType(['type' => 'page']);
$page = $this->drupalCreateNode();
// Create a book, consisting of book nodes.
$book_nodes = $this->createBook();
// Enable the debug output.
\Drupal::state()->set('book_test.debug_book_navigation_cache_context', TRUE);
$this->drupalLogin($this->bookAuthor);
// On non-node route.
$this->drupalGet('');
$this->assertRaw('[route.book_navigation]=book.none');
// On non-book node route.
$this->drupalGet($page->urlInfo());
$this->assertRaw('[route.book_navigation]=book.none');
// On book node route.
$this->drupalGet($book_nodes[0]->urlInfo());
$this->assertRaw('[route.book_navigation]=0|2|3');
$this->drupalGet($book_nodes[1]->urlInfo());
$this->assertRaw('[route.book_navigation]=0|2|3|4');
$this->drupalGet($book_nodes[2]->urlInfo());
$this->assertRaw('[route.book_navigation]=0|2|3|5');
$this->drupalGet($book_nodes[3]->urlInfo());
$this->assertRaw('[route.book_navigation]=0|2|6');
$this->drupalGet($book_nodes[4]->urlInfo());
$this->assertRaw('[route.book_navigation]=0|2|7');
}
/**
* Tests saving the book outline on an empty book.
*/
@ -303,7 +342,7 @@ class BookTest extends WebTestBase {
static $number = 0; // Used to ensure that when sorted nodes stay in same order.
$edit = array();
$edit['title[0][value]'] = $number . ' - SimpleTest test node ' . $this->randomMachineName(10);
$edit['title[0][value]'] = str_pad($number, 2, '0', STR_PAD_LEFT) . ' - SimpleTest test node ' . $this->randomMachineName(10);
$edit['body[0][value]'] = 'SimpleTest test body ' . $this->randomMachineName(32) . ' ' . $this->randomMachineName(32);
$edit['book[bid]'] = $book_nid;

View file

@ -57,7 +57,7 @@ class BookUninstallTest extends KernelTestBase {
$allowed_types[] = $content_type->id();
$book_config->set('allowed_types', $allowed_types)->save();
$node = Node::create(array('type' => $content_type->id()));
$node = Node::create(array('title' => $this->randomString(), 'type' => $content_type->id()));
$node->book['bid'] = 'new';
$node->save();
@ -65,7 +65,7 @@ class BookUninstallTest extends KernelTestBase {
$validation_reasons = \Drupal::service('module_installer')->validateUninstall(['book']);
$this->assertEqual(['To uninstall Book, delete all content that is part of a book'], $validation_reasons['book']);
$book_node = Node::create(array('type' => 'book'));
$book_node = Node::create(array('title' => $this->randomString(), 'type' => 'book'));
$book_node->book['bid'] = FALSE;
$book_node->save();
@ -84,7 +84,7 @@ class BookUninstallTest extends KernelTestBase {
$module_data = _system_rebuild_module_data();
$this->assertFalse(isset($module_data['book']->info['required']), 'The book module is not required.');
$node = Node::create(array('type' => $content_type->id()));
$node = Node::create(array('title' => $this->randomString(), 'type' => $content_type->id()));
$node->save();
// One node exists but is not part of a book therefore the book module is
// not required.

View file

@ -13,7 +13,7 @@ use Drupal\migrate_drupal\Tests\d6\MigrateDrupal6TestBase;
/**
* Upgrade variables to book.settings.yml.
*
* @group book
* @group migrate_drupal_6
*/
class MigrateBookConfigsTest extends MigrateDrupal6TestBase {
@ -31,7 +31,6 @@ class MigrateBookConfigsTest extends MigrateDrupal6TestBase {
*/
protected function setUp() {
parent::setUp();
$this->loadDumps(['Variable.php']);
$this->executeMigration('d6_book_settings');
}

View file

@ -13,7 +13,7 @@ use Drupal\node\Entity\Node;
/**
* Upgrade book structure.
*
* @group book
* @group migrate_drupal_6
*/
class MigrateBookTest extends MigrateDrupal6TestBase {
@ -29,7 +29,15 @@ class MigrateBookTest extends MigrateDrupal6TestBase {
$this->installSchema('book', array('book'));
$this->installSchema('node', array('node_access'));
$id_mappings = array();
// Create a default bogus mapping for all variants of d6_node.
$id_mappings = array(
'd6_node:*' => array(
array(
array(0),
array(0),
),
),
);
for ($i = 4; $i <= 8; $i++) {
$entity = entity_create('node', array(
'type' => 'story',
@ -39,11 +47,9 @@ class MigrateBookTest extends MigrateDrupal6TestBase {
));
$entity->enforceIsNew();
$entity->save();
$id_mappings['d6_node'][] = array(array($i), array($i));
$id_mappings['d6_node__story'][] = array(array($i), array($i));
}
$this->prepareMigrations($id_mappings);
// Load database dumps to provide source data.
$this->loadDumps(['Book.php', 'MenuLinks.php']);
$this->executeMigration('d6_book');
}

View file

@ -0,0 +1,6 @@
name: 'Book module tests'
type: module
description: 'Support module for book module testing.'
package: Testing
version: VERSION
core: 8.x

View file

@ -0,0 +1,21 @@
<?php
/**
* @file
* Test module for testing the book module.
*
* This module's functionality depends on the following state variables:
* - book_test.debug_book_navigation_cache_context: Used in NodeQueryAlterTest to enable the
* node_access_all grant realm.
*
* @see \Drupal\book\Tests\BookTest::testBookNavigationCacheContext()
*/
/**
* Implements hook_page_attachments().
*/
function book_test_page_attachments(array &$page) {
if (\Drupal::state()->get('book_test.debug_book_navigation_cache_context', FALSE)) {
drupal_set_message(\Drupal::service('cache_contexts_manager')->convertTokensToKeys(['route.book_navigation'])->getKeys()[0]);
}
}

View file

@ -7,7 +7,6 @@
use Drupal\Component\Utility\Html;
use Drupal\Core\Template\Attribute;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Language\LanguageInterface;
/**
@ -66,10 +65,10 @@ function template_preprocess_ckeditor_settings_toolbar(&$variables) {
$build_button_item = function($button, $rtl) {
// Value of the button item.
if (isset($button['image_alternative' . $rtl])) {
$value = SafeMarkup::set($button['image_alternative' . $rtl]);
$value = $button['image_alternative' . $rtl];
}
elseif (isset($button['image_alternative'])) {
$value = SafeMarkup::set($button['image_alternative']);
$value = $button['image_alternative'];
}
elseif (isset($button['image'])) {
$value = array(

View file

@ -73,3 +73,5 @@ drupal.ckeditor.stylescombo.admin:
- core/jquery.once
- core/drupal.vertical-tabs
- core/drupalSettings
# Ensure to run after ckeditor/drupal.ckeditor.admin.
- ckeditor/drupal.ckeditor.admin

View file

@ -16,7 +16,7 @@ function ckeditor_help($route_name, RouteMatchInterface $route_match) {
case 'help.page.ckeditor':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The CKEditor module provides a visual text editor and adds a toolbar to text fields. Users can use buttons to format content and to create semantically correct and valid HTML. The CKEditor module uses the framework provided by the <a href="!text_editor">Text Editor module</a>. It requires JavaScript to be enabled in the browser. For more information, see <a href="!doc_url">the online documentation for the CKEditor module</a> and the <a href="!cke_url">CKEditor website</a>.', array( '!doc_url' => 'https://www.drupal.org/documentation/modules/ckeditor', '!cke_url' => 'http://ckeditor.com', '!text_editor' => \Drupal::url('help.page', array('name' => 'editor')))) . '</p>';
$output .= '<p>' . t('The CKEditor module provides a highly-accessible, highly-usable visual text editor and adds a toolbar to text fields. Users can use buttons to format content and to create semantically correct and valid HTML. The CKEditor module uses the framework provided by the <a href="!text_editor">Text Editor module</a>. It requires JavaScript to be enabled in the browser. For more information, see <a href="!doc_url">the online documentation for the CKEditor module</a> and the <a href="!cke_url">CKEditor website</a>.', array( '!doc_url' => 'https://www.drupal.org/documentation/modules/ckeditor', '!cke_url' => 'http://ckeditor.com', '!text_editor' => \Drupal::url('help.page', array('name' => 'editor')))) . '</p>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dl>';
$output .= '<dt>' . t('Enabling CKEditor for individual text formats') . '</dt>';
@ -27,6 +27,10 @@ function ckeditor_help($route_name, RouteMatchInterface $route_match) {
$output .= '<dd>' . t('CKEditor only allow users to format content in accordance with the filter configuration of the specific text format. If a text format excludes certain HTML tags, the corresponding toolbar buttons are not displayed to users when they edit a text field in this format. For more information see the <a href="!filter">Filter help page</a>.', array('!filter' => \Drupal::url('help.page', array('name' => 'filter')))) . '</dd>';
$output .= '<dt>' . t('Toggling between formatted text and HTML source') . '</dt>';
$output .= '<dd>' . t('If the <em>Source</em> button is available in the toolbar, users can click this button to disable the visual editor and edit the HTML source directly. After toggling back, the visual editor uses the allowed HTML tags to format the text — independent of whether buttons for these tags are available in the toolbar. If the text format is set to <em>limit the use of HTML tags</em>, then all excluded tags will be stripped out of the HTML source when the user toggles back to the text editor.') . '</dd>';
$output .= '<dt>' . t('Accessibility features') . '</dt>';
$output .= '<dd>' . t('The built in WYSIWYG editor (CKEditor) comes with a number of <a href="!features">accessibility features</a>. CKEditor comes with built in <a href="!shortcuts">keyboard shortcuts</a>, which can be beneficial for both power users and keyboard only users.', array('!features' => 'http://docs.ckeditor.com/#!/guide/dev_a11y', '!shortcuts' => 'http://docs.ckeditor.com/#!/guide/dev_shortcuts')) . '</dd>';
$output .= '<dt>' . t('Generating accessible content') . '</dt>';
$output .= '<dd>' . t('HTML tables can be created with both table headers as well as caption/summary elements. Alt text is required by default on images added through CKEditor (note that this can be overridden). Semantic HTML5 figure/figcaption are available to add captions to images.') . '</dd>';
$output .= '</dl>';
return $output;
}
@ -96,78 +100,3 @@ function _ckeditor_theme_css($theme = NULL) {
}
return $css;
}
/**
* Implements hook_ENTITY_TYPE_insert() for 'filter_format'.
*
* Recalculates the 'format_tags' CKEditor setting when a text format is added.
*
* @see \Drupal\ckeditor\Plugin\CKEditorPlugin\Internal::generateFormatTagsSetting()
* @see ckeditor_rebuild()
*/
function ckeditor_filter_format_insert() {
ckeditor_rebuild();
}
/**
* Implements hook_ENTITY_TYPE_update() for 'filter_format'.
*
* Recalculates the 'format_tags' CKEditor setting when a text format changes.
*
* @see \Drupal\ckeditor\Plugin\CKEditorPlugin\Internal::generateFormatTagsSetting()
* @see ckeditor_rebuild()
*/
function ckeditor_filter_format_update() {
ckeditor_rebuild();
}
/**
* Implements hook_rebuild().
*
* Calculates the 'format_tags' CKEditor setting for each text format.
*
* If this wouldn't happen in hook_rebuild(), then the first drupal_render()
* call that occurs for a page that contains a #type 'text_format' element will
* cause the CKEditor::getJSSettings() to be called, which will cause
* Internal::generateFormatTagsSetting() to be called, which calls
* check_markup(), which finally calls drupal_render() non-recursively, because
* a filter might add placeholders to replace.
* This would be a root call inside a root call, which breaks the stack-based
* logic for bubbling rendering metadata.
* Therefore this pre-calculates the needed values, and hence performs the
* check_markup() calls outside of a drupal_render() call tree.
*
* @see \Drupal\ckeditor\Plugin\CKEditorPlugin\Internal::generateFormatTagsSetting()
* @see ckeditor_filter_format_insert()
* @see ckeditor_filter_format_update()
*/
function ckeditor_rebuild() {
/** @var \Drupal\filter\FilterFormatInterface[] $formats */
$formats = filter_formats();
foreach ($formats as $format) {
$key = 'ckeditor_internal_format_tags:' . $format->id();
// The <p> tag is always allowed — HTML without <p> tags is nonsensical.
$format_tags = array('p');
// Given the list of possible format tags, automatically determine whether
// the current text format allows this tag, and thus whether it should show
// up in the "Format" dropdown.
$possible_format_tags = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre');
foreach ($possible_format_tags as $tag) {
$input = '<' . $tag . '>TEST</' . $tag . '>';
$output = trim(check_markup($input, $format->id()));
if ($input == $output) {
$format_tags[] = $tag;
}
}
$format_tags = implode(';', $format_tags);
// Cache the "format_tags" configuration. This cache item is infinitely
// valid; it only changes whenever the text format is changed, which is
// guaranteed by the hook_ENTITY_TYPE_update() and hook_ENTITY_TYPE_insert()
// hook implementations.
\Drupal::state()->set($key, $format_tags);
}
}

View file

@ -10,7 +10,14 @@
Drupal.ckeditor = Drupal.ckeditor || {};
/**
* Sets config behaviour and creates config views for the CKEditor toolbar.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches admin behaviour to the CKEditor buttons.
* @prop {Drupal~behaviorDetach} detach
* Detaches admin behaviour from the CKEditor buttons on 'unload'.
*/
Drupal.behaviors.ckeditorAdmin = {
attach: function (context) {
@ -18,14 +25,14 @@
var $configurationForm = $(context).find('.ckeditor-toolbar-configuration').once('ckeditor-configuration');
if ($configurationForm.length) {
var $textarea = $configurationForm
// Hide the textarea that contains the serialized representation of the
// CKEditor configuration.
// Hide the textarea that contains the serialized representation of
// the CKEditor configuration.
.find('.form-item-editor-settings-toolbar-button-groups')
.hide()
// Return the textarea child node from this expression.
.find('textarea');
// The HTML for the CKEditor configuration is assembled on the server and
// The HTML for the CKEditor configuration is assembled on the server
// and sent to the client as a serialized DOM fragment.
$configurationForm.append(drupalSettings.ckeditor.toolbarAdmin);
@ -50,15 +57,16 @@
}
},
detach: function (context, settings, trigger) {
// Early-return if the trigger for detachment is something else than unload.
// Early-return if the trigger for detachment is something else than
// unload.
if (trigger !== 'unload') {
return;
}
// We're detaching because CKEditor as text editor has been disabled; this
// really means that all CKEditor toolbar buttons have been removed. Hence,
// all editor features will be removed, so any reactions from filters will
// be undone.
// really means that all CKEditor toolbar buttons have been removed.
// Hence,all editor features will be removed, so any reactions from
// filters will be undone.
var $configurationForm = $(context).find('.ckeditor-toolbar-configuration').findOnce('ckeditor-configuration');
if ($configurationForm.length && Drupal.ckeditor.models && Drupal.ckeditor.models.Model) {
var config = Drupal.ckeditor.models.Model.toJSON().activeEditorConfig;
@ -93,20 +101,21 @@
models: {},
/**
* Translates a change in CKEditor config DOM structure into the config model.
* Translates changes in CKEditor config DOM structure to the config model.
*
* If the button is moved within an existing group, the DOM structure is simply
* translated to a configuration model. If the button is moved into a new group
* placeholder, then a process is launched to name that group before the button
* move is translated into configuration.
* If the button is moved within an existing group, the DOM structure is
* simply translated to a configuration model. If the button is moved into a
* new group placeholder, then a process is launched to name that group
* before the button move is translated into configuration.
*
* @param {Backbone.View} view
* The Backbone View that invoked this function.
* @param {jQuery} $button
* A jQuery set that contains an li element that wraps a button element.
* @param {function} callback
* A callback to invoke after the button group naming modal dialog has been
* closed.
* A callback to invoke after the button group naming modal dialog has
* been closed.
*
*/
registerButtonMove: function (view, $button, callback) {
var $group = $button.closest('.ckeditor-toolbar-group');
@ -127,10 +136,11 @@
},
/**
* Translates a change in CKEditor config DOM structure into the config model.
* Translates changes in CKEditor config DOM structure to the config model.
*
* Each row has a placeholder group at the end of the row. A user may not move
* an existing button group past the placeholder group at the end of a row.
* Each row has a placeholder group at the end of the row. A user may not
* move an existing button group past the placeholder group at the end of a
* row.
*
* @param {Backbone.View} view
* The Backbone View that invoked this function.
@ -155,15 +165,15 @@
},
/**
* Opens a Drupal dialog with a form for changing the title of a button group.
* Opens a dialog with a form for changing the title of a button group.
*
* @param {Backbone.View} view
* The Backbone View that invoked this function.
* @param {jQuery} $group
* A jQuery set that contains an li element that wraps a group of buttons.
* @param {function} callback
* A callback to invoke after the button group naming modal dialog has been
* closed.
* A callback to invoke after the button group naming modal dialog has
* been closed.
*/
openGroupNameDialog: function (view, $group, callback) {
callback = callback || function () {};
@ -172,8 +182,8 @@
* Validates the string provided as a button group title.
*
* @param {HTMLElement} form
* The form DOM element that contains the input with the new button group
* title string.
* The form DOM element that contains the input with the new button
* group title string.
*
* @return {bool}
* Returns true when an error exists, otherwise returns false.
@ -200,8 +210,8 @@
* @param {string} action
* The dialog action chosen by the user: 'apply' or 'cancel'.
* @param {HTMLElement} form
* The form DOM element that contains the input with the new button group
* title string.
* The form DOM element that contains the input with the new button
* group title string.
*/
function closeDialog(action, form) {
@ -211,7 +221,8 @@
function shutdown() {
dialog.close(action);
// The processing marker can be deleted since the dialog has been closed.
// The processing marker can be deleted since the dialog has been
// closed.
delete view.isProcessing;
}
@ -219,13 +230,14 @@
* Applies a string as the name of a CKEditor button group.
*
* @param {jQuery} $group
* A jQuery set that contains an li element that wraps a group of buttons.
* A jQuery set that contains an li element that wraps a group of
* buttons.
* @param {string} name
* The new name of the CKEditor button group.
*/
function namePlaceholderGroup($group, name) {
// If it's currently still a placeholder, then that means we're creating
// a new group, and we must do some extra work.
// If it's currently still a placeholder, then that means we're
// creating a new group, and we must do some extra work.
if ($group.hasClass('placeholder')) {
// Remove all whitespace from the name, lowercase it and ensure
// HTML-safe encoding, then use this as the group ID for CKEditor
@ -338,12 +350,13 @@
$(event.target).remove();
}
});
// A modal dialog is used because the user must provide a button group name
// or cancel the button placement before taking any other action.
// A modal dialog is used because the user must provide a button group
// name or cancel the button placement before taking any other action.
dialog.showModal();
$(document.querySelector('.ckeditor-name-toolbar-group').querySelector('input'))
// When editing, set the "group name" input in the form to the current value.
// When editing, set the "group name" input in the form to the current
// value.
.attr('value', $group.attr('data-drupal-ckeditor-toolbar-group-name'))
// Focus on the "group name" input in the form.
.trigger('focus');
@ -355,6 +368,9 @@
* Automatically shows/hides settings of buttons-only CKEditor plugins.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches show/hide behaviour to Plugin Settings buttons.
*/
Drupal.behaviors.ckeditorAdminButtonPluginSettings = {
attach: function (context) {
@ -374,10 +390,11 @@
$this.data('ckeditorButtonPluginSettingsActiveButtons', []);
});
// Whenever a button is added or removed, check if we should show or hide
// the corresponding plugin settings. (Note that upon initialization, each
// button that already is part of the toolbar still is considered "added",
// hence it also works correctly for buttons that were added previously.)
// Whenever a button is added or removed, check if we should show or
// hide the corresponding plugin settings. (Note that upon
// initialization, each button that already is part of the toolbar still
// is considered "added", hence it also works correctly for buttons that
// were added previously.)
$context
.find('.ckeditor-toolbar-active')
.off('CKEditorToolbarChanged.ckeditorAdminPluginSettings')
@ -428,6 +445,7 @@
* Themes a blank CKEditor row.
*
* @return {string}
* A HTML string for a CKEditor row.
*/
Drupal.theme.ckeditorRow = function () {
return '<li class="ckeditor-row placeholder" role="group"><ul class="ckeditor-toolbar-groups clearfix"></ul></li>';
@ -437,6 +455,7 @@
* Themes a blank CKEditor button group.
*
* @return {string}
* A HTML string for a CKEditor button group.
*/
Drupal.theme.ckeditorToolbarGroup = function () {
var group = '';
@ -451,6 +470,7 @@
* Themes a form for changing the title of a CKEditor button group.
*
* @return {string}
* A HTML string for the form for the title of a CKEditor button group.
*/
Drupal.theme.ckeditorButtonGroupNameForm = function () {
return '<form><input name="group-name" required="required"></form>';
@ -460,6 +480,7 @@
* Themes a button that will toggle the button group names in active config.
*
* @return {string}
* A HTML string for the button to toggle group names.
*/
Drupal.theme.ckeditorButtonGroupNamesToggle = function () {
return '<a class="ckeditor-groupnames-toggle" role="button" aria-pressed="false"></a>';
@ -469,6 +490,7 @@
* Themes a button that will prompt the user to name a new button group.
*
* @return {string}
* A HTML string for the button to create a name for a new button group.
*/
Drupal.theme.ckeditorNewButtonGroup = function () {
return '<li class="ckeditor-add-new-group"><button role="button" aria-label="' + Drupal.t('Add a CKEditor button group to the end of this row.') + '">' + Drupal.t('Add group') + '</button></li>';

View file

@ -11,6 +11,9 @@
* Provides the summary for the "drupalimage" plugin settings vertical tab.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches summary behaviour to the "drupalimage" settings vertical tab.
*/
Drupal.behaviors.ckeditorDrupalImageSettingsSummary = {
attach: function () {

View file

@ -16,9 +16,12 @@
* Editor attach callback.
*
* @param {HTMLElement} element
* The element to attach the editor to.
* @param {string} format
* The text format for the editor.
*
* @return {bool}
* Whether the call to `CKEDITOR.replace()` created an editor or not.
*/
attach: function (element, format) {
this._loadExternalPlugins(format);
@ -46,10 +49,15 @@
* Editor detach callback.
*
* @param {HTMLElement} element
* The element to detach the editor from.
* @param {string} format
* The text format used for the editor.
* @param {string} trigger
* The event trigger for the detach.
*
* @return {bool}
* Whether the call to `CKEDITOR.dom.element.get(element).getEditor()`
* found an editor or not.
*/
detach: function (element, format, trigger) {
var editor = CKEDITOR.dom.element.get(element).getEditor();
@ -66,11 +74,16 @@
},
/**
* Reacts on a change in the editor element.
*
* @param {HTMLElement} element
* The element where the change occured.
* @param {function} callback
* Callback called with the value of the editor.
*
* @return {bool}
* Whether the call to `CKEDITOR.dom.element.get(element).getEditor()`
* found an editor or not.
*/
onChange: function (element, callback) {
var editor = CKEDITOR.dom.element.get(element).getEditor();
@ -83,13 +96,19 @@
},
/**
* Attaches an inline editor to a DOM element.
*
* @param {HTMLElement} element
* The element to attach the editor to.
* @param {object} format
* @param {string} mainToolbarId
* @param {string} floatedToolbarId
* The text format used in the editor.
* @param {string} [mainToolbarId]
* The id attribute for the main editor toolbar, if any.
* @param {string} [floatedToolbarId]
* The id attribute for the floated editor toolbar, if any.
*
* @return {bool}
* Whether the call to `CKEDITOR.replace()` created an editor or not.
*/
attachInlineEditor: function (element, format, mainToolbarId, floatedToolbarId) {
this._loadExternalPlugins(format);
@ -143,7 +162,10 @@
},
/**
* Loads the required external plugins for the editor.
*
* @param {object} format
* The text format used in the editor.
*/
_loadExternalPlugins: function (format) {
var externalPlugins = format.editorSettings.drupalExternalPlugins;
@ -213,7 +235,7 @@
dialogType: 'modal',
selector: '.ckeditor-dialog-loading-link',
url: url,
progress: {'type': 'throbber'},
progress: {type: 'throbber'},
submit: {
editor_object: existingValues
}

View file

@ -1,6 +1,6 @@
/**
* @file
* CKEditor SylesCombo admin behavior.
* CKEditor StylesCombo admin behavior.
*/
(function ($, Drupal, drupalSettings) {
@ -11,21 +11,25 @@
* Ensures that the "stylescombo" button's metadata remains up-to-date.
*
* Triggers the CKEditorPluginSettingsChanged event whenever the "stylescombo"
* plugin settings change, to ensure that the corresponding feature metadata is
* immediately updated i.e. ensure that HTML tags and classes entered here are
* known to be "required", which may affect filter settings.
* plugin settings change, to ensure that the corresponding feature metadata
* is immediately updated i.e. ensure that HTML tags and classes entered
* here are known to be "required", which may affect filter settings.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches admin behaviour to the "stylescombo" button.
*/
Drupal.behaviors.ckeditorStylesComboSettings = {
attach: function (context) {
var $context = $(context);
// React to changes in the list of user-defined styles: calculate the new
// stylesSet setting up to 2 times per second, and if it is different, fire
// the CKEditorPluginSettingsChanged event with the updated parts of the
// CKEditor configuration. (This will, in turn, cause the hidden CKEditor
// instance to be updated and a drupalEditorFeatureModified event to fire.)
// stylesSet setting up to 2 times per second, and if it is different,
// fire the CKEditorPluginSettingsChanged event with the updated parts of
// the CKEditor configuration. (This will, in turn, cause the hidden
// CKEditor instance to be updated and a drupalEditorFeatureModified event
// to fire.)
var $ckeditorActiveToolbar = $context
.find('.ckeditor-toolbar-configuration')
.find('.ckeditor-toolbar-active');
@ -49,9 +53,9 @@
*
* @see \Drupal\ckeditor\Plugin\ckeditor\plugin\StylesCombo::generateStylesSetSetting()
*
* Note that this is a more forgiving implementation than the PHP version: the
* parsing works identically, but instead of failing on invalid styles, we
* just ignore those.
* Note that this is a more forgiving implementation than the PHP version:
* the parsing works identically, but instead of failing on invalid styles,
* we just ignore those.
*
* @param {string} styles
* The "styles" setting.
@ -88,7 +92,7 @@
// Build the data structure CKEditor's stylescombo plugin expects.
// @see http://docs.cksource.com/CKEditor_3.x/Developers_Guide/Styles
stylesSet.push({
attributes: {'class': classes.join(' ')},
attributes: {class: classes.join(' ')},
element: element,
name: label
});
@ -102,6 +106,9 @@
* Provides the summary for the "stylescombo" plugin settings vertical tab.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches summary behaviour to the plugin settings vertical tab.
*/
Drupal.behaviors.ckeditorStylesComboSettingsSummary = {
attach: function () {

View file

@ -47,6 +47,11 @@
*/
hiddenEditorConfig: null,
/**
* A hash that maps buttons to features.
*/
buttonsToFeatures: null,
/**
* A hash, keyed by a feature name, that details CKEditor plugin features.
*/

View file

@ -131,10 +131,10 @@
// when shifting state, so might deal with a new instance.
widget = editor.widgets.getByElement(image);
// It's first edit, just after widget instance creation, but before it was
// inserted into DOM. So we need to retrieve the widget wrapper from
// inside the DocumentFragment which we cached above and finalize other
// things (like ready event and flag).
// It's first edit, just after widget instance creation, but before
// it was inserted into DOM. So we need to retrieve the widget
// wrapper from inside the DocumentFragment which we cached above
// and finalize other things (like ready event and flag).
if (firstEdit) {
editor.widgets.finalizeCreation(container);
}

View file

@ -52,7 +52,7 @@
// Override requiredContent & allowedContent.
widgetDefinition.requiredContent = 'img[alt,src,width,height,data-entity-type,data-entity-uuid,data-align,data-caption]';
widgetDefinition.allowedContent.img.attributes += ',data-align,data-caption';
widgetDefinition.allowedContent.img.attributes += ',!data-align,!data-caption';
// Override allowedContent setting for the 'caption' nested editable.
// This must match what caption_filter enforces.
@ -213,7 +213,8 @@
}
};
};
}, null, null, 20); // Low priority to ensure drupalimage's event handler runs first.
// Low priority to ensure drupalimage's event handler runs first.
}, null, null, 20);
}
});
@ -224,9 +225,12 @@
* children in DFS order.
*
* @param {CKEDITOR.htmlParser.element} element
* The element to search.
* @param {string} name
* The element name to search for.
*
* @return {CKEDITOR.htmlParser.element}
* @return {?CKEDITOR.htmlParser.element}
* The found element, or null.
*/
function findElementByName(element, name) {
if (element.name === name) {

View file

@ -32,14 +32,14 @@
for (var attrIndex = 0; attrIndex < linkDOMElement.attributes.length; attrIndex++) {
attribute = linkDOMElement.attributes.item(attrIndex);
attributeName = attribute.nodeName.toLowerCase();
// Don't consider data-cke-saved- attributes; they're just there to
// work around browser quirks.
// Don't consider data-cke-saved- attributes; they're just there
// to work around browser quirks.
if (attributeName.substring(0, 15) === 'data-cke-saved-') {
continue;
}
// Store the value for this attribute, unless there's a
// data-cke-saved- alternative for it, which will contain the quirk-
// free, original value.
// data-cke-saved- alternative for it, which will contain the
// quirk-free, original value.
existingValues[attributeName] = linkElement.data('cke-saved-' + attributeName) || attribute.nodeValue;
}
}
@ -97,8 +97,8 @@
editor.fire('saveSnapshot');
};
// Drupal.t() will not work inside CKEditor plugins because CKEditor
// loads the JavaScript file instead of Drupal. Pull translated strings
// from the plugin settings that are translated server-side.
// loads the JavaScript file instead of Drupal. Pull translated
// strings from the plugin settings that are translated server-side.
var dialogSettings = {
title: linkElement ? editor.config.drupalLink_dialogTitleEdit : editor.config.drupalLink_dialogTitleAdd,
dialogClass: 'editor-link-dialog'
@ -210,8 +210,11 @@
* [<a href="#"><b>li]nk</b></a>
*
* @param {CKEDITOR.editor} editor
* The CKEditor editor object
*
* @return {?HTMLElement}
* The selected link element, or null.
*
* @return {?bool}
*/
function getSelectedLink(editor) {
var selection = editor.getSelection();

View file

@ -1,6 +1,7 @@
/**
* @file
* A Backbone View that provides the aural view of CKEditor toolbar configuration.
* A Backbone View that provides the aural view of CKEditor toolbar
* configuration.
*/
(function (Drupal, Backbone, $) {
@ -37,6 +38,7 @@
* Calls announce on buttons and groups when their position is changed.
*
* @param {Drupal.ckeditor.ConfigurationModel} model
* The ckeditor configuration model.
* @param {bool} isDirty
* A model attribute that indicates if the changed toolbar configuration
* has been stored or not.
@ -62,6 +64,7 @@
* Handles the focus event of elements in the active and available toolbars.
*
* @param {jQuery.Event} event
* The focus event that was triggered.
*/
onFocus: function (event) {
event.stopPropagation();
@ -171,6 +174,7 @@
* Provides help information when a button is clicked.
*
* @param {jQuery.Event} event
* The click event for the button click.
*/
announceButtonHelp: function (event) {
var $link = $(event.currentTarget);
@ -199,6 +203,7 @@
* Provides help information when a separator is clicked.
*
* @param {jQuery.Event} event
* The click event for the separator click.
*/
announceSeparatorHelp: function (event) {
var $link = $(event.currentTarget);

View file

@ -179,17 +179,24 @@
CKEFeatureRulesMap[name].push(rule);
}
// Now convert these to Drupal.EditorFeature objects.
// Now convert these to Drupal.EditorFeature objects. And track which
// buttons are mapped to which features.
// @see getFeatureForButton()
var features = {};
var buttonsToFeatures = {};
for (var featureName in CKEFeatureRulesMap) {
if (CKEFeatureRulesMap.hasOwnProperty(featureName)) {
var feature = new Drupal.EditorFeature(featureName);
convertCKERulesToEditorFeature(feature, CKEFeatureRulesMap[featureName]);
features[featureName] = feature;
var command = e.editor.getCommand(featureName);
if (command) {
buttonsToFeatures[command.uiItems[0].name] = featureName;
}
}
}
callback(features);
callback(features, buttonsToFeatures);
}
});
},
@ -213,7 +220,7 @@
// Get a Drupal.editorFeature object that contains all metadata for
// the feature that was just added or removed. Not every feature has
// such metadata.
var featureName = button.toLowerCase();
var featureName = this.model.get('buttonsToFeatures')[button.toLowerCase()];
var featuresMetadata = this.model.get('featuresMetadata');
if (!featuresMetadata[featureName]) {
featuresMetadata[featureName] = new Drupal.EditorFeature(featureName);
@ -227,9 +234,18 @@
*
* @param {object} features
* A map of {@link Drupal.EditorFeature} objects.
* @param {object} buttonsToFeatures
* Object containing the button-to-feature mapping.
*
* @see Drupal.ckeditor.ControllerView#getFeatureForButton
*/
disableFeaturesDisallowedByFilters: function (features) {
disableFeaturesDisallowedByFilters: function (features, buttonsToFeatures) {
this.model.set('featuresMetadata', features);
// Store the button-to-feature mapping. Needs to happen only once, because
// the same buttons continue to have the same features; only the rules for
// specific features may change.
// @see getFeatureForButton()
this.model.set('buttonsToFeatures', buttonsToFeatures);
// Ensure that toolbar configuration changes are broadcast.
this.broadcastConfigurationChanges(this.$el);
@ -271,7 +287,7 @@
.detach()
.appendTo('.ckeditor-toolbar-disabled > .ckeditor-toolbar-available > ul');
// Update the toolbar value field.
this.model.set({'isDirty': true}, {broadcast: false});
this.model.set({isDirty: true}, {broadcast: false});
}
}
},

View file

@ -1,6 +1,6 @@
/**
* @file
* A Backbone View that provides the aural view of CKEditor keyboard UX configuration.
* Backbone View providing the aural view of CKEditor keyboard UX configuration.
*/
(function (Drupal, Backbone, $) {
@ -32,6 +32,7 @@
* Handles keypresses on a CKEditor configuration button.
*
* @param {jQuery.Event} event
* The keypress event triggered.
*/
onPressButton: function (event) {
var upDownKeys = [
@ -69,10 +70,11 @@
var $originalGroup = $group;
var dir;
// Move available buttons between their container and the active toolbar.
// Move available buttons between their container and the active
// toolbar.
if (containerType === 'source') {
// Move the button to the active toolbar configuration when the down or
// up keys are pressed.
// Move the button to the active toolbar configuration when the down
// or up keys are pressed.
if (_.indexOf([40, 63233], event.keyCode) > -1) {
// Move the button to the first row, first button group index
// position.
@ -142,8 +144,8 @@
}
// Move dividers between their container and the active toolbar.
else if (containerType === 'dividers') {
// Move the button to the active toolbar configuration when the down or
// up keys are pressed.
// Move the button to the active toolbar configuration when the down
// or up keys are pressed.
if (_.indexOf([40, 63233], event.keyCode) > -1) {
// Move the button to the first row, first button group index
// position.
@ -169,8 +171,8 @@
else {
view.$el.find('.ui-sortable').sortable('refresh');
}
// Refocus the target button so that the user can continue from a known
// place.
// Refocus the target button so that the user can continue from a
// known place.
$target.trigger('focus');
});
@ -183,6 +185,7 @@
* Handles keypresses on a CKEditor configuration group.
*
* @param {jQuery.Event} event
* The keypress event triggered.
*/
onPressGroup: function (event) {
var upDownKeys = [

View file

@ -34,12 +34,17 @@
},
/**
* Render function for rendering the toolbar configuration.
*
* @param {*} model
* Model used for the view.
* @param {string} [value]
* The value that was changed.
* @param {object} changedAttributes
* The attributes that was changed.
*
* @return {Drupal.ckeditor.VisualView}
* The {@link Drupal.ckeditor.VisualView} object.
*/
render: function (model, value, changedAttributes) {
this.insertPlaceholders();
@ -65,6 +70,7 @@
* Handles clicks to a button group name.
*
* @param {jQuery.Event} event
* The click event on the button group.
*/
onGroupNameClick: function (event) {
var $group = $(event.currentTarget).closest('.ckeditor-toolbar-group');
@ -78,6 +84,7 @@
* Handles clicks on the button group names toggle button.
*
* @param {jQuery.Event} event
* The click event on the toggle button.
*/
onGroupNamesToggleClick: function (event) {
this.model.set('groupNamesVisible', !this.model.get('groupNamesVisible'));
@ -88,6 +95,7 @@
* Prompts the user to provide a name for a new button group; inserts it.
*
* @param {jQuery.Event} event
* The event of the button click.
*/
onAddGroupButtonClick: function (event) {
@ -120,6 +128,7 @@
* Handles jQuery Sortable stop sort of a button group.
*
* @param {jQuery.Event} event
* The event triggered on the group drag.
* @param {object} ui
* A jQuery.ui.sortable argument that contains information about the
* elements involved in the sort action.
@ -138,6 +147,7 @@
* Handles jQuery Sortable start sort of a button.
*
* @param {jQuery.Event} event
* The event triggered on the group drag.
* @param {object} ui
* A jQuery.ui.sortable argument that contains information about the
* elements involved in the sort action.
@ -153,6 +163,7 @@
* Handles jQuery Sortable stop sort of a button.
*
* @param {jQuery.Event} event
* The event triggered on the button drag.
* @param {object} ui
* A jQuery.ui.sortable argument that contains information about the
* elements involved in the sort action.

View file

@ -127,7 +127,18 @@ class Internal extends CKEditorPluginBase implements ContainerFactoryPluginInter
*/
public function getButtons() {
$button = function($name, $direction = 'ltr') {
return '<a href="#" class="cke-icon-only cke_' . $direction . '" role="button" title="' . $name . '" aria-label="' . $name . '"><span class="cke_button_icon cke_button__' . str_replace(' ', '', $name) . '_icon">' . $name . '</span></a>';
// In the markup below, we mostly use the name (which may include spaces),
// but in one spot we use it as a CSS class, so strip spaces.
$class_name = str_replace(' ', '', $name);
return [
'#type' => 'inline_template',
'#template' => '<a href="#" class="cke-icon-only cke_{{ direction }}" role="button" title="{{ name }}" aria-label="{{ name }}"><span class="cke_button_icon cke_button__{{ classname }}_icon">{{ name }}</span></a>',
'#context' => [
'direction' => $direction,
'name' => $name,
'classname' => $class_name,
],
];
};
return array(
@ -256,7 +267,13 @@ class Internal extends CKEditorPluginBase implements ContainerFactoryPluginInter
),
'Format' => array(
'label' => t('HTML block format'),
'image_alternative' => '<a href="#" role="button" aria-label="' . t('Format') . '"><span class="ckeditor-button-dropdown">' . t('Format') . '<span class="ckeditor-button-arrow"></span></span></a>',
'image_alternative' => [
'#type' => 'inline_template',
'#template' => '<a href="#" role="button" aria-label="{{ format_text }}"><span class="ckeditor-button-dropdown">{{ format_text }}<span class="ckeditor-button-arrow"></span></span></a>',
'#context' => [
'format_text' => t('Format'),
],
],
),
// "table" plugin.
'Table' => array(
@ -282,7 +299,13 @@ class Internal extends CKEditorPluginBase implements ContainerFactoryPluginInter
// No plugin, separator "button" for toolbar builder UI use only.
'-' => array(
'label' => t('Separator'),
'image_alternative' => '<a href="#" role="button" aria-label="' . t('Button separator') . '" class="ckeditor-separator"></a>',
'image_alternative' => [
'#type' => 'inline_template',
'#template' => '<a href="#" role="button" aria-label="{{ button_separator_text }}" class="ckeditor-separator"></a>',
'#context' => [
'button_separator_text' => t('Button separator'),
],
],
'attributes' => array(
'class' => array('ckeditor-button-separator'),
'data-drupal-ckeditor-type' => 'separator',
@ -302,10 +325,6 @@ class Internal extends CKEditorPluginBase implements ContainerFactoryPluginInter
*
* @return array
* An array containing the "format_tags" configuration.
*
* @see ckeditor_rebuild()
* @see ckeditor_filter_format_insert()
* @see ckeditor_filter_format_update()
*/
protected function generateFormatTagsSetting(Editor $editor) {
// When no text format is associated yet, assume no tag is allowed.
@ -315,9 +334,35 @@ class Internal extends CKEditorPluginBase implements ContainerFactoryPluginInter
}
$format = $editor->getFilterFormat();
// The <p> tag is always allowed — HTML without <p> tags is nonsensical.
$default = 'p';
return \Drupal::state()->get('ckeditor_internal_format_tags:' . $format->id(), $default);
$cid = 'ckeditor_internal_format_tags:' . $format->id();
if ($cached = $this->cache->get($cid)) {
$format_tags = $cached->data;
}
else {
// The <p> tag is always allowed — HTML without <p> tags is nonsensical.
$format_tags = ['p'];
// Given the list of possible format tags, automatically determine whether
// the current text format allows this tag, and thus whether it should show
// up in the "Format" dropdown.
$possible_format_tags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre'];
foreach ($possible_format_tags as $tag) {
$input = '<' . $tag . '>TEST</' . $tag . '>';
$output = trim(check_markup($input, $editor->id()));
if ($input == $output) {
$format_tags[] = $tag;
}
}
$format_tags = implode(';', $format_tags);
// Cache the "format_tags" configuration. This cache item is infinitely
// valid; it only changes whenever the text format is changed, hence it's
// tagged with the text format's cache tag.
$this->cache->set($cid, $format_tags, Cache::PERMANENT, $format->getCacheTags());
}
return $format_tags;
}
/**

View file

@ -59,7 +59,13 @@ class StylesCombo extends CKEditorPluginBase implements CKEditorPluginConfigurab
return array(
'Styles' => array(
'label' => t('Font style'),
'image_alternative' => '<a href="#" role="button" aria-label="' . t('Styles') . '"><span class="ckeditor-button-dropdown">' . t('Styles') . '<span class="ckeditor-button-arrow"></span></span></a>',
'image_alternative' => [
'#type' => 'inline_template',
'#template' => '<a href="#" role="button" aria-label="{{ styles_text }}"><span class="ckeditor-button-dropdown">{{ styles_text }}<span class="ckeditor-button-arrow"></span></span></a>',
'#context' => [
'styles_text' => t('Styles'),
],
],
),
);
}

View file

@ -331,10 +331,12 @@ class CKEditor extends EditorBase implements ContainerFactoryPluginInterface {
if (empty($langcodes)) {
$langcodes = array();
// Collect languages included with CKEditor based on file listing.
$ckeditor_languages = new \GlobIterator(\Drupal::root() . '/core/assets/vendor/ckeditor/lang/*.js');
foreach ($ckeditor_languages as $language_file) {
$langcode = $language_file->getBasename('.js');
$langcodes[$langcode] = $langcode;
$files = scandir('core/assets/vendor/ckeditor/lang');
foreach ($files as $file) {
if ($file[0] !== '.' && fnmatch('*.js', $file)) {
$langcode = basename($file, '.js');
$langcodes[$langcode] = $langcode;
}
}
\Drupal::cache()->set('ckeditor.langcodes', $langcodes);
}
@ -414,7 +416,7 @@ class CKEditor extends EditorBase implements ContainerFactoryPluginInterface {
public function buildContentsCssJSSetting(EditorEntity $editor) {
$css = array(
drupal_get_path('module', 'ckeditor') . '/css/ckeditor-iframe.css',
drupal_get_path('module', 'system') . '/css/system.module.css',
drupal_get_path('module', 'system') . '/css/components/align.module.css',
);
$this->moduleHandler->alter('ckeditor_css', $css, $editor);
$css = array_merge($css, _ckeditor_theme_css());

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