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