Chris@14
|
1 <?php
|
Chris@14
|
2
|
Chris@14
|
3 namespace Drupal\layout_builder\Controller;
|
Chris@14
|
4
|
Chris@17
|
5 use Drupal\Core\Ajax\AjaxHelperTrait;
|
Chris@14
|
6 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
Chris@14
|
7 use Drupal\Core\Messenger\MessengerInterface;
|
Chris@14
|
8 use Drupal\Core\Plugin\PluginFormInterface;
|
Chris@14
|
9 use Drupal\Core\StringTranslation\StringTranslationTrait;
|
Chris@14
|
10 use Drupal\Core\Url;
|
Chris@14
|
11 use Drupal\layout_builder\Context\LayoutBuilderContextTrait;
|
Chris@14
|
12 use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
|
Chris@14
|
13 use Drupal\layout_builder\OverridesSectionStorageInterface;
|
Chris@14
|
14 use Drupal\layout_builder\Section;
|
Chris@14
|
15 use Drupal\layout_builder\SectionStorageInterface;
|
Chris@14
|
16 use Symfony\Component\DependencyInjection\ContainerInterface;
|
Chris@14
|
17 use Symfony\Component\HttpFoundation\RedirectResponse;
|
Chris@14
|
18
|
Chris@14
|
19 /**
|
Chris@14
|
20 * Defines a controller to provide the Layout Builder admin UI.
|
Chris@14
|
21 *
|
Chris@14
|
22 * @internal
|
Chris@14
|
23 */
|
Chris@14
|
24 class LayoutBuilderController implements ContainerInjectionInterface {
|
Chris@14
|
25
|
Chris@14
|
26 use LayoutBuilderContextTrait;
|
Chris@14
|
27 use StringTranslationTrait;
|
Chris@17
|
28 use AjaxHelperTrait;
|
Chris@14
|
29
|
Chris@14
|
30 /**
|
Chris@14
|
31 * The layout tempstore repository.
|
Chris@14
|
32 *
|
Chris@14
|
33 * @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
|
Chris@14
|
34 */
|
Chris@14
|
35 protected $layoutTempstoreRepository;
|
Chris@14
|
36
|
Chris@14
|
37 /**
|
Chris@14
|
38 * The messenger service.
|
Chris@14
|
39 *
|
Chris@14
|
40 * @var \Drupal\Core\Messenger\MessengerInterface
|
Chris@14
|
41 */
|
Chris@14
|
42 protected $messenger;
|
Chris@14
|
43
|
Chris@14
|
44 /**
|
Chris@14
|
45 * LayoutBuilderController constructor.
|
Chris@14
|
46 *
|
Chris@14
|
47 * @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
|
Chris@14
|
48 * The layout tempstore repository.
|
Chris@14
|
49 * @param \Drupal\Core\Messenger\MessengerInterface $messenger
|
Chris@14
|
50 * The messenger service.
|
Chris@14
|
51 */
|
Chris@14
|
52 public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, MessengerInterface $messenger) {
|
Chris@14
|
53 $this->layoutTempstoreRepository = $layout_tempstore_repository;
|
Chris@14
|
54 $this->messenger = $messenger;
|
Chris@14
|
55 }
|
Chris@14
|
56
|
Chris@14
|
57 /**
|
Chris@14
|
58 * {@inheritdoc}
|
Chris@14
|
59 */
|
Chris@14
|
60 public static function create(ContainerInterface $container) {
|
Chris@14
|
61 return new static(
|
Chris@14
|
62 $container->get('layout_builder.tempstore_repository'),
|
Chris@14
|
63 $container->get('messenger')
|
Chris@14
|
64 );
|
Chris@14
|
65 }
|
Chris@14
|
66
|
Chris@14
|
67 /**
|
Chris@14
|
68 * Provides a title callback.
|
Chris@14
|
69 *
|
Chris@14
|
70 * @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
Chris@14
|
71 * The section storage.
|
Chris@14
|
72 *
|
Chris@14
|
73 * @return string
|
Chris@14
|
74 * The title for the layout page.
|
Chris@14
|
75 */
|
Chris@14
|
76 public function title(SectionStorageInterface $section_storage) {
|
Chris@14
|
77 return $this->t('Edit layout for %label', ['%label' => $section_storage->label()]);
|
Chris@14
|
78 }
|
Chris@14
|
79
|
Chris@14
|
80 /**
|
Chris@14
|
81 * Renders the Layout UI.
|
Chris@14
|
82 *
|
Chris@14
|
83 * @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
Chris@14
|
84 * The section storage.
|
Chris@14
|
85 * @param bool $is_rebuilding
|
Chris@14
|
86 * (optional) Indicates if the layout is rebuilding, defaults to FALSE.
|
Chris@14
|
87 *
|
Chris@14
|
88 * @return array
|
Chris@14
|
89 * A render array.
|
Chris@14
|
90 */
|
Chris@14
|
91 public function layout(SectionStorageInterface $section_storage, $is_rebuilding = FALSE) {
|
Chris@14
|
92 $this->prepareLayout($section_storage, $is_rebuilding);
|
Chris@14
|
93
|
Chris@14
|
94 $output = [];
|
Chris@17
|
95 if ($this->isAjax()) {
|
Chris@17
|
96 $output['status_messages'] = [
|
Chris@17
|
97 '#type' => 'status_messages',
|
Chris@17
|
98 ];
|
Chris@17
|
99 }
|
Chris@14
|
100 $count = 0;
|
Chris@14
|
101 for ($i = 0; $i < $section_storage->count(); $i++) {
|
Chris@14
|
102 $output[] = $this->buildAddSectionLink($section_storage, $count);
|
Chris@14
|
103 $output[] = $this->buildAdministrativeSection($section_storage, $count);
|
Chris@14
|
104 $count++;
|
Chris@14
|
105 }
|
Chris@14
|
106 $output[] = $this->buildAddSectionLink($section_storage, $count);
|
Chris@14
|
107 $output['#attached']['library'][] = 'layout_builder/drupal.layout_builder';
|
Chris@14
|
108 $output['#type'] = 'container';
|
Chris@14
|
109 $output['#attributes']['id'] = 'layout-builder';
|
Chris@14
|
110 // Mark this UI as uncacheable.
|
Chris@14
|
111 $output['#cache']['max-age'] = 0;
|
Chris@14
|
112 return $output;
|
Chris@14
|
113 }
|
Chris@14
|
114
|
Chris@14
|
115 /**
|
Chris@14
|
116 * Prepares a layout for use in the UI.
|
Chris@14
|
117 *
|
Chris@14
|
118 * @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
Chris@14
|
119 * The section storage.
|
Chris@14
|
120 * @param bool $is_rebuilding
|
Chris@14
|
121 * Indicates if the layout is rebuilding.
|
Chris@14
|
122 */
|
Chris@14
|
123 protected function prepareLayout(SectionStorageInterface $section_storage, $is_rebuilding) {
|
Chris@17
|
124 // If the layout has pending changes, add a warning.
|
Chris@17
|
125 if ($this->layoutTempstoreRepository->has($section_storage)) {
|
Chris@17
|
126 $this->messenger->addWarning($this->t('You have unsaved changes.'));
|
Chris@17
|
127 }
|
Chris@17
|
128
|
Chris@14
|
129 // Only add sections if the layout is new and empty.
|
Chris@14
|
130 if (!$is_rebuilding && $section_storage->count() === 0) {
|
Chris@14
|
131 $sections = [];
|
Chris@14
|
132 // If this is an empty override, copy the sections from the corresponding
|
Chris@14
|
133 // default.
|
Chris@14
|
134 if ($section_storage instanceof OverridesSectionStorageInterface) {
|
Chris@14
|
135 $sections = $section_storage->getDefaultSectionStorage()->getSections();
|
Chris@14
|
136 }
|
Chris@14
|
137
|
Chris@14
|
138 // For an empty layout, begin with a single section of one column.
|
Chris@14
|
139 if (!$sections) {
|
Chris@14
|
140 $sections[] = new Section('layout_onecol');
|
Chris@14
|
141 }
|
Chris@14
|
142
|
Chris@14
|
143 foreach ($sections as $section) {
|
Chris@14
|
144 $section_storage->appendSection($section);
|
Chris@14
|
145 }
|
Chris@14
|
146 $this->layoutTempstoreRepository->set($section_storage);
|
Chris@14
|
147 }
|
Chris@14
|
148 }
|
Chris@14
|
149
|
Chris@14
|
150 /**
|
Chris@14
|
151 * Builds a link to add a new section at a given delta.
|
Chris@14
|
152 *
|
Chris@14
|
153 * @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
Chris@14
|
154 * The section storage.
|
Chris@14
|
155 * @param int $delta
|
Chris@14
|
156 * The delta of the section to splice.
|
Chris@14
|
157 *
|
Chris@14
|
158 * @return array
|
Chris@14
|
159 * A render array for a link.
|
Chris@14
|
160 */
|
Chris@14
|
161 protected function buildAddSectionLink(SectionStorageInterface $section_storage, $delta) {
|
Chris@14
|
162 $storage_type = $section_storage->getStorageType();
|
Chris@14
|
163 $storage_id = $section_storage->getStorageId();
|
Chris@14
|
164 return [
|
Chris@14
|
165 'link' => [
|
Chris@14
|
166 '#type' => 'link',
|
Chris@14
|
167 '#title' => $this->t('Add Section'),
|
Chris@14
|
168 '#url' => Url::fromRoute('layout_builder.choose_section',
|
Chris@14
|
169 [
|
Chris@14
|
170 'section_storage_type' => $storage_type,
|
Chris@14
|
171 'section_storage' => $storage_id,
|
Chris@14
|
172 'delta' => $delta,
|
Chris@14
|
173 ],
|
Chris@14
|
174 [
|
Chris@14
|
175 'attributes' => [
|
Chris@17
|
176 'class' => ['use-ajax', 'new-section__link'],
|
Chris@14
|
177 'data-dialog-type' => 'dialog',
|
Chris@14
|
178 'data-dialog-renderer' => 'off_canvas',
|
Chris@14
|
179 ],
|
Chris@14
|
180 ]
|
Chris@14
|
181 ),
|
Chris@14
|
182 ],
|
Chris@14
|
183 '#type' => 'container',
|
Chris@14
|
184 '#attributes' => [
|
Chris@17
|
185 'class' => ['new-section'],
|
Chris@14
|
186 ],
|
Chris@14
|
187 ];
|
Chris@14
|
188 }
|
Chris@14
|
189
|
Chris@14
|
190 /**
|
Chris@14
|
191 * Builds the render array for the layout section while editing.
|
Chris@14
|
192 *
|
Chris@14
|
193 * @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
Chris@14
|
194 * The section storage.
|
Chris@14
|
195 * @param int $delta
|
Chris@14
|
196 * The delta of the section.
|
Chris@14
|
197 *
|
Chris@14
|
198 * @return array
|
Chris@14
|
199 * The render array for a given section.
|
Chris@14
|
200 */
|
Chris@14
|
201 protected function buildAdministrativeSection(SectionStorageInterface $section_storage, $delta) {
|
Chris@14
|
202 $storage_type = $section_storage->getStorageType();
|
Chris@14
|
203 $storage_id = $section_storage->getStorageId();
|
Chris@14
|
204 $section = $section_storage->getSection($delta);
|
Chris@14
|
205
|
Chris@14
|
206 $layout = $section->getLayout();
|
Chris@14
|
207 $build = $section->toRenderArray($this->getAvailableContexts($section_storage), TRUE);
|
Chris@14
|
208 $layout_definition = $layout->getPluginDefinition();
|
Chris@14
|
209
|
Chris@14
|
210 foreach ($layout_definition->getRegions() as $region => $info) {
|
Chris@14
|
211 if (!empty($build[$region])) {
|
Chris@14
|
212 foreach ($build[$region] as $uuid => $block) {
|
Chris@14
|
213 $build[$region][$uuid]['#attributes']['class'][] = 'draggable';
|
Chris@14
|
214 $build[$region][$uuid]['#attributes']['data-layout-block-uuid'] = $uuid;
|
Chris@14
|
215 $build[$region][$uuid]['#contextual_links'] = [
|
Chris@14
|
216 'layout_builder_block' => [
|
Chris@14
|
217 'route_parameters' => [
|
Chris@14
|
218 'section_storage_type' => $storage_type,
|
Chris@14
|
219 'section_storage' => $storage_id,
|
Chris@14
|
220 'delta' => $delta,
|
Chris@14
|
221 'region' => $region,
|
Chris@14
|
222 'uuid' => $uuid,
|
Chris@14
|
223 ],
|
Chris@14
|
224 ],
|
Chris@14
|
225 ];
|
Chris@14
|
226 }
|
Chris@14
|
227 }
|
Chris@14
|
228
|
Chris@14
|
229 $build[$region]['layout_builder_add_block']['link'] = [
|
Chris@14
|
230 '#type' => 'link',
|
Chris@14
|
231 '#title' => $this->t('Add Block'),
|
Chris@14
|
232 '#url' => Url::fromRoute('layout_builder.choose_block',
|
Chris@14
|
233 [
|
Chris@14
|
234 'section_storage_type' => $storage_type,
|
Chris@14
|
235 'section_storage' => $storage_id,
|
Chris@14
|
236 'delta' => $delta,
|
Chris@14
|
237 'region' => $region,
|
Chris@14
|
238 ],
|
Chris@14
|
239 [
|
Chris@14
|
240 'attributes' => [
|
Chris@17
|
241 'class' => ['use-ajax', 'new-block__link'],
|
Chris@14
|
242 'data-dialog-type' => 'dialog',
|
Chris@14
|
243 'data-dialog-renderer' => 'off_canvas',
|
Chris@14
|
244 ],
|
Chris@14
|
245 ]
|
Chris@14
|
246 ),
|
Chris@14
|
247 ];
|
Chris@14
|
248 $build[$region]['layout_builder_add_block']['#type'] = 'container';
|
Chris@17
|
249 $build[$region]['layout_builder_add_block']['#attributes'] = ['class' => ['new-block']];
|
Chris@14
|
250 $build[$region]['layout_builder_add_block']['#weight'] = 1000;
|
Chris@14
|
251 $build[$region]['#attributes']['data-region'] = $region;
|
Chris@14
|
252 $build[$region]['#attributes']['class'][] = 'layout-builder--layout__region';
|
Chris@14
|
253 }
|
Chris@14
|
254
|
Chris@14
|
255 $build['#attributes']['data-layout-update-url'] = Url::fromRoute('layout_builder.move_block', [
|
Chris@14
|
256 'section_storage_type' => $storage_type,
|
Chris@14
|
257 'section_storage' => $storage_id,
|
Chris@14
|
258 ])->toString();
|
Chris@14
|
259 $build['#attributes']['data-layout-delta'] = $delta;
|
Chris@14
|
260 $build['#attributes']['class'][] = 'layout-builder--layout';
|
Chris@14
|
261
|
Chris@14
|
262 return [
|
Chris@14
|
263 '#type' => 'container',
|
Chris@14
|
264 '#attributes' => [
|
Chris@14
|
265 'class' => ['layout-section'],
|
Chris@14
|
266 ],
|
Chris@14
|
267 'configure' => [
|
Chris@14
|
268 '#type' => 'link',
|
Chris@14
|
269 '#title' => $this->t('Configure section'),
|
Chris@14
|
270 '#access' => $layout instanceof PluginFormInterface,
|
Chris@14
|
271 '#url' => Url::fromRoute('layout_builder.configure_section', [
|
Chris@14
|
272 'section_storage_type' => $storage_type,
|
Chris@14
|
273 'section_storage' => $storage_id,
|
Chris@14
|
274 'delta' => $delta,
|
Chris@14
|
275 ]),
|
Chris@14
|
276 '#attributes' => [
|
Chris@14
|
277 'class' => ['use-ajax', 'configure-section'],
|
Chris@14
|
278 'data-dialog-type' => 'dialog',
|
Chris@14
|
279 'data-dialog-renderer' => 'off_canvas',
|
Chris@14
|
280 ],
|
Chris@14
|
281 ],
|
Chris@14
|
282 'remove' => [
|
Chris@14
|
283 '#type' => 'link',
|
Chris@17
|
284 '#title' => $this->t('Remove section <span class="visually-hidden">@section</span>', ['@section' => $delta + 1]),
|
Chris@14
|
285 '#url' => Url::fromRoute('layout_builder.remove_section', [
|
Chris@14
|
286 'section_storage_type' => $storage_type,
|
Chris@14
|
287 'section_storage' => $storage_id,
|
Chris@14
|
288 'delta' => $delta,
|
Chris@14
|
289 ]),
|
Chris@14
|
290 '#attributes' => [
|
Chris@14
|
291 'class' => ['use-ajax', 'remove-section'],
|
Chris@14
|
292 'data-dialog-type' => 'dialog',
|
Chris@14
|
293 'data-dialog-renderer' => 'off_canvas',
|
Chris@14
|
294 ],
|
Chris@14
|
295 ],
|
Chris@14
|
296 'layout-section' => $build,
|
Chris@14
|
297 ];
|
Chris@14
|
298 }
|
Chris@14
|
299
|
Chris@14
|
300 /**
|
Chris@14
|
301 * Saves the layout.
|
Chris@14
|
302 *
|
Chris@14
|
303 * @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
Chris@14
|
304 * The section storage.
|
Chris@14
|
305 *
|
Chris@14
|
306 * @return \Symfony\Component\HttpFoundation\RedirectResponse
|
Chris@14
|
307 * A redirect response.
|
Chris@14
|
308 */
|
Chris@14
|
309 public function saveLayout(SectionStorageInterface $section_storage) {
|
Chris@14
|
310 $section_storage->save();
|
Chris@14
|
311 $this->layoutTempstoreRepository->delete($section_storage);
|
Chris@14
|
312
|
Chris@14
|
313 if ($section_storage instanceof OverridesSectionStorageInterface) {
|
Chris@14
|
314 $this->messenger->addMessage($this->t('The layout override has been saved.'));
|
Chris@14
|
315 }
|
Chris@14
|
316 else {
|
Chris@14
|
317 $this->messenger->addMessage($this->t('The layout has been saved.'));
|
Chris@14
|
318 }
|
Chris@14
|
319
|
Chris@14
|
320 return new RedirectResponse($section_storage->getRedirectUrl()->setAbsolute()->toString());
|
Chris@14
|
321 }
|
Chris@14
|
322
|
Chris@14
|
323 /**
|
Chris@14
|
324 * Cancels the layout.
|
Chris@14
|
325 *
|
Chris@14
|
326 * @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
Chris@14
|
327 * The section storage.
|
Chris@14
|
328 *
|
Chris@14
|
329 * @return \Symfony\Component\HttpFoundation\RedirectResponse
|
Chris@14
|
330 * A redirect response.
|
Chris@14
|
331 */
|
Chris@14
|
332 public function cancelLayout(SectionStorageInterface $section_storage) {
|
Chris@14
|
333 $this->layoutTempstoreRepository->delete($section_storage);
|
Chris@14
|
334
|
Chris@14
|
335 $this->messenger->addMessage($this->t('The changes to the layout have been discarded.'));
|
Chris@14
|
336
|
Chris@14
|
337 return new RedirectResponse($section_storage->getRedirectUrl()->setAbsolute()->toString());
|
Chris@14
|
338 }
|
Chris@14
|
339
|
Chris@14
|
340 }
|