io-8.x-1.x-dev/src/IoManager.php
src/IoManager.php
<?php
namespace Drupal\io;
use Drupal\Core\Block\BlockManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Url;
use Drupal\blazy\BlazyManagerInterface;
use Drupal\block\BlockForm;
use Drupal\block\BlockInterface;
use Drupal\io\Plugin\views\pager\IoPager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides IoManager service.
*/
class IoManager implements IoManagerInterface {
use StringTranslationTrait;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* Drupal\Core\Block\BlockManagerInterface.
*
* @var \Drupal\Core\Block\BlockManagerInterface
*/
protected $blockManager;
/**
* The blazy manager service.
*
* @var \Drupal\blazy\BlazyManagerInterface
*/
protected $blazyManager;
/**
* Checks if IO block is disabled.
*
* @var bool
*/
protected $isIoBlockDisabled;
/**
* Constructs a BlazyManager object.
*/
public function __construct(AccountInterface $current_user, RouteMatchInterface $route_match, BlockManagerInterface $block_manager, BlazyManagerInterface $blazy_manager) {
$this->currentUser = $current_user;
$this->routeMatch = $route_match;
$this->blockManager = $block_manager;
$this->blazyManager = $blazy_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('current_user'),
$container->get('current_route_match'),
$container->get('plugin.manager.block'),
$container->get('blazy.manager')
);
}
/**
* {@inheritdoc}
*/
public function currentUser() {
return $this->currentUser;
}
/**
* {@inheritdoc}
*/
public function routeMatch() {
return $this->routeMatch;
}
/**
* {@inheritdoc}
*/
public function blockManager() {
return $this->blockManager;
}
/**
* {@inheritdoc}
*/
public function blazyManager() {
return $this->blazyManager;
}
/**
* {@inheritdoc}
*/
public function loadEntityByUuid($uuid, $entity_type = 'block'): ?object {
return $this->blazyManager->loadByUuid($uuid, $entity_type);
}
/**
* {@inheritdoc}
*/
public function isBlockApplicable(BlockInterface $block): bool {
if ($this->isIoBlockDisabled()) {
return FALSE;
}
// Excludes from Ultimenu which can ajaxify the entire region instead.
$region = $block->getRegion();
$settings = $block->get('settings');
// Excludes crucial blocks, or those not worth being ajaxified.
if (in_array($block->getPluginId(), $this->excludedBlockPluginIds())
|| (isset($settings['provider']) && in_array($settings['provider'], $this->excludedBlockProviders()))
|| ($region && strpos($region, 'ultimenu_') !== FALSE)) {
return FALSE;
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public function isAllowedBlock(BlockInterface $block): bool {
$this->checkVisibilityConfig($block);
$access = $block->access('view', $this->currentUser, TRUE);
return $access->isAllowed();
}
/**
* {@inheritdoc}
*/
public function getIoPager($view): ?object {
if ($view && $view->ajaxEnabled() && $view->getDisplay()->isPagerEnabled()) {
$pager = $view->getPager();
if ($pager && $pager instanceof IoPager) {
return $pager;
}
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function getIoSettings($type = 'block'): array {
$is_block = $type == 'block';
return [
// Place loader animation inside link element, suitable for replaceWith
// method. If using html method, disable inside so the loader will be
// placed after the triggering element.
'inside' => TRUE,
'addNow' => TRUE,
'selector' => $is_block ? '[data-io-block-trigger]' : '[data-io-pager-trigger]',
'errorClass' => $is_block ? 'io__error' : 'pager__error',
'successClass' => $is_block ? 'io__loaded' : 'pager__loaded',
] + (array) $this->blazyManager->getIoSettings();
}
/**
* {@inheritdoc}
*/
public function blockFormAlter(array &$form, FormStateInterface $form_state, $form_id): void {
// See https://www.drupal.org/node/2897557.
if ($this->isIoBlockDisabled() || !isset($form_state->getBuildInfo()['callback_object'])) {
return;
}
$object = $form_state->getFormObject();
if (!($object instanceof BlockForm)) {
return;
}
/** @var \Drupal\block\BlockInterface $block */
$block = $object->getEntity();
if (!($block instanceof BlockInterface)
|| !$this->isBlockApplicable($block)) {
return;
}
// This will automatically be saved in the third party settings.
$form['third_party_settings']['#tree'] = TRUE;
$form['third_party_settings']['io']['lazyload'] = [
'#type' => 'checkbox',
'#title' => $this->t('Lazyload using Intersection Observer'),
'#description' => $this->t("Reasonable for non-essential or peripheral blocks below the fold, or at sidebars. Widgets like Facebook, Twitter, Google maps, or other third party, statistics, heavy-logic etc. are good candidates. Do not enable this for <a href=':url'>Ultimenu 2.x</a> regions as it is capable of ajaxifying the entire region instead. Check out more excluded blocks at <b>/admin/help/io</b>.", [':url' => 'https://drupal.org/project/ultimenu']),
'#default_value' => $block->getThirdPartySetting('io', 'lazyload'),
];
}
/**
* {@inheritdoc}
*/
public function blazySettingsFormAlter(array &$form): void {
$settings = $this->blazyManager->config();
// Hooks into Blazy UI to support Blazy Filter.
if (isset($settings['admin_css'])) {
$form['extras']['#access'] = TRUE;
$form['extras']['io_fallback'] = [
'#type' => 'textfield',
'#title' => $this->t('IO fallback'),
'#default_value' => $settings['extras']['io_fallback'] ?? '',
'#description' => $this->t('Text to display when an AJAX block fails loading its content. Default to: <b>Loading... Click here if it takes longer.</b>'),
];
$form['extras']['io_block_disabled'] = [
'#type' => 'checkbox',
'#title' => $this->t('Disable IO block'),
'#default_value' => $settings['extras']['io_block_disabled'] ?? '',
'#description' => $this->t('Check here to disable lazyloading blocks sitewide. Only infinite IO pager will be available at Views UI.'),
];
// Adds relevant IO AJAX description to existing Blazy IO options.
$description = $form['io']['disconnect']['#description'];
$form['io']['disconnect']['#description'] = $description . ' ' . $this->t('The same applies to IO ajaxified block and pager observers. The IO must stand-by and be able to watch the next/ subsequent AJAX results. No expensive methods executed on being stand-by. Each item will be unobserved once loaded, instead.');
}
}
/**
* {@inheritdoc}
*/
public function preprocessBlock(array &$variables): void {
if ($this->isIoBlockDisabled()) {
return;
}
// Replace the block content with a fallback, so that we can lazy load it.
// In case the AJAX fails, the user has a link to load/ click it manually.
$uuid = $variables['elements']['#io'];
$variables['content'] = [];
$variables['attributes']['class'][] = 'block--io io';
// Cannot use regular `use-ajax` class as we need to work out errors.
$classes = ['io__lazy'];
if (function_exists('ajaxin')) {
$classes[] = 'io__loading';
}
else {
$classes[] = 'is-b-loading';
$variables['#attached']['library'][] = 'blazy/loading';
}
$variables['content']['io'] = [
'#type' => 'link',
'#title' => [
'#markup' => '<span class="io__text">' . $this->getFallbackText() . '</span>',
'#allowed_tags' => ['small', 'span', 'strong'],
],
'#attributes' => [
'class' => $classes,
'data-io-block-trigger' => TRUE,
'rel' => 'nofollow',
],
'#url' => $this->getBlockUrl($uuid),
];
}
/**
* {@inheritdoc}
*/
public function getBlockUrl($uuid): object {
return Url::fromRoute('io.block', ['ioid' => $uuid]);
}
/**
* {@inheritdoc}
*/
public function isIoBlockDisabled(): bool {
if (!isset($this->isIoBlockDisabled)) {
$this->isIoBlockDisabled = $this->blazyManager->config('extras.io_block_disabled') ?: FALSE;
}
return $this->isIoBlockDisabled;
}
/**
* {@inheritdoc}
*/
public function getFallbackText(): object {
return $this->t('@text', [
'@text' => $this->blazyManager->config('extras.io_fallback')
?: 'Loading... Click here if it takes longer.',
]);
}
/**
* {@inheritdoc}
*/
public function preprocessIoPager(array &$variables): void {
/** @var \Drupal\Core\Pager\PagerManagerInterface $pager_manager */
$pager_manager = $this->blazyManager->service('pager.manager');
if (!$pager_manager) {
return;
}
$element = $variables['element'];
$parameters = $variables['parameters'];
// Nothing to do if there is only one page.
$pager = $pager_manager->getPager($element);
if (!$pager) {
return;
}
$current = $pager->getCurrentPage();
$total = $pager->getTotalPages();
// Current is the page we are currently paged to.
$variables['items']['current'] = $current + 1;
// Calculate various markers within this pager piece:
if ($current < ($total - 1)) {
$options = [
'query' => $pager_manager->getUpdatedParameters($parameters, $element, $current + 1),
];
$variables['items']['next']['href'] = Url::fromRoute('<current>', [], $options)->toString();
$variables['items']['next']['attributes'] = new Attribute();
}
// This is based on the entire current query string. We need to ensure
// cacheability is affected accordingly.
$variables['#cache']['contexts'][] = 'url.query_args';
if (!$this->blazyManager->moduleExists('ajaxin')
&& !empty($variables['options']['autoload'])) {
$variables['content_attributes']['class'][] = 'is-b-loading';
}
}
/**
* Checks IO block visibility config, and includes IO block route.
*/
protected function checkVisibilityConfig(BlockInterface &$block): void {
if ($block && $visibility_config = $block->getVisibility()) {
if (isset($visibility_config['request_path'])
&& $request_path = $visibility_config['request_path']) {
// Include our path into visibility unless sitewide or negated.
if (!empty($request_path['pages']) && empty($request_path['negate'])) {
$pages = "/io/block\r\n";
$pages .= $request_path['pages'];
$request_path['pages'] = $pages;
$block->setVisibilityConfig('request_path', $request_path);
}
}
}
}
/**
* Excludes crucial blocks, or those not worth being ajaxified.
*/
private function excludedBlockPluginIds(): array {
$excludes = [
'help_block',
'local_tasks_block',
'node_syndicate_block',
'page_title_block',
'search_form_block',
'system_branding_block',
'system_breadcrumb_block',
'system_main_block',
'system_messages_block',
'user_login_block',
];
$this->blazyManager->moduleHandler()->alter('io_excluded_block_plugin_ids', $excludes);
return array_unique($excludes);
}
/**
* Excludes blocks by modules.
*/
private function excludedBlockProviders(): array {
$excludes = [
'ultimenu',
'jumper',
];
$this->blazyManager->moduleHandler()->alter('io_excluded_block_providers', $excludes);
return array_unique($excludes);
}
}
