responsive_preview-8.x-1.0/src/ResponsivePreview.php
src/ResponsivePreview.php
<?php
namespace Drupal\responsive_preview;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Ajax\SettingsCommand;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\AdminContext;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\node\NodeForm;
use Drupal\node\NodeInterface;
use Drupal\node\NodeTypeInterface;
use Drupal\responsive_preview\Entity\Device;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Responsive preview service.
*/
class ResponsivePreview {
const RESPONSIVE_PREVIEW_QUERY_PARAMETER = 'responsive_preview';
const RESPONSIVE_PREVIEW_ENABLED = 'enabled';
use StringTranslationTrait;
/**
* ResponsivePreview constructor.
*
* @param \Drupal\Core\Routing\AdminContext $routerAdminContext
* Admin context service.
* @param \Drupal\Core\Routing\RouteMatchInterface $routeMatch
* The route match service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
* @param \Drupal\Core\Session\AccountProxyInterface $currentUser
* The current user.
* @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
* The request stack.
*/
public function __construct(
protected AdminContext $routerAdminContext,
protected RouteMatchInterface $routeMatch,
protected EntityTypeManagerInterface $entityTypeManager,
protected AccountProxyInterface $currentUser,
protected RequestStack $requestStack
) {
}
/**
* Helper to show responsive preview and returns the url for the preview.
*/
public function getPreviewUrl() {
// Determine if responsive preview should be available for this node type.
$url = NULL;
if ($this->routeMatch->getRouteName() === 'node.add') {
$node_type = $this->routeMatch->getParameter('node_type');
if ($node_type && !$this->previewEnabled($node_type)) {
return NULL;
}
$url = Url::fromRoute('<front>');
}
elseif ($form = $this->routeMatch->getRouteObject()->getDefault("_entity_form")) {
$entity_type_id = current(explode('.', $form));
/** @var \Drupal\Core\Entity\EntityInterface $entity */
if (!($entity = $this->routeMatch->getParameter($entity_type_id))) {
return NULL;
}
if ($entity instanceof NodeInterface && !$this->previewEnabled($entity->type->entity)) {
return NULL;
}
if ($entity->hasLinkTemplate('canonical')) {
$url = $entity->toUrl();
}
}
if (!$this->routerAdminContext->isAdminRoute()) {
$url = Url::fromRouteMatch($this->routeMatch);
}
return ($url instanceof Url)
? static::preparePreviewUrl($url)
: NULL;
}
/**
* Prepares the preview URL to format it as expected.
*
* @param \Drupal\Core\Url $url
* The URL object.
*
* @return string
* The preview URL in string format with the proper format.
*/
protected static function preparePreviewUrl(Url $url): string {
$url_options = [
'query' => [
static::RESPONSIVE_PREVIEW_QUERY_PARAMETER => static::RESPONSIVE_PREVIEW_ENABLED,
],
'absolute' => TRUE
];
return $url->mergeOptions($url_options)->toString();
}
/**
* Preview is enabled.
*
* @param \Drupal\node\NodeTypeInterface $node_type
* The node type.
*
* @return bool
* TRUE if preview mode is not disabled.
*/
public function previewEnabled(NodeTypeInterface $node_type) {
return !($node_type->getPreviewMode() === DRUPAL_DISABLED);
}
/**
* Returns an array of enabled devices, suitable for rendering.
*
* @return array
* A render array of enabled devices.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function getRenderableDevicesList() {
$links = [];
/** @var \Drupal\responsive_preview\Entity\Device[] $devices */
$devices = $this->entityTypeManager
->getStorage('responsive_preview_device')
->loadByProperties(['status' => 1]);
uasort($devices, [Device::class, 'sort']);
foreach ($devices as $name => $entity) {
$dimensions = $entity->getDimensions();
$links[$name] = [
'#type' => 'html_tag',
'#tag' => 'button',
'#value' => $entity->label(),
'#attributes' => [
'data-responsive-preview-name' => $name,
'data-responsive-preview-width' => $dimensions['width'],
'data-responsive-preview-height' => $dimensions['height'],
'data-responsive-preview-dppx' => $dimensions['dppx'],
'class' => [
'responsive-preview-device',
'responsive-preview-icon',
'responsive-preview-icon-active',
],
],
];
}
// Add a configuration link.
$links['configure_link'] = [
'#type' => 'link',
'#title' => $this->t('Configure devices'),
'#url' => Url::fromRoute('entity.responsive_preview_device.collection'),
'#access' => $this->currentUser->hasPermission('administer responsive preview'),
'#attributes' => [
'class' => ['responsive-preview-configure'],
],
];
return [
'#theme' => 'item_list__responsive_preview',
'#items' => $links,
'#attributes' => [
'class' => ['responsive-preview-options'],
],
'#wrapper_attributes' => [
'class' => ['responsive-preview-item-list'],
],
];
}
/**
* Handling of form alter, for responsive preview.
*
* Request to this method is piped from module related hook_form_alter().
*
* @param array $form
* Form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* Form state object.
* @param string $form_id
* Form ID.
*/
public function formAlter(array &$form, FormStateInterface $form_state, $form_id) {
if (!$form_state->getFormObject() instanceof NodeForm) {
return;
}
/** @var \Drupal\Core\Entity\Entity $entity */
$node = $form_state->getFormObject()->getEntity();
if ($node instanceof NodeInterface) {
$preview_mode = $node->type->entity->getPreviewMode();
$form['ajax_responsive_preview'] = [
'#type' => 'hidden',
'#name' => 'ajax_responsive_preview',
'#id' => 'ajax_responsive_preview',
'#attributes' => ['id' => 'ajax_responsive_preview'],
'#access' => $preview_mode != DRUPAL_DISABLED && ($node->access('create') || $node->access('update')),
'#submit' => $form['actions']['preview']['#submit'],
'#executes_submit_callback' => TRUE,
'#ajax' => [
'callback' => [
__CLASS__,
'handleAjaxDevicePreview',
],
'event' => 'show-responsive-preview',
'progress' => [
'type' => 'fullscreen',
],
],
'#attached' => [
'drupalSettings' => [
'responsive_preview' => [
'ajax_responsive_preview' => '#ajax_responsive_preview',
],
],
],
];
}
}
/**
* Handles response for AJAX request.
*
* @param array $form
* From array object.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* Form state.
*
* @return \Drupal\Core\Ajax\AjaxResponse|array
* Returns AJAX response object.
*/
public static function handleAjaxDevicePreview(array $form, FormStateInterface $form_state) {
// If there are no errors and everything is fine, then result for opening
// responsive preview will be generated.
$ajax = new AjaxResponse();
// Handling of errors is a bit tricky and here is workaround with triggering
// of normal preview functionality.
if (count($form_state->getErrors()) > 0) {
// Clearing error messages, because they will be generated by clicking on
// "Preview" button.
\Drupal::messenger()->deleteAll();
// Triggering click on "Preview" button, in order to get error messages
// properly displayed in UI, since it's not possible to propagate them
// nicely over AJAX response.
$ajax->addCommand(
new InvokeCommand('#edit-preview', 'click')
);
}
elseif (($triggering_element = $form_state->getTriggeringElement()) && $triggering_element['#name'] === 'ajax_responsive_preview') {
$form_state->disableRedirect(FALSE);
$redirectUrl = $form_state->getRedirect();
$form_state->disableRedirect();
$ajax->addCommand(
new SettingsCommand(
[
'responsive_preview' => [
'url' => static::preparePreviewUrl($redirectUrl),
],
],
TRUE
),
TRUE
);
$deviceId = $form_state->getValue($triggering_element['#name']);
$ajax->addCommand(
new InvokeCommand("[data-responsive-preview-name='{$deviceId}']", 'trigger', ['open-preview'])
);
}
return $ajax;
}
/**
* Implements hook_toolbar().
*/
public function previewToolbar() {
$items = [];
$items['responsive_preview'] = [
'#cache' => [
'contexts' => [
'user.permissions',
],
],
];
if (!$this->currentUser->hasPermission('access responsive preview')) {
return $items;
}
$device_definition = $this->entityTypeManager->getDefinition('responsive_preview_device');
$items['responsive_preview']['#cache']['tags'] = Cache::mergeTags(
$device_definition->getListCacheTags(),
['config:node_type_list'],
);
$items['responsive_preview']['#cache']['contexts'] = Cache::mergeContexts(
$items['responsive_preview']['#cache']['contexts'],
['route.is_admin', 'url'],
);
$url = $this->getPreviewUrl();
if ($url) {
$items['responsive_preview'] += [
'#type' => 'toolbar_item',
'#weight' => 50,
'tab' => [
'trigger' => [
'#type' => 'html_tag',
'#tag' => 'button',
'#value' => $this->t('<span class="visually-hidden">Layout preview</span>'),
'#attributes' => [
'title' => $this->t('Preview page layout'),
'class' => [
'responsive-preview-icon',
'responsive-preview-icon-responsive-preview',
'responsive-preview-trigger',
],
],
],
'device_options' => $this->getRenderableDevicesList(),
],
'#wrapper_attributes' => [
'id' => 'responsive-preview-toolbar-tab',
'class' => ['toolbar-tab-responsive-preview'],
],
'#attached' => [
'library' => ['responsive_preview/drupal.responsive-preview'],
'drupalSettings' => [
'responsive_preview' => [
'url' => $url,
],
],
],
];
}
return $items;
}
/**
* Implements hook_preprocess_html().
*/
public function preprocessHtml(array &$variables): void {
// Remove the admin toolbar/navigation when in same page preview mode.
if ($this->currentUser->hasPermission('access responsive preview') &&
$this->requestStack->getCurrentRequest()->query->get(static::RESPONSIVE_PREVIEW_QUERY_PARAMETER) === static::RESPONSIVE_PREVIEW_ENABLED
) {
unset($variables['page_top']['toolbar'], $variables['page_top']['navigation'], $variables['page_top']['top_bar']);
// @todo Needs additional cleanup to remove toolbar related classes on body.
// Remove contextual links.
$variables['page']['#attached']['library'] = array_values(array_diff($variables['page']['#attached']['library'], ['contextual/drupal.contextual-links']));
}
}
}
