Commit d72c0f98 authored by webchick's avatar webchick

Issue #1510544 by swentel, Bojhan, Gábor Hojtsy, merlinofchaos, Cottser, Wim...

Issue #1510544 by swentel, Bojhan, Gábor Hojtsy, merlinofchaos, Cottser, Wim Leers, plopesc, aspilicious, sannejanssen,  larowlan, tim.plunkett, nod_: Fixed Show previews in front-end theme, able to select different view modes.
parent 7fde4cf6
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#000000" d="M7.951 7.645c-.193.196-.193.516 0 .71l3.258 3.29c.193.193.191.519-.002.709l-1.371 1.371c-.193.192-.512.191-.707 0l-5.335-5.371c-.194-.194-.194-.514 0-.708l5.335-5.369c.195-.195.514-.195.707-.001l1.371 1.371c.193.194.195.513.002.709l-3.258 3.289z"/></svg>
......@@ -35,6 +35,7 @@ class BlockConfigSchemaTest extends KernelTestBase {
// BlockManager->getModuleName() calls system_get_info().
'system',
'taxonomy',
'user',
);
/**
......
......@@ -87,6 +87,7 @@ function testNodeDisplay() {
$edit[$field_name . '[0][display]'] = FALSE;
$edit[$field_name . '[1][display]'] = FALSE;
$this->drupalPostForm("node/$nid/edit", $edit, t('Preview'));
$this->clickLink(t('Back to content editing'));
$this->assertRaw($field_name . '[0][display]', 'First file appears as expected.');
$this->assertRaw($field_name . '[1][display]', 'Second file appears as expected.');
}
......
......@@ -234,7 +234,7 @@ function testFormatWidgetPermissions() {
$this->assertText($edit[$body_value_key], 'Old body found in preview.');
// Save and verify that only the title was changed.
$this->drupalPostForm(NULL, $new_edit, t('Save'));
$this->drupalPostForm('node/' . $node->id() . '/edit', $new_edit, t('Save'));
$this->assertNoText($edit['title[0][value]'], 'Old title not found.');
$this->assertText($new_edit['title[0][value]'], 'New title found.');
$this->assertText($edit[$body_value_key], 'Old body found.');
......
/**
* @file
* Styles for node preview page.
*/
.node-preview-container {
position: fixed;
z-index: 499;
width: 100%;
padding: 10px;
}
@media only screen and (min-width: 36em) {
.node-preview-container .form-type-select {
margin-left: 25%;
}
}
......@@ -10,12 +10,16 @@ drupal.node:
drupal.node.preview:
version: VERSION
css:
theme:
css/node.preview.css: {}
js:
node.preview.js: {}
dependencies:
- core/jquery
- core/jquery.once
- core/drupal
- core/drupal.form
drupal.content_types:
version: VERSION
......
......@@ -169,11 +169,6 @@ function node_theme() {
'file' => 'node.pages.inc',
'template' => 'node-add-list',
),
'node_preview' => array(
'variables' => array('node' => NULL),
'file' => 'node.pages.inc',
'template' => 'node-preview',
),
'node_edit_form' => array(
'render element' => 'form',
'template' => 'node-edit-form',
......@@ -627,7 +622,10 @@ function template_preprocess_node(&$variables) {
));
$variables['label'] = $variables['elements']['title'];
unset($variables['elements']['title']);
$variables['page'] = $variables['view_mode'] == 'full' && node_is_page($node);
// The 'page' variable is set to TRUE in two occasions:
// - The view mode is 'full' and we are on the 'node.view' route.
// - The node is in preview and view mode is either 'full' or 'default'.
$variables['page'] = ($variables['view_mode'] == 'full' && (node_is_page($node)) || (isset($node->in_preview) && in_array($node->preview_view_mode, array('full', 'default'))));
// Helpful $content variable for templates.
$variables += array('content' => array());
......@@ -669,7 +667,7 @@ function template_preprocess_node(&$variables) {
if ($variables['view_mode']) {
$variables['attributes']['class'][] = drupal_html_class('node--view-mode-' . $variables['view_mode']);
}
if (isset($variables['preview'])) {
if (isset($node->preview)) {
$variables['attributes']['class'][] = 'node--preview';
}
}
......@@ -1044,6 +1042,25 @@ function node_view_multiple($nodes, $view_mode = 'teaser', $langcode = NULL) {
return entity_view_multiple($nodes, $view_mode, $langcode);
}
/**
* Implements hook_page_build().
*/
function node_page_build(&$page) {
// Add 'Back to content editing' link on preview page.
$route_match = \Drupal::routeMatch();
if ($route_match->getRouteName() == 'entity.node.preview') {
$page['page_top']['node_preview'] = array(
'#type' => 'container',
'#attributes' => array(
'class' => array('node-preview-container', 'container-inline')
),
);
$form = \Drupal::formBuilder()->getForm('\Drupal\node\Form\NodePreviewForm', $route_match->getParameter('node_preview'));
$page['page_top']['node_preview']['view_mode'] = $form;
}
}
/**
* Implements hook_form_FORM_ID_alter().
*
......
......@@ -37,68 +37,3 @@ function template_preprocess_node_add_list(&$variables) {
}
}
}
/**
* Generates a node preview.
*
* @param \Drupal\node\NodeInterface $node
* The node to preview.
*
* @return
* An HTML-formatted string of a node preview.
*
* @see node_form_build_preview()
*/
function node_preview(NodeInterface $node, FormStateInterface $form_state) {
if ($node->access('create') || $node->access('update')) {
$node->changed = REQUEST_TIME;
// Display a preview of the node.
if (!form_get_errors($form_state)) {
$node->in_preview = TRUE;
$node_preview = array(
'#theme' => 'node_preview',
'#node' => $node,
);
$output = drupal_render($node_preview);
unset($node->in_preview);
}
return $output;
}
}
/**
* Prepares variables for node preview templates.
*
* Default template: node-preview.html.twig.
*
* @param array $variables
* An associative array containing:
* - node: The node entity which is being previewed.
*
* @see NodeForm::preview()
* @see node_preview()
*/
function template_preprocess_node_preview(&$variables) {
$node = $variables['node'];
// Render trimmed teaser version of the post.
$node_teaser = node_view($node, 'teaser');
$node_teaser['#attached']['library'][] = 'node/drupal.node.preview';
$variables['teaser'] = $node_teaser;
// Render full version of the post.
$node_full = node_view($node, 'full');
$variables['full'] = $node_full;
// Display a preview of the teaser only if the content of the teaser is
// different to the full post.
if ($variables['teaser'] != $variables['full']) {
drupal_set_message(t('The trimmed version of your post shows what your post looks like when promoted to the main page or when exported for syndication.<span class="no-js"> You can insert the delimiter "&lt;!--break--&gt;" (without the quotes) to fine-tune where your post gets split.</span>'));
$variables['preview_teaser'] = TRUE;
}
else {
$variables['preview_teaser'] = FALSE;
}
}
......@@ -8,16 +8,16 @@
*/
Drupal.behaviors.nodePreviewDestroyLinks = {
attach: function (context) {
var $preview = $(context).find('.node').once('node-preview');
var $preview = $(context).find('.page-node-preview').once('node-preview');
if ($preview.length) {
$preview.on('click.preview', 'a:not([href^=#])', function (e) {
$preview.on('click.preview', 'a:not([href^=#], #edit-backlink, #toolbar-administration a)', function (e) {
e.preventDefault();
});
}
},
detach: function (context, settings, trigger) {
if (trigger === 'unload') {
var $preview = $(context).find('.node').removeOnce('node-preview');
var $preview = $(context).find('.page-node-preview').removeOnce('node-preview');
if ($preview.length) {
$preview.off('click.preview');
}
......@@ -25,4 +25,18 @@
}
};
/**
* Switch view mode.
*/
Drupal.behaviors.nodePreviewSwitchViewMode = {
attach: function (context) {
var $autosubmit = $(context).find('[data-drupal-autosubmit]').once('autosubmit');
if ($autosubmit.length) {
$autosubmit.on('formUpdated.preview', function() {
$(this.form).trigger('submit');
});
}
}
};
})(jQuery, Drupal);
node.multiple_delete_confirm:
path: '/admin/content/node/delete'
defaults:
......@@ -37,6 +36,18 @@ node.add:
options:
_node_operation_route: TRUE
entity.node.preview:
path: '/node/preview/{node_preview}/{view_mode_id}'
defaults:
_content: '\Drupal\node\Controller\NodePreviewController::view'
_title_callback: '\Drupal\node\Controller\NodePreviewController::title'
requirements:
_node_preview_access: '{node_preview}'
options:
parameters:
node_preview:
type: 'node_preview'
entity.node.canonical:
path: '/node/{node}'
defaults:
......
......@@ -19,8 +19,18 @@ services:
arguments: ['@entity.manager']
tags:
- { name: access_check, applies_to: _node_add_access }
access_check.node.preview:
class: Drupal\node\Access\NodePreviewAccessCheck
arguments: ['@entity.manager']
tags:
- { name: access_check, applies_to: _node_preview_access }
node.admin_path.route_subscriber:
class: Drupal\node\EventSubscriber\NodeAdminRouteSubscriber
arguments: ['@config.factory']
tags:
- { name: event_subscriber }
node_preview:
class: Drupal\node\ParamConverter\NodePreviewConverter
arguments: ['@user.tempstore']
tags:
- { name: paramconverter }
<?php
/**
* @file
* Contains \Drupal\node\Access\NodePreviewAccessCheck.
*/
namespace Drupal\node\Access;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\node\NodeInterface;
/**
* Determines access to node previews.
*/
class NodePreviewAccessCheck implements AccessInterface {
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* Constructs a EntityCreateAccessCheck object.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
*/
public function __construct(EntityManagerInterface $entity_manager) {
$this->entityManager = $entity_manager;
}
/**
* Checks access to the node preview page.
*
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
* @param \Drupal\node\NodeInterface $node_preview
* The node that is being previewed.
*
* @return string
* A \Drupal\Core\Access\AccessInterface constant value.
*/
public function access(AccountInterface $account, NodeInterface $node_preview) {
if ($node_preview->isNew()) {
$access_controller = $this->entityManager->getAccessControlHandler('node');
return $access_controller->createAccess($node_preview->bundle(), $account) ? static::ALLOW : static::DENY;
}
else {
return $node_preview->access('update', $account) ? static::ALLOW : static::DENY;
}
}
}
<?php
/**
* @file
* Contains \Drupal\node\Controller\NodePreviewController.
*/
namespace Drupal\node\Controller;
use Drupal\Component\Utility\String;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Controller\EntityViewController;
/**
* Defines a controller to render a single node in preview.
*/
class NodePreviewController extends EntityViewController {
/**
* {@inheritdoc}
*/
public function view(EntityInterface $node_preview, $view_mode_id = 'full', $langcode = NULL) {
// Do not cache this page.
drupal_page_is_cacheable(FALSE);
$node_preview->preview_view_mode = $view_mode_id;
$build = array('nodes' => parent::view($node_preview, $view_mode_id));
$build['#attached']['library'][] = 'node/drupal.node.preview';
$build['#title'] = $build['nodes']['#title'];
unset($build['nodes']['#title']);
// Don't render cache previews.
unset($build['nodes']['#cache']);
foreach ($node_preview->uriRelationships() as $rel) {
// Set the node path as the canonical URL to prevent duplicate content.
$build['#attached']['drupal_add_html_head_link'][] = array(
array(
'rel' => $rel,
'href' => $node_preview->url($rel),
)
, TRUE);
if ($rel == 'canonical') {
// Set the non-aliased canonical path as a default shortlink.
$build['#attached']['drupal_add_html_head_link'][] = array(
array(
'rel' => 'shortlink',
'href' => $node_preview->url($rel, array('alias' => TRUE)),
)
, TRUE);
}
}
return $build;
}
/**
* The _title_callback for the page that renders a single node in preview.
*
* @param \Drupal\Core\Entity\EntityInterface $node_preview
* The current node.
*
* @return string
* The page title.
*/
public function title(EntityInterface $node_preview) {
return String::checkPlain($this->entityManager->getTranslationFromContext($node_preview)->label());
}
}
<?php
/**
* @file
* Contains \Drupal\node\Form\NodePreviewForm.
*/
namespace Drupal\node\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Contains a form for switching the view mode of a node during preview.
*/
class NodePreviewForm extends FormBase implements ContainerInjectionInterface {
/**
* The entity manager service.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('entity.manager'), $container->get('config.factory'));
}
/**
* Constructs a new NodePreviewForm.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
*/
public function __construct(EntityManagerInterface $entity_manager, ConfigFactoryInterface $config_factory) {
$this->entityManager = $entity_manager;
$this->configFactory = $config_factory;
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'node_preview_form_select';
}
/**
* Form constructor.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param \Drupal\Core\Entity\EntityInterface $node
* The node being previews
*
* @return array
* The form structure.
*/
public function buildForm(array $form, FormStateInterface $form_state, EntityInterface $node = NULL) {
$view_mode = $node->preview_view_mode;
$query_options = $node->isNew() ? array('query' => array('uuid' => $node->uuid())) : array();
$form['backlink'] = array(
'#type' => 'link',
'#title' => $this->t('Back to content editing'),
'#href' => $node->isNew() ? 'node/add/' . $node->bundle() : 'node/' . $node->id() . '/edit',
'#options' => array('attributes' => array('class' => array('node-preview-backlink'))) + $query_options,
);
$view_mode_options = $this->getViewModeOptions($node);
$form['uuid'] = array(
'#type' => 'value',
'#value' => $node->uuid(),
);
$form['view_mode'] = array(
'#type' => 'select',
'#title' => $this->t('View mode'),
'#options' => $view_mode_options,
'#default_value' => $view_mode,
'#attributes' => array(
'data-drupal-autosubmit' => TRUE,
)
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => $this->t('Switch'),
'#attributes' => array(
'class' => array('js-hide'),
),
);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$form_state->setRedirect('entity.node.preview', array(
'node_preview' => $form_state['values']['uuid'],
'view_mode_id' => $form_state['values']['view_mode'],
));
}
/**
* Retrieves the list of available view modes for the current node.
*
* @param EntityInterface $node
* The node being previewed.
*
* @return array
* List of available view modes for the current node.
*/
protected function getViewModeOptions(EntityInterface $node) {
$load_ids = array();
$view_mode_options = array();
// Load all the node's view modes.
$view_modes = $this->entityManager->getViewModes('node');
// Get the list of available view modes for the current node's bundle.
$ids = $this->configFactory->listAll('entity.view_display.node.' . $node->bundle());
foreach ($ids as $id) {
$config_id = str_replace('entity.view_display' . '.', '', $id);
$load_ids[] = $config_id;
}
$displays = entity_load_multiple('entity_view_display', $load_ids);
// Generate the display options array.
foreach ($displays as $display) {
$view_mode_name = $display->get('mode');
// Skip view modes that are not used in the front end.
if (in_array($view_mode_name, array('rss', 'search_index'))) {
continue;
}
if ($display->status()) {
$view_mode_options[$view_mode_name] = ($view_mode_name == 'default') ? t('Default') : $view_modes[$view_mode_name]['label'];
}
}
return $view_mode_options;
}
}
......@@ -13,6 +13,9 @@
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Component\Utility\String;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\user\TempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form controller for the node edit forms.
......@@ -26,6 +29,36 @@ class NodeForm extends ContentEntityForm {
*/
protected $settings;
/**
* The tempstore factory.
*
* @var \Drupal\user\TempStoreFactory
*/
protected $tempStoreFactory;
/**
* Constructs a ContentEntityForm object.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\user\TempStoreFactory $temp_store_factory
* The factory for the temp store object.
*/
public function __construct(EntityManagerInterface $entity_manager, TempStoreFactory $temp_store_factory) {
parent::__construct($entity_manager);
$this->tempStoreFactory = $temp_store_factory;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.manager'),
$container->get('user.tempstore')
);
}
/**
* {@inheritdoc}
*/
......@@ -47,6 +80,28 @@ protected function prepareEntity() {
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
// Try to restore from temp store.
$uuid = $this->entity->uuid();
$store = $this->tempStoreFactory->get('node_preview');
// If the user is creating a new node, the UUID is passed in the request.
if ($request_uuid = \Drupal::request()->query->get('uuid')) {
$uuid = $request_uuid;
}
if ($preview = $store->get($uuid)) {
$form_state = $preview;
// Rebuild the form.
$form_state['rebuild'] = TRUE;
$this->entity = $preview['controller']->getEntity();
unset($this->entity->in_preview);
// Remove the entry from the temp store.
$store->delete($uuid);
}
/** @var \Drupal\node\NodeInterface $node */
$node = $this->entity;
......@@ -56,15 +111,6 @@ public function form(array $form, FormStateInterface $form_state) {
$current_user = \Drupal::currentUser();
$user_config = \Drupal::config('user.settings');
// Some special stuff when previewing a node.
if (isset($form_state['node_preview'])) {
$form['#prefix'] = $form_state['node_preview'];
$node->in_preview = TRUE;
$form['#title'] = $this->t('Preview');
}
else {
unset($node->in_preview);
}
// Override the default CSS class name, since the user-defined node type
// name in 'TYPE-node-form' potentially clashes with third-party class
......@@ -366,11 +412,13 @@ public function submit(array $form, FormStateInterface $form_state) {
* The current state of the form.
*/
public function preview(array $form, FormStateInterface $form_state) {
// @todo Remove this: we should not have explicit includes in autoloaded
// classes.
module_load_include('inc', 'node', 'node.pages');
$form_state['node_preview'] = node_preview($this->entity, $form_state);
$form_state['rebuild'] = TRUE;
$store = $this->tempStoreFactory->get('node_preview');
$this->entity->in_preview = TRUE;
$store->set($this->entity->uuid(), $form_state);
$form_state->setRedirect('entity.node.preview', array(
'node_preview' => $this->entity->uuid(),
'view_mode_id' => 'default',
));
}
/**
......
<?php
/**
* @file
* Contains \Drupal\node\ParamConverter\NodePreviewConverter.
*/
namespace Drupal\node\ParamConverter;
use Drupal\Core\Entity\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
use Drupal\Core\ParamConverter\ParamConverterInterface;
use Drupal\user\TempStoreFactory;
/**
* Provides upcasting for a node entity in preview.
*/
class NodePreviewConverter implements ParamConverterInterface {
/**
* Stores the tempstore factory.
*
* @var \Drupal\user\TempStoreFactory
*/
protected $tempStoreFactory;
/**
* Constructs a new NodePreviewConverter.
*
* @param \Drupal\user\TempStoreFactory $temp_store_factory
* The factory for the temp store object.
*/