Chris@18: layoutTempstoreRepository = $layout_tempstore_repository;
Chris@18: $this->messenger = $messenger;
Chris@18: }
Chris@18:
Chris@18: /**
Chris@18: * {@inheritdoc}
Chris@18: */
Chris@18: public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
Chris@18: return new static(
Chris@18: $configuration,
Chris@18: $plugin_id,
Chris@18: $plugin_definition,
Chris@18: $container->get('layout_builder.tempstore_repository'),
Chris@18: $container->get('messenger')
Chris@18: );
Chris@18: }
Chris@18:
Chris@18: /**
Chris@18: * {@inheritdoc}
Chris@18: */
Chris@18: public function getInfo() {
Chris@18: return [
Chris@18: '#section_storage' => NULL,
Chris@18: '#pre_render' => [
Chris@18: [$this, 'preRender'],
Chris@18: ],
Chris@18: ];
Chris@18: }
Chris@18:
Chris@18: /**
Chris@18: * Pre-render callback: Renders the Layout Builder UI.
Chris@18: */
Chris@18: public function preRender($element) {
Chris@18: if ($element['#section_storage'] instanceof SectionStorageInterface) {
Chris@18: $element['layout_builder'] = $this->layout($element['#section_storage']);
Chris@18: }
Chris@18: return $element;
Chris@18: }
Chris@18:
Chris@18: /**
Chris@18: * Renders the Layout UI.
Chris@18: *
Chris@18: * @param \Drupal\layout_builder\SectionStorageInterface $section_storage
Chris@18: * The section storage.
Chris@18: *
Chris@18: * @return array
Chris@18: * A render array.
Chris@18: */
Chris@18: protected function layout(SectionStorageInterface $section_storage) {
Chris@18: $this->prepareLayout($section_storage);
Chris@18:
Chris@18: $output = [];
Chris@18: if ($this->isAjax()) {
Chris@18: $output['status_messages'] = [
Chris@18: '#type' => 'status_messages',
Chris@18: ];
Chris@18: }
Chris@18: $count = 0;
Chris@18: for ($i = 0; $i < $section_storage->count(); $i++) {
Chris@18: $output[] = $this->buildAddSectionLink($section_storage, $count);
Chris@18: $output[] = $this->buildAdministrativeSection($section_storage, $count);
Chris@18: $count++;
Chris@18: }
Chris@18: $output[] = $this->buildAddSectionLink($section_storage, $count);
Chris@18: $output['#attached']['library'][] = 'layout_builder/drupal.layout_builder';
Chris@18: // As the Layout Builder UI is typically displayed using the frontend theme,
Chris@18: // it is not marked as an administrative page at the route level even though
Chris@18: // it performs an administrative task. Mark this as an administrative page
Chris@18: // for JavaScript.
Chris@18: $output['#attached']['drupalSettings']['path']['currentPathIsAdmin'] = TRUE;
Chris@18: $output['#type'] = 'container';
Chris@18: $output['#attributes']['id'] = 'layout-builder';
Chris@18: $output['#attributes']['class'][] = 'layout-builder';
Chris@18: // Mark this UI as uncacheable.
Chris@18: $output['#cache']['max-age'] = 0;
Chris@18: return $output;
Chris@18: }
Chris@18:
Chris@18: /**
Chris@18: * Prepares a layout for use in the UI.
Chris@18: *
Chris@18: * @param \Drupal\layout_builder\SectionStorageInterface $section_storage
Chris@18: * The section storage.
Chris@18: */
Chris@18: protected function prepareLayout(SectionStorageInterface $section_storage) {
Chris@18: // If the layout has pending changes, add a warning.
Chris@18: if ($this->layoutTempstoreRepository->has($section_storage)) {
Chris@18: $this->messenger->addWarning($this->t('You have unsaved changes.'));
Chris@18: }
Chris@18: // If the layout is an override that has not yet been overridden, copy the
Chris@18: // sections from the corresponding default.
Chris@18: elseif ($section_storage instanceof OverridesSectionStorageInterface && !$section_storage->isOverridden()) {
Chris@18: $sections = $section_storage->getDefaultSectionStorage()->getSections();
Chris@18: foreach ($sections as $section) {
Chris@18: $section_storage->appendSection($section);
Chris@18: }
Chris@18: $this->layoutTempstoreRepository->set($section_storage);
Chris@18: }
Chris@18: }
Chris@18:
Chris@18: /**
Chris@18: * Builds a link to add a new section at a given delta.
Chris@18: *
Chris@18: * @param \Drupal\layout_builder\SectionStorageInterface $section_storage
Chris@18: * The section storage.
Chris@18: * @param int $delta
Chris@18: * The delta of the section to splice.
Chris@18: *
Chris@18: * @return array
Chris@18: * A render array for a link.
Chris@18: */
Chris@18: protected function buildAddSectionLink(SectionStorageInterface $section_storage, $delta) {
Chris@18: $storage_type = $section_storage->getStorageType();
Chris@18: $storage_id = $section_storage->getStorageId();
Chris@18:
Chris@18: // If the delta and the count are the same, it is either the end of the
Chris@18: // layout or an empty layout.
Chris@18: if ($delta === count($section_storage)) {
Chris@18: if ($delta === 0) {
Chris@18: $title = $this->t('Add Section');
Chris@18: }
Chris@18: else {
Chris@18: $title = $this->t('Add Section at end of layout');
Chris@18: }
Chris@18: }
Chris@18: // If the delta and the count are different, it is either the beginning of
Chris@18: // the layout or in between two sections.
Chris@18: else {
Chris@18: if ($delta === 0) {
Chris@18: $title = $this->t('Add Section at start of layout');
Chris@18: }
Chris@18: else {
Chris@18: $title = $this->t('Add Section between @first and @second', ['@first' => $delta, '@second' => $delta + 1]);
Chris@18: }
Chris@18: }
Chris@18:
Chris@18: return [
Chris@18: 'link' => [
Chris@18: '#type' => 'link',
Chris@18: '#title' => $title,
Chris@18: '#url' => Url::fromRoute('layout_builder.choose_section',
Chris@18: [
Chris@18: 'section_storage_type' => $storage_type,
Chris@18: 'section_storage' => $storage_id,
Chris@18: 'delta' => $delta,
Chris@18: ],
Chris@18: [
Chris@18: 'attributes' => [
Chris@18: 'class' => [
Chris@18: 'use-ajax',
Chris@18: 'layout-builder__link',
Chris@18: 'layout-builder__link--add',
Chris@18: ],
Chris@18: 'data-dialog-type' => 'dialog',
Chris@18: 'data-dialog-renderer' => 'off_canvas',
Chris@18: ],
Chris@18: ]
Chris@18: ),
Chris@18: ],
Chris@18: '#type' => 'container',
Chris@18: '#attributes' => [
Chris@18: 'class' => ['layout-builder__add-section'],
Chris@18: 'data-layout-builder-highlight-id' => $this->sectionAddHighlightId($delta),
Chris@18: ],
Chris@18: ];
Chris@18: }
Chris@18:
Chris@18: /**
Chris@18: * Builds the render array for the layout section while editing.
Chris@18: *
Chris@18: * @param \Drupal\layout_builder\SectionStorageInterface $section_storage
Chris@18: * The section storage.
Chris@18: * @param int $delta
Chris@18: * The delta of the section.
Chris@18: *
Chris@18: * @return array
Chris@18: * The render array for a given section.
Chris@18: */
Chris@18: protected function buildAdministrativeSection(SectionStorageInterface $section_storage, $delta) {
Chris@18: $storage_type = $section_storage->getStorageType();
Chris@18: $storage_id = $section_storage->getStorageId();
Chris@18: $section = $section_storage->getSection($delta);
Chris@18:
Chris@18: $layout = $section->getLayout();
Chris@18: $build = $section->toRenderArray($this->getAvailableContexts($section_storage), TRUE);
Chris@18: $layout_definition = $layout->getPluginDefinition();
Chris@18:
Chris@18: $region_labels = $layout_definition->getRegionLabels();
Chris@18: foreach ($layout_definition->getRegions() as $region => $info) {
Chris@18: if (!empty($build[$region])) {
Chris@18: foreach (Element::children($build[$region]) as $uuid) {
Chris@18: $build[$region][$uuid]['#attributes']['class'][] = 'js-layout-builder-block';
Chris@18: $build[$region][$uuid]['#attributes']['class'][] = 'layout-builder-block';
Chris@18: $build[$region][$uuid]['#attributes']['data-layout-block-uuid'] = $uuid;
Chris@18: $build[$region][$uuid]['#attributes']['data-layout-builder-highlight-id'] = $this->blockUpdateHighlightId($uuid);
Chris@18: $build[$region][$uuid]['#contextual_links'] = [
Chris@18: 'layout_builder_block' => [
Chris@18: 'route_parameters' => [
Chris@18: 'section_storage_type' => $storage_type,
Chris@18: 'section_storage' => $storage_id,
Chris@18: 'delta' => $delta,
Chris@18: 'region' => $region,
Chris@18: 'uuid' => $uuid,
Chris@18: ],
Chris@18: // Add metadata about the current operations available in
Chris@18: // contextual links. This will invalidate the client-side cache of
Chris@18: // links that were cached before the 'move' link was added.
Chris@18: // @see layout_builder.links.contextual.yml
Chris@18: 'metadata' => [
Chris@18: 'operations' => 'move:update:remove',
Chris@18: ],
Chris@18: ],
Chris@18: ];
Chris@18: }
Chris@18: }
Chris@18:
Chris@18: $build[$region]['layout_builder_add_block']['link'] = [
Chris@18: '#type' => 'link',
Chris@18: // Add one to the current delta since it is zero-indexed.
Chris@18: '#title' => $this->t('Add Block in section @section, @region region', ['@section' => $delta + 1, '@region' => $region_labels[$region]]),
Chris@18: '#url' => Url::fromRoute('layout_builder.choose_block',
Chris@18: [
Chris@18: 'section_storage_type' => $storage_type,
Chris@18: 'section_storage' => $storage_id,
Chris@18: 'delta' => $delta,
Chris@18: 'region' => $region,
Chris@18: ],
Chris@18: [
Chris@18: 'attributes' => [
Chris@18: 'class' => [
Chris@18: 'use-ajax',
Chris@18: 'layout-builder__link',
Chris@18: 'layout-builder__link--add',
Chris@18: ],
Chris@18: 'data-dialog-type' => 'dialog',
Chris@18: 'data-dialog-renderer' => 'off_canvas',
Chris@18: ],
Chris@18: ]
Chris@18: ),
Chris@18: ];
Chris@18: $build[$region]['layout_builder_add_block']['#type'] = 'container';
Chris@18: $build[$region]['layout_builder_add_block']['#attributes'] = [
Chris@18: 'class' => ['layout-builder__add-block'],
Chris@18: 'data-layout-builder-highlight-id' => $this->blockAddHighlightId($delta, $region),
Chris@18: ];
Chris@18: $build[$region]['layout_builder_add_block']['#weight'] = 1000;
Chris@18: $build[$region]['#attributes']['data-region'] = $region;
Chris@18: $build[$region]['#attributes']['class'][] = 'layout-builder__region';
Chris@18: $build[$region]['#attributes']['class'][] = 'js-layout-builder-region';
Chris@18: $build[$region]['#attributes']['role'] = 'group';
Chris@18: $build[$region]['#attributes']['aria-label'] = $this->t('@region region in section @section', [
Chris@18: '@region' => $info['label'],
Chris@18: '@section' => $delta + 1,
Chris@18: ]);
Chris@18:
Chris@18: // Get weights of all children for use by the region label.
Chris@18: $weights = array_map(function ($a) {
Chris@18: return isset($a['#weight']) ? $a['#weight'] : 0;
Chris@18: }, $build[$region]);
Chris@18:
Chris@18: // The region label is made visible when the move block dialog is open.
Chris@18: $build[$region]['region_label'] = [
Chris@18: '#type' => 'container',
Chris@18: '#attributes' => [
Chris@18: 'class' => ['layout__region-info', 'layout-builder__region-label'],
Chris@18: // A more detailed version of this information is already read by
Chris@18: // screen readers, so this label can be hidden from them.
Chris@18: 'aria-hidden' => TRUE,
Chris@18: ],
Chris@18: '#markup' => $this->t('Region: @region', ['@region' => $info['label']]),
Chris@18: // Ensures the region label is displayed first.
Chris@18: '#weight' => min($weights) - 1,
Chris@18: ];
Chris@18: }
Chris@18:
Chris@18: $build['#attributes']['data-layout-update-url'] = Url::fromRoute('layout_builder.move_block', [
Chris@18: 'section_storage_type' => $storage_type,
Chris@18: 'section_storage' => $storage_id,
Chris@18: ])->toString();
Chris@18:
Chris@18: $build['#attributes']['data-layout-delta'] = $delta;
Chris@18: $build['#attributes']['class'][] = 'layout-builder__layout';
Chris@18: $build['#attributes']['data-layout-builder-highlight-id'] = $this->sectionUpdateHighlightId($delta);
Chris@18:
Chris@18: return [
Chris@18: '#type' => 'container',
Chris@18: '#attributes' => [
Chris@18: 'class' => ['layout-builder__section'],
Chris@18: 'role' => 'group',
Chris@18: 'aria-label' => $this->t('Section @section', ['@section' => $delta + 1]),
Chris@18: ],
Chris@18: 'remove' => [
Chris@18: '#type' => 'link',
Chris@18: '#title' => $this->t('Remove section @section', ['@section' => $delta + 1]),
Chris@18: '#url' => Url::fromRoute('layout_builder.remove_section', [
Chris@18: 'section_storage_type' => $storage_type,
Chris@18: 'section_storage' => $storage_id,
Chris@18: 'delta' => $delta,
Chris@18: ]),
Chris@18: '#attributes' => [
Chris@18: 'class' => [
Chris@18: 'use-ajax',
Chris@18: 'layout-builder__link',
Chris@18: 'layout-builder__link--remove',
Chris@18: ],
Chris@18: 'data-dialog-type' => 'dialog',
Chris@18: 'data-dialog-renderer' => 'off_canvas',
Chris@18: ],
Chris@18: ],
Chris@18: // The section label is added to sections without a "Configure Section"
Chris@18: // link, and is only visible when the move block dialog is open.
Chris@18: 'section_label' => [
Chris@18: '#markup' => $this->t('Section @section', ['@section' => $delta + 1]),
Chris@18: '#access' => !$layout instanceof PluginFormInterface,
Chris@18: ],
Chris@18: 'configure' => [
Chris@18: '#type' => 'link',
Chris@18: // There are two instances of @section, the one wrapped in
Chris@18: // .visually-hidden is for screen readers. The one wrapped in
Chris@18: // .layout-builder__section-label is only visible when the
Chris@18: // move block dialog is open and it is not seen by screen readers.
Chris@18: '#title' => $this->t('Configure section @section@section', ['@section' => $delta + 1]),
Chris@18: '#access' => $layout instanceof PluginFormInterface,
Chris@18: '#url' => Url::fromRoute('layout_builder.configure_section', [
Chris@18: 'section_storage_type' => $storage_type,
Chris@18: 'section_storage' => $storage_id,
Chris@18: 'delta' => $delta,
Chris@18: ]),
Chris@18: '#attributes' => [
Chris@18: 'class' => [
Chris@18: 'use-ajax',
Chris@18: 'layout-builder__link',
Chris@18: 'layout-builder__link--configure',
Chris@18: ],
Chris@18: 'data-dialog-type' => 'dialog',
Chris@18: 'data-dialog-renderer' => 'off_canvas',
Chris@18: ],
Chris@18: ],
Chris@18: 'layout-builder__section' => $build,
Chris@18: ];
Chris@18: }
Chris@18:
Chris@18: }