Chris@18
|
1 <?php
|
Chris@18
|
2
|
Chris@18
|
3 namespace Drupal\layout_builder;
|
Chris@18
|
4
|
Chris@18
|
5 use Drupal\Component\Utility\NestedArray;
|
Chris@18
|
6 use Drupal\Core\Cache\CacheableMetadata;
|
Chris@18
|
7 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
Chris@18
|
8 use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
|
Chris@18
|
9 use Drupal\Core\Entity\EntityInterface;
|
Chris@18
|
10 use Drupal\Core\Entity\EntityTypeManagerInterface;
|
Chris@18
|
11 use Drupal\Core\Entity\FieldableEntityInterface;
|
Chris@18
|
12 use Drupal\Core\Logger\LoggerChannelTrait;
|
Chris@18
|
13 use Drupal\Core\Plugin\Context\Context;
|
Chris@18
|
14 use Drupal\Core\Plugin\Context\ContextDefinition;
|
Chris@18
|
15 use Drupal\Core\Plugin\Context\EntityContext;
|
Chris@18
|
16 use Drupal\Core\Render\Element;
|
Chris@18
|
17 use Drupal\Core\Session\AccountInterface;
|
Chris@18
|
18 use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface;
|
Chris@18
|
19 use Symfony\Component\DependencyInjection\ContainerInterface;
|
Chris@18
|
20
|
Chris@18
|
21 /**
|
Chris@18
|
22 * Helper methods for Quick Edit module integration.
|
Chris@18
|
23 *
|
Chris@18
|
24 * @internal
|
Chris@18
|
25 * This is an internal utility class wrapping hook implementations.
|
Chris@18
|
26 */
|
Chris@18
|
27 class QuickEditIntegration implements ContainerInjectionInterface {
|
Chris@18
|
28
|
Chris@18
|
29 use LoggerChannelTrait;
|
Chris@18
|
30
|
Chris@18
|
31 /**
|
Chris@18
|
32 * The section storage manager.
|
Chris@18
|
33 *
|
Chris@18
|
34 * @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface
|
Chris@18
|
35 */
|
Chris@18
|
36 protected $sectionStorageManager;
|
Chris@18
|
37
|
Chris@18
|
38 /**
|
Chris@18
|
39 * The current user.
|
Chris@18
|
40 *
|
Chris@18
|
41 * @var \Drupal\Core\Session\AccountInterface
|
Chris@18
|
42 */
|
Chris@18
|
43 protected $currentUser;
|
Chris@18
|
44
|
Chris@18
|
45 /**
|
Chris@18
|
46 * The entity type manager.
|
Chris@18
|
47 *
|
Chris@18
|
48 * @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
Chris@18
|
49 */
|
Chris@18
|
50 protected $entityTypeManager;
|
Chris@18
|
51
|
Chris@18
|
52 /**
|
Chris@18
|
53 * Constructs a new QuickEditIntegration object.
|
Chris@18
|
54 *
|
Chris@18
|
55 * @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager
|
Chris@18
|
56 * The section storage manager.
|
Chris@18
|
57 * @param \Drupal\Core\Session\AccountInterface $current_user
|
Chris@18
|
58 * The current user.
|
Chris@18
|
59 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
Chris@18
|
60 * The entity type manager.
|
Chris@18
|
61 */
|
Chris@18
|
62 public function __construct(SectionStorageManagerInterface $section_storage_manager, AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager) {
|
Chris@18
|
63 $this->sectionStorageManager = $section_storage_manager;
|
Chris@18
|
64 $this->currentUser = $current_user;
|
Chris@18
|
65 $this->entityTypeManager = $entity_type_manager;
|
Chris@18
|
66 }
|
Chris@18
|
67
|
Chris@18
|
68 /**
|
Chris@18
|
69 * {@inheritdoc}
|
Chris@18
|
70 */
|
Chris@18
|
71 public static function create(ContainerInterface $container) {
|
Chris@18
|
72 return new static(
|
Chris@18
|
73 $container->get('plugin.manager.layout_builder.section_storage'),
|
Chris@18
|
74 $container->get('current_user'),
|
Chris@18
|
75 $container->get('entity_type.manager')
|
Chris@18
|
76 );
|
Chris@18
|
77 }
|
Chris@18
|
78
|
Chris@18
|
79 /**
|
Chris@18
|
80 * Alters the entity view build for Quick Edit compatibility.
|
Chris@18
|
81 *
|
Chris@18
|
82 * When rendering fields outside of normal view modes, Quick Edit requires
|
Chris@18
|
83 * that modules identify themselves with a view mode ID in the format
|
Chris@18
|
84 * [module_name]-[information the module needs to rerender], as prescribed by
|
Chris@18
|
85 * hook_quickedit_render_field().
|
Chris@18
|
86 *
|
Chris@18
|
87 * @param array $build
|
Chris@18
|
88 * The built entity render array.
|
Chris@18
|
89 * @param \Drupal\Core\Entity\EntityInterface $entity
|
Chris@18
|
90 * The entity.
|
Chris@18
|
91 * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display
|
Chris@18
|
92 * The entity view display.
|
Chris@18
|
93 *
|
Chris@18
|
94 * @see hook_quickedit_render_field()
|
Chris@18
|
95 * @see layout_builder_quickedit_render_field()
|
Chris@18
|
96 */
|
Chris@18
|
97 public function entityViewAlter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
|
Chris@18
|
98 if (!$entity instanceof FieldableEntityInterface || !isset($build['_layout_builder'])) {
|
Chris@18
|
99 return;
|
Chris@18
|
100 }
|
Chris@18
|
101
|
Chris@18
|
102 $build['#cache']['contexts'][] = 'user.permissions';
|
Chris@18
|
103 if (!$this->currentUser->hasPermission('access in-place editing')) {
|
Chris@18
|
104 return;
|
Chris@18
|
105 }
|
Chris@18
|
106
|
Chris@18
|
107 $cacheable_metadata = CacheableMetadata::createFromRenderArray($build);
|
Chris@18
|
108 $section_list = $this->sectionStorageManager->findByContext(
|
Chris@18
|
109 [
|
Chris@18
|
110 'display' => EntityContext::fromEntity($display),
|
Chris@18
|
111 'entity' => EntityContext::fromEntity($entity),
|
Chris@18
|
112 'view_mode' => new Context(new ContextDefinition('string'), $display->getMode()),
|
Chris@18
|
113 ],
|
Chris@18
|
114 $cacheable_metadata
|
Chris@18
|
115 );
|
Chris@18
|
116 $cacheable_metadata->applyTo($build);
|
Chris@18
|
117
|
Chris@18
|
118 if (empty($section_list)) {
|
Chris@18
|
119 return;
|
Chris@18
|
120 }
|
Chris@18
|
121
|
Chris@18
|
122 // Create a hash of the sections and use it in the unique Quick Edit view
|
Chris@18
|
123 // mode ID. Any changes to the sections will result in a different hash,
|
Chris@18
|
124 // forcing Quick Edit's JavaScript to recognize any changes and retrieve
|
Chris@18
|
125 // up-to-date metadata.
|
Chris@18
|
126 $sections_hash = hash('sha256', serialize($section_list->getSections()));
|
Chris@18
|
127
|
Chris@18
|
128 // Track each component by their plugin ID, delta, region, and UUID.
|
Chris@18
|
129 $plugin_ids_to_update = [];
|
Chris@18
|
130 foreach (Element::children($build['_layout_builder']) as $delta) {
|
Chris@18
|
131 $section = $build['_layout_builder'][$delta];
|
Chris@18
|
132 /** @var \Drupal\Core\Layout\LayoutDefinition $layout */
|
Chris@18
|
133 $layout = $section['#layout'];
|
Chris@18
|
134 $regions = $layout->getRegionNames();
|
Chris@18
|
135
|
Chris@18
|
136 foreach ($regions as $region) {
|
Chris@18
|
137 if (isset($section[$region])) {
|
Chris@18
|
138 foreach ($section[$region] as $uuid => $component) {
|
Chris@18
|
139 if (isset($component['#plugin_id']) && $this->supportQuickEditOnComponent($component, $entity)) {
|
Chris@18
|
140 $plugin_ids_to_update[$component['#plugin_id']][$delta][$region][$uuid] = $uuid;
|
Chris@18
|
141 }
|
Chris@18
|
142 }
|
Chris@18
|
143 }
|
Chris@18
|
144 }
|
Chris@18
|
145 }
|
Chris@18
|
146
|
Chris@18
|
147 // @todo Remove when https://www.drupal.org/node/3041850 is resolved.
|
Chris@18
|
148 $plugin_ids_to_update = array_filter($plugin_ids_to_update, function ($info) {
|
Chris@18
|
149 // Delta, region, and UUID each count as one.
|
Chris@18
|
150 return count($info, COUNT_RECURSIVE) === 3;
|
Chris@18
|
151 });
|
Chris@18
|
152
|
Chris@18
|
153 $plugin_ids_to_update = NestedArray::mergeDeepArray($plugin_ids_to_update, TRUE);
|
Chris@18
|
154 foreach ($plugin_ids_to_update as $delta => $regions) {
|
Chris@18
|
155 foreach ($regions as $region => $uuids) {
|
Chris@18
|
156 foreach ($uuids as $uuid => $component) {
|
Chris@18
|
157 $build['_layout_builder'][$delta][$region][$uuid]['content']['#view_mode'] = static::getViewModeId($entity, $display, $delta, $uuid, $sections_hash);
|
Chris@18
|
158 }
|
Chris@18
|
159 }
|
Chris@18
|
160 }
|
Chris@18
|
161 // Alter the Quick Edit view mode ID of all fields outside of the Layout
|
Chris@18
|
162 // Builder sections to force Quick Edit to request to the field metadata.
|
Chris@18
|
163 // @todo Remove this logic in https://www.drupal.org/project/node/2966136.
|
Chris@18
|
164 foreach (Element::children($build) as $field_name) {
|
Chris@18
|
165 if ($field_name !== '_layout_builder') {
|
Chris@18
|
166 $field_build = &$build[$field_name];
|
Chris@18
|
167 if (isset($field_build['#view_mode'])) {
|
Chris@18
|
168 $field_build['#view_mode'] = "layout_builder-{$display->getMode()}-non_component-$sections_hash";
|
Chris@18
|
169 }
|
Chris@18
|
170 }
|
Chris@18
|
171 }
|
Chris@18
|
172 }
|
Chris@18
|
173
|
Chris@18
|
174 /**
|
Chris@18
|
175 * Generates a Quick Edit view mode ID.
|
Chris@18
|
176 *
|
Chris@18
|
177 * @param \Drupal\Core\Entity\EntityInterface $entity
|
Chris@18
|
178 * The entity.
|
Chris@18
|
179 * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display
|
Chris@18
|
180 * The entity view display.
|
Chris@18
|
181 * @param int $delta
|
Chris@18
|
182 * The delta.
|
Chris@18
|
183 * @param string $component_uuid
|
Chris@18
|
184 * The component UUID.
|
Chris@18
|
185 * @param string $sections_hash
|
Chris@18
|
186 * The hash of the sections; must change whenever the sections change.
|
Chris@18
|
187 *
|
Chris@18
|
188 * @return string
|
Chris@18
|
189 * The Quick Edit view mode ID.
|
Chris@18
|
190 *
|
Chris@18
|
191 * @see \Drupal\layout_builder\QuickEditIntegration::deconstructViewModeId()
|
Chris@18
|
192 */
|
Chris@18
|
193 private static function getViewModeId(EntityInterface $entity, EntityViewDisplayInterface $display, $delta, $component_uuid, $sections_hash) {
|
Chris@18
|
194 return implode('-', [
|
Chris@18
|
195 'layout_builder',
|
Chris@18
|
196 $display->getMode(),
|
Chris@18
|
197 $delta,
|
Chris@18
|
198 // Replace the dashes in the component UUID because we need to
|
Chris@18
|
199 // use dashes to join the parts.
|
Chris@18
|
200 str_replace('-', '_', $component_uuid),
|
Chris@18
|
201 $entity->id(),
|
Chris@18
|
202 $sections_hash,
|
Chris@18
|
203 ]);
|
Chris@18
|
204 }
|
Chris@18
|
205
|
Chris@18
|
206 /**
|
Chris@18
|
207 * Deconstructs the Quick Edit view mode ID into its constituent parts.
|
Chris@18
|
208 *
|
Chris@18
|
209 * @param string $quick_edit_view_mode_id
|
Chris@18
|
210 * The Quick Edit view mode ID.
|
Chris@18
|
211 *
|
Chris@18
|
212 * @return array
|
Chris@18
|
213 * An array containing the entity view mode ID, the delta, the component
|
Chris@18
|
214 * UUID, and the entity ID.
|
Chris@18
|
215 *
|
Chris@18
|
216 * @see \Drupal\layout_builder\QuickEditIntegration::getViewModeId()
|
Chris@18
|
217 */
|
Chris@18
|
218 public static function deconstructViewModeId($quick_edit_view_mode_id) {
|
Chris@18
|
219 list(, $entity_view_mode_id, $delta, $component_uuid, $entity_id) = explode('-', $quick_edit_view_mode_id, 7);
|
Chris@18
|
220 return [
|
Chris@18
|
221 $entity_view_mode_id,
|
Chris@18
|
222 // @todo Explicitly cast delta to an integer, remove this in
|
Chris@18
|
223 // https://www.drupal.org/project/drupal/issues/2984509.
|
Chris@18
|
224 (int) $delta,
|
Chris@18
|
225 // Replace the underscores with dash to get back the component UUID.
|
Chris@18
|
226 str_replace('_', '-', $component_uuid),
|
Chris@18
|
227 $entity_id,
|
Chris@18
|
228 ];
|
Chris@18
|
229 }
|
Chris@18
|
230
|
Chris@18
|
231 /**
|
Chris@18
|
232 * Re-renders a field rendered by Layout Builder, edited with Quick Edit.
|
Chris@18
|
233 *
|
Chris@18
|
234 * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
|
Chris@18
|
235 * The entity.
|
Chris@18
|
236 * @param string $field_name
|
Chris@18
|
237 * The field name.
|
Chris@18
|
238 * @param string $quick_edit_view_mode_id
|
Chris@18
|
239 * The Quick Edit view mode ID.
|
Chris@18
|
240 * @param string $langcode
|
Chris@18
|
241 * The language code.
|
Chris@18
|
242 *
|
Chris@18
|
243 * @return array
|
Chris@18
|
244 * The re-rendered field.
|
Chris@18
|
245 */
|
Chris@18
|
246 public function quickEditRenderField(FieldableEntityInterface $entity, $field_name, $quick_edit_view_mode_id, $langcode) {
|
Chris@18
|
247 list($entity_view_mode, $delta, $component_uuid) = static::deconstructViewModeId($quick_edit_view_mode_id);
|
Chris@18
|
248
|
Chris@18
|
249 $entity_build = $this->entityTypeManager->getViewBuilder($entity->getEntityTypeId())->view($entity, $entity_view_mode, $langcode);
|
Chris@18
|
250 $this->buildEntityView($entity_build);
|
Chris@18
|
251
|
Chris@18
|
252 if (isset($entity_build['_layout_builder'][$delta])) {
|
Chris@18
|
253 foreach (Element::children($entity_build['_layout_builder'][$delta]) as $region) {
|
Chris@18
|
254 if (isset($entity_build['_layout_builder'][$delta][$region][$component_uuid])) {
|
Chris@18
|
255 return $entity_build['_layout_builder'][$delta][$region][$component_uuid]['content'];
|
Chris@18
|
256 }
|
Chris@18
|
257 }
|
Chris@18
|
258 }
|
Chris@18
|
259
|
Chris@18
|
260 $this->getLogger('layout_builder')->warning('The field "%field" failed to render.', ['%field' => $field_name]);
|
Chris@18
|
261 return [];
|
Chris@18
|
262 }
|
Chris@18
|
263
|
Chris@18
|
264 /**
|
Chris@18
|
265 * {@inheritdoc}
|
Chris@18
|
266 *
|
Chris@18
|
267 * @todo Replace this hardcoded processing when
|
Chris@18
|
268 * https://www.drupal.org/project/drupal/issues/3041635 is resolved.
|
Chris@18
|
269 *
|
Chris@18
|
270 * @see \Drupal\Tests\EntityViewTrait::buildEntityView()
|
Chris@18
|
271 */
|
Chris@18
|
272 private function buildEntityView(array &$elements) {
|
Chris@18
|
273 // If the default values for this element have not been loaded yet,
|
Chris@18
|
274 // populate them.
|
Chris@18
|
275 if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {
|
Chris@18
|
276 $elements += \Drupal::service('element_info')->getInfo($elements['#type']);
|
Chris@18
|
277 }
|
Chris@18
|
278
|
Chris@18
|
279 // Make any final changes to the element before it is rendered. This means
|
Chris@18
|
280 // that the $element or the children can be altered or corrected before
|
Chris@18
|
281 // the element is rendered into the final text.
|
Chris@18
|
282 if (isset($elements['#pre_render'])) {
|
Chris@18
|
283 foreach ($elements['#pre_render'] as $callable) {
|
Chris@18
|
284 $elements = call_user_func($callable, $elements);
|
Chris@18
|
285 }
|
Chris@18
|
286 }
|
Chris@18
|
287
|
Chris@18
|
288 // And recurse.
|
Chris@18
|
289 $children = Element::children($elements, TRUE);
|
Chris@18
|
290 foreach ($children as $key) {
|
Chris@18
|
291 $this->buildEntityView($elements[$key]);
|
Chris@18
|
292 }
|
Chris@18
|
293 }
|
Chris@18
|
294
|
Chris@18
|
295 /**
|
Chris@18
|
296 * Determines whether a component has Quick Edit support.
|
Chris@18
|
297 *
|
Chris@18
|
298 * Only field_block components for display configurable fields should be
|
Chris@18
|
299 * supported.
|
Chris@18
|
300 *
|
Chris@18
|
301 * @param array $component
|
Chris@18
|
302 * The component render array.
|
Chris@18
|
303 * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
|
Chris@18
|
304 * The entity being displayed.
|
Chris@18
|
305 *
|
Chris@18
|
306 * @return bool
|
Chris@18
|
307 * Whether Quick Edit is supported on the component.
|
Chris@18
|
308 *
|
Chris@18
|
309 * @see \Drupal\layout_builder\Plugin\Block\FieldBlock
|
Chris@18
|
310 */
|
Chris@18
|
311 private function supportQuickEditOnComponent(array $component, FieldableEntityInterface $entity) {
|
Chris@18
|
312 if (isset($component['content']['#field_name'], $component['#base_plugin_id']) && $component['#base_plugin_id'] === 'field_block' && $entity->hasField($component['content']['#field_name'])) {
|
Chris@18
|
313 return $entity->getFieldDefinition($component['content']['#field_name'])->isDisplayConfigurable('view');
|
Chris@18
|
314 }
|
Chris@18
|
315 return FALSE;
|
Chris@18
|
316 }
|
Chris@18
|
317
|
Chris@18
|
318 }
|