Mercurial > hg > cmmr2012-drupal-site
comparison core/modules/layout_builder/src/Element/LayoutBuilder.php @ 5:12f9dff5fda9 tip
Update to Drupal core 8.7.1
author | Chris Cannam |
---|---|
date | Thu, 09 May 2019 15:34:47 +0100 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
4:a9cd425dd02b | 5:12f9dff5fda9 |
---|---|
1 <?php | |
2 | |
3 namespace Drupal\layout_builder\Element; | |
4 | |
5 use Drupal\Core\Ajax\AjaxHelperTrait; | |
6 use Drupal\Core\Messenger\MessengerInterface; | |
7 use Drupal\Core\Plugin\ContainerFactoryPluginInterface; | |
8 use Drupal\Core\Plugin\PluginFormInterface; | |
9 use Drupal\Core\Render\Element; | |
10 use Drupal\Core\Render\Element\RenderElement; | |
11 use Drupal\Core\Url; | |
12 use Drupal\layout_builder\Context\LayoutBuilderContextTrait; | |
13 use Drupal\layout_builder\LayoutBuilderHighlightTrait; | |
14 use Drupal\layout_builder\LayoutTempstoreRepositoryInterface; | |
15 use Drupal\layout_builder\OverridesSectionStorageInterface; | |
16 use Drupal\layout_builder\SectionStorageInterface; | |
17 use Symfony\Component\DependencyInjection\ContainerInterface; | |
18 | |
19 /** | |
20 * Defines a render element for building the Layout Builder UI. | |
21 * | |
22 * @RenderElement("layout_builder") | |
23 * | |
24 * @internal | |
25 * Plugin classes are internal. | |
26 */ | |
27 class LayoutBuilder extends RenderElement implements ContainerFactoryPluginInterface { | |
28 | |
29 use AjaxHelperTrait; | |
30 use LayoutBuilderContextTrait; | |
31 use LayoutBuilderHighlightTrait; | |
32 | |
33 /** | |
34 * The layout tempstore repository. | |
35 * | |
36 * @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface | |
37 */ | |
38 protected $layoutTempstoreRepository; | |
39 | |
40 /** | |
41 * The messenger service. | |
42 * | |
43 * @var \Drupal\Core\Messenger\MessengerInterface | |
44 */ | |
45 protected $messenger; | |
46 | |
47 /** | |
48 * Constructs a new LayoutBuilder. | |
49 * | |
50 * @param array $configuration | |
51 * A configuration array containing information about the plugin instance. | |
52 * @param string $plugin_id | |
53 * The plugin ID for the plugin instance. | |
54 * @param mixed $plugin_definition | |
55 * The plugin implementation definition. | |
56 * @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository | |
57 * The layout tempstore repository. | |
58 * @param \Drupal\Core\Messenger\MessengerInterface $messenger | |
59 * The messenger service. | |
60 */ | |
61 public function __construct(array $configuration, $plugin_id, $plugin_definition, LayoutTempstoreRepositoryInterface $layout_tempstore_repository, MessengerInterface $messenger) { | |
62 parent::__construct($configuration, $plugin_id, $plugin_definition); | |
63 $this->layoutTempstoreRepository = $layout_tempstore_repository; | |
64 $this->messenger = $messenger; | |
65 } | |
66 | |
67 /** | |
68 * {@inheritdoc} | |
69 */ | |
70 public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { | |
71 return new static( | |
72 $configuration, | |
73 $plugin_id, | |
74 $plugin_definition, | |
75 $container->get('layout_builder.tempstore_repository'), | |
76 $container->get('messenger') | |
77 ); | |
78 } | |
79 | |
80 /** | |
81 * {@inheritdoc} | |
82 */ | |
83 public function getInfo() { | |
84 return [ | |
85 '#section_storage' => NULL, | |
86 '#pre_render' => [ | |
87 [$this, 'preRender'], | |
88 ], | |
89 ]; | |
90 } | |
91 | |
92 /** | |
93 * Pre-render callback: Renders the Layout Builder UI. | |
94 */ | |
95 public function preRender($element) { | |
96 if ($element['#section_storage'] instanceof SectionStorageInterface) { | |
97 $element['layout_builder'] = $this->layout($element['#section_storage']); | |
98 } | |
99 return $element; | |
100 } | |
101 | |
102 /** | |
103 * Renders the Layout UI. | |
104 * | |
105 * @param \Drupal\layout_builder\SectionStorageInterface $section_storage | |
106 * The section storage. | |
107 * | |
108 * @return array | |
109 * A render array. | |
110 */ | |
111 protected function layout(SectionStorageInterface $section_storage) { | |
112 $this->prepareLayout($section_storage); | |
113 | |
114 $output = []; | |
115 if ($this->isAjax()) { | |
116 $output['status_messages'] = [ | |
117 '#type' => 'status_messages', | |
118 ]; | |
119 } | |
120 $count = 0; | |
121 for ($i = 0; $i < $section_storage->count(); $i++) { | |
122 $output[] = $this->buildAddSectionLink($section_storage, $count); | |
123 $output[] = $this->buildAdministrativeSection($section_storage, $count); | |
124 $count++; | |
125 } | |
126 $output[] = $this->buildAddSectionLink($section_storage, $count); | |
127 $output['#attached']['library'][] = 'layout_builder/drupal.layout_builder'; | |
128 // As the Layout Builder UI is typically displayed using the frontend theme, | |
129 // it is not marked as an administrative page at the route level even though | |
130 // it performs an administrative task. Mark this as an administrative page | |
131 // for JavaScript. | |
132 $output['#attached']['drupalSettings']['path']['currentPathIsAdmin'] = TRUE; | |
133 $output['#type'] = 'container'; | |
134 $output['#attributes']['id'] = 'layout-builder'; | |
135 $output['#attributes']['class'][] = 'layout-builder'; | |
136 // Mark this UI as uncacheable. | |
137 $output['#cache']['max-age'] = 0; | |
138 return $output; | |
139 } | |
140 | |
141 /** | |
142 * Prepares a layout for use in the UI. | |
143 * | |
144 * @param \Drupal\layout_builder\SectionStorageInterface $section_storage | |
145 * The section storage. | |
146 */ | |
147 protected function prepareLayout(SectionStorageInterface $section_storage) { | |
148 // If the layout has pending changes, add a warning. | |
149 if ($this->layoutTempstoreRepository->has($section_storage)) { | |
150 $this->messenger->addWarning($this->t('You have unsaved changes.')); | |
151 } | |
152 // If the layout is an override that has not yet been overridden, copy the | |
153 // sections from the corresponding default. | |
154 elseif ($section_storage instanceof OverridesSectionStorageInterface && !$section_storage->isOverridden()) { | |
155 $sections = $section_storage->getDefaultSectionStorage()->getSections(); | |
156 foreach ($sections as $section) { | |
157 $section_storage->appendSection($section); | |
158 } | |
159 $this->layoutTempstoreRepository->set($section_storage); | |
160 } | |
161 } | |
162 | |
163 /** | |
164 * Builds a link to add a new section at a given delta. | |
165 * | |
166 * @param \Drupal\layout_builder\SectionStorageInterface $section_storage | |
167 * The section storage. | |
168 * @param int $delta | |
169 * The delta of the section to splice. | |
170 * | |
171 * @return array | |
172 * A render array for a link. | |
173 */ | |
174 protected function buildAddSectionLink(SectionStorageInterface $section_storage, $delta) { | |
175 $storage_type = $section_storage->getStorageType(); | |
176 $storage_id = $section_storage->getStorageId(); | |
177 | |
178 // If the delta and the count are the same, it is either the end of the | |
179 // layout or an empty layout. | |
180 if ($delta === count($section_storage)) { | |
181 if ($delta === 0) { | |
182 $title = $this->t('Add Section'); | |
183 } | |
184 else { | |
185 $title = $this->t('Add Section <span class="visually-hidden">at end of layout</span>'); | |
186 } | |
187 } | |
188 // If the delta and the count are different, it is either the beginning of | |
189 // the layout or in between two sections. | |
190 else { | |
191 if ($delta === 0) { | |
192 $title = $this->t('Add Section <span class="visually-hidden">at start of layout</span>'); | |
193 } | |
194 else { | |
195 $title = $this->t('Add Section <span class="visually-hidden">between @first and @second</span>', ['@first' => $delta, '@second' => $delta + 1]); | |
196 } | |
197 } | |
198 | |
199 return [ | |
200 'link' => [ | |
201 '#type' => 'link', | |
202 '#title' => $title, | |
203 '#url' => Url::fromRoute('layout_builder.choose_section', | |
204 [ | |
205 'section_storage_type' => $storage_type, | |
206 'section_storage' => $storage_id, | |
207 'delta' => $delta, | |
208 ], | |
209 [ | |
210 'attributes' => [ | |
211 'class' => [ | |
212 'use-ajax', | |
213 'layout-builder__link', | |
214 'layout-builder__link--add', | |
215 ], | |
216 'data-dialog-type' => 'dialog', | |
217 'data-dialog-renderer' => 'off_canvas', | |
218 ], | |
219 ] | |
220 ), | |
221 ], | |
222 '#type' => 'container', | |
223 '#attributes' => [ | |
224 'class' => ['layout-builder__add-section'], | |
225 'data-layout-builder-highlight-id' => $this->sectionAddHighlightId($delta), | |
226 ], | |
227 ]; | |
228 } | |
229 | |
230 /** | |
231 * Builds the render array for the layout section while editing. | |
232 * | |
233 * @param \Drupal\layout_builder\SectionStorageInterface $section_storage | |
234 * The section storage. | |
235 * @param int $delta | |
236 * The delta of the section. | |
237 * | |
238 * @return array | |
239 * The render array for a given section. | |
240 */ | |
241 protected function buildAdministrativeSection(SectionStorageInterface $section_storage, $delta) { | |
242 $storage_type = $section_storage->getStorageType(); | |
243 $storage_id = $section_storage->getStorageId(); | |
244 $section = $section_storage->getSection($delta); | |
245 | |
246 $layout = $section->getLayout(); | |
247 $build = $section->toRenderArray($this->getAvailableContexts($section_storage), TRUE); | |
248 $layout_definition = $layout->getPluginDefinition(); | |
249 | |
250 $region_labels = $layout_definition->getRegionLabels(); | |
251 foreach ($layout_definition->getRegions() as $region => $info) { | |
252 if (!empty($build[$region])) { | |
253 foreach (Element::children($build[$region]) as $uuid) { | |
254 $build[$region][$uuid]['#attributes']['class'][] = 'js-layout-builder-block'; | |
255 $build[$region][$uuid]['#attributes']['class'][] = 'layout-builder-block'; | |
256 $build[$region][$uuid]['#attributes']['data-layout-block-uuid'] = $uuid; | |
257 $build[$region][$uuid]['#attributes']['data-layout-builder-highlight-id'] = $this->blockUpdateHighlightId($uuid); | |
258 $build[$region][$uuid]['#contextual_links'] = [ | |
259 'layout_builder_block' => [ | |
260 'route_parameters' => [ | |
261 'section_storage_type' => $storage_type, | |
262 'section_storage' => $storage_id, | |
263 'delta' => $delta, | |
264 'region' => $region, | |
265 'uuid' => $uuid, | |
266 ], | |
267 // Add metadata about the current operations available in | |
268 // contextual links. This will invalidate the client-side cache of | |
269 // links that were cached before the 'move' link was added. | |
270 // @see layout_builder.links.contextual.yml | |
271 'metadata' => [ | |
272 'operations' => 'move:update:remove', | |
273 ], | |
274 ], | |
275 ]; | |
276 } | |
277 } | |
278 | |
279 $build[$region]['layout_builder_add_block']['link'] = [ | |
280 '#type' => 'link', | |
281 // Add one to the current delta since it is zero-indexed. | |
282 '#title' => $this->t('Add Block <span class="visually-hidden">in section @section, @region region</span>', ['@section' => $delta + 1, '@region' => $region_labels[$region]]), | |
283 '#url' => Url::fromRoute('layout_builder.choose_block', | |
284 [ | |
285 'section_storage_type' => $storage_type, | |
286 'section_storage' => $storage_id, | |
287 'delta' => $delta, | |
288 'region' => $region, | |
289 ], | |
290 [ | |
291 'attributes' => [ | |
292 'class' => [ | |
293 'use-ajax', | |
294 'layout-builder__link', | |
295 'layout-builder__link--add', | |
296 ], | |
297 'data-dialog-type' => 'dialog', | |
298 'data-dialog-renderer' => 'off_canvas', | |
299 ], | |
300 ] | |
301 ), | |
302 ]; | |
303 $build[$region]['layout_builder_add_block']['#type'] = 'container'; | |
304 $build[$region]['layout_builder_add_block']['#attributes'] = [ | |
305 'class' => ['layout-builder__add-block'], | |
306 'data-layout-builder-highlight-id' => $this->blockAddHighlightId($delta, $region), | |
307 ]; | |
308 $build[$region]['layout_builder_add_block']['#weight'] = 1000; | |
309 $build[$region]['#attributes']['data-region'] = $region; | |
310 $build[$region]['#attributes']['class'][] = 'layout-builder__region'; | |
311 $build[$region]['#attributes']['class'][] = 'js-layout-builder-region'; | |
312 $build[$region]['#attributes']['role'] = 'group'; | |
313 $build[$region]['#attributes']['aria-label'] = $this->t('@region region in section @section', [ | |
314 '@region' => $info['label'], | |
315 '@section' => $delta + 1, | |
316 ]); | |
317 | |
318 // Get weights of all children for use by the region label. | |
319 $weights = array_map(function ($a) { | |
320 return isset($a['#weight']) ? $a['#weight'] : 0; | |
321 }, $build[$region]); | |
322 | |
323 // The region label is made visible when the move block dialog is open. | |
324 $build[$region]['region_label'] = [ | |
325 '#type' => 'container', | |
326 '#attributes' => [ | |
327 'class' => ['layout__region-info', 'layout-builder__region-label'], | |
328 // A more detailed version of this information is already read by | |
329 // screen readers, so this label can be hidden from them. | |
330 'aria-hidden' => TRUE, | |
331 ], | |
332 '#markup' => $this->t('Region: @region', ['@region' => $info['label']]), | |
333 // Ensures the region label is displayed first. | |
334 '#weight' => min($weights) - 1, | |
335 ]; | |
336 } | |
337 | |
338 $build['#attributes']['data-layout-update-url'] = Url::fromRoute('layout_builder.move_block', [ | |
339 'section_storage_type' => $storage_type, | |
340 'section_storage' => $storage_id, | |
341 ])->toString(); | |
342 | |
343 $build['#attributes']['data-layout-delta'] = $delta; | |
344 $build['#attributes']['class'][] = 'layout-builder__layout'; | |
345 $build['#attributes']['data-layout-builder-highlight-id'] = $this->sectionUpdateHighlightId($delta); | |
346 | |
347 return [ | |
348 '#type' => 'container', | |
349 '#attributes' => [ | |
350 'class' => ['layout-builder__section'], | |
351 'role' => 'group', | |
352 'aria-label' => $this->t('Section @section', ['@section' => $delta + 1]), | |
353 ], | |
354 'remove' => [ | |
355 '#type' => 'link', | |
356 '#title' => $this->t('Remove section <span class="visually-hidden">@section</span>', ['@section' => $delta + 1]), | |
357 '#url' => Url::fromRoute('layout_builder.remove_section', [ | |
358 'section_storage_type' => $storage_type, | |
359 'section_storage' => $storage_id, | |
360 'delta' => $delta, | |
361 ]), | |
362 '#attributes' => [ | |
363 'class' => [ | |
364 'use-ajax', | |
365 'layout-builder__link', | |
366 'layout-builder__link--remove', | |
367 ], | |
368 'data-dialog-type' => 'dialog', | |
369 'data-dialog-renderer' => 'off_canvas', | |
370 ], | |
371 ], | |
372 // The section label is added to sections without a "Configure Section" | |
373 // link, and is only visible when the move block dialog is open. | |
374 'section_label' => [ | |
375 '#markup' => $this->t('<span class="layout-builder__section-label" aria-hidden="true">Section @section</span>', ['@section' => $delta + 1]), | |
376 '#access' => !$layout instanceof PluginFormInterface, | |
377 ], | |
378 'configure' => [ | |
379 '#type' => 'link', | |
380 // There are two instances of @section, the one wrapped in | |
381 // .visually-hidden is for screen readers. The one wrapped in | |
382 // .layout-builder__section-label is only visible when the | |
383 // move block dialog is open and it is not seen by screen readers. | |
384 '#title' => $this->t('Configure section <span class="visually-hidden">@section</span><span aria-hidden="true" class="layout-builder__section-label">@section</span>', ['@section' => $delta + 1]), | |
385 '#access' => $layout instanceof PluginFormInterface, | |
386 '#url' => Url::fromRoute('layout_builder.configure_section', [ | |
387 'section_storage_type' => $storage_type, | |
388 'section_storage' => $storage_id, | |
389 'delta' => $delta, | |
390 ]), | |
391 '#attributes' => [ | |
392 'class' => [ | |
393 'use-ajax', | |
394 'layout-builder__link', | |
395 'layout-builder__link--configure', | |
396 ], | |
397 'data-dialog-type' => 'dialog', | |
398 'data-dialog-renderer' => 'off_canvas', | |
399 ], | |
400 ], | |
401 'layout-builder__section' => $build, | |
402 ]; | |
403 } | |
404 | |
405 } |